- Complete summary of band invitation system implementation - Captures all phases: analysis, backend, frontend, testing - Documents technical decisions, file changes, and current state - Includes unresolved issues and next steps Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
326 lines
10 KiB
Markdown
326 lines
10 KiB
Markdown
# Band Invitation System - Complete Project Summary
|
|
|
|
## 1. User's Primary Goals and Intent
|
|
|
|
### Initial Request
|
|
- **"Make a new branch, we're start working on the band invitation system"**
|
|
- **"Evaluate the current system, and make a deep dive in all functions involved. then plan the new system."**
|
|
|
|
### Core Requirements
|
|
1. ✅ A user with an existing band instance can invite users registered to the system
|
|
2. ✅ Invited users are added to the band
|
|
3. ✅ No link handling needed (token-based system, no email notifications)
|
|
4. ✅ The user with the band instance is the admin (can add/remove members)
|
|
|
|
### Additional Clarifications
|
|
- **"the mvp should be able to invite new members to a band without sending an existing user a link"**
|
|
- Focus on token-based invite system (no email notifications)
|
|
- Admin should be able to manage invites (list, revoke)
|
|
|
|
## 2. Conversation Timeline and Progress
|
|
|
|
### Phase 0: Analysis & Planning
|
|
- **Action**: Created comprehensive analysis documents
|
|
- **Files**: `BAND_INVITATION_ANALYSIS.md`, `IMPLEMENTATION_PLAN.md`
|
|
- **Outcome**: Identified gaps in current system (no invite listing, no revocation, no user search)
|
|
|
|
### Phase 1: Backend Implementation
|
|
- **Action**: Implemented 3 new API endpoints
|
|
- **Files**: 7 files modified, 423 lines added
|
|
- **Outcome**: Backend APIs for listing, revoking, and getting invite info
|
|
- **Tests**: 13 integration tests written
|
|
|
|
### Phase 2: Frontend Implementation
|
|
- **Action**: Created React components for invite management
|
|
- **Files**: 5 files created/modified, 610 lines added
|
|
- **Outcome**: InviteManagement component integrated into BandPage
|
|
|
|
### Phase 3: TypeScript Error Resolution
|
|
- **Action**: Fixed all build errors
|
|
- **Files**: 4 files modified, 16 lines removed
|
|
- **Outcome**: All TypeScript errors resolved (TS6133, TS2304, TS2307)
|
|
|
|
### Current State
|
|
- ✅ Backend: 3 endpoints implemented and tested
|
|
- ✅ Frontend: InviteManagement component working
|
|
- ✅ Build: All TypeScript errors resolved
|
|
- ⏸️ UserSearch: Temporarily disabled (needs backend support)
|
|
|
|
## 3. Technical Context and Decisions
|
|
|
|
### Technologies
|
|
- **Backend**: FastAPI, SQLAlchemy, PostgreSQL, Python 3.11+
|
|
- **Frontend**: React 18, TypeScript, TanStack Query, Vite
|
|
- **Testing**: pytest, integration tests
|
|
- **Deployment**: Docker, Podman Compose
|
|
|
|
### Architectural Decisions
|
|
- **Token-based invites**: 72-hour expiry, random tokens (32 bytes)
|
|
- **Permission model**: Only band admins can manage invites
|
|
- **Repository pattern**: All DB access through BandRepository
|
|
- **Service layer**: BandService handles business logic
|
|
- **Pydantic v2**: Response schemas with from_attributes=True
|
|
|
|
### Key Constraints
|
|
- No email notifications (requirement: "no link handling needed")
|
|
- Existing JWT authentication system
|
|
- Must work with existing Nextcloud integration
|
|
- Follow existing code patterns and conventions
|
|
|
|
### Code Patterns
|
|
```python
|
|
# Backend pattern
|
|
@router.get("/{band_id}/invites", response_model=BandInviteList)
|
|
async def list_invites(band_id: uuid.UUID, ...):
|
|
# Check admin permissions
|
|
# Get invites from repo
|
|
# Return response
|
|
```
|
|
|
|
```typescript
|
|
// Frontend pattern
|
|
const { data, isLoading } = useQuery({
|
|
queryKey: ["invites", bandId],
|
|
queryFn: () => listInvites(bandId),
|
|
});
|
|
```
|
|
|
|
## 4. Files and Code Changes
|
|
|
|
### Backend Files
|
|
|
|
#### `api/src/rehearsalhub/routers/invites.py` (NEW)
|
|
- **Purpose**: Invite management endpoints
|
|
- **Key code**:
|
|
```python
|
|
@router.get("/{token}/info", response_model=InviteInfoRead)
|
|
async def get_invite_info(token: str, session: AsyncSession = Depends(get_session)):
|
|
"""Get invite details (public endpoint)"""
|
|
repo = BandRepository(session)
|
|
invite = await repo.get_invite_by_token(token)
|
|
# Validate and return invite info
|
|
```
|
|
|
|
#### `api/src/rehearsalhub/routers/bands.py` (MODIFIED)
|
|
- **Purpose**: Enhanced with invite listing and revocation
|
|
- **Key additions**:
|
|
```python
|
|
@router.get("/{band_id}/invites", response_model=BandInviteList)
|
|
async def list_invites(band_id: uuid.UUID, ...):
|
|
# Admin-only endpoint to list invites
|
|
|
|
@router.delete("/invites/{invite_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def revoke_invite(invite_id: uuid.UUID, ...):
|
|
# Admin-only endpoint to revoke invites
|
|
```
|
|
|
|
#### `api/src/rehearsalhub/repositories/band.py` (MODIFIED)
|
|
- **Purpose**: Added invite lookup methods
|
|
- **Key additions**:
|
|
```python
|
|
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_invite_by_id(self, invite_id: uuid.UUID) -> BandInvite | None:
|
|
"""Get invite by ID."""
|
|
stmt = select(BandInvite).where(BandInvite.id == invite_id)
|
|
result = await self.session.execute(stmt)
|
|
return result.scalar_one_or_none()
|
|
```
|
|
|
|
#### `api/src/rehearsalhub/schemas/invite.py` (MODIFIED)
|
|
- **Purpose**: Added response schemas
|
|
- **Key additions**:
|
|
```python
|
|
class BandInviteListItem(BaseModel):
|
|
"""Invite for listing (includes creator info)"""
|
|
id: uuid.UUID
|
|
band_id: uuid.UUID
|
|
token: str
|
|
role: str
|
|
expires_at: datetime
|
|
created_at: datetime
|
|
is_used: bool
|
|
used_at: datetime | None = None
|
|
|
|
class BandInviteList(BaseModel):
|
|
"""Response for listing invites"""
|
|
invites: list[BandInviteListItem]
|
|
total: int
|
|
pending: int
|
|
|
|
class InviteInfoRead(BaseModel):
|
|
"""Public invite info (used for /invites/{token}/info)"""
|
|
id: uuid.UUID
|
|
band_id: uuid.UUID
|
|
band_name: str
|
|
band_slug: str
|
|
role: str
|
|
expires_at: datetime
|
|
created_at: datetime
|
|
is_used: bool
|
|
```
|
|
|
|
#### `api/tests/integration/test_api_invites.py` (NEW)
|
|
- **Purpose**: Integration tests for all 3 endpoints
|
|
- **Key tests**:
|
|
```python
|
|
@pytest.mark.asyncio
|
|
async def test_list_invites_admin_can_see(client, db_session, auth_headers_for, band_with_admin):
|
|
"""Test that admin can list invites for their band."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_revoke_invite_admin_can_revoke(client, db_session, auth_headers_for, band_with_admin):
|
|
"""Test that admin can revoke an invite."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_invite_info_valid_token(client, db_session):
|
|
"""Test getting invite info with valid token."""
|
|
```
|
|
|
|
### Frontend Files
|
|
|
|
#### `web/src/types/invite.ts` (NEW)
|
|
- **Purpose**: TypeScript interfaces for invite data
|
|
- **Key interfaces**:
|
|
```typescript
|
|
export interface BandInviteListItem {
|
|
id: string;
|
|
band_id: string;
|
|
token: string;
|
|
role: string;
|
|
expires_at: string;
|
|
created_at: string;
|
|
is_used: boolean;
|
|
used_at: string | null;
|
|
}
|
|
|
|
export interface BandInviteList {
|
|
invites: BandInviteListItem[];
|
|
total: number;
|
|
pending: number;
|
|
}
|
|
|
|
export interface InviteInfo {
|
|
id: string;
|
|
band_id: string;
|
|
band_name: string;
|
|
band_slug: string;
|
|
role: string;
|
|
expires_at: string;
|
|
created_at: string;
|
|
is_used: boolean;
|
|
}
|
|
```
|
|
|
|
#### `web/src/api/invites.ts` (NEW)
|
|
- **Purpose**: API wrapper functions
|
|
- **Key functions**:
|
|
```typescript
|
|
export const listInvites = (bandId: string) => {
|
|
return api.get<BandInviteList>(`/bands/${bandId}/invites`);
|
|
};
|
|
|
|
export const revokeInvite = (inviteId: string) => {
|
|
return api.delete(`/invites/${inviteId}`);
|
|
};
|
|
|
|
export const getInviteInfo = (token: string) => {
|
|
return api.get<InviteInfo>(`/invites/${token}/info`);
|
|
};
|
|
```
|
|
|
|
#### `web/src/components/InviteManagement.tsx` (NEW)
|
|
- **Purpose**: Admin UI for managing invites
|
|
- **Key features**:
|
|
- List all pending invites
|
|
- Revoke invites
|
|
- Copy invite links to clipboard
|
|
- Show invite status (pending/expired/used)
|
|
- **Current state**: Clean, no unused code, all TypeScript errors resolved
|
|
|
|
#### `web/src/pages/BandPage.tsx` (MODIFIED)
|
|
- **Purpose**: Integrated InviteManagement component
|
|
- **Key changes**:
|
|
- Added import: `import { InviteManagement } from "../components/InviteManagement";`
|
|
- Added component: `{amAdmin && <InviteManagement bandId={bandId!} />}`
|
|
- Removed UserSearch (temporarily disabled)
|
|
|
|
## 5. Active Work and Last Actions
|
|
|
|
### Most Recent Work
|
|
- **Task**: Fixing TypeScript build errors
|
|
- **Last action**: Removed unused `useState` import and `isRefreshing` reference
|
|
- **Files modified**:
|
|
- `web/src/components/InviteManagement.tsx`: Removed unused imports and variables
|
|
- `web/src/api/invites.ts`: Removed unused parameters from `listNonMemberUsers`
|
|
|
|
### Current State
|
|
- ✅ All TypeScript errors resolved
|
|
- ✅ Build passing (no TS6133, TS2304, TS2307 errors)
|
|
- ✅ Backend APIs working and tested
|
|
- ✅ Frontend components integrated
|
|
- ⏸️ UserSearch disabled (needs backend support)
|
|
|
|
### Recent Code Changes
|
|
```typescript
|
|
// Before (with errors)
|
|
import React, { useState } from "react";
|
|
// ...
|
|
disabled={revokeMutation.isPending || isRefreshing}
|
|
|
|
// After (fixed)
|
|
import React from "react";
|
|
// ...
|
|
disabled={revokeMutation.isPending}
|
|
```
|
|
|
|
## 6. Unresolved Issues and Pending Tasks
|
|
|
|
### Current Issues
|
|
- **Audio-worker build issue**: `podman_compose:Build command failed` (not related to our changes)
|
|
- **403 errors in frontend**: Invited users getting 403 on `/bands/{id}/invites` and `/versions/{id}/stream`
|
|
|
|
### Pending Tasks
|
|
1. **UserSearch component**: Needs backend endpoint `GET /bands/{band_id}/non-members`
|
|
2. **Direct user invite**: Needs backend support for inviting specific users
|
|
3. **Email notifications**: Optional feature for future phase
|
|
4. **Invite analytics**: Track acceptance rates, etc.
|
|
|
|
### Decisions Waiting
|
|
- Should we implement UserSearch backend endpoint?
|
|
- Should we add email notification system?
|
|
- Should we deploy current MVP to staging?
|
|
|
|
## 7. Immediate Next Step
|
|
|
|
### Priority: Resolve 403 Errors
|
|
The user reported:
|
|
```
|
|
GET /api/v1/bands/96c11cfa-d6bb-4987-af80-845626880383/invites 403 (Forbidden)
|
|
GET /api/v1/versions/973d000c-2ca8-4f02-8359-97646cf59086/stream 403 (Forbidden)
|
|
```
|
|
|
|
**Action**: Investigate permission issues for invited users
|
|
- Check if invited users are properly added to band_members table
|
|
- Verify JWT permissions for band access
|
|
- Review backend permission checks in bands.py and versions.py
|
|
|
|
### Specific Task
|
|
```bash
|
|
# 1. Check if invited user is in band_members
|
|
SELECT * FROM band_members WHERE band_id = '96c11cfa-d6bb-4987-af80-845626880383';
|
|
|
|
# 2. Check invite acceptance flow
|
|
SELECT * FROM band_invites WHERE band_id = '96c11cfa-d6bb-4987-af80-845626880383';
|
|
|
|
# 3. Review permission logic in:
|
|
# - api/src/rehearsalhub/routers/bands.py
|
|
# - api/src/rehearsalhub/routers/versions.py
|
|
```
|
|
|
|
The next step is to diagnose why invited users are getting 403 errors when accessing band resources and audio streams.
|