mono/packages/ui/src/modules/user/client-acl.ts
2026-02-20 13:37:11 +01:00

133 lines
4.7 KiB
TypeScript

/**
* client-acl.ts — Client-side ACL API wrappers
*
* Resource-agnostic: works with any resourceType (vfs, layout, page, etc.)
* Follows the pattern in client-user.ts (fetchWithDeduplication, getAuthToken).
*/
import { supabase as defaultSupabase } from "@/integrations/supabase/client";
import { fetchWithDeduplication, parseQueryKey } from "@/lib/db";
import { queryClient } from "@/lib/queryClient";
// =============================================
// Types (mirrors server-side AclEntry / AclSettings)
// =============================================
export interface AclEntry {
userId?: string;
group?: string;
path?: string;
permissions: string[];
}
export interface AclSettings {
owner: string;
groups?: { name: string; members: string[] }[];
acl: AclEntry[];
}
// =============================================
// Helpers
// =============================================
const getAuthToken = async (): Promise<string> => {
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
if (!token) throw new Error('Not authenticated');
return token;
};
const serverUrl = (): string =>
(import.meta as any).env?.VITE_SERVER_IMAGE_API_URL || '';
// =============================================
// Read
// =============================================
/** Fetch ACL settings for a resource. Cached via React Query / fetchWithDeduplication. */
export const fetchAclSettings = async (
resourceType: string,
resourceId: string,
): Promise<AclSettings> => {
return fetchWithDeduplication(
`acl-${resourceType}-${resourceId}`,
async () => {
const token = await getAuthToken();
const res = await fetch(
`${serverUrl()}/api/acl/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!res.ok) {
if (res.status === 404) return { owner: '', groups: [], acl: [] };
throw new Error(`Failed to fetch ACL: ${res.statusText}`);
}
return await res.json();
},
30000, // 30s stale time
);
};
/** Convenience: fetch just the entries array. */
export const fetchAclEntries = async (
resourceType: string,
resourceId: string,
): Promise<AclEntry[]> => {
const settings = await fetchAclSettings(resourceType, resourceId);
return settings.acl;
};
// =============================================
// Write
// =============================================
/** Grant permissions on a resource. Invalidates local cache. */
export const grantAclPermission = async (
resourceType: string,
resourceId: string,
grant: { userId?: string; group?: string; path?: string; permissions: string[] },
): Promise<{ success: boolean; settings: AclSettings }> => {
const token = await getAuthToken();
const res = await fetch(
`${serverUrl()}/api/acl/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/grant`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(grant),
},
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).error || `Failed to grant: ${res.statusText}`);
}
queryClient.invalidateQueries({ queryKey: parseQueryKey(`acl-${resourceType}-${resourceId}`) });
return await res.json();
};
/** Revoke permissions on a resource. Invalidates local cache. */
export const revokeAclPermission = async (
resourceType: string,
resourceId: string,
target: { userId?: string; group?: string; path?: string },
): Promise<{ success: boolean; settings: AclSettings }> => {
const token = await getAuthToken();
const res = await fetch(
`${serverUrl()}/api/acl/${encodeURIComponent(resourceType)}/${encodeURIComponent(resourceId)}/revoke`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(target),
},
);
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error((err as any).error || `Failed to revoke: ${res.statusText}`);
}
queryClient.invalidateQueries({ queryKey: parseQueryKey(`acl-${resourceType}-${resourceId}`) });
return await res.json();
};