admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / app / routers / auth.py
3305 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | import uuid from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.orm import Session from .. import mfa, models, schemas from ..audit import log_audit from ..database import get_db from ..deps import get_session_user from ..security import verify_password router = APIRouter(prefix="/api/v1/auth", tags=["auth"]) def _establish_session(request: Request, db: Session, user: models.User) -> str: """Grant the full session and log the successful login. Returns the status the client should act on: if the tenant admin has flagged this account as needing MFA but it isn't enrolled yet, the client should send the user straight to enrollment instead of the dashboard.""" request.session["user_id"] = str(user.id) log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="login_success") db.commit() if user.mfa_required and not user.mfa_enabled: return "mfa_enroll_required" return "ok" @router.post("/login") def login_submit(payload: schemas.LoginRequest, request: Request, db: Session = Depends(get_db)): user = db.query(models.User).filter(models.User.email == payload.email.strip().lower()).first() if not user or not user.is_active or not verify_password(payload.password, user.hashed_password): # Only log the attempt when it maps to a real (tenant-scoped) account - # unknown emails have no tenant to attribute the row to. if user: log_audit(db, tenant_id=user.tenant_id, user_id=user.id, action="login_failed") db.commit() raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials.") if user.mfa_enabled: # Password verified but the session isn't established yet - a second # factor is still required before we trust this login. request.session["pending_mfa_user_id"] = str(user.id) return {"status": "mfa_required"} return {"status": _establish_session(request, db, user)} @router.post("/login/mfa") def login_mfa_submit(payload: schemas.LoginMfaRequest, request: Request, db: Session = Depends(get_db)): pending_id = request.session.get("pending_mfa_user_id") if not pending_id: raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="No pending MFA challenge.") user = db.get(models.User, uuid.UUID(pending_id)) if not user or not user.mfa_enabled or not mfa.verify_code(user.mfa_secret, payload.code): raise HTTPException(status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired code.") request.session.pop("pending_mfa_user_id", None) return {"status": _establish_session(request, db, user)} @router.post("/logout") def logout(request: Request): request.session.clear() return {"status": "ok"} @router.get("/me", response_model=schemas.MeOut) def me(user=Depends(get_session_user)): if not user: raise HTTPException(status.HTTP_401_UNAUTHORIZED) return schemas.MeOut( id=user.id, tenant_id=user.tenant_id, tenant_name=user.tenant.name, ai_enabled=user.tenant.ai_enabled, email=user.email, full_name=user.full_name, role=user.role, is_active=user.is_active, mfa_enabled=user.mfa_enabled, mfa_required=user.mfa_required, ) |