// @ts-check
/**
* Generic action-based permissions derived from jobs and function templates.
*
* A function template may declare actions it grants (e.g. `project.create`):
*
* ```json
* {
* "actions": ["project.create"]
* }
* ```
*
* A person holds the action if any of her current jobs (within the current
* organization) links to a function template granting the action. The right is
* evaluated on demand and is NOT materialized in `Visible` - at creation time no
* target object exists yet.
*
* @import {ExpressRequestAuthorized} from '../types.js'
*/
import { query, UUID2hex } from '@commtool/sql-query';
import { isAdmin } from './authChecks.js';
import { errorLoggerRead } from './requestLogger.js';
/** Actions that function templates may grant (extend as new rights are added). */
export const KNOWN_ACTIONS = new Set(['project.create']);
/**
* Katalog der von Funktionsvorlagen gewährbaren Aktionsrechte mit fachlicher
* Beschreibung. Wird über `GET /api/kpe20/function/actions` an die Admin-App
* ausgeliefert (Phase 3: "Die Auswahl wird aus einem Backend-Katalog geladen").
* @returns {{key: string, description: string}[]}
*/
export const getActionCatalog = () => [
{ key: 'project.create', description: 'Darf im Organisationskontext Projekte anlegen' },
];
/**
* Sanitize an actions payload from a function template. Malformed entries
* (non-strings) are dropped. Unknown *string* actions are preserved: the
* server only ever *grants* actions it knows (`hasActionPermission`), stored
* unknown actions are inert and stay available for forward compatibility.
* @param {any} actions
* @returns {string[]}
*/
export const sanitizeActions = (actions) => {
if (!Array.isArray(actions)) return [];
return actions.filter((a) => typeof a === 'string' && a.length > 0);
};
/**
* Load the action names granted to a person through her jobs in an organization.
* @param {string} personUIDhex - person UID as hex string
* @param {string} orgUIDhex - organization UID as hex string
* @returns {Promise<Set<string>>}
*/
export const getPersonActions = async (personUIDhex, orgUIDhex) => {
try {
const rows = await query(
`SELECT Fn.Data
FROM ObjectBase AS Job
INNER JOIN Links AS FLink ON (FLink.UIDTarget = Job.UID AND FLink.Type = 'function')
INNER JOIN ObjectBase AS Fn ON (Fn.UID = FLink.UID AND Fn.Type = 'function' AND Fn.UIDBelongsTo = ?)
WHERE Job.Type = 'job' AND Job.UIDBelongsTo = ?`,
[orgUIDhex, personUIDhex],
{ cast: ['json'] },
);
const actions = new Set();
for (const row of rows) {
const list = row.Data?.actions;
if (Array.isArray(list)) {
for (const action of list) {
if (typeof action === 'string' && action) actions.add(action);
}
}
}
return actions;
} catch (e) {
errorLoggerRead(e);
return new Set();
}
};
/**
* Whether the current user has a given action right (e.g. `project.create`)
* in the current organization context.
* @param {ExpressRequestAuthorized} req
* @param {string} action - action name, e.g. 'project.create'
* @returns {Promise<boolean>}
*/
export const hasActionPermission = async (req, action) => {
try {
if (await isAdmin(req.session)) return true;
if (!req.session?.root || !req.session?.user) return false;
const personUID = UUID2hex(req.session.user);
const orgUID = UUID2hex(req.session.root);
const actions = await getPersonActions(personUID, orgUID);
return actions.has(action);
} catch (e) {
errorLoggerRead(e);
return false;
}
};