Files
sproutly/backend/main.py
derekc 4db9988406 Add multi-user authentication with JWT
- Users table with email/bcrypt-hashed password; register and login via /auth/ endpoints
- JWT tokens (30-day expiry) stored in localStorage; all API routes require Bearer auth
- All data (varieties, batches, settings, notification logs) scoped to the authenticated user
- Login/register screen overlays the app; sidebar shows user email and logout button
- Scheduler sends daily ntfy summaries for every configured user
- DB schema rewritten for multi-user; SECRET_KEY added to env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 00:08:28 -07:00

90 lines
2.6 KiB
Python

import asyncio
import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from database import SessionLocal
from models import Settings, NotificationLog
from routers import varieties, batches, dashboard, settings, notifications
from routers import auth as auth_router
from routers.notifications import build_daily_summary, send_ntfy
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("sproutly")
scheduler = AsyncIOScheduler()
async def scheduled_daily_notification():
db = SessionLocal()
try:
all_settings = db.query(Settings).filter(Settings.ntfy_topic.isnot(None)).all()
for s in all_settings:
if not s.ntfy_topic:
continue
summary = build_daily_summary(db, s.user_id)
ok, detail = await send_ntfy(s, "Sproutly Daily Summary", summary, db)
logger.info(f"Daily notification for user {s.user_id}: {detail}")
except Exception as e:
logger.error(f"Daily notification error: {e}")
log = NotificationLog(message="scheduler error", status="failed", error=str(e))
db.add(log)
db.commit()
finally:
db.close()
def get_notification_schedule(db) -> tuple[int, int]:
"""Use the earliest configured notification time across all users."""
try:
s = db.query(Settings).filter(Settings.ntfy_topic.isnot(None)).first()
if s and s.notification_time:
h, m = s.notification_time.split(":")
return int(h), int(m)
except Exception:
pass
return 7, 0
@asynccontextmanager
async def lifespan(app: FastAPI):
db = SessionLocal()
hour, minute = get_notification_schedule(db)
db.close()
scheduler.add_job(
scheduled_daily_notification,
CronTrigger(hour=hour, minute=minute),
id="daily_summary",
replace_existing=True,
)
scheduler.start()
logger.info(f"Scheduler started — daily notification at {hour:02d}:{minute:02d}")
yield
scheduler.shutdown()
app = FastAPI(title="Sproutly API", version="2.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth_router.router)
app.include_router(varieties.router)
app.include_router(batches.router)
app.include_router(dashboard.router)
app.include_router(settings.router)
app.include_router(notifications.router)
@app.get("/health")
def health():
return {"status": "ok", "service": "sproutly"}