innercontext/backend/innercontext/api/admin.py

206 lines
5.7 KiB
Python

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)