"""Authentication endpoints: register, login, refresh, me.""" from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import func, select from app.api.deps import DB, CurrentUser, rate_limit_auth, rate_limit_demo_session from app.config import get_settings from app.core.security import ( create_access_token, create_refresh_token, decode_token, hash_password, verify_password, ) from app.demo import create_guest_user from app.models import User from app.schemas import ( LoginRequest, RefreshRequest, RegisterRequest, TokenPair, UserOut, ) router = APIRouter(prefix="/auth", tags=["auth"], dependencies=[Depends(rate_limit_auth)]) def _issue_tokens(user_id: int) -> TokenPair: return TokenPair( access_token=create_access_token(user_id), refresh_token=create_refresh_token(user_id), ) @router.post("/register", response_model=TokenPair, status_code=status.HTTP_201_CREATED) async def register(body: RegisterRequest, db: DB) -> TokenPair: email = body.email.lower() existing = await db.execute( select(User).where(func.lower(User.email) == email) ) if existing.scalar_one_or_none() is not None: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="An account with this email already exists", ) user = User( email=email, username=body.username.strip(), password_hash=hash_password(body.password), ) db.add(user) await db.commit() await db.refresh(user) return _issue_tokens(user.id) @router.post("/login", response_model=TokenPair) async def login(body: LoginRequest, db: DB) -> TokenPair: result = await db.execute( select(User).where(func.lower(User.email) == body.email.lower()) ) user = result.scalar_one_or_none() if user is None or not verify_password(body.password, user.password_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", ) return _issue_tokens(user.id) @router.post("/refresh", response_model=TokenPair) async def refresh(body: RefreshRequest, db: DB) -> TokenPair: user_id = decode_token(body.refresh_token, expected_type="refresh") if user_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired refresh token", ) user = await db.get(User, user_id) if user is None or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" ) return _issue_tokens(user.id) @router.post( "/demo", response_model=TokenPair, status_code=status.HTTP_201_CREATED, dependencies=[Depends(rate_limit_demo_session)], ) async def demo_session(db: DB) -> TokenPair: """One-click guest access for the public demo. Creates an isolated throwaway account whose knowledge base is cloned from the seeded sample document, so retrieval works from the first message. """ if not get_settings().demo_mode: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Demo mode is disabled" ) guest = await create_guest_user(db) return _issue_tokens(guest.id) @router.get("/me", response_model=UserOut) async def me(user: CurrentUser) -> UserOut: return UserOut.model_validate(user)