Initial commit: MyMidas personal finance tracker

Full-stack self-hosted finance app with FastAPI backend and React frontend.

Features:
- Accounts, transactions, budgets, investments with GBP base currency
- CSV import with auto-detection for 10 UK bank formats
- ML predictions: spending forecast, net worth projection, Monte Carlo
- 7 selectable themes (Obsidian, Arctic, Midnight, Vault, Terminal, Synthwave, Ledger)
- Receipt/document attachments on transactions (JPEG, PNG, WebP, PDF)
- AES-256-GCM field encryption, RS256 JWT, TOTP 2FA, RLS, audit log
- Encrypted nightly backups + key rotation script
- Mobile-responsive layout with bottom nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-21 11:56:10 +00:00
commit 61a7884ee5
127 changed files with 13323 additions and 0 deletions

View file

@ -0,0 +1,92 @@
"""
FastAPI dependency injection: DB session, Redis, current authenticated user.
"""
from __future__ import annotations
import uuid
from typing import AsyncGenerator
from fastapi import Cookie, Depends, HTTPException, Request, status
from jose import JWTError
from redis.asyncio import Redis
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import decode_token, hash_token
from app.db.models.session import Session
from app.db.models.user import User
# These are set by main.py lifespan
_session_factory = None
_redis_client: Redis | None = None
def set_session_factory(factory):
global _session_factory
_session_factory = factory
def get_session_factory():
return _session_factory
def set_redis_client(client: Redis):
global _redis_client
_redis_client = client
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with _session_factory() as session:
yield session
async def get_redis() -> Redis:
return _redis_client
def _extract_bearer(request: Request) -> str | None:
auth = request.headers.get("Authorization", "")
if auth.startswith("Bearer "):
return auth[7:]
return None
async def get_current_user(
request: Request,
db: AsyncSession = Depends(get_db),
) -> User:
token = _extract_bearer(request)
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
try:
payload = decode_token(token, token_type="access")
user_id = uuid.UUID(payload["sub"])
except (JWTError, ValueError, KeyError):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
token_hash = hash_token(token)
from datetime import datetime, timezone
result = await db.execute(
select(Session).where(
Session.user_id == user_id,
Session.token_hash == token_hash,
Session.revoked_at.is_(None),
Session.expires_at > datetime.now(timezone.utc),
)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired or revoked")
result = await db.execute(
select(User).where(User.id == user_id, User.deleted_at.is_(None))
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
# Set RLS context so PostgreSQL RLS policies apply
await db.execute(text(f"SET LOCAL app.current_user_id = '{user_id}'"))
return user