Catalyst / admin/Synapse-Cortex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Cortex

public

Self Hosted ITSM Tool with RBAC/Tenanting and MFA

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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,
    )