Files
audit-web/app/authenticator/auth.py
T
2026-08-26 14:11:37 +07:00

315 lines
10 KiB
Python

"""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,
}