User, Groups, Logs Update
This commit is contained in:
+246
-4
@@ -1,10 +1,18 @@
|
||||
import { db } from '../db/index.js';
|
||||
import {
|
||||
normalizeAvatarColor,
|
||||
DEFAULT_AVATAR_COLOR,
|
||||
SUPERUSER_GROUP_NAME,
|
||||
pickRandomAvatarColor
|
||||
} from '../auth/superuser.js';
|
||||
import { logEvent } from '../logging/eventLogs.js';
|
||||
|
||||
const selectGroupsWithMembers = db.prepare(`
|
||||
SELECT
|
||||
g.id,
|
||||
g.name,
|
||||
g.description,
|
||||
g.avatar_color,
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
COUNT(DISTINCT u.id) AS member_count,
|
||||
@@ -21,6 +29,7 @@ const selectGroupWithMembersById = db.prepare(`
|
||||
g.id,
|
||||
g.name,
|
||||
g.description,
|
||||
g.avatar_color,
|
||||
g.created_at,
|
||||
g.updated_at,
|
||||
COUNT(DISTINCT u.id) AS member_count,
|
||||
@@ -32,6 +41,12 @@ const selectGroupWithMembersById = db.prepare(`
|
||||
GROUP BY g.id
|
||||
`);
|
||||
|
||||
const selectGroupByIdBasic = db.prepare(`
|
||||
SELECT id, name, description, avatar_color
|
||||
FROM user_groups
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const selectGroupIdByName = db.prepare(`
|
||||
SELECT id
|
||||
FROM user_groups
|
||||
@@ -40,8 +55,28 @@ const selectGroupIdByName = db.prepare(`
|
||||
`);
|
||||
|
||||
const insertGroupStatement = db.prepare(`
|
||||
INSERT INTO user_groups (name, description, created_at, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO user_groups (name, description, avatar_color, created_at, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`);
|
||||
|
||||
const updateGroupStatement = db.prepare(`
|
||||
UPDATE user_groups
|
||||
SET name = ?,
|
||||
description = ?,
|
||||
avatar_color = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const countGroupMembersStatement = db.prepare(`
|
||||
SELECT COUNT(*) AS member_count
|
||||
FROM user_group_memberships
|
||||
WHERE group_id = ?
|
||||
`);
|
||||
|
||||
const deleteGroupStatement = db.prepare(`
|
||||
DELETE FROM user_groups
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const parseMembers = (rawPairs) => {
|
||||
@@ -66,6 +101,7 @@ const sanitizeGroup = (row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description || '',
|
||||
avatarColor: row.avatar_color || null,
|
||||
createdAt: row.created_at || null,
|
||||
updatedAt: row.updated_at || null,
|
||||
memberCount: Number(row.member_count) || 0,
|
||||
@@ -77,6 +113,15 @@ export function listGroups() {
|
||||
return rows.map(sanitizeGroup);
|
||||
}
|
||||
|
||||
export function getGroupById(groupId) {
|
||||
const numericId = Number(groupId);
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
return null;
|
||||
}
|
||||
const row = selectGroupWithMembersById.get(numericId);
|
||||
return row ? sanitizeGroup(row) : null;
|
||||
}
|
||||
|
||||
export function createGroup({ name, description }) {
|
||||
const normalizedName = typeof name === 'string' ? name.trim() : '';
|
||||
if (!normalizedName) {
|
||||
@@ -93,8 +138,205 @@ export function createGroup({ name, description }) {
|
||||
}
|
||||
|
||||
const normalizedDescription = typeof description === 'string' ? description.trim() : null;
|
||||
const insertResult = insertGroupStatement.run(normalizedName, normalizedDescription || null);
|
||||
const avatarColor = pickRandomAvatarColor() || DEFAULT_AVATAR_COLOR;
|
||||
const insertResult = insertGroupStatement.run(normalizedName, normalizedDescription || null, avatarColor);
|
||||
const groupId = Number(insertResult.lastInsertRowid);
|
||||
const row = selectGroupWithMembersById.get(groupId);
|
||||
return row ? sanitizeGroup(row) : null;
|
||||
const group = row ? sanitizeGroup(row) : null;
|
||||
|
||||
logEvent({
|
||||
category: 'benutzergruppe',
|
||||
eventType: 'gruppe-angelegt',
|
||||
action: 'anlegen',
|
||||
status: 'erfolgreich',
|
||||
entityType: 'gruppe',
|
||||
entityId: String(groupId),
|
||||
entityName: group?.name ?? normalizedName,
|
||||
message: `Benutzergruppe "${normalizedName}" angelegt`,
|
||||
metadata: {
|
||||
description: group?.description ?? normalizedDescription ?? null,
|
||||
avatarColor
|
||||
}
|
||||
});
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
export function updateGroupDetails(groupId, { name, description, avatarColor } = {}) {
|
||||
const numericId = Number(groupId);
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
const error = new Error('INVALID_GROUP_ID');
|
||||
error.code = 'INVALID_GROUP_ID';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingRow = selectGroupByIdBasic.get(numericId);
|
||||
if (!existingRow) {
|
||||
const error = new Error('GROUP_NOT_FOUND');
|
||||
error.code = 'GROUP_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalizedName = typeof name === 'string' ? name.trim() : existingRow.name || '';
|
||||
if (!normalizedName) {
|
||||
const error = new Error('GROUP_NAME_REQUIRED');
|
||||
error.code = 'GROUP_NAME_REQUIRED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const nameRow = selectGroupIdByName.get(normalizedName);
|
||||
if (nameRow && Number(nameRow.id) !== numericId) {
|
||||
const error = new Error('GROUP_NAME_TAKEN');
|
||||
error.code = 'GROUP_NAME_TAKEN';
|
||||
throw error;
|
||||
}
|
||||
|
||||
let normalizedDescription;
|
||||
if (description === undefined) {
|
||||
normalizedDescription = existingRow.description ?? null;
|
||||
} else if (description === null) {
|
||||
normalizedDescription = null;
|
||||
} else {
|
||||
const trimmedDescription = typeof description === 'string' ? description.trim() : '';
|
||||
normalizedDescription = trimmedDescription ? trimmedDescription : null;
|
||||
}
|
||||
|
||||
const isSuperuserGroup = (existingRow.name || '').toLowerCase() === SUPERUSER_GROUP_NAME;
|
||||
|
||||
if (isSuperuserGroup) {
|
||||
if (name !== undefined) {
|
||||
const incomingName = typeof name === 'string' ? name.trim() : '';
|
||||
if (incomingName && incomingName !== existingRow.name) {
|
||||
const error = new Error('GROUP_SUPERUSER_PROTECTED');
|
||||
error.code = 'GROUP_SUPERUSER_PROTECTED';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
const existingDescriptionComparable = existingRow.description === null || existingRow.description === undefined
|
||||
? ''
|
||||
: String(existingRow.description).trim();
|
||||
const incomingDescriptionComparable = description === null
|
||||
? ''
|
||||
: typeof description === 'string'
|
||||
? description.trim()
|
||||
: '';
|
||||
|
||||
if (incomingDescriptionComparable !== existingDescriptionComparable) {
|
||||
const error = new Error('GROUP_SUPERUSER_PROTECTED');
|
||||
error.code = 'GROUP_SUPERUSER_PROTECTED';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce original values for protected fields
|
||||
normalizedDescription = existingRow.description ?? null;
|
||||
}
|
||||
|
||||
let colorToPersist = existingRow.avatar_color || null;
|
||||
if (avatarColor !== undefined) {
|
||||
const candidate = String(avatarColor || '').trim();
|
||||
if (!candidate) {
|
||||
colorToPersist = DEFAULT_AVATAR_COLOR;
|
||||
} else {
|
||||
const normalized = normalizeAvatarColor(candidate);
|
||||
if (!normalized) {
|
||||
const error = new Error('INVALID_AVATAR_COLOR');
|
||||
error.code = 'INVALID_AVATAR_COLOR';
|
||||
throw error;
|
||||
}
|
||||
colorToPersist = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
updateGroupStatement.run(
|
||||
isSuperuserGroup ? existingRow.name : normalizedName,
|
||||
normalizedDescription,
|
||||
colorToPersist,
|
||||
numericId
|
||||
);
|
||||
|
||||
const updatedGroup = getGroupById(numericId);
|
||||
|
||||
logEvent({
|
||||
category: 'benutzergruppe',
|
||||
eventType: 'gruppe-aktualisiert',
|
||||
action: 'aktualisieren',
|
||||
status: 'erfolgreich',
|
||||
entityType: 'gruppe',
|
||||
entityId: String(numericId),
|
||||
entityName: updatedGroup?.name ?? normalizedName,
|
||||
message: `Benutzergruppe "${updatedGroup?.name ?? normalizedName}" aktualisiert`,
|
||||
metadata: {
|
||||
previous: {
|
||||
name: existingRow.name,
|
||||
description: existingRow.description ?? null,
|
||||
avatarColor: existingRow.avatar_color ?? null
|
||||
},
|
||||
current: updatedGroup
|
||||
? {
|
||||
name: updatedGroup.name,
|
||||
description: updatedGroup.description ?? null,
|
||||
avatarColor: updatedGroup.avatarColor ?? null,
|
||||
memberCount: updatedGroup.memberCount ?? 0
|
||||
}
|
||||
: {
|
||||
name: normalizedName,
|
||||
description: normalizedDescription ?? null,
|
||||
avatarColor: colorToPersist
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return updatedGroup;
|
||||
}
|
||||
|
||||
export function deleteGroup(groupId) {
|
||||
const numericId = Number(groupId);
|
||||
if (!Number.isFinite(numericId) || numericId <= 0) {
|
||||
const error = new Error('INVALID_GROUP_ID');
|
||||
error.code = 'INVALID_GROUP_ID';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingRow = selectGroupByIdBasic.get(numericId);
|
||||
if (!existingRow) {
|
||||
const error = new Error('GROUP_NOT_FOUND');
|
||||
error.code = 'GROUP_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const isSuperuserGroup = (existingRow.name || '').toLowerCase() === SUPERUSER_GROUP_NAME;
|
||||
if (isSuperuserGroup) {
|
||||
const error = new Error('GROUP_SUPERUSER_PROTECTED');
|
||||
error.code = 'GROUP_SUPERUSER_PROTECTED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { member_count: memberCount } = countGroupMembersStatement.get(numericId);
|
||||
if (Number(memberCount) > 0) {
|
||||
const error = new Error('GROUP_HAS_MEMBERS');
|
||||
error.code = 'GROUP_HAS_MEMBERS';
|
||||
error.memberCount = Number(memberCount);
|
||||
throw error;
|
||||
}
|
||||
|
||||
deleteGroupStatement.run(numericId);
|
||||
|
||||
logEvent({
|
||||
category: 'benutzergruppe',
|
||||
eventType: 'gruppe-gelöscht',
|
||||
action: 'löschen',
|
||||
status: 'erfolgreich',
|
||||
entityType: 'gruppe',
|
||||
entityId: String(numericId),
|
||||
entityName: existingRow.name ?? `ID ${numericId}`,
|
||||
message: `Benutzergruppe "${existingRow.name ?? numericId}" gelöscht`,
|
||||
metadata: {
|
||||
description: existingRow.description ?? null,
|
||||
avatarColor: existingRow.avatar_color ?? null,
|
||||
memberCountBeforeDelete: Number(memberCount)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user