Tags were mutable aliases that caused confusion alongside the immutable version system. This removes tags entirely, keeping only PackageVersion for artifact references. Changes: - Remove tags and tag_history tables (migration 012) - Remove Tag model, TagRepository, and 6 tag API endpoints - Update cache system to create versions instead of tags - Update frontend to display versions instead of tags - Remove tag-related schemas and types - Update artifact cleanup service for version-based ref_count
723 lines
23 KiB
TypeScript
723 lines
23 KiB
TypeScript
import {
|
|
Project,
|
|
Package,
|
|
ArtifactDetail,
|
|
PackageArtifact,
|
|
UploadResponse,
|
|
PaginatedResponse,
|
|
ListParams,
|
|
PackageListParams,
|
|
ArtifactListParams,
|
|
ProjectListParams,
|
|
GlobalSearchResponse,
|
|
Stats,
|
|
DeduplicationStats,
|
|
TimelineStats,
|
|
CrossProjectStats,
|
|
User,
|
|
LoginCredentials,
|
|
APIKey,
|
|
APIKeyCreate,
|
|
APIKeyCreateResponse,
|
|
AdminUser,
|
|
UserCreate,
|
|
UserUpdate,
|
|
AccessPermission,
|
|
AccessPermissionCreate,
|
|
AccessPermissionUpdate,
|
|
AccessLevel,
|
|
OIDCConfig,
|
|
OIDCConfigUpdate,
|
|
OIDCStatus,
|
|
PackageVersion,
|
|
ArtifactDependenciesResponse,
|
|
ReverseDependenciesResponse,
|
|
DependencyResolutionResponse,
|
|
TeamDetail,
|
|
TeamMember,
|
|
TeamCreate,
|
|
TeamUpdate,
|
|
TeamMemberCreate,
|
|
TeamMemberUpdate,
|
|
UpstreamSource,
|
|
UpstreamSourceCreate,
|
|
UpstreamSourceUpdate,
|
|
UpstreamSourceTestResult,
|
|
} from './types';
|
|
|
|
const API_BASE = '/api/v1';
|
|
|
|
// Custom error classes for better error handling
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
export class UnauthorizedError extends ApiError {
|
|
constructor(message: string = 'Not authenticated') {
|
|
super(message, 401);
|
|
this.name = 'UnauthorizedError';
|
|
}
|
|
}
|
|
|
|
export class ForbiddenError extends ApiError {
|
|
constructor(message: string = 'Access denied') {
|
|
super(message, 403);
|
|
this.name = 'ForbiddenError';
|
|
}
|
|
}
|
|
|
|
async function handleResponse<T>(response: Response): Promise<T> {
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
// Handle detail as string or object (backend may return structured errors)
|
|
let message: string;
|
|
if (typeof error.detail === 'object') {
|
|
message = JSON.stringify(error.detail);
|
|
} else {
|
|
message = error.detail || `HTTP ${response.status}`;
|
|
}
|
|
|
|
if (response.status === 401) {
|
|
throw new UnauthorizedError(message);
|
|
}
|
|
if (response.status === 403) {
|
|
throw new ForbiddenError(message);
|
|
}
|
|
throw new ApiError(message, response.status);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
function buildQueryString(params: Record<string, unknown>): string {
|
|
const searchParams = new URLSearchParams();
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
searchParams.append(key, String(value));
|
|
}
|
|
});
|
|
const query = searchParams.toString();
|
|
return query ? `?${query}` : '';
|
|
}
|
|
|
|
// Auth API
|
|
export async function login(credentials: LoginCredentials): Promise<User> {
|
|
const response = await fetch(`${API_BASE}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(credentials),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<User>(response);
|
|
}
|
|
|
|
export async function logout(): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/auth/logout`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/auth/change-password`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
export async function getCurrentUser(): Promise<User | null> {
|
|
try {
|
|
const response = await fetch(`${API_BASE}/auth/me`, {
|
|
credentials: 'include',
|
|
});
|
|
if (response.status === 401) {
|
|
return null;
|
|
}
|
|
return handleResponse<User>(response);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Global Search API
|
|
export async function globalSearch(query: string, limit: number = 5): Promise<GlobalSearchResponse> {
|
|
const params = buildQueryString({ q: query, limit });
|
|
const response = await fetch(`${API_BASE}/search${params}`);
|
|
return handleResponse<GlobalSearchResponse>(response);
|
|
}
|
|
|
|
// Project API
|
|
export async function listProjects(params: ProjectListParams = {}): Promise<PaginatedResponse<Project>> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/projects${query}`);
|
|
return handleResponse<PaginatedResponse<Project>>(response);
|
|
}
|
|
|
|
export async function listProjectsSimple(params: ListParams = {}): Promise<Project[]> {
|
|
const data = await listProjects(params);
|
|
return data.items;
|
|
}
|
|
|
|
export async function createProject(data: { name: string; description?: string; is_public?: boolean; team_id?: string }): Promise<Project> {
|
|
const response = await fetch(`${API_BASE}/projects`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
return handleResponse<Project>(response);
|
|
}
|
|
|
|
export async function getProject(name: string): Promise<Project> {
|
|
const response = await fetch(`${API_BASE}/projects/${name}`);
|
|
return handleResponse<Project>(response);
|
|
}
|
|
|
|
export async function updateProject(
|
|
projectName: string,
|
|
data: { description?: string; is_public?: boolean }
|
|
): Promise<Project> {
|
|
const response = await fetch(`${API_BASE}/projects/${projectName}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<Project>(response);
|
|
}
|
|
|
|
export async function deleteProject(projectName: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/projects/${projectName}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// Package API
|
|
export async function listPackages(projectName: string, params: PackageListParams = {}): Promise<PaginatedResponse<Package>> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/packages${query}`);
|
|
return handleResponse<PaginatedResponse<Package>>(response);
|
|
}
|
|
|
|
export async function listPackagesSimple(projectName: string, params: PackageListParams = {}): Promise<Package[]> {
|
|
const data = await listPackages(projectName, params);
|
|
return data.items;
|
|
}
|
|
|
|
export async function getPackage(projectName: string, packageName: string): Promise<Package> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/packages/${packageName}`);
|
|
return handleResponse<Package>(response);
|
|
}
|
|
|
|
export async function createPackage(projectName: string, data: { name: string; description?: string }): Promise<Package> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/packages`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
});
|
|
return handleResponse<Package>(response);
|
|
}
|
|
|
|
// Artifact API
|
|
export async function getArtifact(artifactId: string): Promise<ArtifactDetail> {
|
|
const response = await fetch(`${API_BASE}/artifact/${artifactId}`);
|
|
return handleResponse<ArtifactDetail>(response);
|
|
}
|
|
|
|
export async function listPackageArtifacts(
|
|
projectName: string,
|
|
packageName: string,
|
|
params: ArtifactListParams = {}
|
|
): Promise<PaginatedResponse<PackageArtifact>> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/artifacts${query}`);
|
|
return handleResponse<PaginatedResponse<PackageArtifact>>(response);
|
|
}
|
|
|
|
// Upload
|
|
export async function uploadArtifact(
|
|
projectName: string,
|
|
packageName: string,
|
|
file: File,
|
|
version?: string
|
|
): Promise<UploadResponse> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (version) {
|
|
formData.append('version', version);
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/upload`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
return handleResponse<UploadResponse>(response);
|
|
}
|
|
|
|
// Download URL
|
|
export function getDownloadUrl(projectName: string, packageName: string, ref: string): string {
|
|
return `${API_BASE}/project/${projectName}/${packageName}/+/${ref}`;
|
|
}
|
|
|
|
// Stats API
|
|
export async function getStats(): Promise<Stats> {
|
|
const response = await fetch(`${API_BASE}/stats`);
|
|
return handleResponse<Stats>(response);
|
|
}
|
|
|
|
export async function getDeduplicationStats(): Promise<DeduplicationStats> {
|
|
const response = await fetch(`${API_BASE}/stats/deduplication`);
|
|
return handleResponse<DeduplicationStats>(response);
|
|
}
|
|
|
|
export async function getTimelineStats(
|
|
period: 'day' | 'week' | 'month' = 'day',
|
|
fromDate?: string,
|
|
toDate?: string
|
|
): Promise<TimelineStats> {
|
|
const params = buildQueryString({ period, from_date: fromDate, to_date: toDate });
|
|
const response = await fetch(`${API_BASE}/stats/timeline${params}`);
|
|
return handleResponse<TimelineStats>(response);
|
|
}
|
|
|
|
export async function getCrossProjectStats(): Promise<CrossProjectStats> {
|
|
const response = await fetch(`${API_BASE}/stats/cross-project`);
|
|
return handleResponse<CrossProjectStats>(response);
|
|
}
|
|
|
|
export async function listAPIKeys(): Promise<APIKey[]> {
|
|
const response = await fetch(`${API_BASE}/auth/keys`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<APIKey[]>(response);
|
|
}
|
|
|
|
export async function createAPIKey(data: APIKeyCreate): Promise<APIKeyCreateResponse> {
|
|
const response = await fetch(`${API_BASE}/auth/keys`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<APIKeyCreateResponse>(response);
|
|
}
|
|
|
|
export async function deleteAPIKey(id: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/auth/keys/${id}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// Admin User Management API
|
|
export async function listUsers(): Promise<AdminUser[]> {
|
|
const response = await fetch(`${API_BASE}/admin/users`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AdminUser[]>(response);
|
|
}
|
|
|
|
export async function createUser(data: UserCreate): Promise<AdminUser> {
|
|
const response = await fetch(`${API_BASE}/admin/users`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AdminUser>(response);
|
|
}
|
|
|
|
export async function updateUser(username: string, data: UserUpdate): Promise<AdminUser> {
|
|
const response = await fetch(`${API_BASE}/admin/users/${username}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AdminUser>(response);
|
|
}
|
|
|
|
export async function resetUserPassword(username: string, newPassword: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/admin/users/${username}/reset-password`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ new_password: newPassword }),
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// Access Permission API
|
|
export interface MyAccessResponse {
|
|
project: string;
|
|
access_level: AccessLevel | null;
|
|
is_owner: boolean;
|
|
}
|
|
|
|
export async function getMyProjectAccess(projectName: string): Promise<MyAccessResponse> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/my-access`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<MyAccessResponse>(response);
|
|
}
|
|
|
|
export async function listProjectPermissions(projectName: string): Promise<AccessPermission[]> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AccessPermission[]>(response);
|
|
}
|
|
|
|
export async function grantProjectAccess(
|
|
projectName: string,
|
|
data: AccessPermissionCreate
|
|
): Promise<AccessPermission> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AccessPermission>(response);
|
|
}
|
|
|
|
export async function updateProjectAccess(
|
|
projectName: string,
|
|
username: string,
|
|
data: AccessPermissionUpdate
|
|
): Promise<AccessPermission> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions/${username}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<AccessPermission>(response);
|
|
}
|
|
|
|
export async function revokeProjectAccess(projectName: string, username: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/permissions/${username}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// OIDC API
|
|
export async function getOIDCStatus(): Promise<OIDCStatus> {
|
|
const response = await fetch(`${API_BASE}/auth/oidc/status`);
|
|
return handleResponse<OIDCStatus>(response);
|
|
}
|
|
|
|
export async function getOIDCConfig(): Promise<OIDCConfig> {
|
|
const response = await fetch(`${API_BASE}/auth/oidc/config`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<OIDCConfig>(response);
|
|
}
|
|
|
|
export async function updateOIDCConfig(data: OIDCConfigUpdate): Promise<OIDCConfig> {
|
|
const response = await fetch(`${API_BASE}/auth/oidc/config`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<OIDCConfig>(response);
|
|
}
|
|
|
|
export function getOIDCLoginUrl(returnTo?: string): string {
|
|
const params = new URLSearchParams();
|
|
if (returnTo) {
|
|
params.set('return_to', returnTo);
|
|
}
|
|
const query = params.toString();
|
|
return `${API_BASE}/auth/oidc/login${query ? `?${query}` : ''}`;
|
|
}
|
|
|
|
// Version API
|
|
export async function listVersions(
|
|
projectName: string,
|
|
packageName: string,
|
|
params: ListParams = {}
|
|
): Promise<PaginatedResponse<PackageVersion>> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/versions${query}`);
|
|
return handleResponse<PaginatedResponse<PackageVersion>>(response);
|
|
}
|
|
|
|
export async function getVersion(
|
|
projectName: string,
|
|
packageName: string,
|
|
version: string
|
|
): Promise<PackageVersion> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/versions/${version}`);
|
|
return handleResponse<PackageVersion>(response);
|
|
}
|
|
|
|
export async function deleteVersion(
|
|
projectName: string,
|
|
packageName: string,
|
|
version: string
|
|
): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/versions/${version}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
});
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new Error(error.detail || `HTTP ${response.status}`);
|
|
}
|
|
}
|
|
|
|
// Dependency API
|
|
export async function getArtifactDependencies(artifactId: string): Promise<ArtifactDependenciesResponse> {
|
|
const response = await fetch(`${API_BASE}/artifact/${artifactId}/dependencies`);
|
|
return handleResponse<ArtifactDependenciesResponse>(response);
|
|
}
|
|
|
|
export async function getDependenciesByRef(
|
|
projectName: string,
|
|
packageName: string,
|
|
ref: string
|
|
): Promise<ArtifactDependenciesResponse> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/+/${ref}/dependencies`);
|
|
return handleResponse<ArtifactDependenciesResponse>(response);
|
|
}
|
|
|
|
export async function getReverseDependencies(
|
|
projectName: string,
|
|
packageName: string,
|
|
params: { page?: number; limit?: number } = {}
|
|
): Promise<ReverseDependenciesResponse> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/reverse-dependencies${query}`);
|
|
return handleResponse<ReverseDependenciesResponse>(response);
|
|
}
|
|
|
|
export async function resolveDependencies(
|
|
projectName: string,
|
|
packageName: string,
|
|
ref: string
|
|
): Promise<DependencyResolutionResponse> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/+/${ref}/resolve`);
|
|
return handleResponse<DependencyResolutionResponse>(response);
|
|
}
|
|
|
|
export async function getEnsureFile(
|
|
projectName: string,
|
|
packageName: string,
|
|
ref: string
|
|
): Promise<string> {
|
|
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/+/${ref}/ensure`);
|
|
if (!response.ok) {
|
|
const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
|
|
throw new ApiError(error.detail || `HTTP ${response.status}`, response.status);
|
|
}
|
|
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);
|
|
}
|
|
|
|
// User search (for autocomplete)
|
|
export interface UserSearchResult {
|
|
id: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
}
|
|
|
|
export async function searchUsers(query: string, limit: number = 10): Promise<UserSearchResult[]> {
|
|
const response = await fetch(`${API_BASE}/users/search?q=${encodeURIComponent(query)}&limit=${limit}`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UserSearchResult[]>(response);
|
|
}
|
|
|
|
// Upstream Sources Admin API
|
|
export interface UpstreamSourceListParams {
|
|
enabled?: boolean;
|
|
source_type?: string;
|
|
}
|
|
|
|
export async function listUpstreamSources(params: UpstreamSourceListParams = {}): Promise<UpstreamSource[]> {
|
|
const query = buildQueryString(params as Record<string, unknown>);
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources${query}`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UpstreamSource[]>(response);
|
|
}
|
|
|
|
export async function createUpstreamSource(data: UpstreamSourceCreate): Promise<UpstreamSource> {
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UpstreamSource>(response);
|
|
}
|
|
|
|
export async function getUpstreamSource(id: string): Promise<UpstreamSource> {
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources/${id}`, {
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UpstreamSource>(response);
|
|
}
|
|
|
|
export async function updateUpstreamSource(id: string, data: UpstreamSourceUpdate): Promise<UpstreamSource> {
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources/${id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data),
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UpstreamSource>(response);
|
|
}
|
|
|
|
export async function deleteUpstreamSource(id: string): Promise<void> {
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources/${id}`, {
|
|
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 testUpstreamSource(id: string): Promise<UpstreamSourceTestResult> {
|
|
const response = await fetch(`${API_BASE}/admin/upstream-sources/${id}/test`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
});
|
|
return handleResponse<UpstreamSourceTestResult>(response);
|
|
}
|
|
|