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
This commit is contained in:
117
backend/app/api/routes/homes.py
Normal file
117
backend/app/api/routes/homes.py
Normal 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"}
|
||||
Reference in New Issue
Block a user