131 lines
4.1 KiB
Python
131 lines
4.1 KiB
Python
"""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") |