feat(mounts): P4 — admin CRUD endpoints + frontend admin tab

Mounts are now configurable at runtime without DB surgery.

Backend (admin-gated, /api/admin/external-mounts):
- GET    list all configured mounts
- POST   create: validate the provider config up front, create a mount-root
         folder under the admin's drive, insert the row, hot-reload the registry
- DELETE remove the mount row + its root folder (host content is left intact),
         then hot-reload
- ExternalMountRepositoryPort gains create/delete (PG impl); registry.reload()
  runs in-process so changes are live immediately
- audit lines event="external_mount.config" action=create|delete

Frontend:
- admin.ts endpoints: listExternalMounts / createExternalMount / deleteExternalMount
- new "External Mounts" tab in the admin page: add form (name + host path +
  read-only) and a list with delete

Integration test for the repo create/delete round-trip (testcontainers).
This commit is contained in:
Bradley Nelson
2026-06-25 01:07:03 -06:00
parent d8229650ef
commit 07320a3925
7 changed files with 510 additions and 4 deletions
+49
View File
@@ -512,3 +512,52 @@ export function getPluginLogs(
credentials: 'same-origin'
});
}
// ── External file mounts ────────────────────────────────────────────────────
/** A configured external mount as returned by the admin API. */
export interface ExternalMount {
mount_folder_id: string;
name: string;
kind: string;
owner_id: string;
read_only: boolean;
drive_id: string;
mount_path: string;
config: Record<string, unknown>;
}
/** Request body for creating an external mount. */
export interface CreateExternalMountInput {
name: string;
host_path: string;
kind?: string;
read_only?: boolean;
}
/** GET /api/admin/external-mounts — list all configured mounts. */
export function listExternalMounts(): Promise<ExternalMount[]> {
return apiJson<ExternalMount[]>('/api/admin/external-mounts', {
credentials: 'same-origin'
});
}
/** POST /api/admin/external-mounts — create a mount in the admin's drive. */
export async function createExternalMount(input: CreateExternalMountInput): Promise<ExternalMount> {
const res = await apiFetch('/api/admin/external-mounts', {
method: 'POST',
credentials: 'same-origin',
headers: { ...JSON_HEADERS, ...getCsrfHeaders() },
body: JSON.stringify(input)
});
if (!res.ok) {
const e = (await res.json().catch(() => ({}))) as { message?: string };
throw new Error(e.message || `Create mount failed: ${res.status}`);
}
return (await res.json()) as ExternalMount;
}
/** DELETE /api/admin/external-mounts/{id} — remove a mount (host content kept). */
export function deleteExternalMount(mountFolderId: string): Promise<void> {
return mutate(`/api/admin/external-mounts/${mountFolderId}`, 'DELETE');
}