2026-05-22 10:15:25 +02:00
|
|
|
|
/**
|
2026-05-29 02:16:15 +02:00
|
|
|
|
* @import {Grant, ResourceTypeEnum, SharedWithMeResponse, OutgoingResourcesResponse} from '../core/types.js'
|
2026-05-22 10:15:25 +02:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-05-25 16:54:50 +02:00
|
|
|
|
import { getCsrfHeaders } from '../core/csrf.js';
|
|
|
|
|
|
|
2026-05-22 10:15:25 +02:00
|
|
|
|
const grants = {
|
|
|
|
|
|
/** @type {Record<String, Record<String, Grant[]>>} */
|
|
|
|
|
|
outgoingGrants: {},
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Record<String, Record<String, Grant[]>>} */
|
|
|
|
|
|
incomingGrants: {},
|
|
|
|
|
|
|
|
|
|
|
|
async fetchOutgoingGrants() {
|
|
|
|
|
|
const response = await fetch('/api/grants/outgoing');
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2026-05-25 16:54:50 +02:00
|
|
|
|
console.error(`error ${response.status} while fetching /api/grants/outgoing`);
|
2026-05-22 10:15:25 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Grant[]} */
|
|
|
|
|
|
const outgoingGrants = await response.json();
|
|
|
|
|
|
|
2026-05-25 16:54:50 +02:00
|
|
|
|
// Reset and rebuild cache
|
|
|
|
|
|
this.outgoingGrants = {};
|
2026-05-22 10:15:25 +02:00
|
|
|
|
|
|
|
|
|
|
// store grants by type, then by id
|
|
|
|
|
|
outgoingGrants.forEach((grant) => {
|
|
|
|
|
|
this.outgoingGrants[grant.resource.type] ??= {};
|
|
|
|
|
|
this.outgoingGrants[grant.resource.type][grant.resource.id] ??= [];
|
|
|
|
|
|
this.outgoingGrants[grant.resource.type][grant.resource.id].push(grant);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* get grant for a resource
|
|
|
|
|
|
* @param {ResourceTypeEnum} resourceType
|
|
|
|
|
|
* @param {String} id
|
|
|
|
|
|
* @returns {Grant[] | null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
getOutgoingGrantsFor(resourceType, id) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return this.outgoingGrants[resourceType][id] ?? [];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async fetchIncomingGrants() {
|
|
|
|
|
|
const response = await fetch('/api/grants/incoming');
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
2026-05-25 16:54:50 +02:00
|
|
|
|
console.error(`error ${response.status} while fetching /api/grants/incoming`);
|
2026-05-22 10:15:25 +02:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Grant[]} */
|
|
|
|
|
|
const incomingGrants = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
// store grants by type, then by id
|
|
|
|
|
|
incomingGrants.forEach((grant) => {
|
|
|
|
|
|
this.incomingGrants[grant.resource.type] ??= {};
|
|
|
|
|
|
this.incomingGrants[grant.resource.type][grant.resource.id] ??= [];
|
|
|
|
|
|
this.incomingGrants[grant.resource.type][grant.resource.id].push(grant);
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* get grant for a resource
|
|
|
|
|
|
* @param {ResourceTypeEnum} resourceType
|
|
|
|
|
|
* @param {String} id
|
|
|
|
|
|
* @returns {Grant[] | null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
getIncomingGrantsFor(resourceType, id) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return this.incomingGrants[resourceType][id] ?? [];
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Fetch a cursor-paginated list of resources shared with the current user,
|
|
|
|
|
|
* with full file / folder metadata resolved server-side.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {object} [opts]
|
|
|
|
|
|
* @param {ResourceTypeEnum[]} [opts.resourceTypes] - Resource types to include (default: ['file','folder']).
|
|
|
|
|
|
* @param {number} [opts.limit] - Max items per page (1–200, default 50).
|
|
|
|
|
|
* @param {string} [opts.cursor] - Opaque cursor from a previous call; omit for first page.
|
2026-05-27 00:32:10 +02:00
|
|
|
|
* @param {string} [opts.orderBy] - Sort dimension: 'granted_at' | 'granted_by' (default: 'granted_at').
|
2026-05-28 00:44:20 +02:00
|
|
|
|
* @param {boolean} [opts.reverse] - Reverse the sort order (default: false).
|
2026-05-22 10:15:25 +02:00
|
|
|
|
* @returns {Promise<SharedWithMeResponse>}
|
|
|
|
|
|
*/
|
2026-05-28 00:44:20 +02:00
|
|
|
|
async fetchSharedWithMe({ resourceTypes = ['file', 'folder'], limit = 50, cursor, orderBy, reverse = false } = {}) {
|
2026-05-22 10:15:25 +02:00
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
|
limit: String(limit),
|
|
|
|
|
|
resource_types: resourceTypes.join(',')
|
|
|
|
|
|
});
|
|
|
|
|
|
if (cursor) params.set('cursor', cursor);
|
2026-05-27 00:32:10 +02:00
|
|
|
|
if (orderBy) params.set('sort_by', orderBy);
|
2026-05-28 00:44:20 +02:00
|
|
|
|
if (reverse) params.set('reverse', 'true');
|
2026-05-22 10:15:25 +02:00
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/grants/incoming/resources?${params}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Failed to fetch shared-with-me items: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response.json();
|
2026-05-25 16:54:50 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-29 02:16:15 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Fetch a cursor-paginated list of resources the current user has shared
|
|
|
|
|
|
* with others, with full file / folder metadata resolved server-side.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {object} [opts]
|
|
|
|
|
|
* @param {number} [opts.limit] - Max items per page (1–200, default 50).
|
|
|
|
|
|
* @param {string} [opts.cursor] - Opaque cursor from a previous call.
|
|
|
|
|
|
* @param {string} [opts.orderBy] - Sort: 'first_shared_at' | 'name' | 'type' | 'subject'.
|
|
|
|
|
|
* @param {boolean} [opts.reverse] - Reverse sort order.
|
|
|
|
|
|
* @returns {Promise<OutgoingResourcesResponse>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fetchMySharesPage({ limit = 50, cursor, orderBy, reverse = false } = {}) {
|
|
|
|
|
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
|
|
|
|
if (cursor) params.set('cursor', cursor);
|
|
|
|
|
|
if (orderBy) params.set('sort_by', orderBy);
|
|
|
|
|
|
if (reverse) params.set('reverse', 'true');
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/grants/outgoing/resources?${params}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Failed to fetch my shares: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-05-25 16:54:50 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Fetch all grants on a specific resource (for the "Manage sharing" panel).
|
|
|
|
|
|
* Refreshes the outgoingGrants cache for this resource.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {ResourceTypeEnum} resourceType
|
|
|
|
|
|
* @param {string} resourceId
|
|
|
|
|
|
* @returns {Promise<Grant[]>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async fetchGrantsForResource(resourceType, resourceId) {
|
|
|
|
|
|
const params = new URLSearchParams({ resource_type: resourceType, resource_id: resourceId });
|
|
|
|
|
|
const response = await fetch(`/api/grants?${params}`, { credentials: 'same-origin' });
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`fetchGrantsForResource: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @type {Grant[]} */
|
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
|
|
|
|
|
|
|
// Refresh the outgoing cache for this resource
|
|
|
|
|
|
this.outgoingGrants[resourceType] ??= {};
|
|
|
|
|
|
this.outgoingGrants[resourceType][resourceId] = result;
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create a new grant.
|
|
|
|
|
|
* Body mirrors `CreateGrantDto`: `{ subject, resource, role }` OR `{ subject, resource, permissions }`.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object} dto - CreateGrantDto shape
|
|
|
|
|
|
* @returns {Promise<Grant[]>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async createGrant(dto) {
|
|
|
|
|
|
const response = await fetch('/api/grants', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
credentials: 'same-origin',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
|
|
|
|
|
body: JSON.stringify(dto)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const body = await response.json().catch(() => ({}));
|
|
|
|
|
|
throw new Error(body.error || `createGrant: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Reconcile a subject's role on a resource (replaces all their permissions).
|
|
|
|
|
|
* Body mirrors `UpdateRoleDto`: `{ subject, resource, role }`.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {Object} dto - UpdateRoleDto shape
|
|
|
|
|
|
* @returns {Promise<Grant[]>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async updateRole(dto) {
|
|
|
|
|
|
const response = await fetch('/api/grants/role', {
|
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
|
credentials: 'same-origin',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json', ...getCsrfHeaders() },
|
|
|
|
|
|
body: JSON.stringify(dto)
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
const body = await response.json().catch(() => ({}));
|
|
|
|
|
|
throw new Error(body.error || `updateRole: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Revoke a single grant by its UUID.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {string} grantId
|
|
|
|
|
|
* @returns {Promise<void>}
|
|
|
|
|
|
*/
|
|
|
|
|
|
async revokeGrant(grantId) {
|
|
|
|
|
|
const response = await fetch(`/api/grants/${encodeURIComponent(grantId)}`, {
|
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
|
credentials: 'same-origin',
|
|
|
|
|
|
headers: getCsrfHeaders()
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`revokeGrant: HTTP ${response.status}`);
|
|
|
|
|
|
}
|
2026-05-22 10:15:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export { grants };
|