Files
yolkbook/backend/routers/eggs.py
derekc aa12648228 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>
2026-03-17 23:19:29 -07:00

90 lines
2.7 KiB
Python

from datetime import date
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from database import get_db
from models import EggCollection, User
from schemas import EggCollectionCreate, EggCollectionUpdate, EggCollectionOut
from auth import get_current_user
router = APIRouter(prefix="/api/eggs", tags=["eggs"])
@router.get("", response_model=list[EggCollectionOut])
def list_eggs(
start: Optional[date] = None,
end: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
q = (
select(EggCollection)
.where(EggCollection.user_id == current_user.id)
.order_by(EggCollection.date.desc())
)
if start:
q = q.where(EggCollection.date >= start)
if end:
q = q.where(EggCollection.date <= end)
return db.scalars(q).all()
@router.post("", response_model=EggCollectionOut, status_code=201)
def create_egg_collection(
body: EggCollectionCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
record = EggCollection(**body.model_dump(), user_id=current_user.id)
db.add(record)
try:
db.commit()
except IntegrityError:
db.rollback()
raise HTTPException(status_code=409, detail=f"An entry for {body.date} already exists. Edit it from the History page.")
db.refresh(record)
return record
@router.put("/{record_id}", response_model=EggCollectionOut)
def update_egg_collection(
record_id: int,
body: EggCollectionUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
record = db.scalars(
select(EggCollection)
.where(EggCollection.id == record_id, EggCollection.user_id == current_user.id)
).first()
if not record:
raise HTTPException(status_code=404, detail="Record not found")
for field, value in body.model_dump(exclude_none=True).items():
setattr(record, field, value)
try:
db.commit()
except IntegrityError:
db.rollback()
raise HTTPException(status_code=409, detail="An entry for that date already exists.")
db.refresh(record)
return record
@router.delete("/{record_id}", status_code=204)
def delete_egg_collection(
record_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
record = db.scalars(
select(EggCollection)
.where(EggCollection.id == record_id, EggCollection.user_id == current_user.id)
).first()
if not record:
raise HTTPException(status_code=404, detail="Record not found")
db.delete(record)
db.commit()