mirror of
https://git.victorphan.net/basketballcantho/ten-project.git
synced 2026-08-05 09:53:10 +07:00
134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
from fastapi import APIRouter, Cookie, Depends, HTTPException, Response, status
|
|
from sqlalchemy.orm import Session
|
|
import os
|
|
|
|
from ..database import get_db
|
|
from ..dependencies import get_current_user
|
|
from ..models import User, UserRole, UserStatus
|
|
from ..schemas import UserLogin, UserOut, UserRegister, UserApprove, ChangePassword
|
|
from ..security import (
|
|
ACCESS_TOKEN_EXPIRE_MINUTES,
|
|
create_access_token,
|
|
hash_password,
|
|
verify_password,
|
|
)
|
|
|
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
|
|
_COOKIE_NAME = "access_token"
|
|
_COOKIE_MAX_AGE = ACCESS_TOKEN_EXPIRE_MINUTES * 60 # seconds
|
|
_COOKIE_SECURE = os.getenv("COOKIE_SECURE", "false").lower() == "true"
|
|
|
|
|
|
def _set_auth_cookie(response: Response, user_id: int) -> None:
|
|
token = create_access_token(user_id)
|
|
response.set_cookie(
|
|
key=_COOKIE_NAME,
|
|
value=token,
|
|
httponly=True,
|
|
secure=_COOKIE_SECURE,
|
|
samesite="lax",
|
|
max_age=_COOKIE_MAX_AGE,
|
|
path="/",
|
|
)
|
|
|
|
|
|
# ── POST /auth/register ───────────────────────────────────────────────────────
|
|
|
|
@router.post("/register", response_model=UserOut, status_code=status.HTTP_201_CREATED)
|
|
def register(body: UserRegister, response: Response, db: Session = Depends(get_db)):
|
|
if db.query(User).filter(User.username == body.username).first():
|
|
raise HTTPException(status_code=400, detail="Username already taken.")
|
|
if db.query(User).filter(User.email == body.email).first():
|
|
raise HTTPException(status_code=400, detail="Email already registered.")
|
|
|
|
# Admin registers as approved immediately; others start as pending
|
|
initial_status = (
|
|
UserStatus.approved if body.role == UserRole.admin else UserStatus.pending
|
|
)
|
|
|
|
user = User(
|
|
username=body.username,
|
|
email=body.email,
|
|
password_hash=hash_password(body.password),
|
|
role=body.role,
|
|
status=initial_status,
|
|
)
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
|
|
# Only set auth cookie if immediately approved (admin self-registration)
|
|
if user.status == UserStatus.approved:
|
|
_set_auth_cookie(response, user.id)
|
|
return user
|
|
|
|
|
|
# ── POST /auth/login ──────────────────────────────────────────────────────────
|
|
|
|
@router.post("/login", response_model=UserOut)
|
|
def login(body: UserLogin, response: Response, db: Session = Depends(get_db)):
|
|
user: User | None = db.query(User).filter(User.username == body.username).first()
|
|
|
|
# Constant-time: always verify even when user is None to prevent timing attacks
|
|
dummy_hash = "$2b$12$KIXyb/r7MBJ3z32Dc3y8YuNfj7DhpNcaYgU2q8uGiHjJ6YFe6RVxm"
|
|
password_ok = verify_password(body.password, user.password_hash if user else dummy_hash)
|
|
|
|
if not user or not password_ok:
|
|
raise HTTPException(status_code=401, detail="Invalid username or password.")
|
|
|
|
if user.status == UserStatus.pending:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Your account is pending admin approval.",
|
|
)
|
|
if user.status == UserStatus.rejected:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Your account has been rejected.",
|
|
)
|
|
|
|
_set_auth_cookie(response, user.id)
|
|
return user
|
|
|
|
|
|
# ── POST /auth/logout ─────────────────────────────────────────────────────────
|
|
|
|
@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT)
|
|
def logout(response: Response):
|
|
response.delete_cookie(key=_COOKIE_NAME, path="/")
|
|
|
|
|
|
# ── GET /auth/me ──────────────────────────────────────────────────────────────
|
|
|
|
@router.get("/me", response_model=UserOut)
|
|
def me(current_user: User = Depends(get_current_user)):
|
|
return current_user
|
|
|
|
|
|
# ── GET /auth/token (return raw JWT for WebSocket auth) ─────────────────────
|
|
|
|
@router.get("/token")
|
|
def get_token(access_token: str | None = Cookie(default=None)):
|
|
"""
|
|
Returns the current JWT so the frontend can pass it as a WebSocket
|
|
query param (browsers can't send cookies on WS upgrade in all cases).
|
|
"""
|
|
if not access_token:
|
|
raise HTTPException(status_code=401, detail="Not authenticated.")
|
|
return {"access_token": access_token}
|
|
|
|
|
|
# ── POST /auth/change-password ────────────────────────────────────────────────
|
|
|
|
@router.post("/change-password", status_code=status.HTTP_204_NO_CONTENT)
|
|
def change_password(
|
|
body: ChangePassword,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
if not verify_password(body.current_password, current_user.password_hash):
|
|
raise HTTPException(status_code=400, detail="Mật khẩu hiện tại không đúng.")
|
|
current_user.password_hash = hash_password(body.new_password)
|
|
db.commit()
|