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:
@@ -3,8 +3,10 @@ from datetime import date, timedelta
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
import httpx
|
||||
|
||||
from auth import get_current_user
|
||||
from database import get_db
|
||||
from models import Settings, Variety, Batch, NotificationLog, BatchStatus
|
||||
from models import Batch, BatchStatus, NotificationLog, Settings, User, Variety
|
||||
|
||||
router = APIRouter(prefix="/notifications", tags=["notifications"])
|
||||
|
||||
@@ -14,8 +16,8 @@ ACTIVE_STATUSES = [
|
||||
]
|
||||
|
||||
|
||||
def build_daily_summary(db: Session) -> str:
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
def build_daily_summary(db: Session, user_id: int) -> str:
|
||||
settings = db.query(Settings).filter(Settings.user_id == user_id).first()
|
||||
today = date.today()
|
||||
lines = [f"Sproutly Daily Summary — {today.strftime('%A, %B %d')}"]
|
||||
lines.append("")
|
||||
@@ -32,7 +34,7 @@ def build_daily_summary(db: Session) -> str:
|
||||
lines.append(f"Last frost in {days_to_frost} days ({last_frost.strftime('%B %d')})!")
|
||||
lines.append("")
|
||||
|
||||
varieties = db.query(Variety).all()
|
||||
varieties = db.query(Variety).filter(Variety.user_id == user_id).all()
|
||||
for v in varieties:
|
||||
full_name = f"{v.name} ({v.variety_name})" if v.variety_name else v.name
|
||||
|
||||
@@ -59,7 +61,7 @@ def build_daily_summary(db: Session) -> str:
|
||||
|
||||
batches = (
|
||||
db.query(Batch)
|
||||
.filter(Batch.status.in_(ACTIVE_STATUSES))
|
||||
.filter(Batch.user_id == user_id, Batch.status.in_(ACTIVE_STATUSES))
|
||||
.all()
|
||||
)
|
||||
for b in batches:
|
||||
@@ -112,44 +114,34 @@ async def send_ntfy(settings: Settings, title: str, message: str, db: Session, p
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(
|
||||
url,
|
||||
content=message.encode("utf-8"),
|
||||
headers=headers,
|
||||
)
|
||||
resp = await client.post(url, content=message.encode("utf-8"), headers=headers)
|
||||
resp.raise_for_status()
|
||||
|
||||
log = NotificationLog(message=message, status="sent")
|
||||
log = NotificationLog(message=message, status="sent", user_id=settings.user_id)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
return True, "sent"
|
||||
|
||||
except Exception as e:
|
||||
log = NotificationLog(message=message, status="failed", error=str(e))
|
||||
log = NotificationLog(message=message, status="failed", error=str(e), user_id=settings.user_id)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
return False, str(e)
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
async def send_test_notification(db: Session = Depends(get_db)):
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
ok, detail = await send_ntfy(
|
||||
settings,
|
||||
"Sproutly Test",
|
||||
"Your Sproutly notifications are working!",
|
||||
db,
|
||||
priority="default",
|
||||
)
|
||||
async def send_test_notification(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
settings = db.query(Settings).filter(Settings.user_id == current_user.id).first()
|
||||
ok, detail = await send_ntfy(settings, "Sproutly Test", "Your Sproutly notifications are working!", db)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
return {"status": "sent"}
|
||||
|
||||
|
||||
@router.post("/daily")
|
||||
async def send_daily_summary(db: Session = Depends(get_db)):
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
summary = build_daily_summary(db)
|
||||
async def send_daily_summary(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
settings = db.query(Settings).filter(Settings.user_id == current_user.id).first()
|
||||
summary = build_daily_summary(db, current_user.id)
|
||||
ok, detail = await send_ntfy(settings, "Sproutly Daily Summary", summary, db)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=400, detail=detail)
|
||||
@@ -157,6 +149,12 @@ async def send_daily_summary(db: Session = Depends(get_db)):
|
||||
|
||||
|
||||
@router.get("/log")
|
||||
def get_notification_log(db: Session = Depends(get_db)):
|
||||
logs = db.query(NotificationLog).order_by(NotificationLog.sent_at.desc()).limit(50).all()
|
||||
def get_notification_log(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
|
||||
logs = (
|
||||
db.query(NotificationLog)
|
||||
.filter(NotificationLog.user_id == current_user.id)
|
||||
.order_by(NotificationLog.sent_at.desc())
|
||||
.limit(50)
|
||||
.all()
|
||||
)
|
||||
return [{"id": l.id, "sent_at": l.sent_at, "status": l.status, "message": l.message, "error": l.error} for l in logs]
|
||||
|
||||
Reference in New Issue
Block a user