diff --git a/partials/landingpage/accountsetup/accountsetup_config.php b/partials/landingpage/accountsetup/accountsetup_config.php index c399589..8144499 100644 --- a/partials/landingpage/accountsetup/accountsetup_config.php +++ b/partials/landingpage/accountsetup/accountsetup_config.php @@ -2,10 +2,9 @@ $appBaseUrl = $GLOBALS['app_base_url'] ?? ''; $defaultNavLinks = [ - ['id' => 'dashboard', 'label' => 'Dashboard', 'href' => $appBaseUrl . '/admin/dashboard.php'], - ['id' => 'settings', 'label' => 'Administration','href' => $appBaseUrl . '/admin/settings.php'], - ['id' => 'bridge', 'label' => 'Bridge Setup', 'href' => $appBaseUrl . '/admin/bridge.php'], - ['id' => 'profile', 'label' => 'Mein Konto', 'href' => $appBaseUrl . '/admin/profile.php'], + ['id' => 'dashboard', 'label' => 'Dashboard', 'href' => $appBaseUrl . '/admin/dashboard.php'], + ['id' => 'settings', 'label' => 'API & Tabellen', 'href' => $appBaseUrl . '/admin/settings.php'], + ['id' => 'profile', 'label' => 'Mein Konto', 'href' => $appBaseUrl . '/admin/profile.php'], ]; if (empty($navLinks)) { diff --git a/partials/landingpage/accountsetup/bridge.php b/partials/landingpage/accountsetup/bridge.php index f796b16..648f3b7 100644 --- a/partials/landingpage/accountsetup/bridge.php +++ b/partials/landingpage/accountsetup/bridge.php @@ -21,10 +21,22 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
- - -

Kommagetrennt oder je Zeile eine Tabelle. Leer lassen = keine Einschränkung.

-
Noch keine Tabellen angegeben.
+ +
+
+

Alle Tabellen (Bridge-Endpunkt)

+ +
+
+ + +
+
+

Whitelist fuer Bridge-Datei

+ +
+
+

Die Auswahl bestimmt, welche Tabellen in der Bridge-Datei erlaubt sind.

diff --git a/partials/landingpage/accountsetup/profile.php b/partials/landingpage/accountsetup/profile.php index 14bb5a1..504d217 100644 --- a/partials/landingpage/accountsetup/profile.php +++ b/partials/landingpage/accountsetup/profile.php @@ -42,7 +42,7 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
-

Teammitglieder, Absender und Integrationen verwaltest du im Bereich Administration. Öffne ihn über das Avatar-Menü oben rechts.

+

Teammitglieder, Absender und Integrationen verwaltest du im Bereich API & Tabellen. Öffne ihn über das Avatar-Menü oben rechts.

diff --git a/partials/landingpage/accountsetup/settings.php b/partials/landingpage/accountsetup/settings.php index ce42763..cd3591f 100644 --- a/partials/landingpage/accountsetup/settings.php +++ b/partials/landingpage/accountsetup/settings.php @@ -1,5 +1,5 @@ Neu erstellen -
- Bridge-Setup & Tabellen öffnen -

Dort kannst du Tabellen-Filter sowie DB-Quellen für die Bridge-Datei konfigurieren.

-
@@ -123,9 +119,180 @@ require dirname(__DIR__) . '/../structure/layout_start.php';
+ +
+
+
+

Bridge-Datei & Tabellenfreigaben

+

Die Bridge-Konfiguration dient ausschliesslich der Erstellung der Bridge-Datei und steuert, welche Tabellen dort freigegeben sind.

+
+
+ + +
+
+ +
+
+

Freigegebene Tabellen (Bridge-Datei)

+ +
+
+ + +
+
+

Tabellen fuer Placeholder-Auswahl

+ +
+
+

Nur diese Tabellen erscheinen spaeter bei der Placeholder-Auswahl.

+
+ + +
+

Bridge-Setup

+ +
+
+

+ Diese Angaben werden nur verwendet, um die emailtemplate_bridge.php zu generieren. Das EmailTemplate-System selbst behält Zugriff auf alle Tabellen; die hier definierten Whitelists greifen ausschliesslich in der Bridge-Datei. +

+
+
+ +
+
+

Alle Tabellen (Bridge-Endpunkt)

+ +
+
+ + +
+
+

Whitelist fuer Bridge-Datei

+ +
+
+

Die Auswahl bestimmt, welche Tabellen in der Bridge-Datei erlaubt sind.

+
+ +
+ Datenbankquelle +

Die Auswahl bestimmt, welche Werte in die Bridge-Datei geschrieben werden.

+
+ + +
+ +
+ + + + + + +
+ + +
+ +
+ + Nutzt Bridge-URL/Token aus den Einstellungen und uebernimmt optional DB-Settings aus der Bridge-Datei. + +
+
Noch nicht gespeichert.
+
+
+
+ + +
+

Beispiel: Mapping einer Config-Datei

+

Angenommen, deine ../config/database.php liefert folgendes Array:

+
 [
+        'connections' => [
+            'default' => [
+                'host' => '127.0.0.1',
+                'port' => 3306,
+                'database' => 'kunden_db',
+                'username' => 'dbuser',
+                'password' => 'secret',
+                'charset' => 'utf8mb4',
+            ],
+        ],
+    ],
+];
+PHP, ENT_QUOTES); ?>
+

Dann trägst du ein:

+
    +
  • Pfad zur Konfigurationsdatei: ../config/database.php
  • +
  • Basis-Pfad: database.connections.default
  • +
  • Host-/Port-/DB-/User-/Pass-/Charset-Key: jeweils host, port, database, username, password, charset
  • +
+

Die Bridge liest dann automatisch die Werte aus diesem Array und baut daraus den DSN.

+
+ +
+
+
'dashboard', 'label' => 'Dashboard', 'href' => $appBaseUrl . '/admin/dashboard.php'], - ['id' => 'settings', 'label' => 'Administration','href' => $appBaseUrl . '/admin/settings.php'], - ['id' => 'bridge', 'label' => 'Bridge Setup', 'href' => $appBaseUrl . '/admin/bridge.php'], - ['id' => 'profile', 'label' => 'Mein Konto', 'href' => $appBaseUrl . '/admin/profile.php'], + ['id' => 'dashboard', 'label' => 'Dashboard', 'href' => $appBaseUrl . '/admin/dashboard.php'], + ['id' => 'settings', 'label' => 'API & Tabellen', 'href' => $appBaseUrl . '/admin/settings.php'], + ['id' => 'profile', 'label' => 'Mein Konto', 'href' => $appBaseUrl . '/admin/profile.php'], ]; $navActive = $navActive ?? null; @@ -48,8 +47,7 @@ $showNavLinks = !$hasHeaderTabs && !empty($navLinks);
diff --git a/public/assets/js/account.js b/public/assets/js/account.js index 55ca390..3bbd21e 100644 --- a/public/assets/js/account.js +++ b/public/assets/js/account.js @@ -1,5 +1,6 @@ import { apiAction } from './api.js'; import { initUserPanel, initAccountPage } from './ui-user.js'; +import { initBridgeSetupPage } from './bridge-setup-page.js'; import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js'; async function ensureAuthenticated() { @@ -24,6 +25,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (!ok) return; initUserPanel(); initAccountPage(); + initBridgeSetupPage(); mountLogoutButton('#btn-logout', { redirect: '/login.php' }); ensureFloatingLogout({ redirect: '/login.php' }); }); diff --git a/public/assets/js/bridge-setup-page.js b/public/assets/js/bridge-setup-page.js index 7f26ad0..fe2c943 100644 --- a/public/assets/js/bridge-setup-page.js +++ b/public/assets/js/bridge-setup-page.js @@ -3,11 +3,15 @@ import { apiAction, toast } from './api.js'; const state = { setup: null, loading: false, + allTables: [], + selectedTables: [], }; let form; -let tablesInput; -let tablesPreview; +let tablesAllSelect; +let tablesSelectedSelect; +let tablesAddBtn; +let tablesRemoveBtn; let modeInputs; let directFields; let configFields; @@ -15,12 +19,18 @@ let statusLabel; let loadBtn; let configExampleBtn; let configExampleDialog; +let bridgeSetupDialog; +let openBridgeSetupBtn; +let adminLoadBridgeBtn; +let closeBridgeSetupBtn; export function initBridgeSetupPage() { form = document.getElementById('bridgeSetupForm'); if (!form) return; - tablesInput = form.elements.tables; - tablesPreview = document.getElementById('selectedTables'); + tablesAllSelect = document.getElementById('bridgeTablesAll'); + tablesSelectedSelect = document.getElementById('bridgeTablesSelected'); + tablesAddBtn = document.getElementById('bridgeTablesAdd'); + tablesRemoveBtn = document.getElementById('bridgeTablesRemove'); directFields = document.getElementById('directFields'); configFields = document.getElementById('configFields'); statusLabel = document.getElementById('setupStatus'); @@ -28,13 +38,33 @@ export function initBridgeSetupPage() { configExampleBtn = document.getElementById('btn-config-example'); configExampleDialog = document.getElementById('configExampleDialog'); modeInputs = Array.from(form.querySelectorAll('input[name="db_mode"]')); + bridgeSetupDialog = document.getElementById('bridgeSetupDialog'); + openBridgeSetupBtn = document.getElementById('btn-open-bridge-setup'); + adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge'); + closeBridgeSetupBtn = document.getElementById('btn-close-bridge-setup'); form.addEventListener('submit', submitBridgeSetup); - tablesInput?.addEventListener('input', () => updateTablesPreview(parseTablesInput())); + tablesAddBtn?.addEventListener('click', () => addSelectedTables(getSelectedOptions(tablesAllSelect))); + tablesRemoveBtn?.addEventListener('click', () => removeSelectedTables(getSelectedOptions(tablesSelectedSelect))); loadBtn?.addEventListener('click', loadTablesFromBridge); configExampleBtn?.addEventListener('click', () => { if (configExampleDialog?.showModal) configExampleDialog.showModal(); }); + openBridgeSetupBtn?.addEventListener('click', () => { + if (bridgeSetupDialog?.showModal) bridgeSetupDialog.showModal(); + if (!state.allTables.length) { + loadTablesFromBridge(null, { preserveSelection: state.selectedTables.length > 0 }); + } + }); + closeBridgeSetupBtn?.addEventListener('click', () => { + if (bridgeSetupDialog?.open) bridgeSetupDialog.close(); + }); + adminLoadBridgeBtn?.addEventListener('click', (ev) => { + if (bridgeSetupDialog?.showModal && !bridgeSetupDialog.open) { + bridgeSetupDialog.showModal(); + } + loadTablesFromBridge(ev, { preserveSelection: state.selectedTables.length > 0 }); + }); modeInputs.forEach(input => { input.addEventListener('change', () => applyModeVisibility(input.value)); }); @@ -82,12 +112,12 @@ async function loadBridgeSetup() { } } -function fillForm(setup) { +function fillForm(setup, options = {}) { const data = normalizeSetupInput(setup); state.setup = data; - if (tablesInput) { - tablesInput.value = data.tables.join(', '); - updateTablesPreview(data.tables); + if (!options.skipTables) { + state.selectedTables = data.tables.slice(); + updateTableSelects(); } const activeMode = (data.mode || 'direct').toLowerCase(); modeInputs.forEach(input => { @@ -135,22 +165,50 @@ function applyModeVisibility(mode) { configFields?.classList[config]('hidden'); } -function parseTablesInput() { - if (!tablesInput) return []; - return tablesInput.value - .split(/[\s,]+/) - .map(part => part.trim()) - .filter(Boolean) - .filter((value, index, arr) => arr.indexOf(value) === index); +function updateTableSelects() { + const all = state.allTables || []; + const selected = state.selectedTables || []; + const selectedSet = new Set(selected); + const orderedSelected = all.length ? all.filter(name => selectedSet.has(name)) : selected; + const available = all.length ? all.filter(name => !selectedSet.has(name)) : []; + renderSelect(tablesAllSelect, available, 'Noch keine Tabellen geladen.'); + renderSelect(tablesSelectedSelect, orderedSelected, 'Noch keine Tabellen ausgewaehlt.'); } -function updateTablesPreview(list) { - if (!tablesPreview) return; +function renderSelect(selectEl, list, emptyLabel) { + if (!selectEl) return; + selectEl.innerHTML = ''; if (!list.length) { - tablesPreview.innerHTML = 'Noch keine Tabellen angegeben.'; + const opt = document.createElement('option'); + opt.textContent = emptyLabel; + opt.disabled = true; + selectEl.appendChild(opt); return; } - tablesPreview.innerHTML = list.map(name => `${escapeHtml(name)}`).join(''); + list.forEach(name => { + const opt = document.createElement('option'); + opt.value = name; + opt.textContent = name; + selectEl.appendChild(opt); + }); +} + +function getSelectedOptions(selectEl) { + if (!selectEl) return []; + return Array.from(selectEl.selectedOptions || []).map(opt => opt.value); +} + +function addSelectedTables(list) { + if (!list.length) return; + state.selectedTables = normalizeTableNames([...state.selectedTables, ...list]); + updateTableSelects(); +} + +function removeSelectedTables(list) { + if (!list.length) return; + const removeSet = new Set(list); + state.selectedTables = state.selectedTables.filter(name => !removeSet.has(name)); + updateTableSelects(); } async function submitBridgeSetup(ev) { @@ -158,7 +216,7 @@ async function submitBridgeSetup(ev) { if (!form) return; const mode = form.querySelector('input[name="db_mode"]:checked')?.value || 'direct'; const payload = { - tables: parseTablesInput(), + tables: normalizeTableNames(state.selectedTables), mode, direct_host: form.direct_host?.value.trim() || '', direct_port: Number(form.direct_port?.value || 0) || 3306, @@ -182,13 +240,17 @@ async function submitBridgeSetup(ev) { fillForm(res.setup || payload); updateStatus('Bridge-Setup gespeichert.'); toast('Bridge-Setup gespeichert', true); + if (bridgeSetupDialog?.open) { + bridgeSetupDialog.close(); + } + window.dispatchEvent(new CustomEvent('bridge-setup-updated', { detail: res.setup || payload })); } catch (err) { console.error(err); toast(err.message || 'Bridge-Setup konnte nicht gespeichert werden', false); } } -async function loadTablesFromBridge(ev) { +async function loadTablesFromBridge(ev, options = {}) { ev?.preventDefault(); if (!loadBtn) return; loadBtn.disabled = true; @@ -196,18 +258,32 @@ async function loadTablesFromBridge(ev) { const res = await apiAction('account.bridge.test', { method: 'POST', data: {} }); if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden'); const fetchedTables = normalizeTableNames(res.tables); - const allowedTables = normalizeTableNames(res.setup_hint?.tables ?? fetchedTables); - const merged = { - ...(state.setup || {}), - tables: allowedTables, - ...(res.setup_hint || {}), - }; + state.allTables = fetchedTables; + + const preserveSelection = !!options.preserveSelection; + const hasSelection = state.selectedTables.length > 0; + if (preserveSelection || hasSelection) { + state.selectedTables = normalizeTableNames(state.selectedTables); + } else { + state.selectedTables = fetchedTables.slice(); + } + if (fetchedTables.length && state.selectedTables.length) { + const fetchedSet = new Set(fetchedTables); + state.selectedTables = state.selectedTables.filter(name => fetchedSet.has(name)); + } + if (res.setup_hint) { + const merged = { + ...(state.setup || {}), + ...(res.setup_hint || {}), + }; merged.mode = res.setup_hint.mode || merged.mode; merged.direct = { ...(merged.direct || {}), ...(res.setup_hint.direct || {}) }; merged.config = { ...(merged.config || {}), ...(res.setup_hint.config || {}) }; + fillForm(merged, { skipTables: true }); } - fillForm(merged); + + updateTableSelects(); updateStatus(`Tabellen geladen (${fetchedTables.length}).`); toast('Tabellen erfolgreich geladen', true); } catch (err) { @@ -246,15 +322,6 @@ function updateStatus(msg) { statusLabel.textContent = `${msg} (${time})`; } -function escapeHtml(str) { - return String(str || '') - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - function normalizeSetupInput(input) { const base = defaultSetup(); if (!input || typeof input !== 'object') return base; diff --git a/public/assets/js/ui-user.js b/public/assets/js/ui-user.js index 9397817..06b2a76 100644 --- a/public/assets/js/ui-user.js +++ b/public/assets/js/ui-user.js @@ -36,6 +36,10 @@ let consoleContainer; let debugStylesInjected = false; let consolePatched = false; const consoleBuffer = []; +let adminTablesAllSelect; +let adminTablesSelectedSelect; +let adminTablesAddBtn; +let adminTablesRemoveBtn; ensureConsoleCapture(); @@ -61,6 +65,10 @@ export function initAccountPage() { userForm = document.getElementById('userForm'); senderTable = document.getElementById('senderTable'); senderForm = document.getElementById('senderForm'); + adminTablesAllSelect = document.getElementById('adminBridgeTablesAll'); + adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected'); + adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd'); + adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove'); document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm()); document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm()); @@ -98,6 +106,18 @@ export function initAccountPage() { }); } + adminTablesAddBtn?.addEventListener('click', () => { + addAdminTables(getSelectedOptions(adminTablesAllSelect)); + }); + adminTablesRemoveBtn?.addEventListener('click', () => { + removeAdminTables(getSelectedOptions(adminTablesSelectedSelect)); + }); + + window.addEventListener('bridge-setup-updated', (ev) => { + const setup = ev?.detail || {}; + refreshAdminTables(setup.tables || [], state.settings.bridge_tables || []); + }); + switchTab(state.currentTab); loadAccountData(); updateRoleVisibility(); @@ -244,6 +264,7 @@ function fillSettingsForm(settings) { settingsForm.sender_token.value = settings.sender_token || ''; settingsForm.external_api_token.value = settings.external_api_token || ''; state.rotate = { bridge: false, sender: false, external: false }; + refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []); } async function submitProfileForm(ev) { @@ -281,11 +302,13 @@ async function submitPasswordForm(ev) { async function submitSettingsForm(ev) { ev.preventDefault(); + const bridgeTables = normalizeTableList(state.settings.bridge_tables || []); const data = { bridge_url: settingsForm.bridge_url.value.trim(), bridge_token: settingsForm.bridge_token.value.trim(), sender_token: settingsForm.sender_token.value.trim(), external_api_token: settingsForm.external_api_token.value.trim(), + bridge_tables: bridgeTables, rotate_bridge_token: state.rotate.bridge ? 1 : 0, rotate_sender_token: state.rotate.sender ? 1 : 0, rotate_external_token: state.rotate.external ? 1 : 0, @@ -322,6 +345,83 @@ async function downloadFile(type) { } } +function normalizeTableList(input) { + const items = Array.isArray(input) ? input : (typeof input === 'string' ? input.split(/[\s,]+/) : []); + const result = []; + const seen = new Set(); + items.forEach(entry => { + const name = String(entry || '').trim(); + if (name && !seen.has(name)) { + seen.add(name); + result.push(name); + } + }); + return result; +} + +function refreshAdminTables(availableTables, selectedTables) { + const whitelist = normalizeTableList(availableTables); + let selected = normalizeTableList(selectedTables); + if (!selected.length) { + selected = whitelist.slice(); + } + if (whitelist.length) { + selected = selected.filter(name => whitelist.includes(name)); + } + state.settings.bridge_tables = selected; + state.settings.bridge_setup = state.settings.bridge_setup || {}; + state.settings.bridge_setup.tables = whitelist; + updateAdminTableSelects(whitelist, selected); +} + +function updateAdminTableSelects(availableTables, selectedTables) { + const selectedSet = new Set(selectedTables); + const available = availableTables.filter(name => !selectedSet.has(name)); + renderSelect(adminTablesAllSelect, available, 'Keine Tabellen freigegeben.'); + renderSelect(adminTablesSelectedSelect, selectedTables, 'Noch keine Tabellen ausgewaehlt.'); +} + +function renderSelect(selectEl, list, emptyLabel) { + if (!selectEl) return; + selectEl.innerHTML = ''; + if (!list.length) { + const opt = document.createElement('option'); + opt.textContent = emptyLabel; + opt.disabled = true; + selectEl.appendChild(opt); + return; + } + list.forEach(name => { + const opt = document.createElement('option'); + opt.value = name; + opt.textContent = name; + selectEl.appendChild(opt); + }); +} + +function getSelectedOptions(selectEl) { + if (!selectEl) return []; + return Array.from(selectEl.selectedOptions || []).map(opt => opt.value); +} + +function addAdminTables(list) { + const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []); + if (!whitelist.length) return; + const selected = normalizeTableList(state.settings.bridge_tables || []); + const merged = normalizeTableList([...selected, ...list]).filter(name => whitelist.includes(name)); + state.settings.bridge_tables = merged; + updateAdminTableSelects(whitelist, merged); +} + +function removeAdminTables(list) { + const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []); + if (!whitelist.length) return; + const removeSet = new Set(list); + const next = normalizeTableList(state.settings.bridge_tables || []).filter(name => !removeSet.has(name)); + state.settings.bridge_tables = next; + updateAdminTableSelects(whitelist, next); +} + async function loadUsers() { try { const res = await apiAction('account.users.list', { method: 'GET' }); diff --git a/src/ApiKernel.php b/src/ApiKernel.php index f269065..49bf361 100644 --- a/src/ApiKernel.php +++ b/src/ApiKernel.php @@ -1473,6 +1473,22 @@ class ApiKernel return; } + $settings = $this->getCustomerSettings($customerId); + $allowedTables = $this->normalizeBridgeTables($settings['bridge_tables'] ?? []); + if ($allowedTables && !empty($schema['tables']) && is_array($schema['tables'])) { + $allowedMap = array_fill_keys($allowedTables, true); + $schema['tables'] = array_values(array_filter($schema['tables'], static function ($entry) use ($allowedMap) { + if (is_string($entry)) { + $name = $entry; + } elseif (is_array($entry)) { + $name = $entry['name'] ?? $entry['table'] ?? $entry['label'] ?? ''; + } else { + $name = ''; + } + return $name !== '' && isset($allowedMap[$name]); + })); + } + $this->respond([ 'ok' => true, 'tables' => $schema['tables'] ?? [], @@ -1664,6 +1680,7 @@ class ApiKernel $rotateBridge = !empty($this->in['rotate_bridge_token']); $rotateSender = !empty($this->in['rotate_sender_token']); $rotateExternal = !empty($this->in['rotate_external_token']); + $bridgeTables = $this->normalizeBridgeTables($this->in['bridge_tables'] ?? []); if ($bridgeUrl && !filter_var($bridgeUrl, FILTER_VALIDATE_URL)) { $this->fail('Ungültige Bridge-URL', null, 422); @@ -1679,6 +1696,7 @@ class ApiKernel 'bridge_token' => $bridgeToken, 'sender_token' => $senderToken, 'external_api_token' => $externalToken, + 'bridge_tables' => $bridgeTables, ]); $this->respond(['ok' => true, 'settings' => $settings]);