Add curator authentication with audit logging and fix empty 3D gallery sessions.
Introduce session-based curator login, gate debug/checkup routes, log mutations to curator_audit_log, and keep guest hall preload public. Fix gallery view mounting so WebGL halls render reliably after navigation.
This commit is contained in:
@@ -10,12 +10,47 @@ import type {
|
||||
|
||||
const API = '/api';
|
||||
|
||||
async function fetchJson<T>(url: string): Promise<T> {
|
||||
const res = await fetch(url);
|
||||
const fetchCredentials: RequestInit = { credentials: 'include' };
|
||||
|
||||
export type AuthRole = 'user' | 'curator';
|
||||
|
||||
export interface AuthState {
|
||||
role: AuthRole;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, { ...fetchCredentials, ...init });
|
||||
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getAuthMe(): Promise<AuthState> {
|
||||
return fetchJson<AuthState>(`${API}/auth/me`);
|
||||
}
|
||||
|
||||
export async function loginCurator(username: string, password: string): Promise<AuthState> {
|
||||
const res = await fetch(`${API}/auth/login`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Login failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function logoutCurator(): Promise<void> {
|
||||
const res = await fetch(`${API}/auth/logout`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Logout failed: ${res.status}`);
|
||||
}
|
||||
|
||||
export function imageUrl(path: string | null | undefined): string {
|
||||
if (!path) return '/placeholder-art.svg';
|
||||
return `/images/${path}`;
|
||||
@@ -84,6 +119,7 @@ async function fileToBase64Payload(file: File): Promise<{ imageData: string; mim
|
||||
|
||||
async function postJsonImageAction<T>(url: string, payload: { imageData: string; mimeType: string }): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -96,7 +132,10 @@ async function postJsonImageAction<T>(url: string, payload: { imageData: string;
|
||||
}
|
||||
|
||||
export async function preloadArtistImages(artistId: number): Promise<{ fetched: number; total: number }> {
|
||||
const res = await fetch(`${API}/artists/${artistId}/preload-images`, { method: 'POST' });
|
||||
const res = await fetch(`${API}/artists/${artistId}/preload-images`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
});
|
||||
if (!res.ok) throw new Error('Preload failed');
|
||||
return res.json();
|
||||
}
|
||||
@@ -198,6 +237,7 @@ export const api = {
|
||||
context?: { searchUrl?: string; source?: string; pageUrl?: string; thumbUrl?: string }
|
||||
) =>
|
||||
fetch(`${API}/paintings/${id}/fix-image`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageUrl, ...context }),
|
||||
@@ -210,7 +250,7 @@ export const api = {
|
||||
}),
|
||||
|
||||
clearPaintingImage: (id: number) =>
|
||||
fetch(`${API}/paintings/${id}/clear-image`, { method: 'POST' }).then(async (res) => {
|
||||
fetch(`${API}/paintings/${id}/clear-image`, { ...fetchCredentials, method: 'POST' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Clear failed: ${res.status}`);
|
||||
@@ -219,7 +259,7 @@ export const api = {
|
||||
}),
|
||||
|
||||
deletePainting: (id: number) =>
|
||||
fetch(`${API}/paintings/${id}`, { method: 'DELETE' }).then(async (res) => {
|
||||
fetch(`${API}/paintings/${id}`, { ...fetchCredentials, method: 'DELETE' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Remove failed: ${res.status}`);
|
||||
@@ -239,6 +279,7 @@ export const api = {
|
||||
flags: { checked?: boolean; fixed?: boolean }
|
||||
) =>
|
||||
fetch(`${API}/paintings/${id}/checkup-flags`, {
|
||||
...fetchCredentials,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(flags),
|
||||
@@ -262,6 +303,7 @@ export const api = {
|
||||
context?: { searchUrl?: string; source?: string; pageUrl?: string; thumbUrl?: string }
|
||||
) =>
|
||||
fetch(`${API}/artists/${id}/fix-portrait`, {
|
||||
...fetchCredentials,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imageUrl, ...context }),
|
||||
@@ -274,7 +316,7 @@ export const api = {
|
||||
}),
|
||||
|
||||
clearArtistPortrait: (id: number) =>
|
||||
fetch(`${API}/artists/${id}/clear-portrait`, { method: 'POST' }).then(async (res) => {
|
||||
fetch(`${API}/artists/${id}/clear-portrait`, { ...fetchCredentials, method: 'POST' }).then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.error || `Clear failed: ${res.status}`);
|
||||
@@ -292,6 +334,7 @@ export const api = {
|
||||
flags: { checked?: boolean; fixed?: boolean }
|
||||
) =>
|
||||
fetch(`${API}/artists/${id}/checkup-flags`, {
|
||||
...fetchCredentials,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(flags),
|
||||
|
||||
Reference in New Issue
Block a user