133 lines
4.2 KiB
Python
Executable File
133 lines
4.2 KiB
Python
Executable File
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": "<uuid>",
|
|
"login": "<username or email>",
|
|
"password": "<plain>"
|
|
}
|
|
"""
|
|
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")
|