Update
This commit is contained in:
+19
-10
@@ -69,6 +69,21 @@ CREATE TABLE user_group_memberships (
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE user_server_assignments (
|
||||
user_id INTEGER NOT NULL,
|
||||
server_id INTEGER NOT NULL,
|
||||
group_id INTEGER,
|
||||
use_global_group INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_id, server_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_user_server_assignments_user ON user_server_assignments (user_id);
|
||||
CREATE INDEX idx_user_server_assignments_server ON user_server_assignments (server_id);
|
||||
|
||||
CREATE TABLE permissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
@@ -100,13 +115,15 @@ CREATE TABLE permission_sections (
|
||||
CREATE TABLE permission_groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
section_id INTEGER NOT NULL,
|
||||
parent_group_id INTEGER,
|
||||
key TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(section_id, key),
|
||||
FOREIGN KEY (section_id) REFERENCES permission_sections(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (section_id) REFERENCES permission_sections(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (parent_group_id) REFERENCES permission_groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE permission_items (
|
||||
@@ -118,6 +135,7 @@ CREATE TABLE permission_items (
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
default_level TEXT NOT NULL,
|
||||
available_levels TEXT NOT NULL,
|
||||
is_global_scope INTEGER NOT NULL DEFAULT 1,
|
||||
is_required INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
@@ -210,15 +228,6 @@ CREATE TABLE server_update_scripts (
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE server_agents (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_id INTEGER NOT NULL UNIQUE,
|
||||
agent_url TEXT,
|
||||
agent_token TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_server_update_scripts_server ON server_update_scripts (server_id);
|
||||
|
||||
CREATE TABLE user_settings (
|
||||
|
||||
+234
-233
@@ -2,7 +2,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { db } from './index.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
const BLUEPRINT_FILENAME = 'dbs';
|
||||
const PERMISSION_BLUEPRINT = [
|
||||
@@ -16,6 +15,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'stacks-redeploy-single',
|
||||
label: 'Redeploy einzeln',
|
||||
sortOrder: 0,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: []
|
||||
@@ -24,6 +24,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'stacks-redeploy-selection',
|
||||
label: 'Redeploy Auswahl',
|
||||
sortOrder: 1,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: []
|
||||
@@ -32,6 +33,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'stacks-redeploy-all',
|
||||
label: 'Redeploy Alle',
|
||||
sortOrder: 2,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: []
|
||||
@@ -48,6 +50,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'logs-access',
|
||||
label: 'Bereich & Navigation',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
isRequired: 1,
|
||||
@@ -57,6 +60,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'logs-export',
|
||||
label: 'Logs Exportieren',
|
||||
sortOrder: 1,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -67,6 +71,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'logs-delete',
|
||||
label: 'Logs löschen',
|
||||
sortOrder: 2,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -85,6 +90,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'users-access',
|
||||
label: 'Bereich & Navigation',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
isRequired: 1,
|
||||
@@ -94,6 +100,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'users-edit',
|
||||
label: 'Benutzer bearbeiten',
|
||||
sortOrder: 1,
|
||||
global: true,
|
||||
defaultLevel: 'read',
|
||||
levels: ['full', 'read'],
|
||||
dependencies: [
|
||||
@@ -104,6 +111,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'users-delete',
|
||||
label: 'Benutzer löschen',
|
||||
sortOrder: 2,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -114,6 +122,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'users-security-phrase',
|
||||
label: 'Sicherheitsschlüssel',
|
||||
sortOrder: 3,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -132,6 +141,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'user-groups-access',
|
||||
label: 'Bereich & Navigation',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
isRequired: 1,
|
||||
@@ -141,6 +151,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'user-groups-edit',
|
||||
label: 'Benutzergruppen bearbeiten',
|
||||
sortOrder: 1,
|
||||
global: true,
|
||||
defaultLevel: 'read',
|
||||
levels: ['full', 'read'],
|
||||
dependencies: [
|
||||
@@ -151,6 +162,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'user-groups-delete',
|
||||
label: 'Benutzergruppen löschen',
|
||||
sortOrder: 2,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -169,6 +181,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'maintenance-access',
|
||||
label: 'Bereich & Navigation',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
isRequired: 1,
|
||||
@@ -185,8 +198,9 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'maintenance-server-manage',
|
||||
label: 'Server-Sektion',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'read', 'none'],
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' }
|
||||
]
|
||||
@@ -195,8 +209,9 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'maintenance-server-edit',
|
||||
label: 'Server bearbeiten',
|
||||
sortOrder: 1,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
levels: ['full', 'read', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-server-manage', requiredLevel: '!=none' }
|
||||
@@ -206,6 +221,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'maintenance-server-delete',
|
||||
label: 'Server löschen',
|
||||
sortOrder: 2,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -214,6 +230,58 @@ const PERMISSION_BLUEPRINT = [
|
||||
{ dependsOnKey: 'maintenance-server-edit', requiredLevel: '=full' }
|
||||
]
|
||||
}
|
||||
],
|
||||
groups: [
|
||||
{
|
||||
key: 'maintenance-portainer-group',
|
||||
title: 'Portainer',
|
||||
sortOrder: 1,
|
||||
items: [
|
||||
{
|
||||
key: 'maintenance-ssh-update',
|
||||
label: 'SSH/Update-Skript',
|
||||
sortOrder: 0,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'read', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-server-edit', requiredLevel: '!=none' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-update',
|
||||
label: 'Update durchführen',
|
||||
sortOrder: 1,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-server-edit', requiredLevel: '!=none' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-duplicates-group',
|
||||
title: 'Doppelte Stacks',
|
||||
sortOrder: 2,
|
||||
items: [
|
||||
{
|
||||
key: 'maintenance-duplicates',
|
||||
label: 'Doppelte Stacks',
|
||||
sortOrder: 0,
|
||||
global: false,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'read', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-server-edit', requiredLevel: '!=none' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -225,6 +293,7 @@ const PERMISSION_BLUEPRINT = [
|
||||
key: 'maintenance-superuser-delete',
|
||||
label: 'Superuser löschen',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
@@ -234,58 +303,20 @@ const PERMISSION_BLUEPRINT = [
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-portainer-group',
|
||||
title: 'Portainer',
|
||||
key: 'maintenance-mtls-group',
|
||||
title: 'mTLS',
|
||||
sortOrder: 2,
|
||||
items: [
|
||||
{
|
||||
key: 'maintenance-portainer',
|
||||
label: 'Portainer-Sektion',
|
||||
key: 'maintenance-mtls',
|
||||
label: 'mTLS Sektion',
|
||||
sortOrder: 0,
|
||||
global: true,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-ssh-update',
|
||||
label: 'SSH/Update-Skript',
|
||||
sortOrder: 1,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'read', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-portainer', requiredLevel: '!=none' }
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-update',
|
||||
label: 'Update durchführen',
|
||||
sortOrder: 2,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' },
|
||||
{ dependsOnKey: 'maintenance-portainer', requiredLevel: '!=none' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'maintenance-duplicates-group',
|
||||
title: 'Doppelte Stacks',
|
||||
sortOrder: 3,
|
||||
items: [
|
||||
{
|
||||
key: 'maintenance-duplicates',
|
||||
label: 'Doppelte Stacks',
|
||||
sortOrder: 0,
|
||||
defaultLevel: 'none',
|
||||
levels: ['full', 'read', 'none'],
|
||||
dependencies: [
|
||||
{ dependsOnKey: 'maintenance-access', requiredLevel: '!=none' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -381,7 +412,13 @@ const tableExists = (tableName) => {
|
||||
};
|
||||
|
||||
const dropDeprecatedArtifacts = () => {
|
||||
const deprecatedTables = ['user_endpoint_permission_overrides', 'endpoints'];
|
||||
const deprecatedTables = [
|
||||
'user_endpoint_permission_overrides',
|
||||
'endpoints',
|
||||
'server_agents',
|
||||
'agent_tls_material',
|
||||
'agent_bootstrap_tokens'
|
||||
];
|
||||
deprecatedTables.forEach((table) => {
|
||||
if (tableExists(table)) {
|
||||
db.exec(`DROP TABLE IF EXISTS ${table}`);
|
||||
@@ -453,20 +490,24 @@ const ensurePermissionSeeds = () => {
|
||||
`);
|
||||
|
||||
const selectGroup = db.prepare(`
|
||||
SELECT id, title, sort_order
|
||||
SELECT id, title, sort_order, parent_group_id
|
||||
FROM permission_groups
|
||||
WHERE section_id = ? AND key = ?
|
||||
LIMIT 1
|
||||
`);
|
||||
const insertGroup = db.prepare(`
|
||||
INSERT INTO permission_groups (section_id, key, title, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
INSERT INTO permission_groups (section_id, parent_group_id, key, title, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`);
|
||||
const updateGroup = db.prepare(`
|
||||
UPDATE permission_groups
|
||||
SET title = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP
|
||||
SET title = ?, sort_order = ?, parent_group_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
const deleteUnusedGroups = db.prepare(`
|
||||
DELETE FROM permission_groups
|
||||
WHERE key NOT IN (SELECT value FROM json_each(?))
|
||||
`);
|
||||
|
||||
const selectItem = db.prepare(`
|
||||
SELECT
|
||||
@@ -477,6 +518,7 @@ const ensurePermissionSeeds = () => {
|
||||
sort_order,
|
||||
default_level,
|
||||
available_levels,
|
||||
is_global_scope,
|
||||
is_required
|
||||
FROM permission_items
|
||||
WHERE key = ?
|
||||
@@ -491,11 +533,12 @@ const ensurePermissionSeeds = () => {
|
||||
sort_order,
|
||||
default_level,
|
||||
available_levels,
|
||||
is_global_scope,
|
||||
is_required,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
const updateItem = db.prepare(`
|
||||
@@ -507,10 +550,24 @@ const ensurePermissionSeeds = () => {
|
||||
sort_order = ?,
|
||||
default_level = ?,
|
||||
available_levels = ?,
|
||||
is_global_scope = ?,
|
||||
is_required = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
const deleteUnusedItems = db.prepare(`
|
||||
DELETE FROM permission_items
|
||||
WHERE key NOT IN (SELECT value FROM json_each(?))
|
||||
`);
|
||||
const deleteOrphanDependencies = db.prepare(`
|
||||
DELETE FROM permission_dependencies
|
||||
WHERE permission_id NOT IN (SELECT id FROM permission_items)
|
||||
OR depends_on_permission_id NOT IN (SELECT id FROM permission_items)
|
||||
`);
|
||||
const deleteOrphanPermissionValues = db.prepare(`
|
||||
DELETE FROM group_permission_values
|
||||
WHERE permission_id NOT IN (SELECT id FROM permission_items)
|
||||
`);
|
||||
|
||||
const selectDependency = db.prepare(`
|
||||
SELECT id, required_level
|
||||
@@ -536,6 +593,100 @@ const ensurePermissionSeeds = () => {
|
||||
`);
|
||||
|
||||
const itemKeyToId = new Map();
|
||||
const dependencyQueue = [];
|
||||
const itemKeys = new Set();
|
||||
const groupKeys = new Set();
|
||||
|
||||
const upsertItem = ({ item, sectionId, groupId = null, sortOrder }) => {
|
||||
const existingItem = selectItem.get(item.key);
|
||||
const itemSortOrder = typeof sortOrder === 'number' ? sortOrder : 0;
|
||||
const levelOptions = Array.isArray(item.levels) && item.levels.length
|
||||
? item.levels
|
||||
: ['full', 'read', 'none'];
|
||||
const availableLevels = JSON.stringify(levelOptions);
|
||||
const isRequired = item.isRequired ? 1 : 0;
|
||||
const isGlobal = item.global === false ? 0 : 1;
|
||||
const label = item.label || item.key || '';
|
||||
|
||||
if (!existingItem) {
|
||||
const result = insertItem.run(
|
||||
sectionId,
|
||||
groupId,
|
||||
item.key,
|
||||
label,
|
||||
itemSortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isGlobal,
|
||||
isRequired
|
||||
);
|
||||
itemKeyToId.set(item.key, Number(result.lastInsertRowid));
|
||||
console.log(`ℹ️ Berechtigung ${item.key} ${groupId ? `in Gruppe ${groupId}` : ''} angelegt`);
|
||||
} else {
|
||||
const hasChanges =
|
||||
existingItem.section_id !== sectionId ||
|
||||
existingItem.group_id !== groupId ||
|
||||
existingItem.label !== label ||
|
||||
(existingItem.sort_order ?? itemSortOrder) !== itemSortOrder ||
|
||||
existingItem.default_level !== (item.defaultLevel || 'none') ||
|
||||
existingItem.available_levels !== availableLevels ||
|
||||
Number(existingItem.is_global_scope ?? 1) !== isGlobal ||
|
||||
Number(existingItem.is_required) !== isRequired;
|
||||
updateItem.run(
|
||||
sectionId,
|
||||
groupId,
|
||||
label,
|
||||
itemSortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isGlobal,
|
||||
isRequired,
|
||||
existingItem.id
|
||||
);
|
||||
itemKeyToId.set(item.key, existingItem.id);
|
||||
if (hasChanges) {
|
||||
console.log(`ℹ️ Berechtigung ${item.key} aktualisiert`);
|
||||
}
|
||||
}
|
||||
|
||||
itemKeys.add(item.key);
|
||||
dependencyQueue.push({ itemKey: item.key, dependencies: item.dependencies || [] });
|
||||
};
|
||||
|
||||
const processGroups = (groups, sectionId, parentGroupId = null) => {
|
||||
const list = Array.isArray(groups) ? groups : [];
|
||||
list.forEach((group, groupIdx) => {
|
||||
const sortOrder = typeof group.sortOrder === 'number' ? group.sortOrder : groupIdx;
|
||||
const existingGroup = selectGroup.get(sectionId, group.key);
|
||||
let groupId;
|
||||
if (!existingGroup) {
|
||||
const result = insertGroup.run(sectionId, parentGroupId, group.key, group.title, sortOrder);
|
||||
groupId = Number(result.lastInsertRowid);
|
||||
console.log(`ℹ️ Berechtigungs-Gruppe ${group.key} in Sektion ${sectionId} angelegt`);
|
||||
} else {
|
||||
const hasGroupChanges =
|
||||
existingGroup.title !== group.title ||
|
||||
(existingGroup.sort_order ?? sortOrder) !== sortOrder ||
|
||||
existingGroup.parent_group_id !== parentGroupId;
|
||||
updateGroup.run(group.title, sortOrder, parentGroupId, existingGroup.id);
|
||||
groupId = existingGroup.id;
|
||||
if (hasGroupChanges) {
|
||||
console.log(`ℹ️ Berechtigungs-Gruppe ${group.key} in Sektion ${sectionId} aktualisiert`);
|
||||
}
|
||||
}
|
||||
|
||||
groupKeys.add(group.key);
|
||||
|
||||
const groupItems = Array.isArray(group.items) ? group.items : [];
|
||||
groupItems.forEach((item, itemIdx) => {
|
||||
upsertItem({ item, sectionId, groupId, sortOrder: typeof item.sortOrder === 'number' ? item.sortOrder : itemIdx });
|
||||
});
|
||||
|
||||
if (group.groups) {
|
||||
processGroups(group.groups, sectionId, groupId);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
PERMISSION_BLUEPRINT.forEach((section, sectionIndex) => {
|
||||
const existingSection = selectSection.get(section.key);
|
||||
@@ -568,190 +719,41 @@ const ensurePermissionSeeds = () => {
|
||||
|
||||
const sectionItems = Array.isArray(section.items) ? section.items : [];
|
||||
sectionItems.forEach((item, itemIdx) => {
|
||||
const existingItem = selectItem.get(item.key);
|
||||
const sortOrder = typeof item.sortOrder === 'number' ? item.sortOrder : itemIdx;
|
||||
const levelOptions = Array.isArray(item.levels) && item.levels.length
|
||||
? item.levels
|
||||
: ['full', 'read', 'none'];
|
||||
const availableLevels = JSON.stringify(levelOptions);
|
||||
const isRequired = item.isRequired ? 1 : 0;
|
||||
if (!existingItem) {
|
||||
const result = insertItem.run(
|
||||
sectionId,
|
||||
null,
|
||||
item.key,
|
||||
item.label,
|
||||
sortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isRequired
|
||||
);
|
||||
itemKeyToId.set(item.key, Number(result.lastInsertRowid));
|
||||
console.log(`ℹ️ Berechtigung ${item.key} angelegt`);
|
||||
} else {
|
||||
const hasChanges =
|
||||
existingItem.section_id !== sectionId ||
|
||||
existingItem.group_id !== null ||
|
||||
existingItem.label !== item.label ||
|
||||
(existingItem.sort_order ?? sortOrder) !== sortOrder ||
|
||||
existingItem.default_level !== (item.defaultLevel || 'none') ||
|
||||
existingItem.available_levels !== availableLevels ||
|
||||
Number(existingItem.is_required) !== isRequired;
|
||||
updateItem.run(
|
||||
sectionId,
|
||||
null,
|
||||
item.label,
|
||||
sortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isRequired,
|
||||
existingItem.id
|
||||
);
|
||||
itemKeyToId.set(item.key, existingItem.id);
|
||||
if (hasChanges) {
|
||||
console.log(`ℹ️ Berechtigung ${item.key} aktualisiert`);
|
||||
}
|
||||
upsertItem({ item, sectionId, groupId: null, sortOrder: typeof item.sortOrder === 'number' ? item.sortOrder : itemIdx });
|
||||
});
|
||||
|
||||
processGroups(section.groups, sectionId, null);
|
||||
});
|
||||
|
||||
dependencyQueue.forEach(({ itemKey, dependencies }) => {
|
||||
const permissionId = itemKeyToId.get(itemKey);
|
||||
if (!permissionId) return;
|
||||
dependencies.forEach((dependency) => {
|
||||
const dependsId = itemKeyToId.get(dependency.dependsOnKey);
|
||||
if (!dependsId) return;
|
||||
const existing = selectDependency.get(permissionId, dependsId);
|
||||
const requiredLevel = dependency.requiredLevel ?? null;
|
||||
if (!existing) {
|
||||
insertDependency.run(permissionId, dependsId, requiredLevel);
|
||||
console.log(`ℹ️ Abhängigkeit ${itemKey} -> ${dependency.dependsOnKey} angelegt`);
|
||||
} else if (existing.required_level !== requiredLevel) {
|
||||
updateDependency.run(requiredLevel, existing.id);
|
||||
console.log(`ℹ️ Abhängigkeit ${itemKey} -> ${dependency.dependsOnKey} aktualisiert`);
|
||||
}
|
||||
});
|
||||
|
||||
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
|
||||
sectionGroups.forEach((group, groupIdx) => {
|
||||
const existingGroup = selectGroup.get(sectionId, group.key);
|
||||
const sortOrder = typeof group.sortOrder === 'number' ? group.sortOrder : groupIdx;
|
||||
let groupId;
|
||||
if (!existingGroup) {
|
||||
const result = insertGroup.run(sectionId, group.key, group.title, sortOrder);
|
||||
groupId = Number(result.lastInsertRowid);
|
||||
console.log(`ℹ️ Berechtigungs-Gruppe ${group.key} in Sektion ${section.key} angelegt`);
|
||||
} else {
|
||||
const hasGroupChanges =
|
||||
existingGroup.title !== group.title ||
|
||||
(existingGroup.sort_order ?? sortOrder) !== sortOrder;
|
||||
updateGroup.run(group.title, sortOrder, existingGroup.id);
|
||||
groupId = existingGroup.id;
|
||||
if (hasGroupChanges) {
|
||||
console.log(`ℹ️ Berechtigungs-Gruppe ${group.key} in Sektion ${section.key} aktualisiert`);
|
||||
}
|
||||
}
|
||||
const groupItems = Array.isArray(group.items) ? group.items : [];
|
||||
groupItems.forEach((item, itemIdx) => {
|
||||
const existingItem = selectItem.get(item.key);
|
||||
const itemSortOrder = typeof item.sortOrder === 'number' ? item.sortOrder : itemIdx;
|
||||
const levelOptions = Array.isArray(item.levels) && item.levels.length
|
||||
? item.levels
|
||||
: ['full', 'read', 'none'];
|
||||
const availableLevels = JSON.stringify(levelOptions);
|
||||
const isRequired = item.isRequired ? 1 : 0;
|
||||
if (!existingItem) {
|
||||
const result = insertItem.run(
|
||||
sectionId,
|
||||
groupId,
|
||||
item.key,
|
||||
item.label,
|
||||
itemSortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isRequired
|
||||
);
|
||||
itemKeyToId.set(item.key, Number(result.lastInsertRowid));
|
||||
console.log(`ℹ️ Berechtigung ${item.key} in Gruppe ${group.key} angelegt`);
|
||||
} else {
|
||||
const hasChanges =
|
||||
existingItem.section_id !== sectionId ||
|
||||
existingItem.group_id !== groupId ||
|
||||
existingItem.label !== item.label ||
|
||||
(existingItem.sort_order ?? itemSortOrder) !== itemSortOrder ||
|
||||
existingItem.default_level !== (item.defaultLevel || 'none') ||
|
||||
existingItem.available_levels !== availableLevels ||
|
||||
Number(existingItem.is_required) !== isRequired;
|
||||
updateItem.run(
|
||||
sectionId,
|
||||
groupId,
|
||||
item.label,
|
||||
itemSortOrder,
|
||||
item.defaultLevel || 'none',
|
||||
availableLevels,
|
||||
isRequired,
|
||||
existingItem.id
|
||||
);
|
||||
itemKeyToId.set(item.key, existingItem.id);
|
||||
if (hasChanges) {
|
||||
console.log(`ℹ️ Berechtigung ${item.key} in Gruppe ${group.key} aktualisiert`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
PERMISSION_BLUEPRINT.forEach((section) => {
|
||||
const sectionItems = Array.isArray(section.items) ? section.items : [];
|
||||
sectionItems.forEach((item) => {
|
||||
const permissionId = itemKeyToId.get(item.key);
|
||||
if (!permissionId) return;
|
||||
(item.dependencies || []).forEach((dependency) => {
|
||||
const dependsId = itemKeyToId.get(dependency.dependsOnKey);
|
||||
if (!dependsId) return;
|
||||
const existing = selectDependency.get(permissionId, dependsId);
|
||||
const requiredLevel = dependency.requiredLevel ?? null;
|
||||
if (!existing) {
|
||||
insertDependency.run(permissionId, dependsId, requiredLevel);
|
||||
console.log(
|
||||
`ℹ️ Abhängigkeit ${item.key} -> ${dependency.dependsOnKey} angelegt`
|
||||
);
|
||||
} else if (existing.required_level !== requiredLevel) {
|
||||
updateDependency.run(requiredLevel, existing.id);
|
||||
console.log(
|
||||
`ℹ️ Abhängigkeit ${item.key} -> ${dependency.dependsOnKey} aktualisiert`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const sectionGroups = Array.isArray(section.groups) ? section.groups : [];
|
||||
sectionGroups.forEach((group) => {
|
||||
const groupItems = Array.isArray(group.items) ? group.items : [];
|
||||
groupItems.forEach((item) => {
|
||||
const permissionId = itemKeyToId.get(item.key);
|
||||
if (!permissionId) return;
|
||||
(item.dependencies || []).forEach((dependency) => {
|
||||
const dependsId = itemKeyToId.get(dependency.dependsOnKey);
|
||||
if (!dependsId) return;
|
||||
const requiredLevel = dependency.requiredLevel ?? null;
|
||||
const existing = selectDependency.get(permissionId, dependsId);
|
||||
if (!existing) {
|
||||
insertDependency.run(permissionId, dependsId, requiredLevel);
|
||||
console.log(
|
||||
`ℹ️ Abhängigkeit ${item.key} -> ${dependency.dependsOnKey} angelegt`
|
||||
);
|
||||
} else if (existing.required_level !== requiredLevel) {
|
||||
updateDependency.run(requiredLevel, existing.id);
|
||||
console.log(
|
||||
`ℹ️ Abhängigkeit ${item.key} -> ${dependency.dependsOnKey} aktualisiert`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const ensureServerAgentEntries = () => {
|
||||
if (!tableExists('server_agents')) return;
|
||||
const servers = db.prepare('SELECT id FROM servers').all();
|
||||
const insertAgent = db.prepare(`
|
||||
INSERT OR IGNORE INTO server_agents (server_id, agent_url, agent_token, created_at, updated_at)
|
||||
VALUES (?, NULL, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`);
|
||||
const updateMissingToken = db.prepare(`
|
||||
UPDATE server_agents
|
||||
SET agent_token = ?
|
||||
WHERE server_id = ? AND (agent_token IS NULL OR agent_token = '')
|
||||
`);
|
||||
servers.forEach((server) => {
|
||||
const token = `sp_${crypto.randomBytes(16).toString('hex')}`;
|
||||
insertAgent.run(server.id, token);
|
||||
updateMissingToken.run(token, server.id);
|
||||
});
|
||||
// Cleanup obsolete entries
|
||||
if (itemKeys.size > 0) {
|
||||
const itemKeyJson = JSON.stringify(Array.from(itemKeys));
|
||||
deleteOrphanDependencies.run();
|
||||
deleteOrphanPermissionValues.run();
|
||||
deleteUnusedItems.run(itemKeyJson);
|
||||
}
|
||||
if (groupKeys.size > 0) {
|
||||
const groupKeyJson = JSON.stringify(Array.from(groupKeys));
|
||||
deleteUnusedGroups.run(groupKeyJson);
|
||||
}
|
||||
};
|
||||
|
||||
export const ensureDatabaseSchema = () => {
|
||||
@@ -762,7 +764,6 @@ export const ensureDatabaseSchema = () => {
|
||||
|
||||
dropDeprecatedArtifacts();
|
||||
ensureTablesAndColumns(tableDefinitions);
|
||||
ensureServerAgentEntries();
|
||||
ensureIndexes(indexDefinitions);
|
||||
ensurePermissionSeeds();
|
||||
} catch (error) {
|
||||
|
||||
+539
-176
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ const selectSections = db.prepare(`
|
||||
`);
|
||||
|
||||
const selectGroups = db.prepare(`
|
||||
SELECT id, section_id, key, title, sort_order
|
||||
SELECT id, section_id, parent_group_id, key, title, sort_order
|
||||
FROM permission_groups
|
||||
ORDER BY section_id ASC, sort_order ASC, id ASC
|
||||
`);
|
||||
@@ -28,6 +28,7 @@ const selectItems = db.prepare(`
|
||||
sort_order,
|
||||
default_level,
|
||||
available_levels,
|
||||
is_global_scope,
|
||||
is_required
|
||||
FROM permission_items
|
||||
ORDER BY section_id ASC, COALESCE(group_id, 0) ASC, sort_order ASC, id ASC
|
||||
@@ -39,7 +40,7 @@ const selectDependencies = db.prepare(`
|
||||
`);
|
||||
|
||||
const selectPermissionItemsForMap = db.prepare(`
|
||||
SELECT id, key, available_levels, default_level
|
||||
SELECT id, key, available_levels, default_level, is_global_scope
|
||||
FROM permission_items
|
||||
`);
|
||||
|
||||
@@ -81,6 +82,8 @@ const getLevelPriority = (level) => {
|
||||
};
|
||||
|
||||
let cachedPermissionItems = null;
|
||||
let cachedServerScopedKeys = null;
|
||||
let cachedScopeByKey = null;
|
||||
|
||||
const getPermissionItems = () => {
|
||||
if (!cachedPermissionItems) {
|
||||
@@ -91,6 +94,24 @@ const getPermissionItems = () => {
|
||||
|
||||
const resetPermissionItemsCache = () => {
|
||||
cachedPermissionItems = null;
|
||||
cachedServerScopedKeys = null;
|
||||
cachedScopeByKey = null;
|
||||
};
|
||||
|
||||
const getScopeCache = () => {
|
||||
if (!cachedScopeByKey || !cachedServerScopedKeys) {
|
||||
const items = getPermissionItems();
|
||||
cachedScopeByKey = new Map();
|
||||
cachedServerScopedKeys = new Set();
|
||||
items.forEach((item) => {
|
||||
const isGlobal = item.is_global_scope !== 0;
|
||||
cachedScopeByKey.set(item.key, { isGlobal });
|
||||
if (!isGlobal) {
|
||||
cachedServerScopedKeys.add(item.key);
|
||||
}
|
||||
});
|
||||
}
|
||||
return { scopeByKey: cachedScopeByKey, serverScopedKeys: cachedServerScopedKeys };
|
||||
};
|
||||
|
||||
export function getPermissionStructure() {
|
||||
@@ -109,10 +130,23 @@ export function getPermissionStructure() {
|
||||
});
|
||||
|
||||
const groupsBySectionId = new Map();
|
||||
groups.forEach((group) => {
|
||||
const list = groupsBySectionId.get(group.section_id) || [];
|
||||
list.push(group);
|
||||
groupsBySectionId.set(group.section_id, list);
|
||||
const groupsByParentId = new Map();
|
||||
const sortedGroups = [...groups].sort((a, b) => {
|
||||
const sortDiff = (a.sort_order ?? 0) - (b.sort_order ?? 0);
|
||||
if (sortDiff !== 0) return sortDiff;
|
||||
return a.id - b.id;
|
||||
});
|
||||
|
||||
sortedGroups.forEach((group) => {
|
||||
if (group.parent_group_id) {
|
||||
const list = groupsByParentId.get(group.parent_group_id) || [];
|
||||
list.push(group);
|
||||
groupsByParentId.set(group.parent_group_id, list);
|
||||
} else {
|
||||
const list = groupsBySectionId.get(group.section_id) || [];
|
||||
list.push(group);
|
||||
groupsBySectionId.set(group.section_id, list);
|
||||
}
|
||||
});
|
||||
|
||||
const itemsBySectionId = new Map();
|
||||
@@ -154,18 +188,26 @@ export function getPermissionStructure() {
|
||||
defaultLevel: item.default_level || 'none',
|
||||
availableLevels,
|
||||
isRequired: Boolean(item.is_required),
|
||||
isGlobal: item.is_global_scope !== 0,
|
||||
dependencies: formattedDependencies
|
||||
};
|
||||
};
|
||||
|
||||
const buildGroupTree = (group) => {
|
||||
const children = groupsByParentId.get(group.id) || [];
|
||||
return {
|
||||
key: group.key,
|
||||
title: group.title,
|
||||
sortOrder: group.sort_order ?? 0,
|
||||
items: (itemsByGroupId.get(group.id) || []).map(formatItem),
|
||||
groups: children.map(buildGroupTree)
|
||||
};
|
||||
};
|
||||
|
||||
const formattedSections = sections.map((section) => {
|
||||
const sectionItems = (itemsBySectionId.get(section.id) || []).map(formatItem);
|
||||
const sectionGroups = (groupsBySectionId.get(section.id) || []).map((group) => ({
|
||||
key: group.key,
|
||||
title: group.title,
|
||||
sortOrder: group.sort_order ?? 0,
|
||||
items: (itemsByGroupId.get(group.id) || []).map(formatItem)
|
||||
}));
|
||||
const rootGroups = groupsBySectionId.get(section.id) || [];
|
||||
const sectionGroups = rootGroups.map(buildGroupTree);
|
||||
|
||||
return {
|
||||
key: section.key,
|
||||
@@ -270,13 +312,24 @@ export function saveGroupPermissionValues(groupId, values = {}) {
|
||||
const serverEditLevel = getSubmittedLevel('maintenance-server-edit');
|
||||
const serverDeleteLevel = getSubmittedLevel('maintenance-server-delete');
|
||||
|
||||
if (getLevelPriority(serverDeleteLevel) >= getLevelPriority('full') &&
|
||||
(getLevelPriority(serverManageLevel) < getLevelPriority('full') ||
|
||||
getLevelPriority(serverEditLevel) < getLevelPriority('full'))) {
|
||||
const error = new Error('PERMISSION_DEPENDENCY_LEVEL');
|
||||
error.code = 'PERMISSION_DEPENDENCY_LEVEL';
|
||||
error.permissionKey = 'maintenance-server-delete';
|
||||
throw error;
|
||||
// Enforce dependency: "server löschen" darf maximal so hoch sein wie manage/edit.
|
||||
const deletePriority = getLevelPriority(serverDeleteLevel);
|
||||
const allowedDeletePriority = Math.min(
|
||||
getLevelPriority(serverManageLevel),
|
||||
getLevelPriority(serverEditLevel)
|
||||
);
|
||||
if (deletePriority > allowedDeletePriority) {
|
||||
const allowedLevelEntry = Object.entries(LEVEL_PRIORITY).find(([, priority]) => priority === allowedDeletePriority);
|
||||
const allowedLevel = allowedLevelEntry ? allowedLevelEntry[0] : 'none';
|
||||
const deleteEntry = normalizedEntries.find((entry) => entry.item.key === 'maintenance-server-delete');
|
||||
if (deleteEntry) {
|
||||
deleteEntry.level = allowedLevel;
|
||||
} else {
|
||||
normalizedEntries.push({
|
||||
item: itemMap.get('maintenance-server-delete'),
|
||||
level: allowedLevel
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const runSave = db.transaction(() => {
|
||||
@@ -324,6 +377,32 @@ export function getSuperuserPermissionMap() {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getServerScopedPermissionKeys() {
|
||||
const { serverScopedKeys } = getScopeCache();
|
||||
return Array.from(serverScopedKeys);
|
||||
}
|
||||
|
||||
export function isServerScopedPermission(permissionKey) {
|
||||
if (!permissionKey) return false;
|
||||
const { serverScopedKeys, scopeByKey } = getScopeCache();
|
||||
if (serverScopedKeys.has(permissionKey)) {
|
||||
return true;
|
||||
}
|
||||
const scope = scopeByKey.get(permissionKey);
|
||||
return scope ? scope.isGlobal === false : false;
|
||||
}
|
||||
|
||||
export function applyServerScopedOverride(basePermissions = {}, overridePermissions = {}) {
|
||||
const merged = { ...basePermissions };
|
||||
const { serverScopedKeys } = getScopeCache();
|
||||
serverScopedKeys.forEach((key) => {
|
||||
if (Object.prototype.hasOwnProperty.call(overridePermissions, key)) {
|
||||
merged[key] = overridePermissions[key];
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function getEffectivePermissionsForGroup(groupId) {
|
||||
const defaults = getDefaultPermissionMap();
|
||||
const numericId = Number(groupId);
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -34,8 +34,8 @@
|
||||
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||
|
||||
|
||||
<script type="module" crossorigin src="/assets/index-027ca62d.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-4d48f088.css">
|
||||
<script type="module" crossorigin src="/assets/index-c3010de9.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-c7ee792b.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
+6
-61
@@ -14,17 +14,6 @@ const updateServer = db.prepare(`
|
||||
SET name = ?, url = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`);
|
||||
const selectAgentByServerId = db.prepare('SELECT * FROM server_agents WHERE server_id = ?');
|
||||
const selectAllAgents = db.prepare('SELECT * FROM server_agents');
|
||||
const upsertAgent = db.prepare(`
|
||||
INSERT INTO server_agents (server_id, agent_url, agent_token, created_at, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(server_id) DO UPDATE SET
|
||||
agent_url = excluded.agent_url,
|
||||
agent_token = excluded.agent_token,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`);
|
||||
|
||||
const deleteServerStmt = db.prepare('DELETE FROM servers WHERE id = ?');
|
||||
|
||||
const selectApiKeyByServerId = db.prepare('SELECT * FROM server_api_keys WHERE server_id = ?');
|
||||
@@ -44,6 +33,10 @@ const deleteApiKeyByServerId = db.prepare('DELETE FROM server_api_keys WHERE ser
|
||||
const countServersStmt = db.prepare('SELECT COUNT(*) as count FROM servers');
|
||||
const selectFirstServerStmt = db.prepare('SELECT * FROM servers ORDER BY id ASC LIMIT 1');
|
||||
|
||||
function listServers() {
|
||||
return selectAllServers.all();
|
||||
}
|
||||
|
||||
const API_KEY_SECRET = crypto.createHash('sha256')
|
||||
.update(process.env.PORTAINER_API_SECRET || process.env.PORTAINER_API_KEY || 'stackpulse-portainer-api-key')
|
||||
.digest();
|
||||
@@ -136,49 +129,6 @@ function ensureServer({ name, url }) {
|
||||
return created;
|
||||
}
|
||||
|
||||
const generateAgentToken = () => `sp_${crypto.randomBytes(16).toString('hex')}`;
|
||||
|
||||
function getAgentConfig(serverId, { autoCreate = false } = {}) {
|
||||
const id = Number(serverId);
|
||||
if (!Number.isFinite(id)) {
|
||||
const error = new Error('SERVER_ID_INVALID');
|
||||
error.code = 'SERVER_ID_INVALID';
|
||||
throw error;
|
||||
}
|
||||
const server = getServerById(id);
|
||||
let agent = selectAgentByServerId.get(server.id) || null;
|
||||
if (!agent && autoCreate) {
|
||||
const token = generateAgentToken();
|
||||
upsertAgent.run(server.id, null, token);
|
||||
agent = selectAgentByServerId.get(server.id) || null;
|
||||
}
|
||||
return agent ? { serverId: server.id, agentUrl: agent.agent_url, agentToken: agent.agent_token } : null;
|
||||
}
|
||||
|
||||
function setAgentConfig(serverId, { agentUrl, agentToken } = {}) {
|
||||
const id = Number(serverId);
|
||||
if (!Number.isFinite(id)) {
|
||||
const error = new Error('SERVER_ID_INVALID');
|
||||
error.code = 'SERVER_ID_INVALID';
|
||||
throw error;
|
||||
}
|
||||
const server = getServerById(id);
|
||||
const normalizedUrl = typeof agentUrl === 'string' && agentUrl.trim().length
|
||||
? agentUrl.trim().replace(/\/+$/, '')
|
||||
: null;
|
||||
let normalizedToken = typeof agentToken === 'string' ? agentToken.trim() : '';
|
||||
if (!normalizedToken) {
|
||||
const existing = selectAgentByServerId.get(server.id);
|
||||
normalizedToken = existing?.agent_token || generateAgentToken();
|
||||
}
|
||||
if (!normalizedToken.startsWith('sp_')) {
|
||||
normalizedToken = `sp_${normalizedToken}`;
|
||||
}
|
||||
upsertAgent.run(server.id, normalizedUrl, normalizedToken);
|
||||
const agent = selectAgentByServerId.get(server.id);
|
||||
return { serverId: server.id, agentUrl: agent.agent_url, agentToken: agent.agent_token };
|
||||
}
|
||||
|
||||
function getServerById(serverId) {
|
||||
const id = Number(serverId);
|
||||
if (!Number.isFinite(id)) {
|
||||
@@ -371,15 +321,12 @@ function ensureDefaultsFromEnv() {
|
||||
function getServerStatus(serverId) {
|
||||
const server = getServerById(serverId);
|
||||
const apiKeyMeta = selectApiKeyByServerId.get(server.id) || null;
|
||||
const agent = getAgentConfig(server.id, { autoCreate: true });
|
||||
|
||||
return {
|
||||
server,
|
||||
apiKey: {
|
||||
hasKey: Boolean(apiKeyMeta),
|
||||
updatedAt: apiKeyMeta?.updated_at ?? null
|
||||
},
|
||||
agent
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -484,10 +431,8 @@ export {
|
||||
ensureDefaultsFromEnv,
|
||||
ensureServer,
|
||||
getServerById,
|
||||
listServers,
|
||||
setServerApiKey,
|
||||
getAgentConfig,
|
||||
setAgentConfig,
|
||||
generateAgentToken,
|
||||
getServerConnection,
|
||||
updateServerDetails,
|
||||
getServerStatus,
|
||||
|
||||
@@ -75,6 +75,48 @@ const insertMembershipForUser = db.prepare(`
|
||||
VALUES (?, ?)
|
||||
`);
|
||||
|
||||
const selectServerByIdForAssignment = db.prepare('SELECT id, name, url FROM servers WHERE id = ?');
|
||||
|
||||
const selectAssignmentsByUser = db.prepare(`
|
||||
SELECT
|
||||
usa.user_id,
|
||||
usa.server_id,
|
||||
usa.group_id,
|
||||
usa.use_global_group,
|
||||
usa.created_at,
|
||||
usa.updated_at,
|
||||
s.name AS server_name,
|
||||
s.url AS server_url,
|
||||
g.name AS group_name
|
||||
FROM user_server_assignments usa
|
||||
JOIN servers s ON s.id = usa.server_id
|
||||
LEFT JOIN user_groups g ON g.id = usa.group_id
|
||||
WHERE usa.user_id = ?
|
||||
ORDER BY s.name ASC, usa.server_id ASC
|
||||
`);
|
||||
|
||||
const upsertUserServerAssignment = db.prepare(`
|
||||
INSERT INTO user_server_assignments (
|
||||
user_id,
|
||||
server_id,
|
||||
group_id,
|
||||
use_global_group,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
)
|
||||
ON CONFLICT(user_id, server_id) DO UPDATE SET
|
||||
group_id = excluded.group_id,
|
||||
use_global_group = excluded.use_global_group,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`);
|
||||
|
||||
const deleteUserServerAssignments = db.prepare(`
|
||||
DELETE FROM user_server_assignments
|
||||
WHERE user_id = ?
|
||||
`);
|
||||
|
||||
const selectSecurityPhraseByUsername = db.prepare(`
|
||||
SELECT
|
||||
id,
|
||||
@@ -883,6 +925,176 @@ export function updateUserDetails(userId, { username, email, password, avatarCol
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
const sanitizeAssignment = (row) => ({
|
||||
userId: row.user_id,
|
||||
serverId: row.server_id,
|
||||
groupId: row.group_id ?? null,
|
||||
groupName: row.group_name || null,
|
||||
serverName: row.server_name || null,
|
||||
serverUrl: row.server_url || null,
|
||||
useGlobalGroup: Boolean(row.use_global_group),
|
||||
createdAt: row.created_at || null,
|
||||
updatedAt: row.updated_at || null
|
||||
});
|
||||
|
||||
export function getUserServerAssignments(userId) {
|
||||
const numericUserId = Number(userId);
|
||||
if (!Number.isFinite(numericUserId) || numericUserId <= 0) {
|
||||
const error = new Error('INVALID_USER_ID');
|
||||
error.code = 'INVALID_USER_ID';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const existingUser = selectUserCredentialsById.get(numericUserId);
|
||||
if (!existingUser) {
|
||||
const error = new Error('USER_NOT_FOUND');
|
||||
error.code = 'USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const superuserMembership = isUserInGroupStatement.get(numericUserId, SUPERUSER_GROUP_NAME);
|
||||
if (superuserMembership?.has_membership) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const rows = selectAssignmentsByUser.all(numericUserId);
|
||||
return rows.map(sanitizeAssignment);
|
||||
}
|
||||
|
||||
export function setUserServerAssignments(userId, assignments = [], options = {}) {
|
||||
const actor = options.actor || null;
|
||||
const numericUserId = Number(userId);
|
||||
if (!Number.isFinite(numericUserId) || numericUserId <= 0) {
|
||||
const error = new Error('INVALID_USER_ID');
|
||||
error.code = 'INVALID_USER_ID';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const userRecord = selectUserCredentialsById.get(numericUserId);
|
||||
if (!userRecord) {
|
||||
const error = new Error('USER_NOT_FOUND');
|
||||
error.code = 'USER_NOT_FOUND';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const superuserMembership = isUserInGroupStatement.get(numericUserId, SUPERUSER_GROUP_NAME);
|
||||
if (superuserMembership?.has_membership) {
|
||||
const error = new Error('USER_SUPERUSER_PROTECTED');
|
||||
error.code = 'USER_SUPERUSER_PROTECTED';
|
||||
throw error;
|
||||
}
|
||||
|
||||
const normalizedList = Array.isArray(assignments) ? assignments : [];
|
||||
const missingServers = [];
|
||||
const missingGroups = [];
|
||||
const normalizedAssignments = new Map();
|
||||
|
||||
normalizedList.forEach((entry) => {
|
||||
const rawServerId = entry?.serverId ?? entry?.server_id ?? entry?.id;
|
||||
const serverId = Number(rawServerId);
|
||||
if (!Number.isFinite(serverId) || serverId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverRow = selectServerByIdForAssignment.get(serverId);
|
||||
if (!serverRow) {
|
||||
missingServers.push(serverId);
|
||||
return;
|
||||
}
|
||||
|
||||
const useGlobalGroup =
|
||||
entry?.useGlobalGroup === true ||
|
||||
entry?.use_global_group === 1 ||
|
||||
entry?.use_global_group === true;
|
||||
|
||||
const rawGroupId = entry?.groupId ?? entry?.group_id;
|
||||
const groupId = Number(rawGroupId);
|
||||
const normalizedGroupId = Number.isFinite(groupId) && groupId > 0 ? groupId : null;
|
||||
|
||||
if (!useGlobalGroup && normalizedGroupId === null) {
|
||||
missingGroups.push(serverId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedGroupId !== null) {
|
||||
const groupRow = selectGroupById.get(normalizedGroupId);
|
||||
if (!groupRow) {
|
||||
missingGroups.push(serverId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
normalizedAssignments.set(serverId, {
|
||||
serverId,
|
||||
groupId: normalizedGroupId,
|
||||
useGlobalGroup
|
||||
});
|
||||
});
|
||||
|
||||
if (missingServers.length > 0) {
|
||||
const error = new Error('SERVER_NOT_FOUND');
|
||||
error.code = 'SERVER_NOT_FOUND';
|
||||
error.missingServerIds = missingServers;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (missingGroups.length > 0) {
|
||||
const error = new Error('GROUP_NOT_FOUND');
|
||||
error.code = 'GROUP_NOT_FOUND';
|
||||
error.missingServerIds = missingGroups;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const previousAssignments = selectAssignmentsByUser.all(numericUserId);
|
||||
|
||||
const applyAssignments = db.transaction(() => {
|
||||
deleteUserServerAssignments.run(numericUserId);
|
||||
Array.from(normalizedAssignments.values()).forEach((assignment) => {
|
||||
upsertUserServerAssignment.run(
|
||||
numericUserId,
|
||||
assignment.serverId,
|
||||
assignment.groupId,
|
||||
assignment.useGlobalGroup ? 1 : 0
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
applyAssignments();
|
||||
|
||||
const updatedAssignments = selectAssignmentsByUser.all(numericUserId);
|
||||
const sanitizedUpdated = updatedAssignments.map(sanitizeAssignment);
|
||||
|
||||
logEvent({
|
||||
category: 'benutzer',
|
||||
eventType: 'benutzer-server-zuordnung-aktualisiert',
|
||||
action: 'aktualisieren',
|
||||
status: 'erfolgreich',
|
||||
entityType: 'benutzer',
|
||||
entityId: String(numericUserId),
|
||||
entityName: userRecord?.username ?? `ID ${numericUserId}`,
|
||||
message: `Serverzuordnungen für Benutzer "${userRecord?.username ?? numericUserId}" aktualisiert`,
|
||||
metadata: {
|
||||
assignments: sanitizedUpdated.map((entry) => ({
|
||||
serverId: entry.serverId,
|
||||
serverName: entry.serverName,
|
||||
useGlobalGroup: entry.useGlobalGroup,
|
||||
groupId: entry.groupId,
|
||||
groupName: entry.groupName
|
||||
})),
|
||||
previousAssignments: previousAssignments.map((entry) => ({
|
||||
serverId: entry.server_id,
|
||||
serverName: entry.server_name,
|
||||
useGlobalGroup: Boolean(entry.use_global_group),
|
||||
groupId: entry.group_id,
|
||||
groupName: entry.group_name || null
|
||||
}))
|
||||
},
|
||||
...resolveActorFields(actor)
|
||||
});
|
||||
|
||||
return sanitizedUpdated;
|
||||
}
|
||||
|
||||
const deleteMembershipsStatement = db.prepare(`
|
||||
DELETE FROM user_group_memberships
|
||||
WHERE user_id = ?
|
||||
|
||||
Reference in New Issue
Block a user