"""Authentication endpoints for WealthWise. This module provides endpoints for user registration, login, and token management using OAuth2 with Password Flow (JWT-based authentication). """ from datetime import timedelta from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy import select from sqlmodel.ext.asyncio.session import AsyncSession from app.api.deps import SessionDep from app.core.security import create_access_token, get_password_hash, verify_password from app.models import User from app.schemas.user import Token, UserCreate, UserPublic router = APIRouter() @router.post( "/register", response_model=UserPublic, status_code=status.HTTP_201_CREATED, summary="Register a new user", description="Create a new user account with email and password.", response_description="Created user information (excluding password)", tags=["Authentication"], ) async def register( user_in: UserCreate, session: AsyncSession = SessionDep, ) -> UserPublic: """Register a new user account. This endpoint creates a new user with the provided email and password. The password is hashed using bcrypt before storage. If the email already exists, a 400 error is returned. Args: user_in: User creation data with email and password session: Database session Returns: UserPublic: Created user information (without password) Raises: HTTPException: 400 if email already registered """ # Check if user with this email already exists statement = select(User).where(User.email == user_in.email) result = await session.exec(statement) existing_user = result.first() if existing_user: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered", ) # Create new user with hashed password user = User( email=user_in.email, hashed_password=get_password_hash(user_in.password), is_active=True, is_superuser=False, ) session.add(user) await session.commit() await session.refresh(user) return user @router.post( "/token", response_model=Token, summary="Login and get access token", description="Authenticate with email and password to receive a JWT access token.", response_description="JWT access token for authenticated requests", tags=["Authentication"], ) async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], session: AsyncSession = SessionDep, ) -> Token: """Authenticate user and return JWT access token. This endpoint accepts username (email) and password via OAuth2 form data, validates the credentials against the database, and returns a JWT access token if authentication is successful. Args: form_data: OAuth2 form with username (email) and password session: Database session Returns: Token: JWT access token with bearer type Raises: HTTPException: 401 if credentials are invalid """ # Find user by email (username field in OAuth2 form) statement = select(User).where(User.email == form_data.username) result = await session.exec(statement) user = result.first() # Validate user exists, is active, and password is correct if not user or not user.is_active: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) if not verify_password(form_data.password, user.hashed_password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) # Create access token access_token = create_access_token( data={"sub": str(user.id)}, ) return Token(access_token=access_token, token_type="bearer")