Initial commit: WealthWise financial analytics platform
This commit is contained in:
1
backend/app/api/v1/__init__.py
Normal file
1
backend/app/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API v1 endpoints
|
||||
38
backend/app/api/v1/api.py
Normal file
38
backend/app/api/v1/api.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""WealthWise API v1 router aggregation.
|
||||
|
||||
This module aggregates all v1 API endpoints into a single router.
|
||||
Each feature domain should register its endpoints here.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints import auth, health, users
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Health check endpoints
|
||||
api_router.include_router(
|
||||
health.router,
|
||||
prefix="/health",
|
||||
tags=["health"],
|
||||
)
|
||||
|
||||
# Authentication endpoints
|
||||
api_router.include_router(
|
||||
auth.router,
|
||||
prefix="/auth",
|
||||
tags=["authentication"],
|
||||
)
|
||||
|
||||
# User endpoints (requires authentication)
|
||||
api_router.include_router(
|
||||
users.router,
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
|
||||
# TODO: Add more endpoint routers as features are implemented
|
||||
# Example:
|
||||
# from app.api.v1.endpoints import portfolios, transactions
|
||||
# api_router.include_router(portfolios.router, prefix="/portfolios", tags=["portfolios"])
|
||||
# api_router.include_router(transactions.router, prefix="/transactions", tags=["transactions"])
|
||||
1
backend/app/api/v1/endpoints/__init__.py
Normal file
1
backend/app/api/v1/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API v1 endpoints package
|
||||
131
backend/app/api/v1/endpoints/auth.py
Normal file
131
backend/app/api/v1/endpoints/auth.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""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")
|
||||
111
backend/app/api/v1/endpoints/health.py
Normal file
111
backend/app/api/v1/endpoints/health.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Health check endpoint for WealthWise API.
|
||||
|
||||
This module provides a comprehensive health check endpoint that verifies:
|
||||
- Application is running
|
||||
- Database connectivity
|
||||
- Overall system status
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import text
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.api.deps import SessionDep
|
||||
from app.core.config import get_settings
|
||||
from app.core.db import check_db_connection
|
||||
|
||||
router = APIRouter()
|
||||
settings = get_settings()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health",
|
||||
summary="Health check endpoint",
|
||||
description="Performs comprehensive health checks including database connectivity.",
|
||||
response_description="Health status with version and database state",
|
||||
tags=["Health"],
|
||||
)
|
||||
async def health_check(session: AsyncSession = SessionDep) -> dict:
|
||||
"""Check application and database health.
|
||||
|
||||
This endpoint performs an actual database query (SELECT 1) to verify
|
||||
that the database connection is working properly. It returns a detailed
|
||||
health status including:
|
||||
- Overall application status
|
||||
- Database connectivity state
|
||||
- Application version
|
||||
|
||||
Returns:
|
||||
dict: Health status object with the following structure:
|
||||
{
|
||||
"status": "healthy" | "degraded",
|
||||
"database": "connected" | "disconnected",
|
||||
"version": "1.0.0",
|
||||
"timestamp": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
|
||||
Raises:
|
||||
HTTPException: 503 if database is unreachable
|
||||
"""
|
||||
health_status = {
|
||||
"status": "healthy",
|
||||
"database": "connected",
|
||||
"version": settings.VERSION,
|
||||
}
|
||||
|
||||
try:
|
||||
# Perform actual database query to verify connectivity
|
||||
result = await session.execute(text("SELECT 1"))
|
||||
db_response = result.scalar()
|
||||
|
||||
if db_response != 1:
|
||||
raise Exception("Unexpected database response")
|
||||
|
||||
except Exception as e:
|
||||
health_status.update({
|
||||
"status": "degraded",
|
||||
"database": "disconnected",
|
||||
"error": str(e),
|
||||
})
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=health_status,
|
||||
)
|
||||
|
||||
return health_status
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health/ready",
|
||||
summary="Readiness probe",
|
||||
description="Kubernetes-style readiness probe endpoint.",
|
||||
tags=["Health"],
|
||||
)
|
||||
async def readiness_probe() -> dict:
|
||||
"""Kubernetes readiness probe.
|
||||
|
||||
Returns 200 if the application is ready to receive traffic.
|
||||
Used by container orchestrators to determine when to route traffic.
|
||||
|
||||
Returns:
|
||||
dict: Simple status object
|
||||
"""
|
||||
return {"ready": True}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/health/live",
|
||||
summary="Liveness probe",
|
||||
description="Kubernetes-style liveness probe endpoint.",
|
||||
tags=["Health"],
|
||||
)
|
||||
async def liveness_probe() -> dict:
|
||||
"""Kubernetes liveness probe.
|
||||
|
||||
Returns 200 if the application is alive and should not be restarted.
|
||||
Used by container orchestrators to detect deadlocks or stuck processes.
|
||||
|
||||
Returns:
|
||||
dict: Simple status object
|
||||
"""
|
||||
return {"alive": True}
|
||||
63
backend/app/api/v1/endpoints/users.py
Normal file
63
backend/app/api/v1/endpoints/users.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""User endpoints for WealthWise API.
|
||||
|
||||
This module provides endpoints for user management and profile operations.
|
||||
All endpoints require authentication via JWT tokens.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.api.deps import get_current_active_user, get_current_user
|
||||
from app.models import User
|
||||
from app.schemas.user import UserPublic
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me",
|
||||
response_model=UserPublic,
|
||||
summary="Get current user information",
|
||||
description="Returns the authenticated user's profile information. "
|
||||
"This endpoint proves that authentication is working correctly.",
|
||||
response_description="User information (id, email, is_active)",
|
||||
tags=["Users"],
|
||||
)
|
||||
async def get_current_user_info(
|
||||
current_user: User = Depends(get_current_user),
|
||||
) -> UserPublic:
|
||||
"""Get current authenticated user's information.
|
||||
|
||||
This endpoint returns the user's basic information from the database.
|
||||
It serves as a simple way to verify that authentication is working.
|
||||
|
||||
Args:
|
||||
current_user: User model injected by get_current_user dependency
|
||||
|
||||
Returns:
|
||||
UserPublic with the user's id, email, and account status
|
||||
"""
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me/active",
|
||||
response_model=UserPublic,
|
||||
summary="Get current active user",
|
||||
description="Returns the authenticated user's profile. "
|
||||
"Demonstrates the get_current_active_user dependency.",
|
||||
tags=["Users"],
|
||||
)
|
||||
async def get_active_user_info(
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
) -> UserPublic:
|
||||
"""Get current active user's information.
|
||||
|
||||
This endpoint demonstrates the get_current_active_user dependency.
|
||||
|
||||
Args:
|
||||
current_user: User model injected by get_current_active_user dependency
|
||||
|
||||
Returns:
|
||||
UserPublic with the user's information
|
||||
"""
|
||||
return current_user
|
||||
Reference in New Issue
Block a user