MyMidas/backend/app/main.py
megaproxy 9897d03d91 Add public demo mode with auto-seeding, hourly reset, and Portainer deploy guide
- DEMO_MODE=true env flag: disables password changes and backup endpoints (403),
  exposes GET /demo/status for frontend detection
- Auto-seed on first startup: creates demo user (demo@mymidas.app / demo123)
  with 6 months of transactions, investments, budgets, subscriptions, and tax
  payslips; takes a pg_dump snapshot immediately after for hourly restore
- Hourly reset: resetter Alpine container with cron restores DB from snapshot
  and purges uploaded attachments every hour on the hour
- Frontend: amber demo banner on all pages, login page shows credentials,
  password change disabled with notice, backups section replaced with notice
- demo/ directory: self-contained docker-compose.yml (ports 4001/8091),
  .env.example, reset.sh, and step-by-step Portainer DEPLOY.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 22:08:24 +00:00

104 lines
2.9 KiB
Python

"""
FastAPI application factory with lifespan management.
"""
from contextlib import asynccontextmanager
import structlog
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from redis.asyncio import Redis, from_url
from app.config import get_settings
from app.core.middleware import CSRFMiddleware, SecurityHeadersMiddleware
from app.db.base import create_engine, create_session_factory
from app.dependencies import set_redis_client, set_session_factory
logger = structlog.get_logger()
@asynccontextmanager
async def lifespan(app: FastAPI):
settings = get_settings()
# Database
engine = create_engine()
session_factory = create_session_factory(engine)
set_session_factory(session_factory)
# Redis
redis: Redis = from_url(settings.redis_url, decode_responses=False)
set_redis_client(redis)
# Seed system categories if needed
from app.services.category_service import seed_system_categories
async with session_factory() as db:
await seed_system_categories(db)
await db.commit()
# Demo mode: seed demo data on first startup, then snapshot
if settings.is_demo:
from app.demo.seed import is_seeded, seed_demo
from app.demo.snapshot import create_snapshot
async with session_factory() as db:
if not await is_seeded(db):
await seed_demo(db)
await db.commit()
logger.info("demo_seed_complete")
await create_snapshot()
logger.info("demo_snapshot_created")
# Background scheduler
from app.workers.scheduler import start_scheduler, stop_scheduler
await start_scheduler()
logger.info("startup_complete", env=settings.environment)
yield
await stop_scheduler()
await redis.aclose()
await engine.dispose()
logger.info("shutdown_complete")
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(
title="Finance Tracker",
version="0.1.0",
docs_url="/docs" if settings.is_development else None,
redoc_url="/redoc" if settings.is_development else None,
openapi_url="/openapi.json" if settings.is_development else None,
lifespan=lifespan,
)
# CORS — only allow same origin in production
origins = ["http://localhost:5173"] if settings.is_development else []
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(CSRFMiddleware)
# Health check (no auth required)
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/demo/status")
async def demo_status():
return {"demo_mode": settings.is_demo}
# API routers
from app.api.router import router
app.include_router(router, prefix="/api/v1")
return app
app = create_app()