feat(api): add admin household management endpoints

This commit is contained in:
Piotr Oleszczyk 2026-03-12 16:02:11 +01:00
parent 4bfa4ea02d
commit 1d5630ed8c
3 changed files with 566 additions and 4 deletions

View file

@ -0,0 +1,206 @@
from datetime import datetime
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlmodel import Session, SQLModel, select
from db import get_session
from innercontext.api.auth_deps import require_admin
from innercontext.api.utils import get_or_404
from innercontext.models import (
Household,
HouseholdMembership,
HouseholdRole,
Role,
User,
)
router = APIRouter(dependencies=[Depends(require_admin)])
SessionDep = Annotated[Session, Depends(get_session)]
class AdminHouseholdPublic(SQLModel):
id: UUID
created_at: datetime
updated_at: datetime
class AdminHouseholdMembershipPublic(SQLModel):
id: UUID
user_id: UUID
household_id: UUID
role: HouseholdRole
created_at: datetime
updated_at: datetime
class AdminUserPublic(SQLModel):
id: UUID
oidc_issuer: str
oidc_subject: str
role: Role
created_at: datetime
updated_at: datetime
household_membership: AdminHouseholdMembershipPublic | None = None
class AdminHouseholdMembershipCreate(SQLModel):
user_id: UUID
role: HouseholdRole = HouseholdRole.MEMBER
def _membership_public(
membership: HouseholdMembership,
) -> AdminHouseholdMembershipPublic:
return AdminHouseholdMembershipPublic(
id=membership.id,
user_id=membership.user_id,
household_id=membership.household_id,
role=membership.role,
created_at=membership.created_at,
updated_at=membership.updated_at,
)
def _household_public(household: Household) -> AdminHouseholdPublic:
return AdminHouseholdPublic(
id=household.id,
created_at=household.created_at,
updated_at=household.updated_at,
)
def _user_public(
user: User,
membership: HouseholdMembership | None,
) -> AdminUserPublic:
return AdminUserPublic(
id=user.id,
oidc_issuer=user.oidc_issuer,
oidc_subject=user.oidc_subject,
role=user.role,
created_at=user.created_at,
updated_at=user.updated_at,
household_membership=(
_membership_public(membership) if membership is not None else None
),
)
def _get_membership_for_user(
session: Session,
user_id: UUID,
) -> HouseholdMembership | None:
return session.exec(
select(HouseholdMembership).where(HouseholdMembership.user_id == user_id)
).first()
@router.get("/users", response_model=list[AdminUserPublic])
def list_users(session: SessionDep):
users = sorted(
session.exec(select(User)).all(),
key=lambda user: (user.created_at, str(user.id)),
)
memberships = session.exec(select(HouseholdMembership)).all()
memberships_by_user_id = {
membership.user_id: membership for membership in memberships
}
return [_user_public(user, memberships_by_user_id.get(user.id)) for user in users]
@router.post(
"/households",
response_model=AdminHouseholdPublic,
status_code=status.HTTP_201_CREATED,
)
def create_household(session: SessionDep):
household = Household()
session.add(household)
session.commit()
session.refresh(household)
return _household_public(household)
@router.post(
"/households/{household_id}/members",
response_model=AdminHouseholdMembershipPublic,
status_code=status.HTTP_201_CREATED,
)
def assign_household_member(
household_id: UUID,
payload: AdminHouseholdMembershipCreate,
session: SessionDep,
):
_ = get_or_404(session, Household, household_id)
_ = get_or_404(session, User, payload.user_id)
existing_membership = _get_membership_for_user(session, payload.user_id)
if existing_membership is not None:
detail = "User already belongs to a household"
if existing_membership.household_id == household_id:
detail = "User already belongs to this household"
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)
membership = HouseholdMembership(
user_id=payload.user_id,
household_id=household_id,
role=payload.role,
)
session.add(membership)
session.commit()
session.refresh(membership)
return _membership_public(membership)
@router.patch(
"/households/{household_id}/members/{user_id}",
response_model=AdminHouseholdMembershipPublic,
)
def move_household_member(
household_id: UUID,
user_id: UUID,
session: SessionDep,
):
_ = get_or_404(session, Household, household_id)
_ = get_or_404(session, User, user_id)
membership = _get_membership_for_user(session, user_id)
if membership is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="HouseholdMembership not found",
)
if membership.household_id == household_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already belongs to this household",
)
membership.household_id = household_id
session.add(membership)
session.commit()
session.refresh(membership)
return _membership_public(membership)
@router.delete(
"/households/{household_id}/members/{user_id}",
status_code=status.HTTP_204_NO_CONTENT,
)
def remove_household_member(
household_id: UUID,
user_id: UUID,
session: SessionDep,
):
_ = get_or_404(session, Household, household_id)
_ = get_or_404(session, User, user_id)
membership = _get_membership_for_user(session, user_id)
if membership is None or membership.household_id != household_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="HouseholdMembership not found",
)
session.delete(membership)
session.commit()
return Response(status_code=status.HTTP_204_NO_CONTENT)