feat(drive): policies management from UI

This commit is contained in:
Edouard Vanbelle
2026-06-26 18:08:34 +02:00
parent 66f2aaa250
commit 26d3c692ba
20 changed files with 720 additions and 80 deletions
+37
View File
@@ -13,6 +13,8 @@ import type {
Drive,
DriveMember,
DriveMemberSubject,
DrivePolicies,
DrivePoliciesPartial,
DriveRole
} from '$lib/api/types';
@@ -130,6 +132,41 @@ export async function deleteDrive(driveId: string): Promise<void> {
}
}
/**
* `PATCH /api/drives/{id}/policies` — update drive policies (D5).
*
* **OxiCloud-admin only.** Owners cannot mutate policies — the carve-out
* exists because policies are a compliance surface (an owner who could
* flip them would defeat the gates by disabling, sharing, re-enabling).
* Non-admin callers receive 404 (anti-enum). The frontend only surfaces
* this from the admin panel.
*
* Body is a partial — keys not present are left untouched at the JSONB
* merge layer. Returns the post-merge typed view.
*/
export async function updateDrivePolicies(
driveId: string,
partial: DrivePoliciesPartial
): Promise<DrivePolicies> {
const res = await apiFetch(`/api/drives/${encodeURIComponent(driveId)}/policies`, {
method: 'PATCH',
headers: { ...JSON_HEADERS, ...getCsrfHeaders() },
credentials: 'same-origin',
body: JSON.stringify(partial)
});
if (!res.ok) {
let detail = '';
try {
const parsed = (await res.json()) as { error?: string; message?: string };
detail = parsed.error ?? parsed.message ?? '';
} catch {
/* response body wasn't JSON */
}
throw new Error(detail || `update policies failed: ${res.status}`);
}
return (await res.json()) as DrivePolicies;
}
/**
* `DELETE /api/drives/{id}/members/{kind}/{sid}` — remove a member.
* Idempotent (removing a non-member returns 204). Refused with 400 if it
+27
View File
@@ -237,12 +237,39 @@ export interface Drive {
root_folder_id: string;
quota_bytes?: number | null;
used_bytes: number;
/**
* Drive policies — raw JSONB bag from the backend. Unknown keys are
* preserved verbatim. For the typed view used by the admin policy
* editor, see [`DrivePolicies`].
*/
policies: Record<string, unknown>;
created_at: string;
updated_at: string;
caller_role?: DriveRole | null;
}
/**
* Typed mirror of the five known D5 policy keys. Every field defaults to
* `false` (= allowed). The wire shape returned by
* `PATCH /api/drives/{id}/policies` carries all five keys; the request
* body uses [`DrivePoliciesPartial`] so unsupplied keys aren't disturbed.
*
* See `docs/plan/drive.md` §8 for what each key gates.
*/
export interface DrivePolicies {
forbid_sharing: boolean;
forbid_external_sharing: boolean;
forbid_public_links: boolean;
forbid_cross_drive_move: boolean;
forbid_owner_role_change: boolean;
}
/**
* Body shape for the admin policy editor — every key optional so omitting
* a field leaves that policy untouched (the backend uses a JSONB merge).
*/
export type DrivePoliciesPartial = Partial<DrivePolicies>;
/**
* Request body for `POST /api/drives` (D3a). Mirrors `CreateDriveDto` in
* `src/interfaces/api/handlers/drive_handler.rs`. `kind: 'personal'` is a