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
@@ -168,11 +168,46 @@ pub struct ExternalMountRecord {
pub mount_path: String,
}
/// The persistable columns of an `external_mounts` row (admin create).
#[derive(Debug, Clone)]
pub struct NewExternalMount {
/// The mount-root folder UUID this mount attaches to.
pub mount_folder_id: Uuid,
/// Provider kind.
pub kind: String,
/// Provider-specific connection config.
pub config: serde_json::Value,
/// Display name.
pub name: String,
/// Owner of the mount configuration.
pub owner_id: Uuid,
/// Whether the mount is read-only.
pub read_only: bool,
}
/// Persistence port for external mount configuration.
#[async_trait]
pub trait ExternalMountRepositoryPort: Send + Sync {
/// Load every (non-trashed) mount joined with its folder, for registry build.
async fn list_all(&self) -> Result<Vec<ExternalMountRecord>, DomainError>;
/// Insert a new mount row. Default errors — only the PG repo implements it
/// (test doubles need `list_all` only).
async fn create(&self, _mount: &NewExternalMount) -> Result<(), DomainError> {
Err(DomainError::operation_not_supported(
"ExternalMount",
"create is not supported by this repository",
))
}
/// Delete a mount row by its mount-root folder id. Returns `true` when a
/// row was removed. Default errors (see `create`).
async fn delete(&self, _mount_folder_id: Uuid) -> Result<bool, DomainError> {
Err(DomainError::operation_not_supported(
"ExternalMount",
"delete is not supported by this repository",
))
}
}
/// Builds [`ExternalMountProvider`]s from a `kind` + `config` pair.