Files
acs/frontend/src/api.ts
2026-07-20 19:48:40 +09:00

232 lines
9.2 KiB
TypeScript

import {
AccessAction,
AccessRecord,
AdminExcelImportResult,
ApiResponse,
AuditLog,
BlacklistCreate,
BlacklistItem,
ChangePasswordRequest,
CurrentUser,
DeliveryStatus,
EmailDiagnosticsResult,
ExcelImportResult,
InsideVisitor,
LoginRequest,
PassDelivery,
PurposeCode,
Role,
SmsDiagnosticsResult,
StatsSummary,
PublicPass,
Team,
VisitRequestCreate,
VisitRequestResetResult,
VisitRequestView,
AdminUser,
Watcher1Settings,
Zone,
} from './types';
const BASE = '/api';
/** Endpoints whose 401 must NOT trigger a redirect (login probe / public pages). */
const NO_REDIRECT_ON_401 = ['/auth/me', '/auth/login', '/auth/logout', '/public/'];
/**
* CSRF double-submit: the backend sets an XSRF-TOKEN cookie; echo it back in the
* X-XSRF-TOKEN header on state-changing requests. GET/HEAD need no token.
*/
function csrfHeaders(method?: string): Record<string, string> {
const m = (method ?? 'GET').toUpperCase();
if (m === 'GET' || m === 'HEAD') return {};
const c = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
return c ? { 'X-XSRF-TOKEN': decodeURIComponent(c[1]) } : {};
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
credentials: 'include',
...init,
headers: { ...(init?.headers ?? {}), ...csrfHeaders(init?.method) },
});
// Session expired / not authenticated on a protected call → send to login.
if (
res.status === 401 &&
!NO_REDIRECT_ON_401.some((p) => path.startsWith(p)) &&
typeof window !== 'undefined' &&
!['/login', '/kiosk'].includes(window.location.pathname) &&
!window.location.pathname.startsWith('/pass/')
) {
window.location.assign('/login');
}
let body: ApiResponse<T> | null = null;
try {
body = (await res.json()) as ApiResponse<T>;
} catch {
// non-JSON (shouldn't happen with our envelope)
}
if (!res.ok || !body) {
throw new Error(body?.message || `HTTP ${res.status}`);
}
return body.data as T;
}
function jsonInit(method: string, payload: unknown): RequestInit {
return {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
};
}
// ===== Auth =====
export const login = (req: LoginRequest) =>
request<CurrentUser>('/auth/login', jsonInit('POST', req));
export const logout = () =>
request<string>('/auth/logout', { method: 'POST' });
export const getCurrentUser = () => request<CurrentUser>('/auth/me');
export const changePassword = (req: ChangePasswordRequest) =>
request<string>('/auth/change-password', jsonInit('POST', req));
// ===== Zones =====
export const listZones = () => request<Zone[]>('/zones');
export const listPurposeCodes = () => request<PurposeCode[]>('/purpose-codes');
// ===== Visit requests =====
export const listVisitRequests = () =>
request<VisitRequestView[]>('/visit-requests');
export const listPendingRequests = () =>
request<VisitRequestView[]>('/visit-requests/pending');
export const getVisitRequest = (id: number) =>
request<VisitRequestView>(`/visit-requests/${id}`);
export const createVisitRequest = (req: VisitRequestCreate) =>
request<VisitRequestView[]>('/visit-requests', jsonInit('POST', req));
export const cancelVisitRequest = (id: number) =>
request<VisitRequestView>(`/visit-requests/${id}/cancel`, { method: 'POST' });
export const deleteVisitRequest = (id: number) =>
request<{ deleted: boolean }>(`/visit-requests/${id}`, { method: 'DELETE' });
export const uploadVisitRequests = (file: File) => {
const form = new FormData();
form.append('file', file);
return request<ExcelImportResult>('/visit-requests/upload', {
method: 'POST',
body: form,
});
};
// ===== Approvals =====
export const approveRequest = (id: number, comment?: string) =>
request<VisitRequestView>(`/approvals/${id}/approve`, jsonInit('POST', { comment }));
export const rejectRequest = (id: number, comment?: string) =>
request<VisitRequestView>(`/approvals/${id}/reject`, jsonInit('POST', { comment }));
// ===== Access console (check-in / out, currently inside) =====
export const searchApprovedForCheckIn = (q: string) =>
request<VisitRequestView[]>(`/access/search?q=${encodeURIComponent(q)}`);
export const checkIn = (payload: { qrToken?: string; visitRequestId?: number; gateId?: string }) =>
request<AccessAction>('/access/check-in', jsonInit('POST', payload));
export const checkOut = (payload: { qrToken?: string; visitRequestId?: number; gateId?: string }) =>
request<AccessAction>('/access/check-out', jsonInit('POST', payload));
export const listInside = () => request<InsideVisitor[]>('/access/inside');
export const listTodayAccess = () => request<AccessRecord[]>('/access/today');
// ===== Passes / badge =====
export const getPass = (id: number) => request<VisitRequestView>(`/passes/${id}`);
export const passQrUrl = (id: number) => `/api/passes/${id}/qr.png`;
// ===== Public pass + self-service kiosk (no login, token-based) =====
export const getPublicPass = (token: string) => request<PublicPass>(`/public/passes/${token}`);
export const publicPassQrUrl = (token: string) => `/api/public/passes/${token}/qr.png`;
export const publicCheckIn = (token: string) =>
request<AccessAction>(`/public/passes/${token}/check-in`, { method: 'POST' });
export const publicCheckOut = (token: string) =>
request<AccessAction>(`/public/passes/${token}/check-out`, { method: 'POST' });
// ===== Stats =====
export const getStatsSummary = () => request<StatsSummary>('/stats/summary');
// ===== Blacklist (ADMIN) =====
export const listBlacklist = () => request<BlacklistItem[]>('/blacklist');
export const addBlacklist = (req: BlacklistCreate) =>
request<BlacklistItem>('/blacklist', jsonInit('POST', req));
export const deleteBlacklist = (id: number) =>
request<string>(`/blacklist/${id}`, { method: 'DELETE' });
// ===== Reports =====
export const listReportVisits = (from: string, to: string) =>
request<VisitRequestView[]>(`/reports/visits?from=${from}&to=${to}`);
export const reportDownloadUrl = (from: string, to: string) =>
`/api/reports/visits.xlsx?from=${from}&to=${to}`;
// ===== Audit log (ADMIN) =====
export const listAudit = () => request<AuditLog[]>('/admin/audit');
// ===== Pass delivery outbox (ADMIN) =====
export const listDeliveries = (status?: DeliveryStatus) =>
request<PassDelivery[]>(`/admin/deliveries${status ? `?status=${status}` : ''}`);
export const retryDelivery = (id: number) =>
request<PassDelivery>(`/admin/deliveries/${id}/retry`, { method: 'POST' });
// ===== Admin management =====
export const listAdminPurposeCodes = () => request<PurposeCode[]>('/admin/purpose-codes');
export const createAdminPurposeCode = (payload: Omit<PurposeCode, 'id'>) =>
request<PurposeCode>('/admin/purpose-codes', jsonInit('POST', payload));
export const updateAdminPurposeCode = (id: number, payload: Omit<PurposeCode, 'id'>) =>
request<PurposeCode>(`/admin/purpose-codes/${id}`, jsonInit('PUT', payload));
export const getWatcher1Settings = () => request<Watcher1Settings>('/settings/watcher1');
export const updateWatcher1Settings = (payload: Watcher1Settings) =>
request<Watcher1Settings>('/admin/settings/watcher1', jsonInit('PUT', payload));
export const runSmsDiagnostics = () =>
request<SmsDiagnosticsResult>('/admin/sms/diagnostics', { method: 'POST' });
export const runEmailDiagnostics = () =>
request<EmailDiagnosticsResult>('/admin/email/diagnostics', { method: 'POST' });
export const resetVisitRequestTestData = (confirm: string) =>
request<VisitRequestResetResult>('/admin/visit-requests/reset-test-data', jsonInit('POST', { confirm }));
export const listAdminUsers = () => request<AdminUser[]>('/admin/users');
export const updateAdminUserRoles = (id: number, roles: Role[]) =>
request<AdminUser>(`/admin/users/${id}/roles`, jsonInit('PUT', { roles }));
export const listAdminTeams = () => request<Team[]>('/admin/teams');
export const createAdminTeam = (payload: Omit<Team, 'id'>) =>
request<Team>('/admin/teams', jsonInit('POST', payload));
export const updateAdminTeam = (id: number, payload: Omit<Team, 'id'>) =>
request<Team>(`/admin/teams/${id}`, jsonInit('PUT', payload));
export const updateAdminUserTeam = (id: number, teamId: number, applyDefaultRoles: boolean) =>
request<AdminUser>(`/admin/users/${id}/team`, jsonInit('PUT', { teamId, applyDefaultRoles }));
export const applyTeamDefaultRolesToUser = (id: number) =>
request<AdminUser>(`/admin/users/${id}/apply-team-default-roles`, { method: 'POST' });
export const applyTeamDefaultRolesToMembers = (teamId: number) =>
request<string>(`/admin/teams/${teamId}/apply-default-roles`, { method: 'POST' });
export const uploadAdminTeams = (file: File, dryRun: boolean) => {
const form = new FormData();
form.append('file', file);
return request<AdminExcelImportResult>(`/admin/teams/upload?dryRun=${dryRun}`, {
method: 'POST',
body: form,
});
};
export const uploadAdminUserRoles = (file: File, dryRun: boolean) => {
const form = new FormData();
form.append('file', file);
return request<AdminExcelImportResult>(`/admin/users/roles/upload?dryRun=${dryRun}`, {
method: 'POST',
body: form,
});
};
export const adminTeamTemplateUrl = () => '/api/admin/teams/template';
export const adminUserRolesTemplateUrl = () => '/api/admin/users/roles/template';