Init
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { apiGet, apiPost, apiFetch } from './api.service';
|
||||
import type { Role } from './auth.service';
|
||||
|
||||
export interface AppUser { id: number; username: string; role: Role; createdAt: string | null; lastLoginAt: string | null; }
|
||||
export interface TokenInfo { id: number; tokenId: string; label: string; createdAt: string | null; lastUsedAt: string | null; expiresAt: string | null; revoked: boolean; }
|
||||
|
||||
export async function listUsers(): Promise<AppUser[]> {
|
||||
return (await apiGet<{ users: AppUser[] }>('/api/users')).users;
|
||||
}
|
||||
export function createUser(username: string, password: string, role: Role): Promise<{ user?: AppUser; error?: string; message?: string }> {
|
||||
return apiPost('/api/users', { username, password, role }, { allowAuthErrorBody: true });
|
||||
}
|
||||
export function deleteUser(username: string): Promise<{ success: boolean }> {
|
||||
return apiFetch(`/api/users/${encodeURIComponent(username)}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export async function listTokens(): Promise<TokenInfo[]> {
|
||||
return (await apiGet<{ tokens: TokenInfo[] }>('/api/tokens')).tokens;
|
||||
}
|
||||
export function createToken(label: string): Promise<{ token: string; label: string }> {
|
||||
return apiPost('/api/tokens', { label });
|
||||
}
|
||||
export function revokeToken(id: number): Promise<{ success: boolean }> {
|
||||
return apiPost(`/api/tokens/${id}/revoke`, {});
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { apiGet } from './api.service';
|
||||
import type { OverviewResponse, SlaMetric, ForgeConfig, InsightsResponse, JiraDurationsResponse } from '../types/analytics.types';
|
||||
|
||||
export function getInsights(): Promise<InsightsResponse> {
|
||||
return apiGet<InsightsResponse>('/api/insights');
|
||||
}
|
||||
|
||||
export function getJiraDurations(): Promise<JiraDurationsResponse> {
|
||||
return apiGet<JiraDurationsResponse>('/api/analytics/jira-durations');
|
||||
}
|
||||
|
||||
export function getOverview(): Promise<OverviewResponse> {
|
||||
return apiGet<OverviewResponse>('/api/analytics/overview');
|
||||
}
|
||||
|
||||
export function getSla(): Promise<{ metrics: SlaMetric[] }> {
|
||||
return apiGet<{ metrics: SlaMetric[] }>('/api/analytics/sla');
|
||||
}
|
||||
|
||||
export function getConfig(): Promise<ForgeConfig> {
|
||||
return apiGet<ForgeConfig>('/api/config');
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Fired when any API call returns 401 (session expired/absent). App registers it
|
||||
// to drop to the login screen.
|
||||
let onUnauthorized: (() => void) | null = null;
|
||||
export function setUnauthorizedHandler(fn: (() => void) | null): void {
|
||||
onUnauthorized = fn;
|
||||
}
|
||||
|
||||
interface ApiOptions extends RequestInit {
|
||||
// When true, a 401 body is returned to the caller instead of throwing +
|
||||
// firing the logout hook. Used by /login to surface { success, error }.
|
||||
allowAuthErrorBody?: boolean;
|
||||
}
|
||||
|
||||
export async function apiFetch<T>(url: string, options?: ApiOptions): Promise<T> {
|
||||
const { allowAuthErrorBody, ...init } = options ?? {};
|
||||
const res = await fetch(url, init);
|
||||
if (!res.ok) {
|
||||
if (res.status === 401) {
|
||||
if (!allowAuthErrorBody) { onUnauthorized?.(); throw new Error('unauthenticated'); }
|
||||
// else fall through and return the { success, error } body
|
||||
} else if (res.status === 400 || res.status === 409) {
|
||||
// Validation / conflict responses carry a { error, message } body the caller reads.
|
||||
} else {
|
||||
const text = await res.text().catch(() => res.statusText);
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function apiGet<T>(url: string): Promise<T> {
|
||||
return apiFetch<T>(url);
|
||||
}
|
||||
|
||||
export function apiPost<T>(url: string, body: unknown, options?: ApiOptions): Promise<T> {
|
||||
return apiFetch<T>(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { apiGet, apiPost } from './api.service';
|
||||
|
||||
export type Role = 'viewer' | 'pm' | 'lead' | 'admin';
|
||||
|
||||
export interface CurrentUser {
|
||||
username: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
const ORDER: Role[] = ['viewer', 'pm', 'lead', 'admin'];
|
||||
export function roleAtLeast(role: Role | undefined, min: Role): boolean {
|
||||
return !!role && ORDER.indexOf(role) >= ORDER.indexOf(min);
|
||||
}
|
||||
export const ROLE_LABELS: Record<Role, string> = {
|
||||
viewer: 'Viewer', pm: 'PM', lead: 'Project Leadership', admin: 'Admin',
|
||||
};
|
||||
|
||||
export async function getMe(): Promise<CurrentUser | null> {
|
||||
const data = await apiGet<{ user: CurrentUser | null }>('/api/me');
|
||||
return data.user;
|
||||
}
|
||||
|
||||
export function login(username: string, password: string): Promise<{ success: boolean; user?: CurrentUser; error?: string }> {
|
||||
return apiPost('/login', { username, password }, { allowAuthErrorBody: true });
|
||||
}
|
||||
|
||||
export function logout(): Promise<{ success: boolean }> {
|
||||
return apiPost('/logout', {});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { apiGet } from './api.service';
|
||||
import type { Ticket, TicketFilters, StatsResponse } from '../types/ticket.types';
|
||||
|
||||
export async function getTickets(filters: TicketFilters = {}): Promise<Ticket[]> {
|
||||
const qs = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(filters)) if (v) qs.set(k, String(v));
|
||||
const data = await apiGet<{ tickets: Ticket[] }>(`/api/tickets?${qs.toString()}`);
|
||||
return data.tickets;
|
||||
}
|
||||
|
||||
export function getTicket(number: string): Promise<Ticket> {
|
||||
return apiGet<Ticket>(`/api/tickets/${encodeURIComponent(number)}`);
|
||||
}
|
||||
|
||||
export function getStats(): Promise<StatsResponse> {
|
||||
return apiGet<StatsResponse>('/api/stats');
|
||||
}
|
||||
Reference in New Issue
Block a user