Initial commit: WealthWise financial analytics platform

This commit is contained in:
2026-02-14 21:16:57 +05:30
commit b8588df583
171 changed files with 29048 additions and 0 deletions

View File

@@ -0,0 +1 @@
# API package

153
backend/app/api/deps.py Normal file
View File

@@ -0,0 +1,153 @@
"""WealthWise API dependencies.
This module provides reusable dependencies for FastAPI endpoints including:
- Database session injection
- Authentication dependencies with JWT validation
- Common dependency utilities
"""
from typing import Optional
from uuid import UUID
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from sqlalchemy import select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.config import get_settings
from app.core.db import get_session
from app.models import User
from app.schemas.user import TokenPayload
settings = get_settings()
# OAuth2 scheme for JWT token authentication
# Uses the token endpoint for OAuth2 Password Flow
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/auth/token",
auto_error=False, # Allow optional auth for public endpoints
)
# Database session dependency
# Re-export get_session for cleaner imports in endpoint modules
SessionDep = Depends(get_session)
async def get_current_user(
token: Optional[str] = Depends(oauth2_scheme),
session: AsyncSession = SessionDep,
) -> User:
"""Get current authenticated user from JWT token.
This dependency extracts the Bearer token from the Authorization header,
validates it locally using the application's secret key, queries the
database for the user, and returns the User model.
Args:
token: JWT access token from Authorization header (Bearer token)
session: Database session for user lookup
Returns:
User: Authenticated user model from database
Raises:
HTTPException: 401 if token is missing, invalid, or user not found/inactive
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
if not token:
raise credentials_exception
try:
# Decode and validate JWT
payload = jwt.decode(
token,
settings.SECRET_KEY,
algorithms=[settings.ALGORITHM],
)
# Extract user ID from subject claim
user_id: Optional[str] = payload.get("sub")
if user_id is None:
raise credentials_exception
# Validate token type
token_type = payload.get("type")
if token_type != "access":
raise credentials_exception
except JWTError:
raise credentials_exception
# Query database for user
statement = select(User).where(User.id == UUID(user_id))
result = await session.exec(statement)
user = result.first()
# Validate user exists and is active
if user is None or not user.is_active:
raise credentials_exception
return user
async def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""Verify user is active (non-superuser check optional).
This dependency extends get_current_user. Currently just returns the user
since get_current_user already checks is_active. Can be extended for
additional checks like email verification.
Args:
current_user: User from get_current_user dependency
Returns:
Active User model
Raises:
HTTPException: 401 if user is inactive
"""
return current_user
async def get_current_superuser(
current_user: User = Depends(get_current_user),
) -> User:
"""Verify user is a superuser/admin.
This dependency requires the user to have superuser privileges.
Args:
current_user: User from get_current_user dependency
Returns:
Superuser User model
Raises:
HTTPException: 403 if user is not a superuser
"""
if not current_user.is_superuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return current_user
# Convenience exports for endpoint modules
__all__ = [
"get_session",
"SessionDep",
"oauth2_scheme",
"get_current_user",
"get_current_active_user",
"get_current_superuser",
]

View File

@@ -0,0 +1 @@
# API v1 endpoints

38
backend/app/api/v1/api.py Normal file
View 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"])

View File

@@ -0,0 +1 @@
# API v1 endpoints package

View 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")

View 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}

View 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