/**
* Group Controller
*
* Contains all the business logic for group operations:
* - Account read-only checks
* - Sister group link management
* - Banner recreation and inheritance
* - Group creation and updates
* - Group deletion
* - Group data retrieval
* - Admin permission checks
* - Minimum timestamp calculations
*/
// @ts-check
/**
* @import {ExpressRequestAuthorized, ExpressResponse} from './../../types.js'
*/
import { query, transaction, UUID2hex, HEX2uuid } from '@commtool/sql-query';
import { parseTimestampToSeconds, parseTimestampToSecondsOrDefault } from '../../utils/parseTimestamp.js';
import { renderObject } from '../../utils/renderTemplates.js';
import { Templates, BannerRules, getConfig, addExtraConfigToOrgaData } 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 { recreateJobsGroup } from '../job/utilities.js';
import { isObjectAdmin } from '../../utils/authChecks.js';
import { publishEvent } from '../../utils/events.js';
import { rebuildFeesGroup } from '../../tree/rebuildFees.js';
import { errorLoggerRead, errorLoggerUpdate } from '../../utils/requestLogger.js';
import { keysEqual } from '../../utils/keyCompare.js';
import { timestampCheck } from '../person/migratePerson.js';
/**
* Checks if the current user has read-only access to group accounts
* @param {Object} req - Express request object
* @returns {Promise<boolean>} True if accounts are read-only for this user
*/
const accountsReadOnly = async (req) => {
// Holt die Konfiguration für die aktuelle Anfrage (z.B. Einstellungen zu Gebühren und Gruppen)
const config = await getConfig('db', req);
// Prüft, ob in der Konfiguration eine spezielle Liste für Gruppen-Kontenmanager existiert
if (config.fees?.listManagersGroupAccounts) {
// Führt eine SQL-Abfrage aus, um zu prüfen, ob der aktuelle Benutzer Mitglied in der speziellen Manager-Gruppe ist
const result = await query(`
SELECT Main.Title, Member.Display, ObjectBase.Data
FROM ObjectBase
INNER JOIN ObjectBase AS Main ON (Main.UID = ObjectBase.UIDBelongsTo)
INNER JOIN Member ON (Member.UID = ObjectBase.UIDBelongsTo)
INNER JOIN Links ON (Links.UID = ObjectBase.UID AND Links.Type IN ('member','memberA'))
INNER JOIN ObjectBase AS list ON (list.UID = Links.UIDTarget AND list.Type IN ('list','dlist'))
WHERE Main.Type IN ('person','extern')
AND ObjectBase.UIDBelongsTo = ? -- aktueller Benutzer
AND Links.UIDTarget = ? -- spezielle Manager-Gruppe
`, [
UUID2hex(req.session.user), // Benutzer-UUID in hex
UUID2hex(config.fees.listManagersGroupAccounts) // Manager-Gruppen-UUID in hex
]);
// Gibt true zurück, wenn der Benutzer NICHT in der Manager-Gruppe ist (also nur Leserechte hat)
return result.length === 0;
} else {
// Gibt false zurück, wenn keine spezielle Manager-Gruppe definiert ist (also keine Einschränkung)
return false;
}
};
/**
* Verarbeitet die Verknüpfung einer Gruppe mit einer "Schwestergruppe" (Sister Group).
*
* Diese Funktion prüft, ob die aktuelle Gruppe (UID) mit einer anderen Gruppe (belongsTo) als Schwestergruppe verknüpft werden soll.
* Falls sich die Verknüpfung geändert hat oder entfernt werden soll, werden die entsprechenden Links in der Datenbank aktualisiert.
* Zusätzlich werden alle Mitglieder der Gruppe ggf. auch in die Schwestergruppe übernommen.
*
* @param {ExpressRequestAuthorized} req - Die aktuelle Express-Request-Instanz (enthält z.B. belongsTo in req.query)
* @param {Buffer} UID - Die UID der aktuellen Gruppe
* @returns {Promise<object|null>} - Gibt die Schwestergruppe zurück, falls eine neue gesetzt wurde, sonst null
*/
const handleSisterLink = async (req, UID) => {
// Extrahiere die Zielgruppe (belongsTo) aus den Query-Parametern, falls vorhanden
const authReq = /** @type {ExpressRequestAuthorized} */ (/** @type {unknown} */ (req));
const belongsTo = req.query.belongsTo ? UUID2hex(req.query.belongsTo) : null;
const rootHex = UUID2hex(authReq.session.root);
const userHex = UUID2hex(authReq.session.user);
// Hole die bisherige Schwestergruppen-Verknüpfung (falls vorhanden)
const oldBT = await query(`SELECT UIDTarget FROM Links WHERE UID=? AND Type='memberS'`, [UID]);
// Prüfe, ob sich die Schwestergruppe geändert hat oder entfernt werden soll
if (oldBT.length === 0 || !belongsTo || !oldBT[0].UIDTarget.equals(belongsTo)) {
// Entferne alte Schwestergruppen-Verknüpfung
await query(`DELETE FROM Links WHERE UID=? AND Type='memberS'`, [UID]);
// Hole alle Mitglieder der aktuellen Gruppe
const members = await query(`SELECT MLink.UID AS UID, ObjectBase.Type FROM Links AS MLink
INNER JOIN ObjectBase ON (ObjectBase.UID=MLink.UID AND ObjectBase.Type IN ('person','extern','job','guest','group','ggroup'))
WHERE MLink.UIDTarget=? AND MLink.Type IN ('member','memberA','memberS')`, [UID]);
if (belongsTo) {
// Setze neue Schwestergruppen-Verknüpfung
await query(`INSERT INTO Links (UID,Type,UIDTarget) VALUES(?,'memberS',?)`, [UID, belongsTo]);
// Füge alle Mitglieder der aktuellen Gruppe auch der Schwestergruppe hinzu
if (members.length > 0) {
await query(`INSERT IGNORE INTO Links (UID,Type,UIDTarget) VALUES(?,'member',?)`,
members.map(m => ([m.UID, belongsTo])), { batch: true });
}
// Hole die Daten der neuen Schwestergruppe
const s = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display AS Display,
ObjectBase.dindex, Member.Data AS Data,ObjectBase.gender, ObjectBase.hierarchie, ObjectBase.stage ,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE ObjectBase.UID=? AND ObjectBase.Type ='group'`,
[belongsTo],
{ cast: ['UUID', 'json'], log: false }
);
// Hole die UID der alten Schwestergruppe (falls vorhanden)
const oldBTUID = oldBT.length > 0 ? oldBT[0].UIDTarget : null;
if (oldBTUID) {
// Trage Änderung in die Warteschlange für die alte Schwestergruppe ein
await queueAdd(UUID2hex(req.session.root), UUID2hex(req.session.user), 'group', UID, UID, oldBTUID, null);
}
// Trage Änderung in die Warteschlange für die neue Schwestergruppe ein
await queueAdd(rootHex, userHex, 'group', UID, UID, null, belongsTo, null);
// Gib die neue Schwestergruppe zurück
return s[0];
} else {
// Wenn keine neue Schwestergruppe gesetzt wurde, entferne alle Mitgliedschaften aus der alten Schwestergruppe
const oldBTUID = oldBT.length > 0 ? oldBT[0].UIDTarget : null;
if (oldBTUID) {
// Trage Änderung in die Warteschlange für die alte Schwestergruppe ein
await queueAdd(rootHex, userHex, 'group', UID, UID, oldBTUID, null, null);
}
}
}
// Wenn keine Änderung, gib null zurück
return null;
};
/**
* Recreates and propagates banner to child groups and members
* @param {Buffer} UID - Group UID
* @param {string} banner - Banner URL/path
* @param {Object} bannerRules - Banner inheritance rules
*/
const recreateBanner = async (UID, banner, bannerRules, req) => {
try {
// 1. Holt alle direkten Unterobjekte (Kinder) der aktuellen Gruppe (UID), die ein Banner erben könnten.
const children = await query(`SELECT Member.Data, ObjectBase.Type, ObjectBase.Data AS OData,
ObjectBase.UID, gMember.Data AS gData, Links.Type AS LType
FROM ObjectBase
INNER JOIN Member ON (Member.UID = ObjectBase.UIDBelongsTo)
INNER JOIN Links AS gLink ON (gLink.UID=ObjectBase.UID AND gLink.Type='memberA')
INNER JOIN Member AS gMember ON (gMember.UID=gLink.UIDTarget)
INNER JOIN Links ON (Links.UID=ObjectBase.UID AND Links.Type IN ('memberA') )
WHERE Links.UIDTarget=? AND ObjectBase.Type IN ('job','group','person')
GROUP BY ObjectBase.UID
`,
[UID],
{
cast: ['json'],
filter: (entry) => {
// 2. Filtert die Kinder: Nur solche, die das Banner erben sollen oder direkte Mitglieder sind.
return (bannerRules.group[entry.Data.hierarchie][0] === 'inherit' && entry.Type === 'group' ||
bannerRules.group[entry.gData.hierarchie].includes('inherit') && entry.Type === 'job' ||
(bannerRules.group[entry.gData.hierarchie].includes('inherit') || entry.LType === 'memberA') && entry.Type === 'person'
);
}
});
const updatedGroups = [];
for (const child of children) {
if (child.Type === 'group' || child.Type === 'person') {
// 3a. Für Gruppen und Personen: Banner im Member-Data aktualisieren.
const NewData = { ...child.Data, banner: banner };
await query(`UPDATE Member SET Data = ? WHERE UID=?`,
[JSON.stringify(NewData), child.UID]);
if (child.Type === 'group') {
// 4. Rekursiver Aufruf für Untergruppen, damit das Banner weitervererbt wird.
recreateBanner(child.UID, banner, bannerRules, req);
updatedGroups.push(child.UID);
// 5. Event veröffentlichen, dass sich das Banner geändert hat.
publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
organization: req.session.root,
data: { banner: NewData.banner }
});
}
} else {
// 3b. Für Jobs: Banner im ObjectBase-Data aktualisieren (nur wenn sich das Banner geändert hat).
if (child.OData.banner !== banner) {
await query(`UPDATE ObjectBase SET Data = ? WHERE UID=?`,
[JSON.stringify({ ...child.OData, banner: banner }), child.UID]);
}
}
}
// 6. Aktualisiert die Eltern-Objekte und die geänderten Gruppen für die Synchronisation.
const parents = await query(`SELECT Links.UIDTarget AS UID FROM Links WHERE Links.UID=? AND Links.Type IN ('member','memberA','memberS','memberG')`, [UID]);
addUpdateList(parents.map(el => el.UID));
addUpdateList([UID, ...updatedGroups]);
} catch (e) {
// 7. Fehlerbehandlung
errorLoggerUpdate(e);
}
};
/**
* Creates or updates a group
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const putGroup = async ( req, res) => {
try {
const authReq = /** @type {ExpressRequestAuthorized} */ (/** @type {unknown} */ (req));
const rootHex = UUID2hex(authReq.session.root);
const userHex = UUID2hex(authReq.session.user);
// Extrahiere optionalen Zeitstempel aus der Query (für Backdating)
const timestamp = parseTimestampToSeconds(req.query.timestamp ? String(req.query.timestamp) : undefined);
// Lade das Gruppentemplate für das aktuelle Root
const template = Templates[authReq.session.root].group;
// Ermittle die UID für die Gruppe (neu oder bestehend)
const UID = await getUID(req);
// Erzeuge das Gruppenobjekt aus Template und Requestdaten
const object = await renderObject(template, req.body, req);
// Hole die Zielgruppen-UID aus den URL-Parametern (hex-codiert)
const UIDgroup = UUID2hex(req.params.UIDparent);
// Prüfe, ob der aktuelle User nur Leserechte auf Konten hat
const accountsProtected = await accountsReadOnly(req);
// Lade die Daten der Zielgruppe aus der Datenbank
const mgroups = await query(`
SELECT Member.Data,ObjectBase.UID,Member.Display,ObjectBase.Title,
UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE ObjectBase.UID=? AND ObjectBase.Type='group'`,
[UIDgroup], { cast: ['json'] }
);
// Wenn die Zielgruppe nicht existiert, Fehler zurückgeben
if (!mgroups.length) {
res.status(300).json({ success: false, message: 'invalid group UID' });
return;
}
// Prüfe, ob das Geschlecht der Gruppe mit dem Request übereinstimmt
const group = mgroups[0];
if (group.Data.gender !== req.body.gender && group.Data.gender !== 'C' && group.Data.gender !== 'B' && req.body.gender !== 'B' && req.body.gender !== 'C') {
res.status(300).json({ success: false, message: 'invalid group gender' });
return;
}
// Prüfe, ob die Stufe der Gruppe mit dem Request übereinstimmt
if (group.Data.stage !== req.body.stage && group.Data.stage !== 0) {
res.status(300).json({ success: false, message: 'invalid group stage' });
return;
}
// Banner-Vererbung: Setze ggf. das Banner aus der Eltern-Gruppe
if (BannerRules[req.session.root].group[req.body.hierarchie] === 'inherit') {
if (group.Data.banner) {
req.body.banner = group.Data.banner;
}
} else if (BannerRules[req.session.root].group[req.body.hierarchie].includes('inherit') && !req.body.banner) {
req.body.banner = group.Data.banner;
}
// Prüfe, ob die Gruppe bereits existiert (für den aktuellen User)
const groups = await query(`
SELECT Member.Data,Links.UIDTarget AS MemberA,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom,
Title,UIDBelongsTo
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
INNER JOIN Links ON(Links.Type='memberA' AND Links.UID=ObjectBase.UID)
WHERE ObjectBase.UID=? AND ObjectBase.Type='group'`, [UID]);
// Wenn die Gruppe noch nicht existiert, lege sie neu an
if (!groups.length) {
// Ermittle das erlaubte Backdate-Limit
const limitedTimestamp = await timestampCheck(UIDgroup, timestamp);
// Prüfe, ob die UID im Body gültig ist
if (!isValidUID(req.body.UID)) {
res.status(300).json({ success: false, message: 'invalid UID format in body.UID' });
return;
}
// Erzeuge die Daten für die neue Gruppe
const NewData = { ...req.body, UID: undefined };
if (accountsProtected) {
NewData.accounts = [];
}
// Schreibe die neue Gruppe in einer Transaktion in die Datenbank
await transaction(async (connection) => {
// Objektbasis anlegen
await query(`
INSERT INTO ObjectBase(UID,UIDuser,Type,UIDBelongsTo,Title, dindex,hierarchie,stage,gender)
VALUES (?,?,'group',?,?,?,?,?,?)`,
[UID, userHex, UID, object.Title, 0,
object.hierarchie, object.stage, object.gender],
{ connection, log: false });
// Link zur Eltern-Gruppe anlegen
await query(`INSERT IGNORE INTO Links(UID, Type, UIDTarget,UIDuser) VALUES (?,'memberA',?,?)`,
[UID, UIDgroup, userHex],
{ connection, log: false });
}, { backDate: limitedTimestamp });
// Member-Eintrag anlegen oder aktualisieren
await query(`INSERT INTO Member
(UID,Display,SortName,FullTextIndex, Data)
VALUES(?,?,?,?,?)
ON DUPLICATE KEY UPDATE Display=VALUE(Display),SortName=VALUE(SortName),FullTextIndex=VALUE(FullTextIndex),Data=VALUE(Data)`,
[UID, object.Display, object.SortIndex, object.FullTextIndex, JSON.stringify(NewData)]);
// Änderungen in die Warteschlange eintragen
queueAdd(rootHex, userHex, 'group', object.UID, object.UID, null, UIDgroup, timestamp);
// Verknüpfung mit Schwestergruppen, falls vorhanden
object.sibling = await handleSisterLink(req, UID);
// Event veröffentlichen, dass eine neue Gruppe angelegt wurde
publishEvent(`/add/group/group/${authReq.session.root}/${req.params.UIDparent}`, {
organization: authReq.session.root,
data: [HEX2uuid(object.UID)]
});
// Ergebnis zurückgeben
res.json({ success: true, result: { ...object, UID: HEX2uuid(object.UID), Data: NewData } });
} else {
// Gruppe existiert bereits: Prüfe Adminrechte
if (!isObjectAdmin(req, UIDgroup)) {
res.status(403).json({ success: false, message: 'user not authorized to change this group' });
return;
}
// Lade aktuelle Gruppendaten
const existingGroup = { ...groups[0], Data: JSON.parse(groups[0].Data) };
const updateTimestamp = parseTimestampToSecondsOrDefault(req.query.timestamp, groups[0].validFrom + 1);
// Prüfe, ob Hierarchie, Stage oder Gender geändert werden sollen (nicht erlaubt)
if (existingGroup.Data.hierarchie !== req.body.hierarchie) {
res.json({ success: false, message: 'you can not update hierarchie of an existing group' });
return;
}
if (existingGroup.Data.stage !== req.body.stage) {
res.json({ success: false, message: 'you can not update stage of an existing group' });
return;
}
if (existingGroup.Data.gender !== req.body.gender) {
res.json({ success: false, message: 'you can not update gender of an existing group' });
return;
}
// Wenn sich die Gebühren geändert haben, Gebührenstruktur neu aufbauen
if (JSON.stringify(existingGroup.Data.fees) !== JSON.stringify(req.body.fees)) {
rebuildFeesGroup(UID, req.session.root);
}
// Neue Daten für das Update erzeugen
const NewData = { ...req.body, UID: undefined };
if (accountsProtected) {
NewData.accounts = existingGroup.Data.accounts;
}
// Wenn sich der Titel geändert hat, führe das Update in einer Transaktion durch
if (existingGroup.Title !== object.Title) {
await transaction(async (connection) => {
await connection.query(`UPDATE ObjectBase SET Title=? WHERE UID=?`,
[object.Title, UID]);
// Gruppen-Migration ist aktuell nicht unterstützt
if (!existingGroup.MemberA.equals(UIDgroup)) {
return { result: false, message: 'group migration currently not supported' };
}
// Aktualisiere alle Jobs der Gruppe
recreateJobsGroup(req, UIDgroup, updateTimestamp);
return { success: true, result: { ...object, UID: HEX2uuid(object.UID), Data: NewData } };
}, { backDate: updateTimestamp });
}
// Aktualisiere Member-Daten der Gruppe
await query(`UPDATE Member SET
Display=?,SortName=?,FullTextIndex=?, Data=?
WHERE UID=?`,
[object.Display, object.SortIndex, object.FullTextIndex, JSON.stringify(NewData), UID]);
// Verknüpfung mit Schwestergruppen, falls vorhanden
object.sibling = await handleSisterLink(req, UID);
// Update-Listen pflegen
addUpdateList(UID);
// Prüfe, ob sich Daten geändert haben und veröffentliche ggf. ein Event
const [equal, diff] = keysEqual(NewData, existingGroup.Data);
if (!equal) {
publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
organization: req.session.root,
data: { UID: HEX2uuid(UID), diff },
backDate: updateTimestamp
});
}
// Update-Entry für die Gruppe anlegen
addUpdateEntry(UID, { group: { ...object, Data: NewData, UID: req.params.UID, parent: existingGroup } });
// Ergebnis zurückgeben
res.json({ success: true, result: { ...object, UID: HEX2uuid(UID), Data: NewData } });
}
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerUpdate(e);
}
};
/**
* Updates an existing group
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const updateGroup = async ( req, res) => {
try {
// Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
const UID = UUID2hex(req.params.UID);
// Prüfe, ob der aktuelle User nur Leserechte auf Konten hat
const accountsProtected = await accountsReadOnly(req);
// Lade die aktuellen Daten der Gruppe aus der Datenbank
const mgroups = await query(`SELECT Member.Data,ObjectBase.Title,
UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN Member ON (ObjectBase.UID=Member.UID)
WHERE ObjectBase.UID=? `, [UID], { cast: ['json'] });
// Wenn die Gruppe nicht existiert, Fehler zurückgeben
if (!mgroups.length) {
res.status(300).json({ success: false, message: 'invalid group UID' });
return;
}
// Ermittle den Zeitstempel für das Update (entweder aus Query oder aus der Gruppe)
const timestamp = parseTimestampToSecondsOrDefault(req.query.timestamp, Number(mgroups[0].validFrom));
const group = mgroups[0];
// Prüfe, ob Hierarchie, Stage oder Gender geändert werden sollen (nicht erlaubt)
if (req.body.hierarchie && group.Data.hierarchie !== req.body.hierarchie) {
res.json({ success: false, message: 'you can not update hierarchie of an existing group' });
return;
}
if (req.body.stage && group.Data.stage !== req.body.stage) {
res.json({ success: false, message: 'you can not update stage of an existing group' });
return;
}
if (req.body.gender && group.Data.gender !== req.body.gender) {
res.json({ success: false, message: 'you can not update gender of an existing group' });
return;
}
// Erzeuge die neuen Gruppendaten (alte Daten + neue Werte aus dem Request)
const NewData = ({ ...group.Data, ...req.body, UID: UID });
// Wenn Konten geschützt sind, überschreibe Accounts mit den alten Werten
if (accountsProtected) {
NewData.accounts = group.Data.accounts;
}
// Hole das Gruppentemplate für das aktuelle Root
const template = Templates[req.session.root].group;
// Rendere das Gruppenobjekt für die Anzeige und Speicherung
const object = await renderObject(template, NewData, req);
// Wenn sich der Titel geändert hat, aktualisiere ihn in ObjectBase
if (mgroups[0].Title !== object.Title) {
await query(`UPDATE ObjectBase SET
Title=?
WHERE UID=?`,
[object.Title, UID], { backDate: timestamp });
}
// Aktualisiere die Member-Tabelle mit den neuen Daten
await query(`UPDATE Member SET
Display=?,SortName=?,FullTextIndex=?,Data=?
WHERE UID=?`,
[object.Display, object.SortIndex, object.FullTextIndex,
JSON.stringify({ ...NewData, UID: undefined }), UID]);
// Falls eine Schwestergruppe angegeben ist, aktualisiere die Verknüpfung
if (req.query.belongsTo && req.query.belongsTo !== 'undefined') {
object.sibling = await handleSisterLink(req, UID);
}
// Aktualisiere alle Jobs der Gruppe, da sich die Anzeige geändert haben könnte
recreateJobsGroup(req, UID, timestamp);
// Prüfe, ob das Banner geändert wurde und vererbe es ggf. weiter
if (NewData.banner !== group.Data.banner) {
recreateBanner(UID, NewData.banner, BannerRules[req.session.root], req);
}
// Entferne UID aus den Daten für den Vergleich/Event
delete NewData.UID;
// Vergleiche die alten und neuen Daten, ermittle Unterschiede
const [equal, diff] = keysEqual(NewData, group.Data);
// Wenn sich etwas geändert hat, veröffentliche ein Event
if (!equal) {
publishEvent(`/change/group/${req.session.root}/${HEX2uuid(UID)}`, {
organization: req.session.root,
data: { UID: HEX2uuid(UID), diff },
backDate: timestamp
});
// wenn es die root gruppe ist, dann sende auch ein change für die organiation
if(req.params.UID===req.session.root){
const config= await getConfig('db',req.session.root);
publishEvent(`/change/orga/${req.session.root}`, {
organization: req.session.root,
data: {Display: object.Display, OrgaShort: config.OrgaShort },
backDate: timestamp
});
}
}
// Prüfe, ob sich die Gebühren geändert haben und baue sie ggf. neu auf
if (req.body.fees && (JSON.stringify(group.Data.fees) !== JSON.stringify(req.body.fees))) {
rebuildFeesGroup(UID, req.session.root);
}
// ist das für organisations Route
// enthält es extraConfig
// ändere config und triggere clients
if(req.params.UID===req.session.root && req.body.extraConfig){
addExtraConfigToOrgaData(req.session.root, req.body.extraConfig);
}
// Trage die Änderung in die Update-Liste für die Synchronisation ein
addUpdateEntry(UID, { group: { ...object, Data: NewData, UID: req.params.UID } });
// Sende das Ergebnis zurück mit Status 201 (Created/Updated)
res.status(201).json({ success: true, result: { ...object, UID: HEX2uuid(UID), Data: NewData } });
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerUpdate(e);
}
};
/**
* Deletes a group (only if it has no members)
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const deleteGroup = async ( req, res) => {
try {
// Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
const UID = UUID2hex(req.params.UID);
// Prüfe, ob es noch Mitglieder oder Unterobjekte in der Gruppe gibt
const hasLinks = await query(`
SELECT ObjectBase.UID,
Member.Display AS Display,
ObjectBase.Title
FROM Links
INNER JOIN ObjectBase ON (ObjectBase.UID=Links.UID)
INNER JOIN Member ON (Member.UID=ObjectBase.UIDbelongsTo)
WHERE Links.UIDTarget=?
AND Links.UID<>? -- sich selbst nicht zählen
AND Links.Type IN ('member','memberA')
AND ObjectBase.Type IN ('person','extern','guest','ggroup','group','job')
`, [UID, UID], { cast: ['UUID'] });
// Wenn noch Mitglieder vorhanden sind, Abbruch und Info zurückgeben
if (hasLinks.length) {
res.status(300).json({ success: false, message: 'has still members', result: hasLinks });
console.log('delete group not empty', hasLinks.map(l => (HEX2uuid(l.UID))));
} else {
// Lösche die Gruppe aus allen relevanten Tabellen
query(`DELETE FROM ObjectBase WHERE UID =?`, [UID]);
query(`DELETE FROM Member WHERE UID =?`, [UID]);
// Hole alle Links (z.B. zur Eltern-Gruppe), um später Updates zu triggern
const update = await query(`SELECT UIDTarget,Type FROM Links WHERE UID = ? `, [UID]);
const parentGroup = update.find(l => l.Type === 'memberA')?.UIDTarget;
// Lösche alle Links und Sichtbarkeiten der Gruppe
await query(`DELETE FROM Links WHERE UID=?`, [UID]);
await query(`DELETE FROM Visible WHERE UID=?`, [UID]);
// Informiere andere Systemteile über die Änderung (z.B. für UI-Updates)
addUpdateList(update.map(el => el.UIDTarget));
// Event veröffentlichen, dass die Gruppe entfernt wurde
publishEvent(`/remove/group/group/${parentGroup}`, {
organization: req.session.root,
data: [req.params.UID]
});
// Erfolg zurückgeben
res.json({ success: true });
}
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerUpdate(e);
}
};
/**
* Gets group details with optional parent and sibling information
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const getGroup = async ( req, res) => {
try {
const UID = UUID2hex(req.params.UID);
if (!UID) {
res.status(300).json({ success: false, message: 'no UID supplied' });
return;
}
const resa = await query(`SELECT ObjectBase.UID,ObjectBase.UIDBelongsTo,ObjectBase.Type,ObjectBase.Title,
Member.Display AS Display,
ObjectBase.dindex,ObjectBase.gender, ObjectBase.hierarchie, ObjectBase.stage,
Member.Data AS Data,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
WHERE ObjectBase.UID=? AND ObjectBase.Type IN('group','ggroup')`,
[UID],
{ cast: ['UUID', 'json'] }
);
if (resa.length === 0) {
res.json({ success: false, message: `group ${req.params.UID} does not exist` });
return;
}
const result = resa[0];
if (req.query.parent) {
const p = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display,
ObjectBase.dindex,Member.Data, ObjectBase.gender, ObjectBase.hierarchie,
ObjectBase.stage, UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom,
ObjectBase.UIDBelongsTo ,UNIX_TIMESTAMP(Links.validFrom) AS LvalidFrom
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='memberA')
WHERE Links.UID=?`,
[UID],
{ cast: ['UUID', 'json'] }
);
if (p.length > 0) {
// entry with member and sibling of parent(s)
const ps = await query(`
SELECT Siblings.UID,Siblings.Title,Member.Display, Siblings.dindex,Member.Data,Siblings.gender, Siblings.hierarchie,Siblings.stage,UNIX_TIMESTAMP(Siblings.validFrom) AS validFrom
FROM Links AS SLinks
INNER JOIN ObjectBase AS Siblings ON (Siblings.UID=SLinks.UIDTarget AND Siblings.Type='group')
INNER JOIN Member ON (Member.UID=Siblings.UID)
INNER JOIN Links ON (Links.UIDTarget=Siblings.UID AND Links.Type='member')
WHERE Links.UID=? AND SLinks.UID IN (?) AND SLinks.Type='memberS';
`,
[UID, p.map(pp => UUID2hex(pp.UID))],
{ cast: ['UUID', 'json'], log: false }
);
if (p.length > 0) {
result.parent = p[0];
}
if (ps.length > 0) {
result.parentS = ps[0];
}
}
}
if (req.query.sibling) {
const s = await query(`SELECT ObjectBase.UID,ObjectBase.Type,ObjectBase.Title,Member.Display AS Display,
ObjectBase.dindex, Member.Data AS Data,ObjectBase.gender, ObjectBase.hierarchie, ObjectBase.stage ,UNIX_TIMESTAMP(ObjectBase.validFrom) AS validFrom
FROM ObjectBase
INNER JOIN Member ON (Member.UID=ObjectBase.UID)
INNER JOIN Links ON (Links.UIDTarget=ObjectBase.UID AND Links.Type='memberS')
WHERE Links.UID=? AND ObjectBase.Type ='group'`,
[UID],
{ cast: ['UUID', 'json'], log: false }
);
result.sibling = s[0];
}
res.json({
success: true,
result: result,
});
} catch (e) {
errorLoggerRead(e);
}
};
/**
* Gets the minimum backdate timestamp for a group
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const minTimestamp = async ( req, res) => {
// Konvertiere die Gruppen-UID aus dem URL-Parameter in das interne Hex-Format
const UID = UUID2hex(req.params.UID);
// Ermittle das kleinste erlaubte Backdate-Datum für die Gruppe und ggf. den angegebenen Timestamp
const minTimestampValue = await timestampCheck(UID, parseTimestampToSecondsOrDefault(req.query.timestamp, 0));
// Sende das Ergebnis (in Millisekunden) zurück
res.json({ success: true, result: minTimestampValue * 1000 });
};
/**
* Checks if the current user is admin for the group
* @param {ExpressRequestAuthorized} req - Express request object
* @param {ExpressResponse} res - Express response object
*/
export const checkGroupAdmin = async ( req, res) => {
try {
// Prüfe Adminrechte für die angegebene Gruppe
const admin = await isObjectAdmin(req, req.params.UID);
// Sende das Ergebnis zurück
res.json({ success: true, result: admin });
} catch (e) {
// Fehlerbehandlung: Fehler loggen
errorLoggerRead(e);
}
};