/**
* Guest Controller
*
* Contains all the business logic for guest operations:
* - Adding individual guests to groups
* - Bulk adding guests to groups
* - Creating guest groups from existing groups
* - Retrieving guests from groups
* - Deleting guests and guest groups
*
* All functions handle both regular guests (person) and guest groups (ggroup).
*/
// @ts-check
/**
* @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
*/
import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, configs } from '../../utils/compileTemplates.js';
import { getUID, isValidUID } from '../../utils/UUIDs.js';
import { queueAdd, queueAddArray } from '../../tree/treeQueue/treeQueue.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { parseTimestampToSeconds } from '../../utils/parseTimestamp.js';
/**
* Fügt mehrere Gäste oder Gastgruppen zu einer Gruppe hinzu.
* @param {Array} newGuests - Array mit UIDs der Personen/Gruppen, die als Gäste hinzugefügt werden sollen
* @param {Buffer} UIDgroup - Zielgruppe (als Hex)
* @param {string} type - 'guest' oder 'ggroup'
* @param {string} root - Root-Organisation
* @param {number|null} timestamp - Optionaler Zeitstempel für Backdating
* @param {string|null} UIDuser - Optional: User, der die Aktion ausführt
* @returns {Promise<Array>} Liste der neu angelegten Gäste/Gastgruppen
*/
export const addGuests = async (newGuests, UIDgroup, type, root, timestamp = null, UIDuser = null) => {
try {
// Lade das passende Template für Gäste oder Gastgruppen
const template = Templates[HEX2uuid(root)][type];
let objects;
if (type === 'guest') {
// Für jeden neuen Gast: Prüfe, ob schon ein Gast existiert, sonst UID generieren
objects = await Promise.all(newGuests.map(guest => {
const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';
return query(`SELECT Member.UID,Member.Data,IF(guest.UID,guest.UID,UIDV1()) AS UIDnew , ? AS timestamp,
guest.UID AS UIDguest
FROM Member
LEFT JOIN (ObjectBase ${asOf} AS guest
INNER JOIN Links ${asOf} ON (Links.UID=guest.UID AND Links.Type IN ('member','memberA') AND Links.UIDTarget=?)
) ON (guest.UIDBelongsTo=Member.UID AND guest.Type='guest')
WHERE Member.UID =?
`, [timestamp, UIDgroup, guest], { log: true, cast: ['json'] });
}));
}
if (type === 'ggroup') {
// Für Gastgruppen analog, aber Typ 'ggroup'
objects = await Promise.all(newGuests.map(guest => {
const asOf = timestamp ? `FOR SYSTEM_TIME AS OF FROM_UNIXTIME(${timestamp})` : '';
return query(`SELECT Member.UID,Member.Data,IF(guest.UID,guest.UID,UIDV1()) AS UIDnew , ? AS timestamp,
guest.UID AS UIDguest
FROM Member
LEFT JOIN (ObjectBase ${asOf} AS guest
INNER JOIN Links ${asOf} ON (Links.UID=guest.UID AND Links.Type IN ('member','memberA') AND Links.UIDTarget=?)
) ON (guest.UIDBelongsTo=Member.UID AND guest.Type='ggroup')
WHERE Member.UID =?
`, [timestamp, UIDgroup, guest], { cast: ['json'] });
}));
}
// Flache Liste der gefundenen/angelegten Objekte
objects = objects.map(guest => guest[0]);
// Lade die Gruppendaten für die Zielgruppe
const [groupDB] = await query(`SELECT Member.Data FROM Member
INNER JOIN ObjectBase ON (ObjectBase.UIDBelongsTo=Member.UID)
WHERE ObjectBase.UID=? `, [UIDgroup], { cast: ['json'] });
// Bereite die Daten für das Rendern vor
const guestsDB = objects.map(p => {
const result = { ...p.Data, UIDguest: p.UIDguest, timestamp: p.timestamp, UID: p.UID, UIDnew: p.UIDnew };
if (type === 'guest') {
result.hierarchie = groupDB.Data.hierarchie;
}
return result;
});
// Rendere die Gäste-Daten aus den Originaldaten
const guests = await Promise.all(guestsDB.map((p, Index) => (new Promise(async (fullfill, reject) => {
const rendered=await renderObject(template,{...p,group:groupDB.Data},{session:{root:HEX2uuid(root)}} /*mocked req*/)
fullfill({
...rendered,
UIDnew: p.UIDnew,
UIDguest: p.UIDguest,
Data: type === 'guest' ? JSON.stringify({}) : objects[Index].Data,
timestamp: p.timestamp
});
}))));
// Schreibe alle neuen Gäste/Gastgruppen in die Datenbank (Transaktion)
await Promise.all(guests.map(guest =>
transaction(async (connection) => {
if (!guest.UIDguest) {
// Lege neuen Gast/Gastgruppe an
await connection.query(`
INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title,Display,SortName,FullTextIndex, dindex,hierarchie,stage,gender,Data)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
`,
[guest.UIDnew, UUID2hex(UIDuser), type, guest.UID, guest.Title, guest.Display, guest.SortBase, guest.FullTextIndex, guest.dindex,
guest.hierarchie, guest.stage, guest.gender, guest.Data])
} else {
// Gast existiert schon, UID übernehmen
guest.UIDnew = guest.UIDguest;
guest.UIDguest = null;
}
// Verknüpfe Gast/Gastgruppe mit der Zielgruppe
await connection.query(`
INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?,?,?)`, [guest.UIDnew, type === 'ggroup' ? 'memberGA' : 'memberA', UIDgroup]);
}, { backDate: timestamp ? Math.max(guest.timestamp, timestamp) : null }
)));
// Gib die neuen Gäste/Gastgruppen zurück
return guests.filter(o => !o.UIDguest).map(o => ({ UID: o.UIDnew, UIDBelongsTo: o.UID, timestamp: o.timestamp }));
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerUpdate(e);
throw e;
}
};
/**
* Erstellt eine Gastgruppe für eine Zielgruppe und übernimmt Mitglieder/Jobs als Gäste.
* @param {Object} req - Express request object
* @param {Buffer} UIDgroup - Zielgruppe (als Hex)
* @param {Buffer} UIDparent - Eltern-Gruppe (als Hex)
* @param {number|null} timestamp - Optionaler Zeitstempel
* @returns {Promise<Object>} Erfolg oder Fehlermeldung
*/
export const addGroupGuest = async (req, UIDgroup, UIDparent, timestamp) => {
try {
// 1. Prüfe, ob die Zielgruppe existiert
const group = await query(`SELECT ObjectBase.Type,ObjectBase.UIDBelongsTo, ObjectBase.UID
FROM ObjectBase
WHERE UID=?`, [UIDgroup]);
if (!group.length) {
return { success: false, message: 'group not found' };
}
// 2. Prüfe, ob die Eltern-Gruppe existiert und vom richtigen Typ ist
const parents = await query(`SELECT Type,UIDBelongsTo,UID FROM ObjectBase WHERE UID=? AND Type IN ('group','ggroup')`, [UIDparent]);
if (!parents.length) {
return { success: false, message: 'parent group not found' };
}
const parent = parents[0].UID;
// 3. Erstelle die Gastgruppe (ggroup) für die Zielgruppe und verknüpfe sie mit der Eltern-Gruppe
const guestGroups = await addGuests([UIDgroup], parent, 'ggroup', req.session.root);
const guestGroup = guestGroups[0];
queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'ggroup', guestGroup.UID, guestGroup.UIDBelongsTo, null, parent);
// 4. Ermittle alle Personen und Gäste der Zielgruppe
const objects = await query(`SELECT ObjectBase.Type,ObjectBase.UIDBelongsTo, ObjectBase.UID
FROM Links
INNER JOIN ObjectBase ON (Links.UID=ObjectBase.UID AND Links.Type IN('member','memberA'))
WHERE (Links.UIDTarget=?) AND ObjectBase.Type IN ('person','guest','job')
GROUP BY ObjectBase.UID `, [UIDgroup]);
// 5. Füge alle Personen und Gäste als Gäste zur neuen Gastgruppe hinzu
const gpersons = objects.filter(o => o.Type === 'person' || o.Type === 'guest').map(o => o.UID);
if (gpersons.length) {
const guestPersons = await addGuests(gpersons, guestGroup.UID, 'guest', req.session.root);
queueAddArray(req, guestPersons.map(g => ({
type: 'guest',
UID: g.UID,
UIDBelongsTo: g.UIDBelongsTo,
oldTarget: null,
newTarget: parents[0].UID,
})));
}
// 6. Verknüpfe alle Jobs als "tree"-Elemente mit der Eltern-Gruppe
const jobs = objects.filter(o => o.Type === 'job');
if (jobs.length) {
queueAddArray(req, jobs.map(j => ({
type: 'tree',
UID: j.UID,
UIDBelongsTo: j.UIDBelongsTo,
oldTarget: null,
newTarget: parents[0].UID,
})));
}
// 7. Erfolg zurückgeben
return { success: true };
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerUpdate(e);
throw e;
}
};
/**
* Adds a single person as guest to a group
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
export const addSingleGuest = async (/** @type {ExpressRequestAuthorized} */ req, /** @type {ExpressResponse} */ res) => {
try {
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
// Prüfe, ob der User Adminrechte für die Zielgruppe hat
if (!isObjectAdmin(req, req.params.group)) {
res.status(403).json({ success: false, message: 'user not authorized for this group' });
return;
}
// Konvertiere UIDs ins interne Hex-Format
const UIDgroup = UUID2hex(req.params.group);
const UIDperson = UUID2hex(req.params.UID);
const UIDguest = await getUID(req);
const template = Templates[req.session.root].guest;
// Lade Person und Gruppe aus der Datenbank
const [personDB] = await query(`SELECT Data FROM Member WHERE UID=? `, [UIDperson]);
if (!personDB) {
res.status(404).json({ success: false, message: 'invalid person UID' });
return;
}
const person = JSON.parse(personDB.Data);
const [groupDB] = await query(`SELECT Data FROM Member WHERE UID=? `, [UIDgroup]);
if (!groupDB) {
res.status(404).json({ success: false, message: 'invalid group UID' });
return;
}
const groupData = JSON.parse(groupDB.Data);
// Setze Hierarchie und Stage für den Gast passend zur Gruppe
person.hierarchie = groupData.hierarchie;
person.stage = groupData.stage && groupData.stage > 0 ? groupData.stage : 4;
// Prüfe, ob die Person schon als Gast oder Mitglied in der Gruppe ist
const persons = await query(
`SELECT ObjectBase.UID,ObjectBase.Type FROM ObjectBase
INNER JOIN Links ON (ObjectBase.UID=Links.UID AND Links.Type IN ('member','memberA'))
WHERE ObjectBase.UIDBelongsTo=? AND ObjectBase.Type IN ('person','guest') AND Links.UIDTarget=?`,
[UIDperson, UIDgroup]
);
let guest;
if (persons.length > 0) {
if (persons[0].Type === 'guest') {
guest = persons[0];
} else {
res.status(409).json({ success: false, message: 'person is already a member of this group' });
return;
}
}
// Rendere das Gast-Objekt
const object = await renderObject(template, { ...person, UID: req.body.UID, group: groupData }, req);
if (!guest) {
// Lege neuen Gast an
if (!isValidUID(req.body.UID)) {
res.status(400).json({ success: false, message: 'invalid UID format in body.UID' });
return;
}
await query(`
INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title,SortName,FullTextIndex, dindex,hierarchie,stage,gender,Data)
VALUES (?,?,'guest',?,?,?,?,?,?,?,?,?)
`,
[object.UID, HEX2uuid(req.session.user), UIDperson, object.Title, object.SortBase, object.FullTextIndex, object.dindex,
object.hierarchie, object.stage, object.gender, JSON.stringify({})]);
await query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget) VALUES (?,'memberA',?)`,
[object.UID, UIDgroup]);
queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'guest', object.UID, UIDperson, null, UIDgroup);
publishEvent(`/add/group/guest/${HEX2uuid(UIDgroup)}`, {
organization: req.session.root,
data: [HEX2uuid(object.UID)]
});
} else {
// Aktualisiere bestehenden Gast
await query(`UPDATE ObjectBase SET
Title=?,SortName=?,FullTextIndex=?,dindex=?,hierarchie=?,stage=?,gender=?
WHERE UID=?`,
[object.Title, object.SortBase, object.FullTextIndex, object.dindex,
object.hierarchie, object.stage, object.gender, guest.UID]);
}
res.status(201).json({ success: true, result: { ...object, UID: HEX2uuid(object.UID) } });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};
/**
* Bulk add guests to a group
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const bulkAddGuests = async (req, res) => {
try {
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
// Prüfe Admin-Rechte
if (!isObjectAdmin(req, req.params.group)) {
res.status(403).json({ success: false, message: 'user not authorized for this group' });
return;
}
// Hole alle Personen-Objekte anhand der übergebenen UIDs
const persons = await query(`SELECT UID,UIDBelongsTo
FROM ObjectBase WHERE UID IN (?)`, [req.body]);
// Füge die Personen als Gäste zur Gruppe hinzu
const guests = await addGuests(persons.map(p => p.UIDBelongsTo),
UUID2hex(req.params.group),
'guest',
req.session.root,
timestamp,
req.session.user
);
// Trage die Änderungen für die Synchronisation ein
queueAddArray(req, guests.map(g => ({
UID: g.UID,
UIDBelongsTo: g.UIDBelongsTo,
type: 'guest',
oldTarget: null,
newTarget: UUID2hex(req.params.group),
timestamp: null
})));
res.status(201).json({ success: true, result: guests });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};
/**
* Create guest group from existing group
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
export const createGuestGroup = async (req, res) => {
try {
// Extrahiere optionalen Zeitstempel für Backdating
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
// Rufe die Hilfsfunktion auf, die die Gastgruppe anlegt und Mitglieder/Jobs übernimmt
const result = await addGroupGuest(
req,
UUID2hex(req.params.UID), // Zielgruppe (als Hex)
UUID2hex(req.params.parent), // Eltern-Gruppe (als Hex)
timestamp // Optionaler Zeitstempel
);
// Sende das Ergebnis zurück (Erfolg oder Fehlermeldung)
res.status(result.success ? 201 : 400).json(result);
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};
/**
* Get all guests from a group
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
export const getGroupGuests = async (req, res) => {
try {
// Hole alle direkten Gäste (guest) und Gastgruppen (ggroup) der angegebenen Gruppe
const result = await query(`
SELECT ObjectBase.Title, Member.Display, ObjectBase.UID, ObjectBase.Type, ObjectBase.UIDBelongsTo,
JSON_VALUE(ObjectBase.Data,'$.banner') AS banner,ObjectBase.dindex, pGroup.Display AS pGroup
FROM
ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UIDBelongsTo)
INNER JOIN Links as aLinks ON (aLinks.UID=Member.UID AND aLinks.Type='memberA')
INNER JOIN ObjectBase pGroup ON (pGroup.UID=aLinks.UIDTarget)
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type='memberA')
WHERE ObjectBase.Type IN ('guest','ggroup') AND Links.UIDTarget=?
ORDER BY ObjectBase.Type,ObjectBase.SortName,Member.SortName
`,
[UUID2hex(req.params.UIDgroup)],
{ cast: ['UUID', 'json'] }
);
// Sende die gefundenen Gäste/Gastgruppen als Ergebnis zurück
res.json({ success: true, result: result });
} catch (e) {
errorLoggerRead(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};
/**
* Delete a guest or guest group
* @param {Object} req - Express request object
* @param {Object} res - Express response object
*/
export const deleteGuest = async (req, res) => {
try {
const UID = UUID2hex(req.params.UID);
const objects = await query(`SELECT ObjectBase.Type,ObjectBase.UID,ObjectBase.UIDBelongsTo, Links.UIDTarget AS UIDparent
FROM ObjectBase
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND ((Links.Type='memberA' AND ObjectBase.Type='guest') OR (Links.Type='memberGA' AND ObjectBase.Type='ggroup')))
WHERE ObjectBase.UID=?
GROUP BY ObjectBase.UID
`, [UID], { log: true });
if (objects.length === 0) {
res.status(404).json({ success: false, message: 'guest not found' });
return;
}
const object = objects[0];
if (object.Type === 'ggroup') {
queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'groupGuest', UUID2hex(req.params.UID), object.UIDBelongsTo, object.UIDparent, null);
} else if (object.Type === 'guest') {
queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'guest', UUID2hex(req.params.UID), object.UIDBelongsTo, object.UIDparent, null);
}
// Entferne die Verknüpfung
await query(`DELETE FROM Links WHERE UID =? AND Type IN ('memberA', 'memberGA')`, [UID]);
res.json({ success: true });
} catch (e) {
errorLoggerUpdate(e);
res.status(500).json({ success: false, message: 'Internal server error' });
}
};