"""Authentication: user register/login (JWT in HttpOnly cookie), Google OAuth 2.0 flow.""" import json import secrets from datetime import datetime, timezone import redis as redis_lib from fastapi import APIRouter, Depends, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse from pydantic import BaseModel, EmailStr, Field from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_current_user, get_db from app.core.config import settings from app.core.security import (create_access_token, hash_password, log_audit, verify_password) from app.db.session import AsyncSessionLocal from app.models import User from app.services import gmail_service router = APIRouter(prefix="/auth", tags=["auth"]) try: _oauth_redis = redis_lib.from_url(settings.REDIS_URL, socket_timeout=2) except Exception: _oauth_redis = None class RegisterIn(BaseModel): email: EmailStr password: str = Field(min_length=10, max_length=128) full_name: str = "" class LoginIn(BaseModel): email: EmailStr password: str def _set_session_cookie(response: Response, token: str) -> None: response.set_cookie( settings.SESSION_COOKIE_NAME, token, httponly=True, samesite="lax", secure=(settings.ENVIRONMENT == "production"), max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, path="/", ) response.set_cookie( settings.CSRF_COOKIE_NAME, secrets.token_urlsafe(24), httponly=False, samesite="lax", secure=(settings.ENVIRONMENT == "production"), max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, path="/", ) @router.post("/register", status_code=201) async def register(payload: RegisterIn, request: Request, response: Response, db: AsyncSession = Depends(get_db)): existing = (await db.execute(select(User).where(User.email == payload.email.lower()))).scalar_one_or_none() if existing: raise HTTPException(409, "Email already registered") user = User(email=payload.email.lower(), password_hash=hash_password(payload.password), full_name=payload.full_name, is_admin=True) db.add(user) await db.commit() token = create_access_token(user.id) _set_session_cookie(response, token) await log_audit(db, user.id, "user.register", "user", user.id, {"email": user.email}, request.client.host) return {"id": user.id, "email": user.email, "token": token} @router.post("/login") async def login(payload: LoginIn, request: Request, response: Response, db: AsyncSession = Depends(get_db)): user = (await db.execute(select(User).where(User.email == payload.email.lower()))).scalar_one_or_none() if not user or not verify_password(payload.password, user.password_hash): raise HTTPException(401, "Invalid email or password") if not user.is_active: raise HTTPException(403, "Account disabled") user.last_login_at = datetime.now(timezone.utc) await db.commit() token = create_access_token(user.id) _set_session_cookie(response, token) return {"id": user.id, "email": user.email, "token": token} @router.post("/logout") async def logout(response: Response): response.delete_cookie(settings.SESSION_COOKIE_NAME) response.delete_cookie(settings.CSRF_COOKIE_NAME) return {"ok": True}