from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from app.db.session import get_db from app.models.api_client import ApiClient from app.repositories.member_organizations_repo import MemberOrganizationsRepository from app.repositories.organizations_repo import OrganizationsRepository from app.repositories.users_repo import UsersRepository from app.schemas.members import ( MemberCreateRequest, MemberListResponse, MemberOrganizationsResponse, MemberSummary, MemberUpdateRequest, ) from app.schemas.organizations import ( OrganizationCreateRequest, OrganizationListResponse, OrganizationSummary, OrganizationUpdateRequest, ) from app.security.api_client_auth import require_api_client router = APIRouter(prefix="/admin", tags=["admin"]) def _to_member_summary(member) -> MemberSummary: return MemberSummary( id=member.id, authentik_sub=member.authentik_sub, authentik_user_id=member.authentik_user_id, email=member.email, display_name=member.display_name, is_active=member.is_active, ) def _to_org_summary(org) -> OrganizationSummary: return OrganizationSummary( id=org.id, org_code=org.org_code, name=org.name, tax_id=org.tax_id, status=org.status, ) @router.get("/organizations", response_model=OrganizationListResponse) def list_organizations( _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), keyword: str | None = Query(default=None), status_filter: str | None = Query(default=None, alias="status"), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), ) -> OrganizationListResponse: repo = OrganizationsRepository(db) items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset) return OrganizationListResponse(items=[_to_org_summary(i) for i in items], total=total, limit=limit, offset=offset) @router.post("/organizations", response_model=OrganizationSummary) def create_organization( payload: OrganizationCreateRequest, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> OrganizationSummary: repo = OrganizationsRepository(db) existing = repo.get_by_code(payload.org_code) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="organization_code_already_exists") org = repo.create(org_code=payload.org_code, name=payload.name, tax_id=payload.tax_id, status=payload.status) return _to_org_summary(org) @router.get("/organizations/{org_id}", response_model=OrganizationSummary) def get_organization( org_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> OrganizationSummary: repo = OrganizationsRepository(db) org = repo.get_by_id(org_id) if not org: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found") return _to_org_summary(org) @router.patch("/organizations/{org_id}", response_model=OrganizationSummary) def update_organization( org_id: str, payload: OrganizationUpdateRequest, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> OrganizationSummary: repo = OrganizationsRepository(db) org = repo.get_by_id(org_id) if not org: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found") updated = repo.update(org, name=payload.name, tax_id=payload.tax_id, status=payload.status) return _to_org_summary(updated) @router.post("/organizations/{org_id}/activate", response_model=OrganizationSummary) def activate_organization( org_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> OrganizationSummary: repo = OrganizationsRepository(db) org = repo.get_by_id(org_id) if not org: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found") return _to_org_summary(repo.update(org, status="active")) @router.post("/organizations/{org_id}/deactivate", response_model=OrganizationSummary) def deactivate_organization( org_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> OrganizationSummary: repo = OrganizationsRepository(db) org = repo.get_by_id(org_id) if not org: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found") return _to_org_summary(repo.update(org, status="inactive")) @router.get("/members", response_model=MemberListResponse) def list_members( _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), keyword: str | None = Query(default=None), is_active: bool | None = Query(default=None), limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), ) -> MemberListResponse: repo = UsersRepository(db) items, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset) return MemberListResponse(items=[_to_member_summary(i) for i in items], total=total, limit=limit, offset=offset) @router.post("/members", response_model=MemberSummary) def create_member( payload: MemberCreateRequest, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberSummary: repo = UsersRepository(db) existing = repo.get_by_sub(payload.authentik_sub) if existing: raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="member_sub_already_exists") member = repo.upsert_by_sub( authentik_sub=payload.authentik_sub, email=payload.email, display_name=payload.display_name, is_active=payload.is_active, ) return _to_member_summary(member) @router.get("/members/{member_id}", response_model=MemberSummary) def get_member( member_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberSummary: repo = UsersRepository(db) member = repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") return _to_member_summary(member) @router.patch("/members/{member_id}", response_model=MemberSummary) def update_member( member_id: str, payload: MemberUpdateRequest, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberSummary: repo = UsersRepository(db) member = repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") updated = repo.update_member(member, email=payload.email, display_name=payload.display_name, is_active=payload.is_active) return _to_member_summary(updated) @router.post("/members/{member_id}/activate", response_model=MemberSummary) def activate_member( member_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberSummary: repo = UsersRepository(db) member = repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") return _to_member_summary(repo.update_member(member, is_active=True)) @router.post("/members/{member_id}/deactivate", response_model=MemberSummary) def deactivate_member( member_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberSummary: repo = UsersRepository(db) member = repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") return _to_member_summary(repo.update_member(member, is_active=False)) @router.get("/members/{member_id}/organizations", response_model=MemberOrganizationsResponse) def list_member_organizations( member_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> MemberOrganizationsResponse: users_repo = UsersRepository(db) link_repo = MemberOrganizationsRepository(db) member = users_repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") orgs = link_repo.list_organizations_by_member_id(member_id) return MemberOrganizationsResponse(member=_to_member_summary(member), organizations=[_to_org_summary(o) for o in orgs]) @router.post("/members/{member_id}/organizations/{org_id}") def bind_member_organization( member_id: str, org_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, str]: users_repo = UsersRepository(db) orgs_repo = OrganizationsRepository(db) link_repo = MemberOrganizationsRepository(db) member = users_repo.get_by_id(member_id) if not member: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="member_not_found") org = orgs_repo.get_by_id(org_id) if not org: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="organization_not_found") relation = link_repo.add_if_not_exists(member_id=member_id, organization_id=org_id) return {"relation_id": relation.id, "result": "bound"} @router.delete("/members/{member_id}/organizations/{org_id}") def unbind_member_organization( member_id: str, org_id: str, _: ApiClient = Depends(require_api_client), db: Session = Depends(get_db), ) -> dict[str, int | str]: link_repo = MemberOrganizationsRepository(db) deleted = link_repo.remove(member_id=member_id, organization_id=org_id) return {"deleted": deleted, "result": "unbound"}