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