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,117 @@
import secrets
from datetime import UTC, datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import current_user, require_home_owner
from app.core.security import hash_token
from app.db.session import get_db
from app.models.entities import Home, HomeMembership, HomeRole, User
from app.schemas.common import HomeCreate, HomeOut, HomeSettingsUpdate, JoinCodeOut, Message
from app.services.audit import audit
router = APIRouter()
def home_out(home: Home, role: str | None = None) -> HomeOut:
return HomeOut.model_validate(home).model_copy(update={"role": role})
@router.get("", response_model=list[HomeOut])
def list_homes(user: User = Depends(current_user)) -> list[HomeOut]:
return [home_out(m.home, m.role) for m in user.memberships]
@router.post("", response_model=HomeOut, status_code=201)
def create_home(payload: HomeCreate, user: User = Depends(current_user), db: Session = Depends(get_db)) -> HomeOut:
home = Home(name=payload.name)
db.add(home)
db.flush()
db.add(HomeMembership(home_id=home.id, user_id=user.id, role=HomeRole.OWNER))
audit(db, user, "home.create", "home", home.id)
db.commit()
db.refresh(home)
return home_out(home, HomeRole.OWNER)
def join_home_by_code(join_code: str, user: User, db: Session) -> Home:
home = db.scalar(select(Home).where(Home.join_code_hash == hash_token(join_code.upper())))
if not home or not home.join_code_expires_at or home.join_code_expires_at < datetime.now(UTC):
raise HTTPException(status_code=400, detail="Invalid join code")
exists = db.scalar(select(HomeMembership).where(HomeMembership.home_id == home.id, HomeMembership.user_id == user.id))
if not exists:
db.add(HomeMembership(home_id=home.id, user_id=user.id, role=HomeRole.MEMBER))
return home
@router.post("/join", response_model=HomeOut)
def join(payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> HomeOut:
home = join_home_by_code(str(payload.get("join_code", "")), user, db)
audit(db, user, "home.join", "home", home.id)
db.commit()
return home_out(home, HomeRole.MEMBER)
@router.patch("/{home_id}", response_model=HomeOut)
def update_home(home_id: str, payload: HomeSettingsUpdate, user: User = Depends(current_user), db: Session = Depends(get_db)) -> HomeOut:
membership = require_home_owner(home_id, db, user)
home = db.get(Home, home_id)
if not home:
raise HTTPException(status_code=404, detail="Home not found")
for key, value in payload.model_dump(exclude_unset=True).items():
setattr(home, key, value)
audit(db, user, "home.update", "home", home.id)
db.commit()
return home_out(home, membership.role)
@router.post("/{home_id}/join-code", response_model=JoinCodeOut)
def create_join_code(home_id: str, payload: dict | None = None, user: User = Depends(current_user), db: Session = Depends(get_db)) -> JoinCodeOut:
require_home_owner(home_id, db, user)
home = db.get(Home, home_id)
if not home:
raise HTTPException(status_code=404, detail="Home not found")
days = int((payload or {}).get("days", 7))
code = f"NX-{secrets.randbelow(900000) + 100000}"
home.join_code_hash = hash_token(code)
home.join_code_expires_at = datetime.now(UTC) + timedelta(days=max(1, min(days, 60)))
audit(db, user, "home.join_code.create", "home", home.id)
db.commit()
return JoinCodeOut(join_code=code, expires_at=home.join_code_expires_at)
@router.delete("/{home_id}/join-code", response_model=Message)
def disable_join_code(home_id: str, user: User = Depends(current_user), db: Session = Depends(get_db)) -> Message:
require_home_owner(home_id, db, user)
home = db.get(Home, home_id)
if not home:
raise HTTPException(status_code=404, detail="Home not found")
home.join_code_hash = None
home.join_code_expires_at = None
audit(db, user, "home.join_code.disable", "home", home.id)
db.commit()
return Message(message="Join code disabled")
@router.get("/{home_id}/members")
def members(home_id: str, user: User = Depends(current_user), db: Session = Depends(get_db)) -> list[dict]:
require_home_owner(home_id, db, user)
rows = db.scalars(select(HomeMembership).where(HomeMembership.home_id == home_id)).all()
return [{"id": m.id, "user_id": m.user_id, "name": m.user.name, "email": m.user.email, "role": m.role} for m in rows]
@router.patch("/{home_id}/members/{membership_id}")
def update_member(home_id: str, membership_id: str, payload: dict, user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
require_home_owner(home_id, db, user)
membership = db.get(HomeMembership, membership_id)
if not membership or membership.home_id != home_id:
raise HTTPException(status_code=404, detail="Member not found")
if payload.get("role") in [HomeRole.OWNER, HomeRole.MEMBER, HomeRole.READ_ONLY]:
membership.role = payload["role"]
if "notification_preferences" in payload:
membership.notification_preferences = payload["notification_preferences"]
audit(db, user, "home.member.update", "membership", membership.id)
db.commit()
return {"message": "Member updated"}