forked from EduCraft/curriculum-project-hub
12628c9233
- scaffold hub/database-admin as SvelteKit 2 + Svelte 5 static SPA
with aurora/glass visual style (paths.base='/database')
- add lib/{api,session,org}.ts + Aurora.svelte component
- add routes: root redirect, /admin login page, /dashboard (OWNER/ADMIN only)
- backend: replace server-rendered HTML routes with /database/config JSON endpoint
- add hub/src/database/static.ts to serve SPA under /database/*
- wire registerDatabaseSpa into plugin.ts
- exempt /database/* from silo rate-limit (same treatment as /admin/*)
- add database:dev + database:build npm scripts; update deploy scripts
- update hub/src/database/README.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
77 lines
1.9 KiB
TypeScript
77 lines
1.9 KiB
TypeScript
/**
|
|
* Thin API client for the database-admin backend. Same-origin cookie auth,
|
|
* reusing the platform session (`cph_session`) and the admin plane's /api/me.
|
|
*/
|
|
|
|
export class ApiError extends Error {
|
|
code: string;
|
|
status: number;
|
|
constructor(code: string, message: string, status: number) {
|
|
super(message);
|
|
this.name = 'ApiError';
|
|
this.code = code;
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
async function request(method: string, url: string, body?: unknown): Promise<unknown> {
|
|
const init: RequestInit = {
|
|
method,
|
|
credentials: 'same-origin',
|
|
headers: body !== undefined ? { 'content-type': 'application/json' } : undefined,
|
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
};
|
|
const res = await fetch(url, init);
|
|
const text = await res.text();
|
|
let data: unknown = null;
|
|
if (text !== '') {
|
|
try {
|
|
data = JSON.parse(text);
|
|
} catch {
|
|
data = text;
|
|
}
|
|
}
|
|
if (!res.ok) {
|
|
const err = (data as { error?: { code?: string; message?: string } } | null)?.error;
|
|
throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
const get = (u: string) => request('GET', u);
|
|
const post = (u: string, b?: unknown) => request('POST', u, b);
|
|
|
|
// --- Types ---
|
|
|
|
export interface OrgMembership {
|
|
id: string;
|
|
slug: string;
|
|
name: string;
|
|
status: string;
|
|
role: 'OWNER' | 'ADMIN' | 'MEMBER';
|
|
}
|
|
|
|
export interface MeResponse {
|
|
user: {
|
|
id: string;
|
|
feishuOpenId: string;
|
|
displayName: string;
|
|
avatarUrl: string | null;
|
|
};
|
|
organizations: OrgMembership[];
|
|
}
|
|
|
|
/** Unauthenticated bootstrap the login page needs: which org to OAuth against + dev toggle. */
|
|
export interface DatabaseConfig {
|
|
siloOrganizationSlug: string;
|
|
devLoginEnabled: boolean;
|
|
}
|
|
|
|
// --- API ---
|
|
|
|
export const api = {
|
|
me: () => get('/api/me') as Promise<MeResponse>,
|
|
logout: () => post('/auth/logout'),
|
|
databaseConfig: () => get('/database/config') as Promise<DatabaseConfig>,
|
|
};
|