"""Authentication router for login and JWT token management.""" from fastapi import APIRouter, Depends, HTTPException, status, Form from sqlalchemy.orm import Session from typing import Dict, Any from .. import crud, schemas from ..database import get_db from ..utils import create_access_token, decode_access_token from fastapi import Request, Query from ..security import get_current_user, revoke_token_db, REVOKED_TOKENS, set_latest_jti import os from .radius_login import check_radius_login from ..models import RevokedToken import requests import xml.etree.ElementTree as ET router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login", response_model=schemas.LoginResponse) async def login(login_req: schemas.LoginRequest, db: Session = Depends(get_db)): # Convert env về boolean chuẩn IS_PRODUCT = os.getenv("IS_PRODUCT", "False").lower() == "true" # ============================= # 🚀 PRODUCT MODE (mock data) # ============================= if IS_PRODUCT: return { "access_token": "test", "token_type": "bearer", "user": { "id": 1, "email": "test@gmail.com", "fullname": "Test User", "unit_id": 1, "unit_name": "MEDIA", "role_id": 1, "role_name": "Administrator", "status": 2, "status_name": "active", "created_at": "2025-12-02T14:32:10", "updated_at": "2025-12-23T04:24:04" } } # ============================= # 🧪 DEV MODE (login thật) # ============================= user = crud.authenticate_user(db, login_req.email, login_req.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) if int(getattr(user, "status", 0) or 0) != 2: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User is not active", headers={"WWW-Authenticate": "Bearer"}, ) user_details = crud.get_user_with_details(db, user) token_data = { "sub": str(user.id), "email": user.email, "fullname": user.fullname, "unit_id": user.unit_id, "status": user.status, } access_token = create_access_token(data=token_data) try: payload = decode_access_token(access_token) jti = payload.get("jti") if jti: set_latest_jti(int(user.id), jti) except Exception: pass return { "access_token": access_token, "token_type": "bearer", "user": user_details } @router.post("/logout", response_model=schemas.APIResponse, dependencies=[Depends(get_current_user)]) async def logout(request: Request, db: Session = Depends(get_db)): """Logout endpoint: revoke current JWT token.""" auth_header = request.headers.get("Authorization") if not auth_header: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing authorization header", headers={"WWW-Authenticate": "Bearer"}, ) try: scheme, token = auth_header.split() if scheme.lower() != "bearer": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication scheme", headers={"WWW-Authenticate": "Bearer"}, ) except ValueError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authorization header", headers={"WWW-Authenticate": "Bearer"}, ) revoke_token_db(db, token) return {"message": "success", "data": {"revoked": True}} @router.get("/validate-token", response_model=schemas.APIResponse) async def validate_token(request: Request, db: Session = Depends(get_db)): auth_header = request.headers.get("Authorization") if not auth_header: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing authorization header", headers={"WWW-Authenticate": "Bearer"}, ) try: scheme, token = auth_header.split() if scheme.lower() != "bearer": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication scheme", headers={"WWW-Authenticate": "Bearer"}, ) except ValueError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authorization header", headers={"WWW-Authenticate": "Bearer"}, ) if token in REVOKED_TOKENS: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has been revoked", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = decode_access_token(token) except Exception: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token", headers={"WWW-Authenticate": "Bearer"}, ) jti = payload.get("jti") if jti and db.query(RevokedToken).filter(RevokedToken.jti == jti).first(): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has been revoked", headers={"WWW-Authenticate": "Bearer"}, ) return {"message": "success", "data": payload} @router.post("/login_sso", response_model=schemas.LoginResponse) async def login_sso( login_req: schemas.LoginSSORequest, db: Session = Depends(get_db), ): server = os.getenv("RADIUS_SERVER") secret = os.getenv("RADIUS_SECRET") port = int(os.getenv("RADIUS_PORT", 1812)) realm = os.getenv("RADIUS_REALM", "") if not server or not secret: raise HTTPException(status_code=500, detail="Configuration is missing") try: local_part = (login_req.email or "").split("@")[0] except Exception: local_part = login_req.email or "" username = f"{local_part}{realm}" radius_password = f"{login_req.password}{login_req.otp or ''}" ok = check_radius_login(server, port, secret, username, radius_password) if not ok: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password or OTP") user = crud.get_user_by_email(db, login_req.email) if not user: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found. Please contact to Admin.") if int(getattr(user, "status", 0) or 0) != 2: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User is not active", headers={"WWW-Authenticate": "Bearer"}, ) user_details = crud.get_user_with_details(db, user) token_data = { "sub": str(user.id), "email": user.email, "fullname": user.fullname, "unit_id": user.unit_id, "status": user.status, } access_token = create_access_token(data=token_data) try: payload = decode_access_token(access_token) jti = payload.get("jti") if jti: set_latest_jti(int(user.id), jti) except Exception: pass return { "access_token": access_token, "token_type": "bearer", "user": user_details, } @router.get("/login_sso_vnpt", response_model=schemas.LoginResponse) async def login_sso_vnpt( ticket: str = Query(..., description="CAS ticket"), db: Session = Depends(get_db), ): # ===== 1. CAS CONFIG ===== CAS_HOST = os.getenv("CAS_HOST") # vd: https://cas.vnpt.vn/cas SERVICE_URL = os.getenv("CAS_SERVICE_URL") if not CAS_HOST or not SERVICE_URL: raise HTTPException( status_code=500, detail="CAS configuration missing", ) # ===== 2. BUILD VALIDATE URL ===== validate_url = ( f"{CAS_HOST.rstrip('/')}/serviceValidate" f"?ticket={ticket}&service={SERVICE_URL}" ) print(validate_url) # ===== 3. CALL CAS ===== try: resp = requests.get(validate_url, timeout=5) resp.raise_for_status() except Exception: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="CAS validate service unavailable", ) # ===== 4. PARSE XML ===== try: root = ET.fromstring(resp.text) except ET.ParseError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid CAS response", ) namespace = {"cas": "http://www.yale.edu/tp/cas"} user_node = root.find(".//cas:user", namespace) if user_node is None or not user_node.text: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="CAS returned but refused to validate your identity", ) # ===== 5. NETID → EMAIL ===== netid = user_node.text.strip() email = f"{netid.lower()}@vnpt.vn" # ===== 6. CHECK USER DB ===== user = crud.get_user_by_email(db, email) if not user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found. Please contact to Admin.", ) if int(getattr(user, "status", 0) or 0) != 2: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User is not active", headers={"WWW-Authenticate": "Bearer"}, ) # ===== 7. USER DETAILS ===== user_details = crud.get_user_with_details(db, user) # ===== 8. TOKEN DATA (GIỮ NGUYÊN THEO YÊU CẦU) ===== token_data = { "sub": str(user.id), "email": user.email, "fullname": user.fullname, "unit_id": user.unit_id, "status": user.status, } access_token = create_access_token(data=token_data) try: payload = decode_access_token(access_token) jti = payload.get("jti") if jti: set_latest_jti(int(user.id), jti) except Exception: pass # ===== 9. RESPONSE ===== return { "access_token": access_token, "token_type": "bearer", "user": user_details, }