133 lines
4.7 KiB
TypeScript
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();
|
|
};
|