/** * 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 => { 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 => { 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 => { 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(); };