chore: initial project setup with backend, frontend, and infrastructure
Some checks failed
CI / backend (push) Failing after 31s
CI / frontend (push) Successful in 40s
CI / docker (push) Has been skipped

Add complete NexaPantry application structure including:
- Docker Compose configuration with PostgreSQL, Redis, FastAPI backend, worker, frontend and Caddy
- Environment configuration template with database, auth, and service settings
- GitHub Actions CI workflow for backend/frontend linting, testing, auditing and Docker builds
- AGPL-3.0 license and comprehensive README with setup, development, and security documentation
- Backend
This commit is contained in:
2026-06-04 10:26:38 +02:00
commit 3792ca55e7
74 changed files with 13417 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
from app.models.entities import (
AppSetting,
AuditLog,
Home,
HomeMembership,
InvitationToken,
MailSetting,
Notification,
PasswordResetToken,
Product,
ShoppingItem,
User,
)
__all__ = [
"AppSetting",
"AuditLog",
"Home",
"HomeMembership",
"InvitationToken",
"MailSetting",
"Notification",
"PasswordResetToken",
"Product",
"ShoppingItem",
"User",
]

View File

@@ -0,0 +1,187 @@
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=True)
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)