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

@@ -35,10 +35,25 @@ class BatchStatus(str, enum.Enum):
failed = "failed"
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
email = Column(String(255), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
created_at = Column(DateTime, server_default=func.now())
varieties = relationship("Variety", back_populates="user", cascade="all, delete-orphan")
batches = relationship("Batch", back_populates="user", cascade="all, delete-orphan")
settings = relationship("Settings", back_populates="user", uselist=False, cascade="all, delete-orphan")
notification_logs = relationship("NotificationLog", back_populates="user", cascade="all, delete-orphan")
class Variety(Base):
__tablename__ = "varieties"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
name = Column(String(100), nullable=False)
variety_name = Column(String(100))
category = Column(Enum(Category), default=Category.vegetable)
@@ -54,6 +69,7 @@ class Variety(Base):
notes = Column(Text)
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="varieties")
batches = relationship("Batch", back_populates="variety", cascade="all, delete-orphan")
@@ -61,6 +77,7 @@ class Batch(Base):
__tablename__ = "batches"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
variety_id = Column(Integer, ForeignKey("varieties.id"), nullable=False)
label = Column(String(100))
quantity = Column(Integer, default=1)
@@ -72,13 +89,15 @@ class Batch(Base):
notes = Column(Text)
created_at = Column(DateTime, server_default=func.now())
user = relationship("User", back_populates="batches")
variety = relationship("Variety", back_populates="batches")
class Settings(Base):
__tablename__ = "settings"
id = Column(Integer, primary_key=True, default=1)
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), unique=True, nullable=False)
last_frost_date = Column(Date)
first_frost_fall_date = Column(Date)
ntfy_topic = Column(String(200))
@@ -90,12 +109,17 @@ class Settings(Base):
ntfy_password = Column(String(200))
ntfy_api_key = Column(String(200))
user = relationship("User", back_populates="settings")
class NotificationLog(Base):
__tablename__ = "notification_log"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True)
sent_at = Column(DateTime, server_default=func.now())
message = Column(Text)
status = Column(String(20))
error = Column(Text)
user = relationship("User", back_populates="notification_logs")