diff --git a/backend/data/stackpulse copy.db b/backend/data/stackpulse copy.db deleted file mode 100644 index 62d4c05..0000000 Binary files a/backend/data/stackpulse copy.db and /dev/null differ diff --git a/backend/data/stackpulse copy.db-shm b/backend/data/stackpulse copy.db-shm deleted file mode 100644 index 87c5c73..0000000 Binary files a/backend/data/stackpulse copy.db-shm and /dev/null differ diff --git a/backend/data/stackpulse copy.db-wal b/backend/data/stackpulse copy.db-wal deleted file mode 100644 index adf384c..0000000 Binary files a/backend/data/stackpulse copy.db-wal and /dev/null differ 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",