v2.3
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
"""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,
|
||||
}
|
||||
Reference in New Issue
Block a user