Files
NexaPantry/backend/app/models/entities.py
nessi 5ed613d441
Some checks failed
CI / backend (push) Failing after 17s
CI / frontend (push) Successful in 31s
CI / docker (push) Has been skipped
fix: improve SMTP configuration and error handling
- Change default use_tls from True to False to match typical STARTTLS setup
- Add MailDeliveryError exception for mail delivery failures
- Wrap send_mail calls in try-catch blocks to handle errors gracefully
- Return 502 status code with error details when mail delivery fails
- Add SMTP security mode selector in frontend (STARTTLS/TLS/None)
- Add test mail form to admin panel
- Handle empty SMTP credentials properly in update_mail_settings
- Catch
2026-06-04 11:00:11 +02:00

188 lines
9.2 KiB
Python

from datetime import UTC, datetime
from enum import StrEnum
from uuid import uuid4
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.session import Base
def now_utc() -> datetime:
return datetime.now(UTC)
class InstanceRole(StrEnum):
ADMIN = "instance_admin"
USER = "user"
class HomeRole(StrEnum):
OWNER = "home_owner"
MEMBER = "home_member"
READ_ONLY = "read_only"
class Theme(StrEnum):
LIGHT = "light"
DARK = "dark"
SYSTEM = "system"
class Language(StrEnum):
DE = "de"
EN = "en"
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
email: Mapped[str] = mapped_column(String(320), unique=True, index=True)
name: Mapped[str] = mapped_column(String(160))
password_hash: Mapped[str | None] = mapped_column(String(255), nullable=True)
instance_role: Mapped[str] = mapped_column(String(40), default=InstanceRole.USER)
language: Mapped[str] = mapped_column(String(8), default=Language.DE)
theme: Mapped[str] = mapped_column(String(16), default=Theme.SYSTEM)
timezone: Mapped[str] = mapped_column(String(80), default="Europe/Vienna")
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
onboarding_completed: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
memberships: Mapped[list["HomeMembership"]] = relationship(back_populates="user", cascade="all, delete")
class Home(Base):
__tablename__ = "homes"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
name: Mapped[str] = mapped_column(String(160))
join_code_hash: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
join_code_expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
expiry_warning_days: Mapped[int] = mapped_column(Integer, default=5)
daily_summary_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
daily_summary_time: Mapped[str] = mapped_column(String(5), default="08:00")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
memberships: Mapped[list["HomeMembership"]] = relationship(back_populates="home", cascade="all, delete")
products: Mapped[list["Product"]] = relationship(back_populates="home", cascade="all, delete")
class HomeMembership(Base):
__tablename__ = "home_memberships"
__table_args__ = (UniqueConstraint("home_id", "user_id", name="uq_home_user"),)
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"))
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
role: Mapped[str] = mapped_column(String(40), default=HomeRole.MEMBER)
notification_preferences: Mapped[dict] = mapped_column(JSONB, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
home: Mapped[Home] = relationship(back_populates="memberships")
user: Mapped[User] = relationship(back_populates="memberships")
class Product(Base):
__tablename__ = "products"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), index=True)
name: Mapped[str] = mapped_column(String(220))
barcode: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
brand: Mapped[str | None] = mapped_column(String(160), nullable=True)
category: Mapped[str] = mapped_column(String(120), default="Other")
location: Mapped[str] = mapped_column(String(120), default="Pantry")
quantity: Mapped[float] = mapped_column(default=1)
unit: Mapped[str] = mapped_column(String(32), default="pcs")
expires_at: Mapped[datetime | None] = mapped_column(Date, nullable=True)
min_quantity: Mapped[float] = mapped_column(default=0)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
image_url: Mapped[str | None] = mapped_column(Text, nullable=True)
created_by_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
home: Mapped[Home] = relationship(back_populates="products")
class ShoppingItem(Base):
__tablename__ = "shopping_items"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
home_id: Mapped[str] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), index=True)
name: Mapped[str] = mapped_column(String(220))
category: Mapped[str] = mapped_column(String(120), default="Other")
quantity: Mapped[float] = mapped_column(default=1)
unit: Mapped[str] = mapped_column(String(32), default="pcs")
checked: Mapped[bool] = mapped_column(Boolean, default=False)
product_id: Mapped[str | None] = mapped_column(ForeignKey("products.id", ondelete="SET NULL"), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class AppSetting(Base):
__tablename__ = "app_settings"
key: Mapped[str] = mapped_column(String(120), primary_key=True)
value: Mapped[dict] = mapped_column(JSONB, default=dict)
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class MailSetting(Base):
__tablename__ = "mail_settings"
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
smtp_host: Mapped[str | None] = mapped_column(String(220), nullable=True)
smtp_port: Mapped[int] = mapped_column(Integer, default=587)
smtp_user: Mapped[str | None] = mapped_column(String(220), nullable=True)
smtp_password_encrypted: Mapped[str | None] = mapped_column(Text, nullable=True)
use_tls: Mapped[bool] = mapped_column(Boolean, default=False)
use_starttls: Mapped[bool] = mapped_column(Boolean, default=True)
sender_address: Mapped[str | None] = mapped_column(String(320), nullable=True)
sender_name: Mapped[str] = mapped_column(String(160), default="NexaPantry")
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class InvitationToken(Base):
__tablename__ = "invitation_tokens"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class PasswordResetToken(Base):
__tablename__ = "password_reset_tokens"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
token_hash: Mapped[str] = mapped_column(String(128), unique=True, index=True)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
consumed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class Notification(Base):
__tablename__ = "notifications"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
user_id: Mapped[str] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), index=True)
home_id: Mapped[str | None] = mapped_column(ForeignKey("homes.id", ondelete="CASCADE"), nullable=True)
title: Mapped[str] = mapped_column(String(220))
body: Mapped[str] = mapped_column(Text)
kind: Mapped[str] = mapped_column(String(80), default="info")
read_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)
class AuditLog(Base):
__tablename__ = "audit_logs"
id: Mapped[str] = mapped_column(UUID(as_uuid=False), primary_key=True, default=lambda: str(uuid4()))
actor_user_id: Mapped[str | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"))
action: Mapped[str] = mapped_column(String(160), index=True)
target_type: Mapped[str | None] = mapped_column(String(80), nullable=True)
target_id: Mapped[str | None] = mapped_column(String(80), nullable=True)
metadata_json: Mapped[dict] = mapped_column(JSONB, default=dict)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=now_utc)