import os import logging from contextlib import asynccontextmanager from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import select, update, text from database import Base, engine, SessionLocal from models import User, EggCollection, FlockHistory, FeedPurchase, OtherPurchase from auth import hash_password from routers import eggs, flock, feed, stats, other from routers import auth_router, admin logger = logging.getLogger("yolkbook") def _seed_admin(): """Create or update the admin user from environment variables. Also assigns any records with NULL user_id to the admin (post-migration). """ admin_username = os.environ["ADMIN_USERNAME"] admin_password = os.environ["ADMIN_PASSWORD"] with SessionLocal() as db: admin_user = db.scalars( select(User).where(User.username == admin_username) ).first() if admin_user is None: admin_user = User( username=admin_username, hashed_password=hash_password(admin_password), is_admin=True, ) db.add(admin_user) db.commit() db.refresh(admin_user) logger.info("Admin user '%s' created.", admin_username) else: # Always sync password + admin flag from env vars admin_user.hashed_password = hash_password(admin_password) admin_user.is_admin = True db.commit() # Assign orphaned records (from pre-migration data) to admin for model in [EggCollection, FlockHistory, FeedPurchase, OtherPurchase]: db.execute( update(model) .where(model.user_id == None) # noqa: E711 .values(user_id=admin_user.id) ) db.commit() def _run_migrations(): """Apply incremental schema changes that create_all won't handle on existing tables.""" with SessionLocal() as db: # v2.1 — timezone column on users try: db.execute(text( "ALTER TABLE users ADD COLUMN timezone VARCHAR(64) NOT NULL DEFAULT 'UTC'" )) db.commit() except Exception: db.rollback() # column already exists — safe to ignore @asynccontextmanager async def lifespan(app: FastAPI): Base.metadata.create_all(bind=engine) _run_migrations() _seed_admin() yield app = FastAPI(title="Yolkbook API", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.include_router(auth_router.router) app.include_router(admin.router) app.include_router(eggs.router) app.include_router(flock.router) app.include_router(feed.router) app.include_router(other.router) app.include_router(stats.router) @app.get("/api/health") def health(): return {"status": "ok"}