feat: implement comprehensive API structure for authentication, users, forum, bugs, events, and team management
This commit is contained in:
@@ -1,4 +1,192 @@
|
||||
import type {
|
||||
User,
|
||||
ForumCategory,
|
||||
ForumThread,
|
||||
ForumReply,
|
||||
BugReport,
|
||||
BugComment,
|
||||
BugReportFormData,
|
||||
BugSeverity,
|
||||
BugStatus,
|
||||
EventPost,
|
||||
TeamMember,
|
||||
} from '../types';
|
||||
|
||||
// API base URL.
|
||||
// - In Docker (Coolify): nginx proxies /api/* to the backend, so we use a relative path.
|
||||
// - Set VITE_API_URL at build time to call the backend directly (e.g. during local dev without nginx).
|
||||
export const API_BASE = import.meta.env.VITE_API_URL ?? '/api'
|
||||
export const API_BASE = import.meta.env.VITE_API_URL ?? '/api';
|
||||
|
||||
// ── Token storage ─────────────────────────────────────────────────────────────
|
||||
|
||||
const TOKEN_KEY = 'crowmate_auth_token';
|
||||
|
||||
export function getToken(): string | null {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export function setToken(token: string): void {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
export function clearToken(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
// ── Core fetch helper ─────────────────────────────────────────────────────────
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch<T = unknown>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> ?? {}),
|
||||
};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, { ...options, headers });
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
const message =
|
||||
typeof body.error === 'string'
|
||||
? body.error
|
||||
: body.error?.message ?? `Request failed (${res.status})`;
|
||||
throw new ApiError(res.status, message);
|
||||
}
|
||||
|
||||
// 204 No Content
|
||||
if (res.status === 204) return undefined as T;
|
||||
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
// ── Auth API ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const authApi = {
|
||||
login: (email: string, password: string) =>
|
||||
apiFetch<{ token: string; user: User }>('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
}),
|
||||
|
||||
register: (username: string, email: string, password: string) =>
|
||||
apiFetch<{ token: string; user: User }>('/auth/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, email, password }),
|
||||
}),
|
||||
|
||||
me: () => apiFetch<User>('/auth/me'),
|
||||
};
|
||||
|
||||
// ── Users API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const usersApi = {
|
||||
updateUsername: (username: string) =>
|
||||
apiFetch<User>('/users/me/username', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ username }),
|
||||
}),
|
||||
|
||||
changePassword: (currentPassword: string, newPassword: string) =>
|
||||
apiFetch<{ message: string }>('/users/me/password', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ currentPassword, newPassword }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ── Forum API ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export const forumApi = {
|
||||
getCategories: () =>
|
||||
apiFetch<ForumCategory[]>('/forum/categories'),
|
||||
|
||||
getThreads: (params?: { categoryId?: string; page?: number; limit?: number }) => {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.categoryId) q.set('categoryId', params.categoryId);
|
||||
q.set('page', String(params?.page ?? 1));
|
||||
q.set('limit', String(params?.limit ?? 100));
|
||||
return apiFetch<{ data: ForumThread[]; total: number; page: number; pages: number }>(
|
||||
`/forum/threads?${q}`
|
||||
);
|
||||
},
|
||||
|
||||
getThread: (id: string) =>
|
||||
apiFetch<ForumThread & { replies: ForumReply[] }>(`/forum/threads/${id}`),
|
||||
|
||||
createReply: (threadId: string, content: string) =>
|
||||
apiFetch<ForumReply>(`/forum/threads/${threadId}/replies`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ── Bugs API ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const bugsApi = {
|
||||
getBugs: (params?: { status?: BugStatus | 'all'; severity?: BugSeverity | 'all'; page?: number; limit?: number }) => {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.status && params.status !== 'all') q.set('status', params.status);
|
||||
if (params?.severity && params.severity !== 'all') q.set('severity', params.severity);
|
||||
q.set('page', String(params?.page ?? 1));
|
||||
q.set('limit', String(params?.limit ?? 100));
|
||||
return apiFetch<{ data: BugReport[]; total: number; page: number; pages: number }>(
|
||||
`/bugs?${q}`
|
||||
);
|
||||
},
|
||||
|
||||
getBug: (id: string) =>
|
||||
apiFetch<BugReport & { comments: BugComment[] }>(`/bugs/${id}`),
|
||||
|
||||
createBug: (data: BugReportFormData) =>
|
||||
apiFetch<BugReport>('/bugs', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}),
|
||||
|
||||
toggleMeToo: (bugId: string) =>
|
||||
apiFetch<{ message: string }>(`/bugs/${bugId}/me-too`, { method: 'POST' }),
|
||||
|
||||
addComment: (bugId: string, content: string) =>
|
||||
apiFetch<BugComment>(`/bugs/${bugId}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ── Events API ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const eventsApi = {
|
||||
getEvents: (publicOnly = true) => {
|
||||
const q = new URLSearchParams({ limit: '50' });
|
||||
if (publicOnly) q.set('public', 'true');
|
||||
return apiFetch<{ data: EventPost[]; total: number; page: number; pages: number }>(
|
||||
`/events?${q}`
|
||||
);
|
||||
},
|
||||
|
||||
vote: (eventId: string, optionIds: string[]) =>
|
||||
apiFetch<EventPost>(`/events/${eventId}/vote`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ optionIds }),
|
||||
}),
|
||||
};
|
||||
|
||||
// ── Team API ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export const teamApi = {
|
||||
getMembers: () => apiFetch<TeamMember[]>('/team'),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user