hoàn thành phần Authentication

This commit is contained in:
2026-03-31 14:15:32 +07:00
commit ebfa869a26
12 changed files with 389 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.orm import Session
from ..database import get_db
from ..dependencies import get_current_user
from ..models import User
from ..schemas import UserLogin, UserOut, UserRegister
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
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=True, # send only over HTTPS (Nginx handles TLS in prod)
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.")
user = User(
username=body.username,
email=body.email,
password_hash=hash_password(body.password),
)
db.add(user)
db.commit()
db.refresh(user)
_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.")
_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