From fc778da204e0cc30f1b625e15d5362c90342501f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Nov 2025 19:32:50 +0000 Subject: [PATCH] DB-Skript Update --- backend/data/stackpulse copy.db | Bin 172032 -> 0 bytes backend/data/stackpulse copy.db-shm | Bin 32768 -> 0 bytes backend/data/stackpulse copy.db-wal | Bin 45352 -> 0 bytes backend/db/dbs | 227 ++++++ backend/db/ensure.js | 14 + backend/db/migrate.js | 1013 --------------------------- backend/db/schemaEnsure.js | 710 +++++++++++++++++++ backend/docker-entrypoint.sh | 6 +- backend/index.js | 2 + backend/package.json | 2 +- 10 files changed, 957 insertions(+), 1017 deletions(-) delete mode 100644 backend/data/stackpulse copy.db delete mode 100644 backend/data/stackpulse copy.db-shm delete mode 100644 backend/data/stackpulse copy.db-wal create mode 100644 backend/db/dbs create mode 100644 backend/db/ensure.js delete mode 100644 backend/db/migrate.js create mode 100644 backend/db/schemaEnsure.js diff --git a/backend/data/stackpulse copy.db b/backend/data/stackpulse copy.db deleted file mode 100644 index 62d4c058f544369b63e6466ebc376b927bbcebeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172032 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 O00ba#GYR}lUf^$MWx)&p diff --git a/backend/db/dbs b/backend/db/dbs new file mode 100644 index 0000000..a5214c1 --- /dev/null +++ b/backend/db/dbs @@ -0,0 +1,227 @@ +-- Stackpulse database schema blueprint +-- This document captures the expected structure of all tables, indexes and core seed requirements. + +CREATE TABLE event_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, + category TEXT NOT NULL, + event_type TEXT, + action TEXT, + status TEXT, + severity TEXT, + entity_type TEXT, + entity_id TEXT, + entity_name TEXT, + actor_type TEXT, + actor_id TEXT, + actor_name TEXT, + source TEXT, + context_type TEXT, + context_id TEXT, + context_label TEXT, + message TEXT, + metadata TEXT +); +CREATE INDEX idx_event_logs_timestamp ON event_logs (timestamp DESC); +CREATE INDEX idx_event_logs_category ON event_logs (category); +CREATE INDEX idx_event_logs_event_type ON event_logs (event_type); +CREATE INDEX idx_event_logs_entity ON event_logs (entity_type, entity_id); +CREATE INDEX idx_event_logs_context ON event_logs (context_type, context_id); + +CREATE TABLE settings ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + email TEXT UNIQUE, + password_hash TEXT NOT NULL, + password_salt TEXT, + avatar_color TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + last_login DATETIME, + security_phrase_content TEXT, + security_phrase_iv TEXT, + security_phrase_tag TEXT, + security_phrase_downloaded_at DATETIME, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE user_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + avatar_color TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE user_group_memberships ( + user_id INTEGER NOT NULL, + group_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, group_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE +); + +CREATE TABLE permissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE user_group_permissions ( + group_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (group_id, permission_id), + FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE +); + +CREATE TABLE permission_sections ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + title TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + has_navigation_flag INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE permission_groups ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + section_id INTEGER NOT NULL, + 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 +); + +CREATE TABLE permission_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + section_id INTEGER NOT NULL, + group_id INTEGER, + key TEXT NOT NULL UNIQUE, + label TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + default_level TEXT NOT NULL, + available_levels TEXT NOT NULL, + is_required INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (section_id) REFERENCES permission_sections(id) ON DELETE CASCADE, + FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE SET NULL +); + +CREATE TABLE permission_dependencies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + permission_id INTEGER NOT NULL, + depends_on_permission_id INTEGER NOT NULL, + required_level TEXT DEFAULT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (permission_id) REFERENCES permission_items(id) ON DELETE CASCADE, + FOREIGN KEY (depends_on_permission_id) REFERENCES permission_items(id) ON DELETE CASCADE, + UNIQUE(permission_id, depends_on_permission_id) +); +CREATE INDEX idx_permission_dependencies_required + ON permission_dependencies (permission_id, depends_on_permission_id); + +CREATE TABLE group_permission_values ( + group_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + level TEXT NOT NULL, + effective_level TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (group_id, permission_id), + FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permission_items(id) ON DELETE CASCADE +); + +CREATE TABLE servers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE endpoints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + server_id INTEGER NOT NULL, + name TEXT NOT NULL, + external_id TEXT NOT NULL, + is_default INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, + UNIQUE (server_id, external_id) +); + +CREATE TABLE user_server_permission_overrides ( + user_id INTEGER NOT NULL, + server_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + change_type TEXT NOT NULL CHECK (change_type IN ('ADD', 'REMOVE')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, server_id, permission_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE +); +CREATE INDEX idx_user_server_permission_overrides_user_server + ON user_server_permission_overrides (user_id, server_id); + +CREATE TABLE user_endpoint_permission_overrides ( + user_id INTEGER NOT NULL, + endpoint_id INTEGER NOT NULL, + permission_id INTEGER NOT NULL, + change_type TEXT NOT NULL CHECK (change_type IN ('ADD', 'REMOVE')), + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE (user_id, endpoint_id, permission_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (endpoint_id) REFERENCES endpoints(id) ON DELETE CASCADE, + FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE +); +CREATE INDEX idx_user_endpoint_permission_overrides_user_endpoint + ON user_endpoint_permission_overrides (user_id, endpoint_id); + +CREATE TABLE server_api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + server_id INTEGER NOT NULL UNIQUE, + key_cipher TEXT NOT NULL, + key_iv TEXT NOT NULL, + key_tag 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 TABLE user_settings ( + user_id INTEGER NOT NULL, + setting_key TEXT NOT NULL, + setting_value TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (user_id, setting_key), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Core seed expectations (keys only – maintained via schemaEnsure) +-- permission_sections: stacks, logs, users, user-groups, maintenance +-- permission_items: includes users-security-phrase (available_levels ["full","none"]) diff --git a/backend/db/ensure.js b/backend/db/ensure.js new file mode 100644 index 0000000..b5bec22 --- /dev/null +++ b/backend/db/ensure.js @@ -0,0 +1,14 @@ +import { ensureDatabaseSchema } from './schemaEnsure.js'; + +const run = async () => { + try { + ensureDatabaseSchema(); + console.log('✅ Datenbankschema abgeglichen.'); + process.exit(0); + } catch (error) { + console.error('⚠️ Schema-Abgleich fehlgeschlagen:', error); + process.exit(1); + } +}; + +run(); diff --git a/backend/db/migrate.js b/backend/db/migrate.js deleted file mode 100644 index eabc1ec..0000000 --- a/backend/db/migrate.js +++ /dev/null @@ -1,1013 +0,0 @@ -import { db } from './index.js'; - -db.exec('PRAGMA foreign_keys = ON;'); - -const createEventLogsTable = ` -CREATE TABLE IF NOT EXISTS event_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, - category TEXT NOT NULL, - event_type TEXT, - action TEXT, - status TEXT, - severity TEXT, - entity_type TEXT, - entity_id TEXT, - entity_name TEXT, - actor_type TEXT, - actor_id TEXT, - actor_name TEXT, - source TEXT, - context_type TEXT, - context_id TEXT, - context_label TEXT, - message TEXT, - metadata TEXT -); -`; - -const createSettingsTable = ` -CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT, - updated_at TEXT DEFAULT CURRENT_TIMESTAMP -); -`; - -const createUsersTable = ` -CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - email TEXT UNIQUE, - password_hash TEXT NOT NULL, - password_salt TEXT, - avatar_color TEXT, - is_active INTEGER NOT NULL DEFAULT 1, - last_login DATETIME, - security_phrase_content TEXT, - security_phrase_iv TEXT, - security_phrase_tag TEXT, - security_phrase_downloaded_at DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -`; - -const createUserGroupsTable = ` -CREATE TABLE IF NOT EXISTS user_groups ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - description TEXT, - avatar_color TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -`; - -const createUserGroupMembershipsTable = ` -CREATE TABLE IF NOT EXISTS user_group_memberships ( - user_id INTEGER NOT NULL, - group_id INTEGER NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, group_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE -); -`; - -const createPermissionsTable = ` -CREATE TABLE IF NOT EXISTS permissions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT NOT NULL UNIQUE, - description TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -`; - -const createUserGroupPermissionsTable = ` -CREATE TABLE IF NOT EXISTS user_group_permissions ( - group_id INTEGER NOT NULL, - permission_id INTEGER NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (group_id, permission_id), - FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE -); -`; - -const createPermissionSectionsTable = ` -CREATE TABLE IF NOT EXISTS permission_sections ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - key TEXT NOT NULL UNIQUE, - title TEXT NOT NULL, - sort_order INTEGER NOT NULL DEFAULT 0, - has_navigation_flag INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -`; - -const createPermissionGroupsTable = ` -CREATE TABLE IF NOT EXISTS permission_groups ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - section_id INTEGER NOT NULL, - 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 -); -`; - -const createPermissionItemsTable = ` -CREATE TABLE IF NOT EXISTS permission_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - section_id INTEGER NOT NULL, - group_id INTEGER, - key TEXT NOT NULL UNIQUE, - label TEXT NOT NULL, - sort_order INTEGER NOT NULL DEFAULT 0, - default_level TEXT NOT NULL, - available_levels TEXT NOT NULL, - is_required INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (section_id) REFERENCES permission_sections(id) ON DELETE CASCADE, - FOREIGN KEY (group_id) REFERENCES permission_groups(id) ON DELETE SET NULL -); -`; - -const createPermissionDependenciesTable = ` -CREATE TABLE IF NOT EXISTS permission_dependencies ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - permission_id INTEGER NOT NULL, - depends_on_permission_id INTEGER NOT NULL, - required_level TEXT DEFAULT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (permission_id) REFERENCES permission_items(id) ON DELETE CASCADE, - FOREIGN KEY (depends_on_permission_id) REFERENCES permission_items(id) ON DELETE CASCADE, - UNIQUE(permission_id, depends_on_permission_id) -); -`; - -const createPermissionDependenciesIndex = ` -CREATE INDEX IF NOT EXISTS idx_permission_dependencies_required -ON permission_dependencies (permission_id, depends_on_permission_id); -`; - -const createGroupPermissionValuesTable = ` -CREATE TABLE IF NOT EXISTS group_permission_values ( - group_id INTEGER NOT NULL, - permission_id INTEGER NOT NULL, - level TEXT NOT NULL, - effective_level TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (group_id, permission_id), - FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permission_items(id) ON DELETE CASCADE -); -`; - - -const createServersTable = ` -CREATE TABLE IF NOT EXISTS servers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - url TEXT NOT NULL UNIQUE, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP -); -`; - -const createEndpointsTable = ` -CREATE TABLE IF NOT EXISTS endpoints ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - server_id INTEGER NOT NULL, - name TEXT NOT NULL, - external_id TEXT NOT NULL, - is_default INTEGER NOT NULL DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, - UNIQUE (server_id, external_id) -); -`; - -const createUserServerPermissionOverridesTable = ` -CREATE TABLE IF NOT EXISTS user_server_permission_overrides ( - user_id INTEGER NOT NULL, - server_id INTEGER NOT NULL, - permission_id INTEGER NOT NULL, - change_type TEXT NOT NULL CHECK (change_type IN ('ADD', 'REMOVE')), - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE (user_id, server_id, permission_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE -); -`; - -const createUserEndpointPermissionOverridesTable = ` -CREATE TABLE IF NOT EXISTS user_endpoint_permission_overrides ( - user_id INTEGER NOT NULL, - endpoint_id INTEGER NOT NULL, - permission_id INTEGER NOT NULL, - change_type TEXT NOT NULL CHECK (change_type IN ('ADD', 'REMOVE')), - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE (user_id, endpoint_id, permission_id), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, - FOREIGN KEY (endpoint_id) REFERENCES endpoints(id) ON DELETE CASCADE, - FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE -); -`; - -const createUserServerPermissionOverridesIndex = ` -CREATE INDEX IF NOT EXISTS idx_user_server_permission_overrides_user_server -ON user_server_permission_overrides (user_id, server_id); -`; - -const createUserEndpointPermissionOverridesIndex = ` -CREATE INDEX IF NOT EXISTS idx_user_endpoint_permission_overrides_user_endpoint -ON user_endpoint_permission_overrides (user_id, endpoint_id); -`; - - -const createServerApiKeysTable = ` -CREATE TABLE IF NOT EXISTS server_api_keys ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - server_id INTEGER NOT NULL UNIQUE, - key_cipher TEXT NOT NULL, - key_iv TEXT NOT NULL, - key_tag 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 -); -`; - -const createUserSettingsTable = ` -CREATE TABLE IF NOT EXISTS user_settings ( - user_id INTEGER NOT NULL, - setting_key TEXT NOT NULL, - setting_value TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (user_id, setting_key), - FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -); -`; - -const tableExists = (tableName) => { - return Boolean( - db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?") - .get(tableName) - ); -}; - -const serializeMetadata = (payload) => { - if (!payload || typeof payload !== 'object' || !Object.keys(payload).length) { - return null; - } - try { - return JSON.stringify(payload); - } catch (err) { - console.error('⚠️ Konnte Migrations-Metadaten nicht serialisieren:', err.message); - return null; - } -}; - -db.exec(createEventLogsTable); -db.exec('CREATE INDEX IF NOT EXISTS idx_event_logs_timestamp ON event_logs (timestamp DESC);'); -db.exec('CREATE INDEX IF NOT EXISTS idx_event_logs_category ON event_logs (category);'); -db.exec('CREATE INDEX IF NOT EXISTS idx_event_logs_event_type ON event_logs (event_type);'); -db.exec('CREATE INDEX IF NOT EXISTS idx_event_logs_entity ON event_logs (entity_type, entity_id);'); -db.exec('CREATE INDEX IF NOT EXISTS idx_event_logs_context ON event_logs (context_type, context_id);'); -console.log('✅ event_logs table ready'); - -if (tableExists('redeploy_logs')) { - try { - const legacyRows = db.prepare(` - SELECT id, timestamp, stack_id, stack_name, status, message, endpoint, redeploy_type - FROM redeploy_logs - ORDER BY id ASC - `).all(); - - if (legacyRows.length) { - const insertEventLog = db.prepare(` - INSERT INTO event_logs ( - id, - timestamp, - category, - event_type, - action, - status, - entity_type, - entity_id, - entity_name, - context_type, - context_id, - message, - metadata - ) VALUES ( - @id, - @timestamp, - 'stack', - @eventType, - 'redeploy', - @status, - 'stack', - @entityId, - @entityName, - @contextType, - @contextId, - @message, - @metadata - ) - `); - - const migrate = db.transaction((rows) => { - rows.forEach((row) => { - const contextType = row.endpoint !== null && row.endpoint !== undefined ? 'endpoint' : null; - const contextId = contextType ? String(row.endpoint) : null; - const metadataPayload = { - origin: 'redeploy_logs' - }; - if (row.redeploy_type) { - metadataPayload.redeployType = row.redeploy_type; - } - if (contextId) { - metadataPayload.endpointId = contextId; - } - insertEventLog.run({ - id: row.id, - timestamp: row.timestamp, - eventType: row.redeploy_type ?? null, - status: row.status ?? null, - entityId: row.stack_id !== undefined && row.stack_id !== null ? String(row.stack_id) : null, - entityName: row.stack_name ?? null, - contextType, - contextId, - message: row.message ?? null, - metadata: serializeMetadata(metadataPayload) - }); - }); - }); - - migrate(legacyRows); - console.log(`ℹ️ ${legacyRows.length} Einträge aus redeploy_logs migriert`); - } - - db.exec('DROP TABLE redeploy_logs;'); - console.log('ℹ️ Alte Tabelle redeploy_logs entfernt'); - } catch (err) { - console.error('⚠️ Migration von redeploy_logs fehlgeschlagen:', err.message); - } -} - -db.exec(createSettingsTable); -console.log('✅ settings table ready'); - -db.exec(createUsersTable); -console.log('✅ users table ready'); - -try { - const userColumns = db.prepare('PRAGMA table_info(users)').all(); - const hasAvatarColor = userColumns.some((column) => column.name === 'avatar_color'); - if (!hasAvatarColor) { - db.exec('ALTER TABLE users ADD COLUMN avatar_color TEXT'); - console.log('ℹ️ avatar_color column in Tabelle users hinzugefügt'); - } - const userGroupColumns = db.prepare('PRAGMA table_info(user_groups)').all(); - const hasAvatarColorGroup = userGroupColumns.some((column) => column.name === 'avatar_color'); - if (!hasAvatarColorGroup) { - db.exec('ALTER TABLE user_groups ADD COLUMN avatar_color TEXT'); - console.log('ℹ️ avatar_color column in Tabelle user_groups hinzugefügt'); - } - const emailColumn = userColumns.find((column) => column.name === 'email'); - if (emailColumn && emailColumn.notnull === 1) { - try { - db.exec('PRAGMA foreign_keys = OFF;'); - db.exec('DROP TABLE IF EXISTS users_backup;'); - db.exec('ALTER TABLE users RENAME TO users_backup;'); - db.exec(` - CREATE TABLE users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - email TEXT UNIQUE, - password_hash TEXT NOT NULL, - password_salt TEXT, - avatar_color TEXT, - is_active INTEGER NOT NULL DEFAULT 1, - last_login DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ); - `); - db.exec(` - INSERT INTO users (id, username, email, password_hash, password_salt, avatar_color, is_active, last_login, created_at, updated_at) - SELECT id, username, email, password_hash, password_salt, avatar_color, is_active, last_login, created_at, updated_at - FROM users_backup; - `); - db.exec('DROP TABLE IF EXISTS users_backup;'); - console.log('ℹ️ users.email Spalte erlaubt jetzt NULL-Werte'); - } catch (migrationError) { - console.error('⚠️ Umbau der users Tabelle fehlgeschlagen:', migrationError.message); - try { - db.exec('ALTER TABLE users_backup RENAME TO users;'); - } catch (restoreError) { - console.error('⚠️ Konnte ursprüngliche users Tabelle nicht wiederherstellen:', restoreError.message); - } - } finally { - db.exec('PRAGMA foreign_keys = ON;'); - } - } -} catch (err) { - console.error('⚠️ Konnte avatar_color Spalte nicht prüfen/erstellen:', err.message); -} - -db.exec(createUserGroupsTable); -console.log('✅ user_groups table ready'); - -db.exec(createUserGroupMembershipsTable); -console.log('✅ user_group_memberships table ready'); - -db.exec(createPermissionsTable); -console.log('✅ permissions table ready'); - -db.exec(createUserGroupPermissionsTable); -console.log('✅ user_group_permissions table ready'); - -db.exec(createPermissionSectionsTable); -console.log('✅ permission_sections table ready'); - -db.exec(createPermissionGroupsTable); -console.log('✅ permission_groups table ready'); - -db.exec(createPermissionItemsTable); -console.log('✅ permission_items table ready'); - -db.exec(createPermissionDependenciesTable); -db.exec(createPermissionDependenciesIndex); -console.log('✅ permission_dependencies table ready'); - -db.exec(createGroupPermissionValuesTable); -console.log('✅ group_permission_values table ready'); - -const permissionsSeeded = () => { - try { - const row = db.prepare('SELECT COUNT(*) as count FROM permission_sections').get(); - return row?.count > 0; - } catch (err) { - return false; - } -}; - -const seedPermissions = () => { - if (permissionsSeeded()) { - console.log('ℹ️ permission structure already seeded'); - return; - } - - const insertSection = db.prepare(` - INSERT INTO permission_sections (key, title, sort_order, has_navigation_flag) - VALUES (@key, @title, @sort_order, @has_navigation_flag) - `); - - const insertGroup = db.prepare(` - INSERT INTO permission_groups (section_id, key, title, sort_order) - VALUES (@section_id, @key, @title, @sort_order) - `); - - const insertItem = db.prepare(` - INSERT INTO permission_items ( - section_id, - group_id, - key, - label, - sort_order, - default_level, - available_levels, - is_required - ) VALUES ( - @section_id, - @group_id, - @key, - @label, - @sort_order, - @default_level, - @available_levels, - @is_required - ) - `); - - const insertDependency = db.prepare(` - INSERT INTO permission_dependencies (permission_id, depends_on_permission_id, required_level) - VALUES (@permission_id, @depends_on_permission_id, @required_level) - `); - - const sections = [ - { - key: 'stacks', - title: 'Stacks', - sortOrder: 0, - hasNavigation: 0, - groups: [], - items: [ - { - key: 'stacks-redeploy-single', - label: 'Redeploy einzeln', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [] - }, - { - key: 'stacks-redeploy-selection', - label: 'Redeploy Auswahl', - sortOrder: 1, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [] - }, - { - key: 'stacks-redeploy-all', - label: 'Redeploy Alle', - sortOrder: 2, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [] - } - ] - }, - { - key: 'logs', - title: 'Logs', - sortOrder: 1, - hasNavigation: 1, - groups: [], - items: [ - { - key: 'logs-access', - label: 'Bereich & Navigation', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 1, - dependencies: [] - }, - { - key: 'logs-export', - label: 'Logs Exportieren', - sortOrder: 1, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'logs-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'logs-delete', - label: 'Logs löschen', - sortOrder: 2, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'logs-access', - requiredLevel: '!=none' - } - ] - } - ] - }, - { - key: 'users', - title: 'Benutzer', - sortOrder: 2, - hasNavigation: 1, - groups: [], - items: [ - { - key: 'users-access', - label: 'Bereich & Navigation', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 1, - dependencies: [] - }, - { - key: 'users-edit', - label: 'Benutzer bearbeiten', - sortOrder: 1, - defaultLevel: 'read', - levels: ['full', 'read'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'users-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'users-delete', - label: 'Benutzer löschen', - sortOrder: 2, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'users-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'users-security-phrase', - label: 'Sicherheitsschlüssel', - sortOrder: 3, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'users-access', - requiredLevel: '!=none' - } - ] - } - ] - }, - { - key: 'user-groups', - title: 'Benutzergruppen', - sortOrder: 3, - hasNavigation: 1, - groups: [], - items: [ - { - key: 'user-groups-access', - label: 'Bereich & Navigation', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 1, - dependencies: [] - }, - { - key: 'user-groups-edit', - label: 'Benutzergruppen bearbeiten', - sortOrder: 1, - defaultLevel: 'read', - levels: ['full', 'read'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'user-groups-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'user-groups-delete', - label: 'Benutzergruppen löschen', - sortOrder: 2, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'user-groups-access', - requiredLevel: '!=none' - } - ] - } - ] - }, - { - key: 'maintenance', - title: 'Wartung', - sortOrder: 4, - hasNavigation: 1, - groups: [ - { - key: 'maintenance-server-group', - title: 'Server & Endpoints', - sortOrder: 0, - items: [ - { - key: 'maintenance-server-manage', - label: 'Server/Endpoint-Sektion', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'read', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'maintenance-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'maintenance-server-delete', - label: 'Server/Endpoint löschen', - sortOrder: 1, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'maintenance-access', - requiredLevel: '!=none' - }, - { - dependsOnKey: 'maintenance-server-manage', - requiredLevel: '!=none' - } - ] - } - ] - }, - { - key: 'maintenance-portainer-group', - title: 'Portainer', - sortOrder: 1, - items: [ - { - key: 'maintenance-portainer', - label: 'Portainer-Sektion', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'maintenance-access', - requiredLevel: '!=none' - } - ] - }, - { - key: 'maintenance-ssh-update', - label: 'SSH/Update-Skript', - sortOrder: 1, - defaultLevel: 'none', - levels: ['full', 'read', 'none'], - isRequired: 0, - 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'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'maintenance-access', - requiredLevel: '!=none' - }, - { - dependsOnKey: 'maintenance-portainer', - requiredLevel: '!=none' - } - ] - } - ] - }, - { - key: 'maintenance-duplicates-group', - title: 'Doppelte Stacks', - sortOrder: 2, - items: [ - { - key: 'maintenance-duplicates', - label: 'Doppelte Stacks', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'read', 'none'], - isRequired: 0, - dependencies: [ - { - dependsOnKey: 'maintenance-access', - requiredLevel: '!=none' - } - ] - } - ] - } - ], - items: [ - { - key: 'maintenance-access', - label: 'Bereich & Navigation', - sortOrder: 0, - defaultLevel: 'none', - levels: ['full', 'none'], - isRequired: 1, - dependencies: [] - } - ] - } - ]; - - const itemKeyToId = new Map(); - - const insertPermissions = db.transaction(() => { - sections.forEach((section) => { - const sectionResult = insertSection.run({ - key: section.key, - title: section.title, - sort_order: section.sortOrder, - has_navigation_flag: section.hasNavigation ? 1 : 0 - }); - - const sectionId = sectionResult.lastInsertRowid; - - const sectionItems = section.items ?? []; - sectionItems.forEach((item, index) => { - const itemResult = insertItem.run({ - section_id: sectionId, - group_id: null, - key: item.key, - label: item.label, - sort_order: item.sortOrder ?? index, - default_level: item.defaultLevel, - available_levels: JSON.stringify(item.levels), - is_required: item.isRequired ? 1 : 0 - }); - itemKeyToId.set(item.key, itemResult.lastInsertRowid); - }); - - const groups = section.groups ?? []; - groups.forEach((group, groupIdx) => { - const groupResult = insertGroup.run({ - section_id: sectionId, - key: group.key, - title: group.title, - sort_order: group.sortOrder ?? groupIdx - }); - const groupId = groupResult.lastInsertRowid; - const groupItems = group.items ?? []; - groupItems.forEach((item, itemIdx) => { - const itemResult = insertItem.run({ - section_id: sectionId, - group_id: groupId, - key: item.key, - label: item.label, - sort_order: item.sortOrder ?? itemIdx, - default_level: item.defaultLevel, - available_levels: JSON.stringify(item.levels), - is_required: item.isRequired ? 1 : 0 - }); - itemKeyToId.set(item.key, itemResult.lastInsertRowid); - }); - }); - }); - - sections.forEach((section) => { - const collectDependencies = (items = []) => { - items.forEach((item) => { - if (!item.dependencies || !item.dependencies.length) { - return; - } - const permissionId = itemKeyToId.get(item.key); - item.dependencies.forEach((dependency) => { - const dependsId = itemKeyToId.get(dependency.dependsOnKey); - if (!dependsId) { - console.warn(`⚠️ dependency target ${dependency.dependsOnKey} not found for ${item.key}`); - return; - } - insertDependency.run({ - permission_id: permissionId, - depends_on_permission_id: dependsId, - required_level: dependency.requiredLevel ?? null - }); - }); - }); - }; - - collectDependencies(section.items); - (section.groups ?? []).forEach((group) => collectDependencies(group.items)); - }); - }); - - insertPermissions(); - console.log('✅ permission structure seeded'); -}; - -seedPermissions(); - -const ensureUserSecurityPhraseColumns = () => { - const columns = db.prepare('PRAGMA table_info(users)').all(); - const columnNames = new Set(columns.map((column) => column.name)); - - const addColumnIfMissing = (name, sql) => { - if (!columnNames.has(name)) { - db.exec(sql); - } - }; - - addColumnIfMissing('security_phrase_content', 'ALTER TABLE users ADD COLUMN security_phrase_content TEXT'); - addColumnIfMissing('security_phrase_iv', 'ALTER TABLE users ADD COLUMN security_phrase_iv TEXT'); - addColumnIfMissing('security_phrase_tag', 'ALTER TABLE users ADD COLUMN security_phrase_tag TEXT'); - addColumnIfMissing('security_phrase_downloaded_at', 'ALTER TABLE users ADD COLUMN security_phrase_downloaded_at DATETIME'); -}; - -ensureUserSecurityPhraseColumns(); - -const ensurePermissionDefaultLevels = () => { - const desiredDefaults = [ - { key: 'stacks-redeploy-single', defaultLevel: 'none' }, - { key: 'stacks-redeploy-selection', defaultLevel: 'none' }, - { key: 'stacks-redeploy-all', defaultLevel: 'none' }, - { key: 'logs-access', defaultLevel: 'none' }, - { key: 'logs-export', defaultLevel: 'none' }, - { key: 'logs-delete', defaultLevel: 'none' }, - { key: 'users-access', defaultLevel: 'none' }, - { key: 'users-edit', defaultLevel: 'read' }, - { key: 'users-delete', defaultLevel: 'none' }, - { key: 'users-security-phrase', defaultLevel: 'none' }, - { key: 'user-groups-access', defaultLevel: 'none' }, - { key: 'user-groups-edit', defaultLevel: 'read' }, - { key: 'user-groups-delete', defaultLevel: 'none' }, - { key: 'maintenance-access', defaultLevel: 'none' }, - { key: 'maintenance-server-manage', defaultLevel: 'none' }, - { key: 'maintenance-server-delete', defaultLevel: 'none' }, - { key: 'maintenance-portainer', defaultLevel: 'none' }, - { key: 'maintenance-ssh-update', defaultLevel: 'none' }, - { key: 'maintenance-update', defaultLevel: 'none' }, - { key: 'maintenance-duplicates', defaultLevel: 'none' } - ]; - - const updateDefaultLevel = db.prepare(` - UPDATE permission_items - SET default_level = @default_level - WHERE key = @key AND default_level != @default_level - `); - - db.transaction(() => { - desiredDefaults.forEach(({ key, defaultLevel }) => { - updateDefaultLevel.run({ key, default_level: defaultLevel }); - }); - })(); -}; - -ensurePermissionDefaultLevels(); - -db.exec( - "DELETE FROM group_permission_values WHERE group_id IN (SELECT id FROM user_groups WHERE lower(name) = 'superuser')" -); - -db.exec(createServersTable); -console.log('✅ servers table ready'); - -db.exec(createEndpointsTable); -console.log('✅ endpoints table ready'); - -db.exec(createUserServerPermissionOverridesTable); -console.log('✅ user_server_permission_overrides table ready'); - -db.exec(createUserEndpointPermissionOverridesTable); -console.log('✅ user_endpoint_permission_overrides table ready'); - -db.exec(createUserServerPermissionOverridesIndex); -db.exec(createUserEndpointPermissionOverridesIndex); - -db.exec(createServerApiKeysTable); -console.log('✅ server_api_keys table ready'); - -db.exec(createUserSettingsTable); -console.log('✅ user_settings table ready'); - - -db.close(); diff --git a/backend/db/schemaEnsure.js b/backend/db/schemaEnsure.js new file mode 100644 index 0000000..09787d8 --- /dev/null +++ b/backend/db/schemaEnsure.js @@ -0,0 +1,710 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { db } from './index.js'; + +const BLUEPRINT_FILENAME = 'dbs'; +const PERMISSION_BLUEPRINT = [ + { + key: 'stacks', + title: 'Stacks', + sortOrder: 0, + hasNavigation: 0, + items: [ + { + key: 'stacks-redeploy-single', + label: 'Redeploy einzeln', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [] + }, + { + key: 'stacks-redeploy-selection', + label: 'Redeploy Auswahl', + sortOrder: 1, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [] + }, + { + key: 'stacks-redeploy-all', + label: 'Redeploy Alle', + sortOrder: 2, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [] + } + ] + }, + { + key: 'logs', + title: 'Logs', + sortOrder: 1, + hasNavigation: 1, + items: [ + { + key: 'logs-access', + label: 'Bereich & Navigation', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'none'], + isRequired: 1, + dependencies: [] + }, + { + key: 'logs-export', + label: 'Logs Exportieren', + sortOrder: 1, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'logs-access', requiredLevel: '!=none' } + ] + }, + { + key: 'logs-delete', + label: 'Logs löschen', + sortOrder: 2, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'logs-access', requiredLevel: '!=none' } + ] + } + ] + }, + { + key: 'users', + title: 'Benutzer', + sortOrder: 2, + hasNavigation: 1, + items: [ + { + key: 'users-access', + label: 'Bereich & Navigation', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'none'], + isRequired: 1, + dependencies: [] + }, + { + key: 'users-edit', + label: 'Benutzer bearbeiten', + sortOrder: 1, + defaultLevel: 'read', + levels: ['full', 'read'], + dependencies: [ + { dependsOnKey: 'users-access', requiredLevel: '!=none' } + ] + }, + { + key: 'users-delete', + label: 'Benutzer löschen', + sortOrder: 2, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'users-access', requiredLevel: '!=none' } + ] + }, + { + key: 'users-security-phrase', + label: 'Sicherheitsschlüssel', + sortOrder: 3, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'users-access', requiredLevel: '!=none' } + ] + } + ] + }, + { + key: 'user-groups', + title: 'Benutzergruppen', + sortOrder: 3, + hasNavigation: 1, + items: [ + { + key: 'user-groups-access', + label: 'Bereich & Navigation', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'none'], + isRequired: 1, + dependencies: [] + }, + { + key: 'user-groups-edit', + label: 'Benutzergruppen bearbeiten', + sortOrder: 1, + defaultLevel: 'read', + levels: ['full', 'read'], + dependencies: [ + { dependsOnKey: 'user-groups-access', requiredLevel: '!=none' } + ] + }, + { + key: 'user-groups-delete', + label: 'Benutzergruppen löschen', + sortOrder: 2, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'user-groups-access', requiredLevel: '!=none' } + ] + } + ] + }, + { + key: 'maintenance', + title: 'Wartung', + sortOrder: 4, + hasNavigation: 1, + items: [ + { + key: 'maintenance-access', + label: 'Bereich & Navigation', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'none'], + isRequired: 1, + dependencies: [] + } + ], + groups: [ + { + key: 'maintenance-server-group', + title: 'Server & Endpoints', + sortOrder: 0, + items: [ + { + key: 'maintenance-server-manage', + label: 'Server/Endpoint-Sektion', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'read', 'none'], + dependencies: [ + { dependsOnKey: 'maintenance-access', requiredLevel: '!=none' } + ] + }, + { + key: 'maintenance-server-delete', + label: 'Server/Endpoint löschen', + sortOrder: 1, + defaultLevel: 'none', + levels: ['full', 'none'], + dependencies: [ + { dependsOnKey: 'maintenance-access', requiredLevel: '!=none' }, + { dependsOnKey: 'maintenance-server-manage', requiredLevel: '!=none' } + ] + } + ] + }, + { + key: 'maintenance-portainer-group', + title: 'Portainer', + sortOrder: 1, + items: [ + { + key: 'maintenance-portainer', + label: 'Portainer-Sektion', + sortOrder: 0, + 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: 2, + items: [ + { + key: 'maintenance-duplicates', + label: 'Doppelte Stacks', + sortOrder: 0, + defaultLevel: 'none', + levels: ['full', 'read', 'none'], + dependencies: [ + { dependsOnKey: 'maintenance-access', requiredLevel: '!=none' } + ] + } + ] + } + ] + } +]; + +const TABLE_REGEX = /^CREATE TABLE\s+([^\s(]+)\s*\(/i; +const INDEX_REGEX = /^CREATE INDEX\s+([^\s(]+)\s+ON\s+([^\s(]+)\s*\(/i; +const COLUMN_SKIP_PREFIXES = ['PRIMARY KEY', 'FOREIGN KEY', 'UNIQUE', 'CHECK', 'CONSTRAINT']; + +const normalizeIdentifier = (value) => value.replace(/`|"/g, '').trim(); + +const loadBlueprintStatements = () => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const filePath = path.join(__dirname, BLUEPRINT_FILENAME); + const content = fs.readFileSync(filePath, 'utf8'); + const cleanContent = content + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('--')) + .join('\n'); + + return cleanContent + .split(';') + .map((statement) => statement.trim()) + .filter(Boolean); +}; + +const parseTableDefinitions = (statements) => { + const tables = new Map(); + + statements.forEach((statement) => { + const match = statement.match(TABLE_REGEX); + if (!match) { + return; + } + + const tableName = normalizeIdentifier(match[1]); + const body = statement.slice(statement.indexOf('(') + 1, statement.lastIndexOf(')')); + const lines = body + .split('\n') + .map((line) => line.trim()) + .filter(Boolean); + + const columns = []; + lines.forEach((line) => { + const cleaned = line.replace(/,$/, ''); + const upper = cleaned.toUpperCase(); + if (COLUMN_SKIP_PREFIXES.some((prefix) => upper.startsWith(prefix))) { + return; + } + const [rawName, ...restParts] = cleaned.split(/\s+/); + if (!rawName || restParts.length === 0) { + return; + } + const columnName = normalizeIdentifier(rawName); + const definition = restParts.join(' '); + columns.push({ name: columnName, definition }); + }); + + tables.set(tableName, { + createStatement: statement, + columns + }); + }); + + return tables; +}; + +const parseIndexStatements = (statements) => { + const indexes = []; + statements.forEach((statement) => { + const match = statement.match(INDEX_REGEX); + if (!match) { + return; + } + const indexName = normalizeIdentifier(match[1]); + indexes.push({ + name: indexName, + statement + }); + }); + return indexes; +}; + +const tableExists = (tableName) => { + const result = db + .prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1`) + .get(tableName); + return Boolean(result); +}; + +const getExistingColumns = (tableName) => { + const pragma = db.prepare(`PRAGMA table_info(${tableName})`).all(); + return new Set(pragma.map((column) => column.name)); +}; + +const indexExists = (indexName) => { + const result = db + .prepare(`SELECT name FROM sqlite_master WHERE type = 'index' AND name = ? LIMIT 1`) + .get(indexName); + return Boolean(result); +}; + +const ensureTablesAndColumns = (tables) => { + tables.forEach(({ createStatement, columns }, tableName) => { + if (!tableExists(tableName)) { + const statementWithGuard = createStatement.replace(/^CREATE TABLE/i, 'CREATE TABLE IF NOT EXISTS'); + db.exec(`${statementWithGuard};`); + console.log(`ℹ️ Tabelle ${tableName} angelegt`); + return; + } + + const existingColumns = getExistingColumns(tableName); + const missingColumns = columns.filter((column) => !existingColumns.has(column.name)); + missingColumns.forEach((column) => { + try { + db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.definition};`); + console.log(`ℹ️ Column ${column.name} added to ${tableName}`); + } catch (error) { + console.warn(`⚠️ Column ${column.name} could not be added to ${tableName}: ${error.message}`); + } + }); + }); +}; + +const ensureIndexes = (indexes) => { + indexes.forEach(({ name, statement }) => { + if (indexExists(name)) { + return; + } + const statementWithGuard = statement.replace(/^CREATE INDEX/i, 'CREATE INDEX IF NOT EXISTS'); + db.exec(`${statementWithGuard};`); + console.log(`ℹ️ Index ${name} angelegt`); + }); +}; + +const ensurePermissionSeeds = () => { + const selectSection = db.prepare(` + SELECT id, title, sort_order, has_navigation_flag + FROM permission_sections + WHERE key = ? + LIMIT 1 + `); + const insertSection = db.prepare(` + INSERT INTO permission_sections (key, title, sort_order, has_navigation_flag, created_at, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `); + const updateSection = db.prepare(` + UPDATE permission_sections + SET title = ?, sort_order = ?, has_navigation_flag = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `); + + const selectGroup = db.prepare(` + SELECT id, title, sort_order + 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) + `); + const updateGroup = db.prepare(` + UPDATE permission_groups + SET title = ?, sort_order = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `); + + const selectItem = db.prepare(` + SELECT + id, + section_id, + group_id, + label, + sort_order, + default_level, + available_levels, + is_required + FROM permission_items + WHERE key = ? + LIMIT 1 + `); + const insertItem = db.prepare(` + INSERT INTO permission_items ( + section_id, + group_id, + key, + label, + sort_order, + default_level, + available_levels, + is_required, + created_at, + updated_at + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ) + `); + const updateItem = db.prepare(` + UPDATE permission_items + SET + section_id = ?, + group_id = ?, + label = ?, + sort_order = ?, + default_level = ?, + available_levels = ?, + is_required = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `); + + const selectDependency = db.prepare(` + SELECT id, required_level + FROM permission_dependencies + WHERE permission_id = ? AND depends_on_permission_id = ? + LIMIT 1 + `); + const insertDependency = db.prepare(` + INSERT INTO permission_dependencies ( + permission_id, + depends_on_permission_id, + required_level, + created_at, + updated_at + ) VALUES ( + ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + ) + `); + const updateDependency = db.prepare(` + UPDATE permission_dependencies + SET required_level = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `); + + const itemKeyToId = new Map(); + + PERMISSION_BLUEPRINT.forEach((section, sectionIndex) => { + const existingSection = selectSection.get(section.key); + let sectionId; + if (!existingSection) { + const result = insertSection.run( + section.key, + section.title, + section.sortOrder ?? sectionIndex, + section.hasNavigation ? 1 : 0 + ); + sectionId = Number(result.lastInsertRowid); + console.log(`ℹ️ Berechtigungs-Sektion ${section.key} angelegt`); + } else { + updateSection.run( + section.title, + section.sortOrder ?? sectionIndex, + section.hasNavigation ? 1 : 0, + existingSection.id + ); + sectionId = existingSection.id; + if ( + existingSection.title !== section.title || + (existingSection.sort_order ?? sectionIndex) !== (section.sortOrder ?? sectionIndex) || + Number(existingSection.has_navigation_flag) !== (section.hasNavigation ? 1 : 0) + ) { + console.log(`ℹ️ Berechtigungs-Sektion ${section.key} aktualisiert`); + } + } + + 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`); + } + } + }); + + 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` + ); + } + }); + }); + }); + }); +}; + +export const ensureDatabaseSchema = () => { + try { + const statements = loadBlueprintStatements(); + const tableDefinitions = parseTableDefinitions(statements); + const indexDefinitions = parseIndexStatements(statements); + + ensureTablesAndColumns(tableDefinitions); + ensureIndexes(indexDefinitions); + ensurePermissionSeeds(); + } catch (error) { + console.error('⚠️ Schema-Abgleich fehlgeschlagen:', error); + } +}; diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index 4585dd3..1a2f668 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/sh set -e -echo "🔧 Starte Migration (idempotent)..." -node db/migrate.js +echo "🔧 Überprüfe Datenbankschema..." +node db/ensure.js -echo "✅ Migration abgeschlossen. Starte Anwendung..." +echo "✅ Schema-Abgleich abgeschlossen. Starte Anwendung..." exec "$@" diff --git a/backend/index.js b/backend/index.js index 3baa196..e99faff 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,6 +11,7 @@ import os from 'os'; import crypto from 'crypto'; import { fileURLToPath } from 'url'; import { db } from './db/index.js'; +import { ensureDatabaseSchema } from './db/schemaEnsure.js'; import { ensureSuperuserFromEnv, getSuperuserSummary, @@ -73,6 +74,7 @@ import { dotenv.config(); +ensureDatabaseSchema(); ensureSuperuserFromEnv(); ensureDefaultsFromEnv(); diff --git a/backend/package.json b/backend/package.json index 788889b..6be6190 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "start": "node index.js", - "migrate": "node db/migrate.js" + "migrate": "node db/ensure.js" }, "dependencies": { "axios": "^1.7.0",