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>
This commit is contained in:
2026-03-09 00:08:28 -07:00
parent 1bed02ebb5
commit 4db9988406
17 changed files with 470 additions and 115 deletions

View File

@@ -9,6 +9,7 @@ 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)
@@ -20,13 +21,13 @@ scheduler = AsyncIOScheduler()
async def scheduled_daily_notification():
db = SessionLocal()
try:
s = db.query(Settings).filter(Settings.id == 1).first()
if not s or not s.ntfy_topic:
logger.info("Daily notification skipped: ntfy not configured")
return
summary = build_daily_summary(db)
ok, detail = await send_ntfy(s, "Sproutly Daily Summary", summary, db)
logger.info(f"Daily notification: {detail}")
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))
@@ -37,8 +38,9 @@ async def scheduled_daily_notification():
def get_notification_schedule(db) -> tuple[int, int]:
"""Use the earliest configured notification time across all users."""
try:
s = db.query(Settings).filter(Settings.id == 1).first()
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)
@@ -65,7 +67,7 @@ async def lifespan(app: FastAPI):
scheduler.shutdown()
app = FastAPI(title="Sproutly API", version="1.0.0", lifespan=lifespan)
app = FastAPI(title="Sproutly API", version="2.0.0", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
@@ -74,6 +76,7 @@ app.add_middleware(
allow_headers=["*"],
)
app.include_router(auth_router.router)
app.include_router(varieties.router)
app.include_router(batches.router)
app.include_router(dashboard.router)