from datetime import timedelta from typing import Dict, Any from fastapi import APIRouter, HTTPException from app.core.config import settings from app.core.security import create_token, decode_token router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login") def login(payload: Dict[str, Any]): """ payload: { "org_id": "", "login": "", "password": "" } """ from app.db.session import get_conn conn = get_conn() try: cur = conn.cursor() cur.execute("BEGIN;") org_id = (payload.get("org_id") or "").strip() login_id = (payload.get("login") or "").strip() password = payload.get("password") or "" if not org_id or not login_id or not password: raise HTTPException(status_code=400, detail="org_id, login, password required") # RLS miatt kötelező: org kontextus beállítás cur.execute("SELECT set_config('app.tenant_org_id', %s, false);", (org_id,)) # account + credential cur.execute( """ SELECT a.account_id::text, a.org_id::text, a.username::text, a.email::text, c.password_hash, c.is_active FROM app.account a JOIN app.account_credential c ON c.account_id = a.account_id WHERE a.org_id = %s::uuid AND (a.username = %s::citext OR a.email = %s::citext) AND c.is_active = true LIMIT 1; """, (org_id, login_id, login_id), ) row = cur.fetchone() if not row: raise HTTPException(status_code=401, detail="Invalid credentials") account_id, org_id_db, username, email, password_hash, cred_active = row # Jelszó ellenőrzés pgcrypto-val: crypt(plain, stored_hash) = stored_hash cur.execute("SELECT crypt(%s, %s) = %s;", (password, password_hash, password_hash)) ok = cur.fetchone()[0] if not ok: raise HTTPException(status_code=401, detail="Invalid credentials") # MVP: role később membershipből; most fixen tenant_admin role_code = "tenant_admin" is_platform_admin = False access = create_token( { "sub": account_id, "org_id": org_id_db, "role": role_code, "is_platform_admin": is_platform_admin, "type": "access", }, settings.JWT_SECRET, timedelta(minutes=settings.JWT_ACCESS_MINUTES), ) refresh = create_token( { "sub": account_id, "org_id": org_id_db, "role": role_code, "is_platform_admin": is_platform_admin, "type": "refresh", }, settings.JWT_SECRET, timedelta(days=settings.JWT_REFRESH_DAYS), ) conn.commit() return {"access_token": access, "refresh_token": refresh, "token_type": "bearer"} except HTTPException: conn.rollback() raise except Exception as e: conn.rollback() raise HTTPException(status_code=500, detail=str(e)) finally: conn.close() @router.post("/refresh") def refresh_token(payload: Dict[str, Any]): token = payload.get("refresh_token") or "" if not token: raise HTTPException(status_code=400, detail="refresh_token required") try: claims = decode_token(token, settings.JWT_SECRET) if claims.get("type") != "refresh": raise HTTPException(status_code=401, detail="Invalid refresh token type") access = create_token( { "sub": claims.get("sub"), "org_id": claims.get("org_id"), "role": claims.get("role"), "is_platform_admin": claims.get("is_platform_admin", False), "type": "access", }, settings.JWT_SECRET, timedelta(minutes=settings.JWT_ACCESS_MINUTES), ) return {"access_token": access, "token_type": "bearer"} except Exception: raise HTTPException(status_code=401, detail="Invalid or expired refresh token")