"""Groups router — predefined sets of Duniter identities used in decision circles.""" from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import Response from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.database import get_db from app.models.group import Group, GroupMember from app.schemas.group import GroupCreate, GroupMemberCreate, GroupMemberOut, GroupOut, GroupSummary from app.services.auth_service import get_current_identity router = APIRouter() def _org_id_from_header(request_headers) -> uuid.UUID | None: raw = request_headers.get("x-organization") if not raw: return None try: return uuid.UUID(raw) except ValueError: return None @router.get("/", response_model=list[GroupSummary]) async def list_groups( db: AsyncSession = Depends(get_db), ) -> list[GroupSummary]: """List all groups. No auth required — groups are public within the workspace.""" result = await db.execute( select(Group).options(selectinload(Group.members)).order_by(Group.name) ) groups = result.scalars().all() return [ GroupSummary( id=g.id, name=g.name, description=g.description, organization_id=g.organization_id, member_count=len(g.members), ) for g in groups ] @router.post("/", response_model=GroupOut, status_code=201) async def create_group( payload: GroupCreate, db: AsyncSession = Depends(get_db), _identity=Depends(get_current_identity), ) -> GroupOut: group = Group(name=payload.name, description=payload.description) db.add(group) await db.commit() await db.refresh(group) await db.execute(select(Group).where(Group.id == group.id).options(selectinload(Group.members))) return GroupOut.model_validate(group) @router.get("/{group_id}", response_model=GroupOut) async def get_group(group_id: uuid.UUID, db: AsyncSession = Depends(get_db)) -> GroupOut: result = await db.execute( select(Group).where(Group.id == group_id).options(selectinload(Group.members)) ) group = result.scalar_one_or_none() if group is None: raise HTTPException(status_code=404, detail="Group not found") return GroupOut.model_validate(group) @router.delete("/{group_id}", status_code=204, response_class=Response, response_model=None) async def delete_group( group_id: uuid.UUID, db: AsyncSession = Depends(get_db), _identity=Depends(get_current_identity), ) -> None: result = await db.execute(select(Group).where(Group.id == group_id)) group = result.scalar_one_or_none() if group is None: raise HTTPException(status_code=404, detail="Group not found") await db.delete(group) await db.commit() @router.post("/{group_id}/members", response_model=GroupMemberOut, status_code=201) async def add_member( group_id: uuid.UUID, payload: GroupMemberCreate, db: AsyncSession = Depends(get_db), _identity=Depends(get_current_identity), ) -> GroupMemberOut: result = await db.execute(select(Group).where(Group.id == group_id)) if result.scalar_one_or_none() is None: raise HTTPException(status_code=404, detail="Group not found") member = GroupMember( group_id=group_id, display_name=payload.display_name, identity_id=payload.identity_id, ) db.add(member) await db.commit() await db.refresh(member) return GroupMemberOut.model_validate(member) @router.delete("/{group_id}/members/{member_id}", status_code=204, response_class=Response, response_model=None) async def remove_member( group_id: uuid.UUID, member_id: uuid.UUID, db: AsyncSession = Depends(get_db), _identity=Depends(get_current_identity), ) -> None: result = await db.execute( select(GroupMember).where(GroupMember.id == member_id, GroupMember.group_id == group_id) ) member = result.scalar_one_or_none() if member is None: raise HTTPException(status_code=404, detail="Member not found") await db.delete(member) await db.commit()