Implement authentication system with access control UI
This commit is contained in:
@@ -17,14 +17,62 @@ import {
|
||||
DeduplicationStats,
|
||||
TimelineStats,
|
||||
CrossProjectStats,
|
||||
User,
|
||||
LoginCredentials,
|
||||
APIKey,
|
||||
APIKeyCreate,
|
||||
APIKeyCreateResponse,
|
||||
AdminUser,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
AccessPermission,
|
||||
AccessPermissionCreate,
|
||||
AccessPermissionUpdate,
|
||||
AccessLevel,
|
||||
OIDCConfig,
|
||||
OIDCConfigUpdate,
|
||||
OIDCStatus,
|
||||
} 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' }));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
const 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();
|
||||
}
|
||||
@@ -40,6 +88,55 @@ function buildQueryString(params: Record<string, unknown>): string {
|
||||
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 });
|
||||
@@ -186,3 +283,163 @@ 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}` : ''}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user