fix: gate LocalUser machinery on multi_user_enabled for single-user backward compat

- auth() only queries LocalUser table when multi_user_enabled=True
- login() only shows signup link when multi_user_enabled AND allow_local_signup
- signup page and POST endpoint both check multi_user_enabled first
- Move local-auth imports to module level in auth.py (no re-import overhead)
- Fix signup rollback: flush before email send, commit only on success
- Update allow_local_signup config description to document prerequisite
- Add test: single-user mode skips LocalUser table entirely
- Patch multi_user_enabled=True on all local-login integration tests

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-07 13:35:10 +00:00
parent 43f3f6bdbe
commit b5b285ebe6
4 changed files with 114 additions and 46 deletions
+16 -11
View File
@@ -86,7 +86,9 @@ class PasswordResetBody(BaseModel):
@router.get("/signup", include_in_schema=False)
async def signup_page(request: Request) -> Any:
"""Render the signup page, or redirect to login when signup is disabled."""
"""Render the signup page, or redirect to login when multi-user / signup is disabled."""
if not settings.multi_user_enabled:
return RedirectResponse(url="/login?error=Multi-user+mode+is+not+enabled", status_code=302)
if not settings.allow_local_signup:
return RedirectResponse(url="/login?error=Registration+is+not+enabled", status_code=302)
return templates.TemplateResponse(
@@ -130,13 +132,16 @@ async def signup(request: Request, body: SignupBody, db: DbSession) -> dict[str,
"""Create a new local user account and send a verification email.
The account is inactive until the user clicks the email link.
Both ``MULTI_USER_ENABLED`` and ``ALLOW_LOCAL_SIGNUP`` must be ``True``.
Raises:
403: Local signup is disabled.
403: Multi-user mode or local signup is disabled.
503: SMTP is not configured.
422: Passwords do not match.
409: Email or username already registered.
"""
if not settings.multi_user_enabled:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Multi-user mode is not enabled.")
if not settings.allow_local_signup:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Registration is not enabled.")
if not settings.email_host:
@@ -170,8 +175,12 @@ async def signup(request: Request, body: SignupBody, db: DbSession) -> dict[str,
)
db.add(profile)
# Flush to the DB so constraint violations (duplicate key etc.) surface NOW,
# before we attempt to send the email. We do NOT commit yet — the commit only
# happens after the email is sent successfully so that a failed email leaves
# no orphan records in the database.
try:
db.commit()
db.flush()
except Exception:
db.rollback()
raise
@@ -180,20 +189,16 @@ async def signup(request: Request, body: SignupBody, db: DbSession) -> dict[str,
try:
send_verification_email(body.email, body.username, token, base_url)
except Exception as exc:
# Clean up orphan records — don't leave an unverifiable account
try:
db.delete(user)
db.delete(profile)
db.commit()
except Exception:
db.rollback()
logger.exception("Failed to clean up orphan records for %s after email send failure", body.email)
# Email failed — roll back so no unverifiable user row persists.
# The user can simply try registering again once SMTP is fixed.
db.rollback()
logger.warning("Signup email failed for %s: %s", body.email, exc)
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=("Failed to send verification email. Please check that SMTP is correctly configured and try again."),
) from exc
db.commit()
logger.info("New local user registered: %s", body.email)
return {"message": "Verification email sent. Please check your inbox."}