/**
* External Member Controller
*
* Contains all the business logic for external member operations:
* - Creating or updating external members in groups
* - Updating group memberships for existing external members
* - Managing membership migrations between groups
* - Deleting external members and their associated data
* - Publishing exit events when members leave organizations
*/
// @ts-check
/**
* @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
*/
import './../../types.js';
import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js'
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, phonetikArray, getConfig } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { queueAdd } from '../../tree/treeQueue/treeQueue.js';
import { addUpdateList, addUpdateEntry } from '../../server.ws.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { addToTree, migratePerson } from './migratePerson.js';
import _ from 'lodash';
import { encryptIbans } from '../../utils/crypto.js';
import { requestUpdateLogger, readLogger, errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { updatePerson, getPerson, personAdmin, personHistorie, personDuplicates } from '../person.js';
import {
updatePersonData,
handleGroupMembershipMigration,
publishGroupEvent,
updateGroupMembershipShared,
updatePersonEmbedding
} from './personHelpers.js';
/**
* Create or update external member (PUT /:group)
*
* Handles the creation or update of an "extern" member in a group.
* Validates input, fetches group and member data, renders and encrypts member data,
* and performs the necessary database operations (insert or update) depending on
* whether the extern already exists.
*/
/**
* Create or update external member (PUT /: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})` : '';
// Validate group parameter
if (!req.params.group) {
res.json({ success: false, message: 'group null or undefined' });
return;
}
const UIDgroup = UUID2hex(req.params.group);
const UIDextern = await getUID(req);
const template = Templates[req.session.root].extern;
// Fetch group data
const groups = await query(`SELECT ObjectBase.UID,ObjectBase.Title,Member.Display,
ObjectBase.dindex,Member.Data, ObjectBase.hierarchie, ObjectBase.gender,
ObjectBase.stage, ObjectBase.validFrom, ObjectBase.UIDBelongsTo
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE ObjectBase.UID=? `, [UIDgroup], { cast: ['json', 'UUID'] });
if (groups.length === 0) {
res.json({ success: false, message: 'invalid group UID' });
return;
}
const group = groups[0];
const groupData = group.Data || {};
// Validate UID format
if (!isValidUID(req.body.UID)) {
res.status(300).json({ success: false, message: 'invalid UID format' });
return;
}
// Render object data
const object = await renderObject(template, { ...req.body, group: groupData }, req);
object.Data = { ...req.body, UID: undefined };
encryptIbans(await getConfig('db', req), object.Data);
// Check if extern already exists
const externs = await query(`SELECT Member.UID,Member.Data,Links.UIDTarget AS MemberA, ObjectBase.Data AS BaseData,
ObjectBase.dindex, ObjectBase.Type, ObjectBase.SortName AS SortBase
FROM ObjectBase ${asOf}
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN Links ${asOf} ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID)
INNER JOIN ObjectBase ${asOf} AS MainGroup ON (Links.UIDTarget=MainGroup.UID AND MainGroup.Type='group')
WHERE ObjectBase.UID=? AND ObjectBase.Type IN ('person','extern')`,
[UIDextern], { cast: ['json'] }
);
if (externs.length === 0) {
// Create new extern
await createNewExtern(object, UIDgroup, req, timestamp);
} else {
// Update existing extern
await updateExistingExtern(externs[0], object, UIDgroup, req, timestamp);
}
// Update WebSocket clients
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: 'Internal server error' });
}
};
/**
* Helper function to create a new extern member
*/
const createNewExtern = async (object, UIDgroup, req, timestamp) => {
await transaction(async (connection) => {
await connection.query(`
INSERT INTO ObjectBase(UID,UIDuser,Type,SortName,UIDBelongsTo,Title,hierarchie,stage,gender,Data)
VALUES (?,?,'extern',?,?,?,?,?,?,'{}')
`,
[object.UID, UUID2hex(req.session.user), object.SortBase, object.UID, object.Title, 0, 0, object.gender],
);
await query(`
INSERT INTO Member(UID,Display,SortName, FullTextIndex,PhonetikIndex,Data)
VALUES (?,?,?,?,?,?)
`,
[
object.UID,
object.Display,
object.SortIndex,
object.FullTextIndex,
phonetikArray([object.Data.firstName, object.Data.lastName]),
JSON.stringify(object.Data)
]);
await connection.query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget)
VALUES (?,'memberA',?)`,
[object.UID, UIDgroup]);
}, { backDate: timestamp });
await addToTree(object.UID, UIDgroup, timestamp);
queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'extern', object.UID, object.UID, null, UIDgroup, timestamp);
await query(`INSERT INTO Visible (UID,Type,UIDUser) VALUES (?,'changeable',?)
ON DUPLICATE KEY UPDATE Type='changeable'`, [object.UID, UUID2hex(req.session.user)]);
await publishGroupEvent('new', object.UID,'extern',UIDgroup, req.session.root, timestamp);
// Asynchronously create AI embeddings (non-blocking)
updatePersonEmbedding(object.UID, object.Data, 'extern', UUID2hex(req.session.root)).catch(err => {
console.error('Background embedding creation failed:', err);
});
};
/**
* Helper function to update an existing extern member
*/
const updateExistingExtern = async (extern, object, UIDgroup, req, timestamp) => {
// Set extern-specific properties
object.Type = 'extern';
object.hierarchie = 0;
object.stage = 0;
// Use the shared updatePersonData helper function
await updatePersonData(object.UID, object.Data, object, req, timestamp);
// Extern-specific: Handle group migration if necessary
if (!extern.MemberA || !extern.MemberA.equals(UIDgroup) || extern.Type === 'person') {
await handleGroupMembershipMigration(extern, object.UID, UIDgroup, req, timestamp, 'extern');
}
};
/**
* Update group membership (POST /:group/:UID)
*
* Updates the group membership of an external member, mutating their membership if required.
* Handles the mutation of group membership for a person or extern, including updating
* their group, stage, hierarchy, and related data.
*/
/**
* Update group membership (POST /:group/:UID)
*
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const updateGroupMembership = async (req, res) => {
return updateGroupMembershipShared(req, res, 'extern', isObjectAdmin);
};
/**
* Delete external member (DELETE /:UID)
*
* Deletes a user and associated links from the database based on the provided UID.
* This removes all external member data, guest entries, jobs, and other associated records.
*/
/**
* Delete external member (DELETE /:UID)
*
* @param {ExpressRequestAuthorized} req
* @param {ExpressResponse} res
*/
export const deleteExtern = async (req, res) => {
try {
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
// Validate UID parameter
if (!req.params.UID) {
res.json({ success: false, message: 'UID null or undefined' });
return;
}
const UID = UUID2hex(req.params.UID);
const update = await query(`SELECT UIDTarget FROM Links WHERE UID = ? AND Links.Type IN ('member','memberA') `, [UID]);
// Delete all associated data
await query(`DELETE ObjectBase, Links
FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID OR Links.UIDTarget=ObjectBase.UID)
WHERE ObjectBase.UIDBelongsTo =? AND ObjectBase.Type IN ('extern','guest','job','entry')`,
[UID], { backDate: timestamp });
addUpdateList(update.map(el => el.UIDTarget));
res.json({ success: true });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};
/**
* Get external member details (GET /:UID)
* Delegates to the person controller since externs use the same data structure
*/
export const getExtern = getPerson;
/**
* Check admin rights for external member (GET /admin/:UID)
* Delegates to the person controller since admin checks are the same
*/
export const getExternAdmin = personAdmin;
/**
* Get external member history (GET /history/:UID)
* Delegates to the person controller since history tracking is the same
*/
export const getExternHistory = personHistorie;
/**
* Find duplicate external members (GET /duplicates/:firstName/:lastName)
* Delegates to the person controller since duplicate detection logic is the same
*/
export const getExternDuplicates = personDuplicates;
/**
* Update external member data (POST /:UID)
* Delegates to the person controller since data update logic is the same
*/
export const updateExtern = updatePerson;