Add multi-tenancy with Teams feature
Implement team-based organization for projects with role-based access control: Backend: - Add teams and team_memberships database tables (migrations 009, 009b) - Add Team and TeamMembership ORM models with relationships - Implement TeamAuthorizationService for team-level access control - Add team CRUD, membership, and projects API endpoints - Update project creation to support team assignment Frontend: - Add TeamContext for managing team state with localStorage persistence - Add TeamSelector component for switching between teams - Add TeamsPage, TeamDashboardPage, TeamSettingsPage, TeamMembersPage - Add team API client functions - Update navigation with Teams link Security: - Team role hierarchy: owner > admin > member - Membership checked before system admin fallback - Self-modification prevention for role changes - Email visibility restricted to team admins/owners - Slug validation rejects consecutive hyphens Tests: - Unit tests for TeamAuthorizationService - Integration tests for all team API endpoints
This commit is contained in:
@@ -36,6 +36,12 @@ import {
|
||||
ArtifactDependenciesResponse,
|
||||
ReverseDependenciesResponse,
|
||||
DependencyResolutionResponse,
|
||||
TeamDetail,
|
||||
TeamMember,
|
||||
TeamCreate,
|
||||
TeamUpdate,
|
||||
TeamMemberCreate,
|
||||
TeamMemberUpdate,
|
||||
} from './types';
|
||||
|
||||
const API_BASE = '/api/v1';
|
||||
@@ -562,3 +568,103 @@ export async function getEnsureFile(
|
||||
}
|
||||
return response.text();
|
||||
}
|
||||
|
||||
// Team API
|
||||
export async function listTeams(params: ListParams = {}): Promise<PaginatedResponse<TeamDetail>> {
|
||||
const query = buildQueryString(params as Record<string, unknown>);
|
||||
const response = await fetch(`${API_BASE}/teams${query}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<PaginatedResponse<TeamDetail>>(response);
|
||||
}
|
||||
|
||||
export async function createTeam(data: TeamCreate): Promise<TeamDetail> {
|
||||
const response = await fetch(`${API_BASE}/teams`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamDetail>(response);
|
||||
}
|
||||
|
||||
export async function getTeam(slug: string): Promise<TeamDetail> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamDetail>(response);
|
||||
}
|
||||
|
||||
export async function updateTeam(slug: string, data: TeamUpdate): Promise<TeamDetail> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamDetail>(response);
|
||||
}
|
||||
|
||||
export async function deleteTeam(slug: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new ApiError(error.detail || `HTTP ${response.status}`, response.status);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTeamMembers(slug: string): Promise<TeamMember[]> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}/members`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamMember[]>(response);
|
||||
}
|
||||
|
||||
export async function addTeamMember(slug: string, data: TeamMemberCreate): Promise<TeamMember> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}/members`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamMember>(response);
|
||||
}
|
||||
|
||||
export async function updateTeamMember(
|
||||
slug: string,
|
||||
username: string,
|
||||
data: TeamMemberUpdate
|
||||
): Promise<TeamMember> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}/members/${username}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<TeamMember>(response);
|
||||
}
|
||||
|
||||
export async function removeTeamMember(slug: string, username: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}/members/${username}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
||||
throw new ApiError(error.detail || `HTTP ${response.status}`, response.status);
|
||||
}
|
||||
}
|
||||
|
||||
export async function listTeamProjects(
|
||||
slug: string,
|
||||
params: ProjectListParams = {}
|
||||
): Promise<PaginatedResponse<Project>> {
|
||||
const query = buildQueryString(params as Record<string, unknown>);
|
||||
const response = await fetch(`${API_BASE}/teams/${slug}/projects${query}`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
return handleResponse<PaginatedResponse<Project>>(response);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user