/**
* External Member Controller — HTTP Request/Response Handling
*
* Extracts request parameters and delegates all business logic to externService.
* Read/shared operations reuse the person controller directly since externs and
* persons share the same data structure.
*/
// @ts-check
/**
* @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
*/
import { UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, getConfig } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { addUpdateList, addUpdateEntry } from '../../server.ws.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { encryptIbans } from '../../utils/crypto.js';
import { errorLoggerUpdate } from '../../utils/requestLogger.js';
import { updateGroupMembershipShared } from './personHelpers.js';
import { updatePerson, getPerson, personAdmin, personHistorie, personDuplicates } from '../person.js';
import * as externService from './externService.js';
// ---------------------------------------------------------------------------
// Mutation handlers
// ---------------------------------------------------------------------------
/**
* PUT /:group — Create or update an extern in a group.
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const createOrUpdateExtern = async (req, res) => {
try {
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';
const UIDgroup = UUID2hex(req.params.group);
const UIDextern = await getUID(req);
const template = Templates[req.session.root].extern;
const group = await externService.fetchGroup(UIDgroup);
if (!group) {
res.json({ success: false, message: 'invalid group UID' });
return;
}
if (!isValidUID(req.body.UID)) {
res.status(300).json({ success: false, message: 'invalid UID format' });
return;
}
const object = await renderObject(template, { ...req.body, group: group.Data || {} }, req);
object.Data = { ...req.body, UID: undefined };
encryptIbans(await getConfig('db', req), object.Data);
const externs = await externService.fetchExternExists(UIDextern, asOf);
// Safeguard: when changing an existing person → extern with a backdate,
// the timestamp must not land before the latest ObjectBase history row.
if (externs.length > 0 && timestamp && externs[0].Type !== 'extern') {
const latestValidFrom = await externService.fetchLatestObjectValidFrom(UIDextern);
if (latestValidFrom !== null && timestamp < latestValidFrom) {
res.status(300).json({
success: false,
message: `Cannot backdate to ${timestamp}: type change (person → extern) must not precede the latest existing history row (${latestValidFrom}).`,
});
return;
}
}
if (externs.length === 0) {
await externService.createNewExtern(object, UIDgroup, req.session.user, req.session.root, timestamp);
} else {
await externService.updateExistingExtern(externs[0], object, UIDgroup, req, timestamp);
}
addUpdateEntry(UIDextern, {
person: { Type: 'extern', ...object, Data: { ...req.body, UID: undefined }, UID: HEX2uuid(object.UID) },
parent: group,
});
addUpdateList([UIDgroup, UIDextern]);
res.json({ success: true, result: { ...object, UID: HEX2uuid(object.UID) } });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: e.message });
}
};
/**
* POST /:group/:UID — Update group membership for an extern.
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const updateGroupMembership = async (req, res) => {
return updateGroupMembershipShared(req, res, 'extern', isObjectAdmin);
};
/**
* DELETE /:UID — Delete an extern and all associated data.
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const deleteExtern = async (req, res) => {
try {
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
const UID = UUID2hex(req.params.UID);
const affectedGroups = await externService.deleteExternById(UID, timestamp);
addUpdateList(affectedGroups);
res.json({ success: true });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: e.message });
}
};
/**
* POST /rebuildAccess/:UID — Rebuild visibility and list access for an extern.
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const rebuildExternAccess = async (req, res) => {
try {
const UID = UUID2hex(req.params.UID);
await externService.rebuildExternAccess(UID, req.session.root);
res.json({ success: true });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, error: e.message });
}
};
// ---------------------------------------------------------------------------
// Delegations — externs share data structure and read logic with persons
// ---------------------------------------------------------------------------
/** GET /:UID — Delegates to person controller (same data structure) */
export const getExtern = getPerson;
/** GET /admin/:UID — Delegates to person controller (same admin check) */
export const getExternAdmin = personAdmin;
/** GET /history/:UID — Delegates to person controller (same history logic) */
export const getExternHistory = personHistorie;
/** GET /duplicates/:firstName/:lastName — Delegates to person controller */
export const getExternDuplicates = personDuplicates;
/** POST /:UID — Partial update; delegates to person controller (same structure) */
export const updateExtern = updatePerson;