Initial commit: Wallet App - FastAPI backend with React frontend for personal finance tracking

This commit is contained in:
RemTools
2026-09-04 08:37:14 +00:00 Unverified
commit d7b52f24d2
39572 changed files with 1617122 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
"""Route barrel — every router the app serves is mounted here.
Adding a resource is two steps: create ``app/routes/<name>.py`` with a
module-level ``router``, then append one ``include_router`` line below. A route
file that is never mounted 404s silently.
"""
from fastapi import APIRouter
from app.routes import health, items, auth, wallet
api_router = APIRouter()
api_router.include_router(health.router, tags=["health"])
api_router.include_router(items.router, tags=["items"])
api_router.include_router(auth.router, tags=["auth"])
api_router.include_router(wallet.router, tags=["wallet"])
+66
View File
@@ -0,0 +1,66 @@
"""Authentication routes."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User
from app.schemas import UserRegister, UserLogin, TokenResponse, UserResponse
from app.auth import hash_password, verify_password, create_access_token
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=TokenResponse)
def register(user_data: UserRegister, db: Session = Depends(get_db)):
"""Register a new user."""
# Check if user already exists
existing_user = db.query(User).filter(
(User.username == user_data.username) | (User.email == user_data.email)
).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username or email already registered"
)
# Create new user
hashed_password = hash_password(user_data.password)
new_user = User(
username=user_data.username,
email=user_data.email,
hashed_password=hashed_password
)
db.add(new_user)
db.commit()
db.refresh(new_user)
# Create access token
access_token = create_access_token(data={"sub": new_user.id})
return {
"access_token": access_token,
"token_type": "bearer",
"user": UserResponse.from_orm(new_user)
}
@router.post("/login", response_model=TokenResponse)
def login(user_data: UserLogin, db: Session = Depends(get_db)):
"""Login a user."""
user = db.query(User).filter(User.username == user_data.username).first()
if not user or not verify_password(user_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid username or password"
)
# Create access token
access_token = create_access_token(data={"sub": user.id})
return {
"access_token": access_token,
"token_type": "bearer",
"user": UserResponse.from_orm(user)
}
+15
View File
@@ -0,0 +1,15 @@
"""Liveness endpoint. The shape every other route module copies."""
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class Health(BaseModel):
status: str
@router.get("/health", response_model=Health)
def health() -> Health:
return Health(status="ok")
+21
View File
@@ -0,0 +1,21 @@
"""One demo resource. Its rows mirror the frontend seed, so Preview and the
deployed app render the same screen. Replace both together."""
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class Item(BaseModel):
id: int
name: str
status: str
@router.get("/items", response_model=list[Item])
def list_items() -> list[Item]:
return [
Item(id=1, name="Draft the launch checklist", status="in progress"),
Item(id=2, name="Review onboarding copy", status="todo"),
]
+140
View File
@@ -0,0 +1,140 @@
"""Wallet entry routes."""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import User, WalletEntry, EntryType
from app.schemas import (
WalletEntryCreate,
WalletEntryUpdate,
WalletEntryResponse,
DashboardMetrics
)
from app.auth import get_current_user
router = APIRouter(prefix="/wallet", tags=["wallet"])
@router.post("/entries", response_model=WalletEntryResponse)
def create_entry(
entry_data: WalletEntryCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create a new wallet entry."""
new_entry = WalletEntry(
user_id=current_user.id,
amount=entry_data.amount,
entry_type=entry_data.entry_type,
description=entry_data.description
)
db.add(new_entry)
db.commit()
db.refresh(new_entry)
return new_entry
@router.get("/entries", response_model=list[WalletEntryResponse])
def get_entries(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100
):
"""Get all wallet entries for the current user."""
entries = db.query(WalletEntry).filter(
WalletEntry.user_id == current_user.id
).order_by(WalletEntry.created_at.desc()).offset(skip).limit(limit).all()
return entries
@router.get("/entries/{entry_id}", response_model=WalletEntryResponse)
def get_entry(
entry_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get a specific wallet entry."""
entry = db.query(WalletEntry).filter(
(WalletEntry.id == entry_id) & (WalletEntry.user_id == current_user.id)
).first()
if not entry:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Entry not found"
)
return entry
@router.put("/entries/{entry_id}", response_model=WalletEntryResponse)
def update_entry(
entry_id: int,
entry_data: WalletEntryUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update a wallet entry."""
entry = db.query(WalletEntry).filter(
(WalletEntry.id == entry_id) & (WalletEntry.user_id == current_user.id)
).first()
if not entry:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Entry not found"
)
if entry_data.amount is not None:
entry.amount = entry_data.amount
if entry_data.entry_type is not None:
entry.entry_type = entry_data.entry_type
if entry_data.description is not None:
entry.description = entry_data.description
db.commit()
db.refresh(entry)
return entry
@router.delete("/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_entry(
entry_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Delete a wallet entry."""
entry = db.query(WalletEntry).filter(
(WalletEntry.id == entry_id) & (WalletEntry.user_id == current_user.id)
).first()
if not entry:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Entry not found"
)
db.delete(entry)
db.commit()
@router.get("/metrics", response_model=DashboardMetrics)
def get_metrics(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get dashboard metrics for the current user."""
entries = db.query(WalletEntry).filter(
WalletEntry.user_id == current_user.id
).all()
total_income = sum(e.amount for e in entries if e.entry_type == EntryType.INCOME)
total_expenses = sum(e.amount for e in entries if e.entry_type == EntryType.EXPENSE)
total_balance = total_income - total_expenses
return {
"total_balance": total_balance,
"total_income": total_income,
"total_expenses": total_expenses,
"entry_count": len(entries)
}