Files
nessi 3792ca55e7
Some checks failed
CI / backend (push) Failing after 31s
CI / frontend (push) Successful in 40s
CI / docker (push) Has been skipped
chore: initial project setup with backend, frontend, and infrastructure
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
2026-06-04 10:26:38 +02:00

118 lines
5.2 KiB
Python

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"}