Add multi-user auth, admin panel, and timezone support; rename to Yolkbook
- Rename app from Eggtracker to Yolkbook throughout - Add JWT-based authentication (python-jose, passlib/bcrypt) - Add users table; all data tables gain user_id FK for full data isolation - Super admin credentials sourced from ADMIN_USERNAME/ADMIN_PASSWORD env vars, synced on every startup; orphaned rows auto-assigned to admin post-migration - Login page with self-registration; JWT stored in localStorage (30-day expiry) - Admin panel (/admin): list users, reset passwords, disable/enable, delete, and impersonate (Login As) with Return to Admin banner - Settings modal (gear icon in nav): timezone selector and change password - Timezone stored per-user; stats date windows computed in user's timezone; date input setToday() respects user timezone via Intl API - migrate_v2.sql for existing single-user installs - Auto-migration adds timezone column to users on startup - Updated README with full setup, auth, admin, and migration docs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,81 @@
|
||||
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
|
||||
|
||||
app = FastAPI(title="Eggtracker API")
|
||||
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)
|
||||
|
||||
# Allow requests from the Nginx frontend (same host, different port internally)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
@@ -13,6 +83,8 @@ app.add_middleware(
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user