315 lines
10 KiB
Python
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,
|
|
}
|