Phase 1 backend implementation: Add invite management endpoints

Implements core invite management features for band admins:
- GET /bands/{band_id}/invites - List all invites for a band (admin only)
- DELETE /invites/{invite_id} - Revoke pending invite (admin only)
- GET /invites/{token}/info - Get invite details (public)

Backend changes:
- Add invites router with 3 endpoints
- Update BandRepository with get_invites_for_band and get_invite_by_id methods
- Add new schemas for invite listing and info
- Register invites router in main.py

Tests:
- Integration tests for all 3 endpoints
- Permission tests (admin vs non-admin)
- Edge cases (not found, expired, etc.)

This addresses the core requirements:
- Admins can see pending invites
- Admins can revoke pending invites
- Users can view invite details before accepting

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
Mistral Vibe
2026-04-01 11:30:52 +02:00
parent ce228919df
commit 56ffd98f5e
7 changed files with 423 additions and 1 deletions

View File

@@ -81,6 +81,17 @@ class BandRepository(BaseRepository[Band]):
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def get_invite_by_id(self, invite_id: uuid.UUID) -> BandInvite | None:
stmt = select(BandInvite).where(BandInvite.id == invite_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def get_invites_for_band(self, band_id: uuid.UUID) -> list[BandInvite]:
"""Get all invites for a specific band."""
stmt = select(BandInvite).where(BandInvite.band_id == band_id)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def get_by_nc_folder_prefix(self, path: str) -> Band | None:
"""Return the band whose nc_folder_path is a prefix of path."""
stmt = select(Band).where(Band.nc_folder_path.is_not(None))