feat(users): passwordless user creation via emailed set-password link, display full name instead of username
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

Admins now create users with a name + email (no password) and role;
a Send Email action (per-user, also reusable for resets) issues a
one-time token and POSTs it to an admin-configurable webhook URL —
intended for a Power Automate flow that delivers the actual email.
The old admin-sets-the-password flow is kept server-side
(LegacyUserCreateWithPassword) but no longer wired into the UI.

Every user-facing surface (top bar, sidebar, user management, audit
log, assignee pickers, dispute notifications) now shows full_name
instead of username, falling back to username when unset.

Also fixes long attack_procedure/expected_detection/suggested_text
text overflowing its container in the template review panels and
Red/Blue team fields (missing break-words on whitespace-pre-wrap
blocks).
This commit is contained in:
kitos
2026-07-17 16:58:25 +02:00
parent 60ab2e558f
commit 4dea19cae9
36 changed files with 974 additions and 159 deletions
+41 -1
View File
@@ -57,7 +57,7 @@ from app.models.user import User
from app.schemas.auth import TokenResponse, UserOut
# Import PasswordChange from app.schemas.user
from app.schemas.user import PasswordChange
from app.schemas.user import PasswordChange, SetPasswordRequest, SetPasswordTokenValidateOut
# Import log_action from app.services.audit_service
from app.services.audit_service import log_action
@@ -72,6 +72,12 @@ from app.services.auth_service import (
change_password as auth_change_password,
)
from app.domain.errors import EntityNotFoundError
from app.services.password_setup_service import (
consume_token_and_set_password,
validate_token as validate_password_setup_token,
)
# Assign router = APIRouter(prefix="/auth", tags=["auth"])
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -381,3 +387,37 @@ def change_password(
# Return {"detail": "Password changed successfully"}
return {"detail": "Password changed successfully"}
# ---------------------------------------------------------------------------
# Set password via a one-time emailed link — public (no auth), rate-limited
# ---------------------------------------------------------------------------
@router.get("/set-password/validate", response_model=SetPasswordTokenValidateOut)
@limiter.limit("20/minute")
def validate_set_password_token(request: Request, token: str, db: Session = Depends(get_db)) -> SetPasswordTokenValidateOut:
"""Check a set-password token before rendering the form. Public endpoint."""
try:
user = validate_password_setup_token(db, token)
except (EntityNotFoundError, BusinessRuleViolation):
return SetPasswordTokenValidateOut(valid=False)
return SetPasswordTokenValidateOut(valid=True, full_name=user.full_name)
@router.post("/set-password")
@limiter.limit("10/minute")
def set_password(request: Request, body: SetPasswordRequest, db: Session = Depends(get_db)) -> dict:
"""Complete a password setup/reset using a one-time token. Public endpoint."""
with UnitOfWork(db) as uow:
user = consume_token_and_set_password(db, body.token, body.new_password)
log_action(
db,
user.id,
"SET_PASSWORD_VIA_TOKEN",
"auth",
str(user.id),
details={},
)
uow.commit()
return {"detail": "Password set successfully — you can now log in"}