Private
Public Access
Site admins can invite a brand-new person by email from the Admin portal Users tab -- a signup-link email lets them set their own username/password and lands them in the app already logged in. Existing users invited to a room now also get an email. Closes the "invited but never notified" gap from both directions. SMTP is configured through the Admin Settings tab at runtime (not the env file), persisted in a new smtp_settings table with the password encrypted at rest via a Fernet key derived from SESSION_SECRET -- the first reversible secret this app stores in the database. A "send test email" button surfaces real delivery errors; the invite/notification paths themselves never fail loudly, since an SMTP outage shouldn't block an action that already succeeded in the database. New site_invites table mirrors RoomInvite's shape but targets an email address with no room context; the raw signup token is hashed the same way API tokens are, and only ever exists in the email link. POST /api/signup is the first genuinely public, unauthenticated account-creation endpoint in this app, reusing the existing register_user path for identical validation.
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.schemas.site_invite import SignupComplete, SignupValidateRead
|
|
from app.schemas.user import UserRead
|
|
from app.services.auth_service import DuplicateUserError
|
|
from app.services.site_invite_service import (
|
|
SiteInviteInvalidError,
|
|
complete_signup,
|
|
validate_signup_token,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/signup", tags=["signup"])
|
|
|
|
|
|
@router.get("/validate", response_model=SignupValidateRead)
|
|
async def validate_signup_endpoint(
|
|
token: str = Query(...),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
invite = await validate_signup_token(db, token)
|
|
except SiteInviteInvalidError:
|
|
raise HTTPException(status_code=400, detail="This invite link is invalid or has expired")
|
|
return SignupValidateRead(email=invite.email)
|
|
|
|
|
|
@router.post("", response_model=UserRead)
|
|
async def complete_signup_endpoint(
|
|
data: SignupComplete,
|
|
request: Request,
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
user = await complete_signup(db, data.token, data.username, data.password)
|
|
except SiteInviteInvalidError:
|
|
raise HTTPException(status_code=400, detail="This invite link is invalid or has expired")
|
|
except DuplicateUserError:
|
|
raise HTTPException(status_code=409, detail="That username or email is already taken")
|
|
|
|
request.session["user_id"] = str(user.id)
|
|
return user
|