Source: utils/actionPermissions.js

// @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;
    }
};