Private
Public Access
Replaces the stateless signed-cookie session (bare user_id) with a real server-side sessions table -- the cookie now just carries an opaque session id, resolved against the DB on every request. Each session records IP address (respects X-Forwarded-For), a parsed device label, and last-seen time (throttled updates, not written on every request). New GET/DELETE /api/auth/sessions endpoints and an "Active sessions" section in Profile settings let a user see every device they're logged in from and revoke one they don't recognize -- including their own current session, which just signs them out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
45 lines
1.5 KiB
Python
45 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.session_service import start_session
|
|
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")
|
|
|
|
await start_session(request, db, user.id)
|
|
return user
|