up
This commit is contained in:
@@ -35,6 +35,8 @@ $bridgeConfig = [
|
|||||||
'tables_allow' => [], // optional whitelist: ['customers', 'orders']
|
'tables_allow' => [], // optional whitelist: ['customers', 'orders']
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// {{BRIDGE_DB_SETUP}}
|
||||||
|
|
||||||
$localOverride = __DIR__ . '/emailtemplate.bridge.conf.php';
|
$localOverride = __DIR__ . '/emailtemplate.bridge.conf.php';
|
||||||
if (is_file($localOverride)) {
|
if (is_file($localOverride)) {
|
||||||
$override = include $localOverride;
|
$override = include $localOverride;
|
||||||
@@ -96,6 +98,35 @@ function bridgeDb(array $config): PDO
|
|||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bridge_array_get($data, string $path, $default = null)
|
||||||
|
{
|
||||||
|
if (is_object($data)) {
|
||||||
|
$data = (array)$data;
|
||||||
|
}
|
||||||
|
if (!is_array($data)) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
$path = trim($path);
|
||||||
|
if ($path === '') {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
$segments = array_values(array_filter(array_map('trim', explode('.', $path)), static function ($segment) {
|
||||||
|
return $segment !== '';
|
||||||
|
}));
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
if (is_array($data) && array_key_exists($segment, $data)) {
|
||||||
|
$value = $data[$segment];
|
||||||
|
if (is_object($value)) {
|
||||||
|
$value = (array)$value;
|
||||||
|
}
|
||||||
|
$data = $value;
|
||||||
|
} else {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
bridgeRequireToken($bridgeConfig);
|
bridgeRequireToken($bridgeConfig);
|
||||||
|
|
||||||
$action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema'));
|
$action = strtolower((string)($_GET['action'] ?? $_POST['action'] ?? 'schema'));
|
||||||
|
|||||||
158
partials/landingpage/admin/bridge.php
Normal file
158
partials/landingpage/admin/bridge.php
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<?php
|
||||||
|
$assetVersion = defined('ASSET_VERSION') ? ASSET_VERSION : time();
|
||||||
|
$appBaseUrl = rtrim($GLOBALS['app_base_url'] ?? '', '/');
|
||||||
|
$assetBase = $appBaseUrl !== '' ? $appBaseUrl : '';
|
||||||
|
$appApiBase = rtrim($GLOBALS['app_api_base'] ?? '', '/');
|
||||||
|
$debugRedirect = isset($_GET['debug_redirect']);
|
||||||
|
?>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>Email Template System – Bridge Setup</title>
|
||||||
|
<script>document.documentElement.classList.add('auth-pending');</script>
|
||||||
|
<style>html.auth-pending body{visibility:hidden;}</style>
|
||||||
|
<script>
|
||||||
|
window.APP_BASE_URL = <?= json_encode($appBaseUrl, JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
window.APP_API_BASE = <?= json_encode($appApiBase, JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
<?php if ($debugRedirect): ?>
|
||||||
|
window.DISABLE_AUTH_REDIRECT = true;
|
||||||
|
<?php endif; ?>
|
||||||
|
</script>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<?php if ($debugRedirect): ?>
|
||||||
|
<script src="<?= $assetBase ?>/assets/js/debug-location.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/admin.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<link rel="stylesheet" href="<?= $assetBase ?>/assets/css/toast.css?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>">
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; }
|
||||||
|
.btn{display:inline-flex;align-items:center;gap:.5rem;padding:.35rem .7rem;border-radius:.7rem;border:1px solid #e5e7eb;background:#fff;font-size:.9rem;cursor:pointer;}
|
||||||
|
.btn:hover{background:#f8fafc}
|
||||||
|
.btn-avatar{padding:.35rem;border-radius:999px;width:42px;height:42px;justify-content:center;font-weight:600;background:#0ea5e9;color:#fff;border:none}
|
||||||
|
.btn-avatar:hover{background:#0284c7}
|
||||||
|
.section-card{background:#fff;border:1px solid #e2e8f0;border-radius:1rem;padding:1.25rem;margin-bottom:1.5rem}
|
||||||
|
.section-card h4{margin:0 0 1rem;font-size:1rem;font-weight:600;color:#0f172a}
|
||||||
|
.input{width:100%;border:1px solid #cbd5f5;border-radius:.5rem;padding:.5rem .75rem}
|
||||||
|
.badge{display:inline-flex;align-items:center;padding:.1rem .5rem;border-radius:999px;font-size:.75rem;background:#e2e8f0;color:#0f172a}
|
||||||
|
.chip{display:inline-flex;align-items:center;padding:.15rem .55rem;border-radius:999px;background:#f1f5f9;color:#0f172a;border:1px solid #e2e8f0;font-size:.8rem}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-slate-50 text-slate-800" data-page="bridge-setup">
|
||||||
|
<header class="sticky top-0 z-30 bg-white/90 border-b backdrop-blur">
|
||||||
|
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center gap-3">
|
||||||
|
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="btn" title="Zurück zur Administration">← Administration</a>
|
||||||
|
<h1 class="font-semibold text-lg">Bridge Setup</h1>
|
||||||
|
<div class="ms-auto flex gap-2 items-center">
|
||||||
|
<div class="relative" id="userMenu">
|
||||||
|
<button id="btn-user" type="button" class="btn-avatar" aria-haspopup="true" aria-expanded="false">
|
||||||
|
<span id="userAvatar">U</span>
|
||||||
|
</button>
|
||||||
|
<div id="userMenuPanel" class="user-menu hidden" role="menu">
|
||||||
|
<a href="<?= $appBaseUrl ?>/admin/profile.php" class="user-menu-item" data-menu="profile">Profil</a>
|
||||||
|
<a href="<?= $appBaseUrl ?>/admin/dashboard.php" class="user-menu-item" data-role="admin">Dashboard</a>
|
||||||
|
<a href="<?= $appBaseUrl ?>/admin/settings.php" class="user-menu-item" data-role="admin">Administration</a>
|
||||||
|
<button id="btn-logout" type="button" class="user-menu-item text-red-600">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="max-w-4xl mx-auto p-4 md:p-6 space-y-6">
|
||||||
|
<section class="section-card">
|
||||||
|
<h4>Bridge-Datei vorbereiten</h4>
|
||||||
|
<p class="text-sm text-slate-600 mb-3">
|
||||||
|
Diese Angaben werden nur verwendet, um die <strong>emailtemplate_bridge.php</strong> zu generieren. Das EmailTemplate-System selbst behält Zugriff auf alle Tabellen; die hier definierten Whitelists greifen ausschließlich in der Bridge-Datei.
|
||||||
|
</p>
|
||||||
|
<form id="bridgeSetupForm" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Tabellen-Whitelist (optional)</label>
|
||||||
|
<textarea name="tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Kommagetrennt oder je Zeile eine Tabelle. Leer lassen = keine Einschränkung.</p>
|
||||||
|
<div id="selectedTables" class="flex flex-wrap gap-2 text-sm text-slate-600 mt-2">Noch keine Tabellen angegeben.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="border border-slate-200 rounded-xl p-4">
|
||||||
|
<legend class="px-2 text-sm font-semibold">Datenbankquelle</legend>
|
||||||
|
<div class="flex flex-wrap gap-4 text-sm text-slate-600 mb-3">
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<input type="radio" name="db_mode" value="direct" checked> Direkte Angaben (Host, DB, Benutzer …)
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center gap-2">
|
||||||
|
<input type="radio" name="db_mode" value="config"> Aus bestehender Konfigurationsdatei laden
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="directFields" class="grid md:grid-cols-2 gap-3">
|
||||||
|
<label class="block text-sm text-slate-600">Server / Host
|
||||||
|
<input type="text" name="direct_host" class="input mt-1" placeholder="127.0.0.1">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Port
|
||||||
|
<input type="number" name="direct_port" class="input mt-1" placeholder="3306">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Datenbankname
|
||||||
|
<input type="text" name="direct_database" class="input mt-1" placeholder="kunden_db">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Zeichensatz
|
||||||
|
<input type="text" name="direct_charset" class="input mt-1" value="utf8mb4">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Benutzername
|
||||||
|
<input type="text" name="direct_user" class="input mt-1" placeholder="db_user">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Passwort
|
||||||
|
<input type="text" name="direct_password" class="input mt-1" placeholder="••••">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="configFields" class="hidden space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Pfad zur Konfigurationsdatei</label>
|
||||||
|
<input type="text" name="config_file" class="input mt-1" placeholder="../config/database.php">
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Relativ zur Bridge-Datei oder absolut. Die Datei sollte ein Array oder Objekt mit den Zugangsdaten liefern.</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-slate-600">Basis-Pfad im Array (optional)</label>
|
||||||
|
<input type="text" name="config_base" class="input mt-1" placeholder="database.connections.mysql">
|
||||||
|
<p class="text-xs text-slate-500 mt-1">Dot-Notation, um in verschachtelte Arrays zu springen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid md:grid-cols-2 gap-3">
|
||||||
|
<label class="block text-sm text-slate-600">Host-Key
|
||||||
|
<input type="text" name="config_host_key" class="input mt-1" placeholder="host">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Port-Key
|
||||||
|
<input type="text" name="config_port_key" class="input mt-1" placeholder="port">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">DB-Name-Key
|
||||||
|
<input type="text" name="config_database_key" class="input mt-1" placeholder="database">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Charset-Key
|
||||||
|
<input type="text" name="config_charset_key" class="input mt-1" placeholder="charset">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">User-Key
|
||||||
|
<input type="text" name="config_user_key" class="input mt-1" placeholder="username">
|
||||||
|
</label>
|
||||||
|
<label class="block text-sm text-slate-600">Passwort-Key
|
||||||
|
<input type="text" name="config_password_key" class="input mt-1" placeholder="password">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500">Alle Keys nutzen Dot-Notation relativ zum Basis-Pfad.</p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button type="button" id="btn-load-remote" class="btn" data-role="admin">Tabellen vom Bridge-Endpunkt laden</button>
|
||||||
|
<button type="submit" class="btn">Bridge-Setup speichern</button>
|
||||||
|
</div>
|
||||||
|
<div id="setupStatus" class="text-xs text-slate-500">Noch nicht gespeichert.</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="toast-root"></div>
|
||||||
|
|
||||||
|
<script src="<?= $assetBase ?>/assets/js/toast.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
<script type="module" src="<?= $assetBase ?>/assets/js/bridge-setup.js?v=<?= htmlspecialchars($assetVersion, ENT_QUOTES) ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -170,14 +170,9 @@ $debugRedirect = isset($_GET['debug_redirect']);
|
|||||||
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
<button type="button" class="btn" data-rotate="external">Neu erstellen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-slate-600">Verfügbare Tabellen (kommagetrennt oder Zeilen)</label>
|
|
||||||
<textarea name="bridge_tables" class="input mt-1" rows="3" placeholder="z.B. customers, orders"></textarea>
|
|
||||||
<p class="text-xs text-slate-500 mt-1">Nur diese Tabellen werden als Platzhalter angeboten. Leer = alle.</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<button type="button" id="btn-validate-bridge" class="btn w-max" data-role="admin">Verbindung prüfen & Tabellen laden</button>
|
<a href="<?= $appBaseUrl ?>/admin/bridge.php" class="btn w-max" data-role="admin">Bridge-Setup & Tabellen öffnen</a>
|
||||||
<div id="bridgeTablesPreview" class="flex flex-wrap gap-2 text-sm text-slate-600">Noch nicht geprüft.</div>
|
<p class="text-xs text-slate-500">Dort kannst du Tabellen-Filter sowie DB-Quellen für die Bridge-Datei konfigurieren.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
<div class="flex justify-between gap-2 flex-wrap pt-2">
|
||||||
<div class="flex gap-2" data-role="admin">
|
<div class="flex gap-2" data-role="admin">
|
||||||
|
|||||||
3
public/admin/bridge.php
Normal file
3
public/admin/bridge.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '../config/fileload.php';
|
||||||
|
require_once $_SERVER['DOCUMENT_ROOT'] . '../partials/landingpage/admin/bridge.php';
|
||||||
242
public/assets/js/bridge-setup-page.js
Normal file
242
public/assets/js/bridge-setup-page.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { apiAction, toast } from './api.js';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
setup: null,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let form;
|
||||||
|
let tablesInput;
|
||||||
|
let tablesPreview;
|
||||||
|
let modeInputs;
|
||||||
|
let directFields;
|
||||||
|
let configFields;
|
||||||
|
let statusLabel;
|
||||||
|
let loadBtn;
|
||||||
|
|
||||||
|
export function initBridgeSetupPage() {
|
||||||
|
form = document.getElementById('bridgeSetupForm');
|
||||||
|
if (!form) return;
|
||||||
|
tablesInput = form.elements.tables;
|
||||||
|
tablesPreview = document.getElementById('selectedTables');
|
||||||
|
directFields = document.getElementById('directFields');
|
||||||
|
configFields = document.getElementById('configFields');
|
||||||
|
statusLabel = document.getElementById('setupStatus');
|
||||||
|
loadBtn = document.getElementById('btn-load-remote');
|
||||||
|
modeInputs = Array.from(form.querySelectorAll('input[name="db_mode"]'));
|
||||||
|
|
||||||
|
form.addEventListener('submit', submitBridgeSetup);
|
||||||
|
tablesInput?.addEventListener('input', () => updateTablesPreview(parseTablesInput()));
|
||||||
|
loadBtn?.addEventListener('click', loadTablesFromBridge);
|
||||||
|
modeInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', () => applyModeVisibility(input.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
loadBridgeSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultSetup() {
|
||||||
|
return {
|
||||||
|
tables: [],
|
||||||
|
mode: 'direct',
|
||||||
|
direct: {
|
||||||
|
host: '',
|
||||||
|
port: 3306,
|
||||||
|
database: '',
|
||||||
|
user: '',
|
||||||
|
password: '',
|
||||||
|
charset: 'utf8mb4',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
file: '',
|
||||||
|
base: '',
|
||||||
|
host_key: '',
|
||||||
|
port_key: '',
|
||||||
|
database_key: '',
|
||||||
|
user_key: '',
|
||||||
|
password_key: '',
|
||||||
|
charset_key: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBridgeSetup() {
|
||||||
|
state.loading = true;
|
||||||
|
try {
|
||||||
|
const res = await apiAction('account.bridge.setup.get', { method: 'GET' });
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'Bridge-Setup konnte nicht geladen werden');
|
||||||
|
state.setup = res.setup || defaultSetup();
|
||||||
|
fillForm(state.setup);
|
||||||
|
updateStatus('Daten geladen.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast(err.message || 'Bridge-Setup konnte nicht geladen werden', false);
|
||||||
|
} finally {
|
||||||
|
state.loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillForm(setup) {
|
||||||
|
const data = { ...defaultSetup(), ...(setup || {}) };
|
||||||
|
if (tablesInput) {
|
||||||
|
tablesInput.value = (data.tables || []).join(', ');
|
||||||
|
updateTablesPreview(parseTablesInput());
|
||||||
|
}
|
||||||
|
const activeMode = (data.mode || 'direct').toLowerCase();
|
||||||
|
modeInputs.forEach(input => {
|
||||||
|
input.checked = input.value === activeMode;
|
||||||
|
});
|
||||||
|
applyModeVisibility(activeMode);
|
||||||
|
|
||||||
|
if (directFields) {
|
||||||
|
const directMap = {
|
||||||
|
direct_host: data.direct.host || '',
|
||||||
|
direct_port: String(data.direct.port || 3306),
|
||||||
|
direct_database: data.direct.database || '',
|
||||||
|
direct_charset: data.direct.charset || 'utf8mb4',
|
||||||
|
direct_user: data.direct.user || '',
|
||||||
|
direct_password: data.direct.password || '',
|
||||||
|
};
|
||||||
|
Object.entries(directMap).forEach(([name, value]) => {
|
||||||
|
const input = directFields.querySelector(`[name="${name}"]`);
|
||||||
|
if (input) input.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configFields) {
|
||||||
|
const configMap = {
|
||||||
|
config_file: data.config.file || '',
|
||||||
|
config_base: data.config.base || '',
|
||||||
|
config_host_key: data.config.host_key || '',
|
||||||
|
config_port_key: data.config.port_key || '',
|
||||||
|
config_database_key: data.config.database_key || '',
|
||||||
|
config_user_key: data.config.user_key || '',
|
||||||
|
config_password_key: data.config.password_key || '',
|
||||||
|
config_charset_key: data.config.charset_key || '',
|
||||||
|
};
|
||||||
|
Object.entries(configMap).forEach(([name, value]) => {
|
||||||
|
const input = configFields.querySelector(`[name="${name}"]`);
|
||||||
|
if (input) input.value = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyModeVisibility(mode) {
|
||||||
|
const direct = mode === 'config' ? 'add' : 'remove';
|
||||||
|
const config = mode === 'config' ? 'remove' : 'add';
|
||||||
|
directFields?.classList[direct]('hidden');
|
||||||
|
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 updateTablesPreview(list) {
|
||||||
|
if (!tablesPreview) return;
|
||||||
|
if (!list.length) {
|
||||||
|
tablesPreview.innerHTML = '<span class="text-xs text-slate-500">Noch keine Tabellen angegeben.</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tablesPreview.innerHTML = list.map(name => `<span class="chip">${escapeHtml(name)}</span>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitBridgeSetup(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
if (!form) return;
|
||||||
|
const mode = form.querySelector('input[name="db_mode"]:checked')?.value || 'direct';
|
||||||
|
const payload = {
|
||||||
|
tables: parseTablesInput(),
|
||||||
|
mode,
|
||||||
|
direct_host: form.direct_host?.value.trim() || '',
|
||||||
|
direct_port: Number(form.direct_port?.value || 0) || 3306,
|
||||||
|
direct_database: form.direct_database?.value.trim() || '',
|
||||||
|
direct_charset: form.direct_charset?.value.trim() || 'utf8mb4',
|
||||||
|
direct_user: form.direct_user?.value.trim() || '',
|
||||||
|
direct_password: form.direct_password?.value || '',
|
||||||
|
config_file: form.config_file?.value.trim() || '',
|
||||||
|
config_base: form.config_base?.value.trim() || '',
|
||||||
|
config_host_key: form.config_host_key?.value.trim() || '',
|
||||||
|
config_port_key: form.config_port_key?.value.trim() || '',
|
||||||
|
config_database_key: form.config_database_key?.value.trim() || '',
|
||||||
|
config_user_key: form.config_user_key?.value.trim() || '',
|
||||||
|
config_password_key: form.config_password_key?.value.trim() || '',
|
||||||
|
config_charset_key: form.config_charset_key?.value.trim() || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiAction('account.bridge.setup.save', { method: 'POST', data: payload });
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'Bridge-Setup konnte nicht gespeichert werden');
|
||||||
|
state.setup = res.setup || payload;
|
||||||
|
fillForm(state.setup);
|
||||||
|
updateStatus('Bridge-Setup gespeichert.');
|
||||||
|
toast('Bridge-Setup gespeichert', true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast(err.message || 'Bridge-Setup konnte nicht gespeichert werden', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTablesFromBridge(ev) {
|
||||||
|
ev?.preventDefault();
|
||||||
|
if (!loadBtn) return;
|
||||||
|
loadBtn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await apiAction('account.bridge.test', { method: 'POST', data: {} });
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden');
|
||||||
|
const tables = normalizeTableNames(res.tables);
|
||||||
|
if (tablesInput) {
|
||||||
|
tablesInput.value = tables.join(', ');
|
||||||
|
updateTablesPreview(tables);
|
||||||
|
}
|
||||||
|
updateStatus(`Tabellen geladen (${tables.length}).`);
|
||||||
|
toast('Tabellen erfolgreich geladen', true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
toast(err.message || 'Bridge konnte nicht geprüft werden', false);
|
||||||
|
} finally {
|
||||||
|
loadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTableNames(list) {
|
||||||
|
if (!Array.isArray(list)) return [];
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const entry of list) {
|
||||||
|
let name = '';
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
name = entry;
|
||||||
|
} else if (entry && typeof entry === 'object') {
|
||||||
|
name = entry.name || entry.table || entry.label || '';
|
||||||
|
}
|
||||||
|
if (typeof name === 'string') {
|
||||||
|
const trimmed = name.trim();
|
||||||
|
if (trimmed && !seen.has(trimmed)) {
|
||||||
|
seen.add(trimmed);
|
||||||
|
result.push(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(msg) {
|
||||||
|
if (!statusLabel) return;
|
||||||
|
const time = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
statusLabel.textContent = `${msg} (${time})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
30
public/assets/js/bridge-setup.js
Normal file
30
public/assets/js/bridge-setup.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { apiAction } from './api.js';
|
||||||
|
import { initUserPanel } from './ui-user.js';
|
||||||
|
import { mountLogoutButton, ensureFloatingLogout } from './ui-auth.js';
|
||||||
|
import { initBridgeSetupPage } from './bridge-setup-page.js';
|
||||||
|
|
||||||
|
async function ensureAuthenticated() {
|
||||||
|
try {
|
||||||
|
const me = await apiAction('auth.me', { method: 'GET' });
|
||||||
|
if (!me?.ok || !me?.user) {
|
||||||
|
if (!window.DISABLE_AUTH_REDIRECT) {
|
||||||
|
window.location.href = '/login.php';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
window.__currentUser = me.user;
|
||||||
|
document.documentElement.classList.remove('auth-pending');
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const ok = await ensureAuthenticated();
|
||||||
|
if (!ok) return;
|
||||||
|
initUserPanel();
|
||||||
|
initBridgeSetupPage();
|
||||||
|
mountLogoutButton('#btn-logout', { redirect: '/login.php' });
|
||||||
|
ensureFloatingLogout({ redirect: '/login.php' });
|
||||||
|
});
|
||||||
@@ -7,7 +7,6 @@ const state = {
|
|||||||
userMap: new Map(),
|
userMap: new Map(),
|
||||||
senders: [],
|
senders: [],
|
||||||
senderMap: new Map(),
|
senderMap: new Map(),
|
||||||
bridgeTables: [],
|
|
||||||
currentTab: 'profile',
|
currentTab: 'profile',
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
@@ -25,8 +24,6 @@ let teamTable;
|
|||||||
let userForm;
|
let userForm;
|
||||||
let senderTable;
|
let senderTable;
|
||||||
let senderForm;
|
let senderForm;
|
||||||
let bridgePreview;
|
|
||||||
let validateBridgeBtn;
|
|
||||||
let menuInitialized = false;
|
let menuInitialized = false;
|
||||||
let menuOpen = false;
|
let menuOpen = false;
|
||||||
let debugButton;
|
let debugButton;
|
||||||
@@ -64,8 +61,6 @@ export function initAccountPage() {
|
|||||||
userForm = document.getElementById('userForm');
|
userForm = document.getElementById('userForm');
|
||||||
senderTable = document.getElementById('senderTable');
|
senderTable = document.getElementById('senderTable');
|
||||||
senderForm = document.getElementById('senderForm');
|
senderForm = document.getElementById('senderForm');
|
||||||
bridgePreview = document.getElementById('bridgeTablesPreview');
|
|
||||||
validateBridgeBtn = document.getElementById('btn-validate-bridge');
|
|
||||||
|
|
||||||
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
|
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
|
||||||
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
|
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
|
||||||
@@ -80,8 +75,6 @@ export function initAccountPage() {
|
|||||||
settingsForm?.addEventListener('submit', submitSettingsForm);
|
settingsForm?.addEventListener('submit', submitSettingsForm);
|
||||||
teamTable?.addEventListener('click', handleTeamTableClick);
|
teamTable?.addEventListener('click', handleTeamTableClick);
|
||||||
senderTable?.addEventListener('click', handleSenderTableClick);
|
senderTable?.addEventListener('click', handleSenderTableClick);
|
||||||
validateBridgeBtn?.addEventListener('click', validateBridgeSettings);
|
|
||||||
|
|
||||||
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
document.querySelectorAll('[data-user-tab]').forEach(btn => {
|
||||||
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
|
||||||
});
|
});
|
||||||
@@ -250,11 +243,6 @@ function fillSettingsForm(settings) {
|
|||||||
settingsForm.bridge_token.value = settings.bridge_token || '';
|
settingsForm.bridge_token.value = settings.bridge_token || '';
|
||||||
settingsForm.sender_token.value = settings.sender_token || '';
|
settingsForm.sender_token.value = settings.sender_token || '';
|
||||||
settingsForm.external_api_token.value = settings.external_api_token || '';
|
settingsForm.external_api_token.value = settings.external_api_token || '';
|
||||||
const tables = normalizeTableNames(settings.bridge_tables);
|
|
||||||
if (settingsForm.bridge_tables) {
|
|
||||||
settingsForm.bridge_tables.value = tables.join(', ');
|
|
||||||
}
|
|
||||||
applyBridgePreview(tables);
|
|
||||||
state.rotate = { bridge: false, sender: false, external: false };
|
state.rotate = { bridge: false, sender: false, external: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +289,6 @@ async function submitSettingsForm(ev) {
|
|||||||
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
|
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
|
||||||
rotate_sender_token: state.rotate.sender ? 1 : 0,
|
rotate_sender_token: state.rotate.sender ? 1 : 0,
|
||||||
rotate_external_token: state.rotate.external ? 1 : 0,
|
rotate_external_token: state.rotate.external ? 1 : 0,
|
||||||
bridge_tables: parseBridgeTablesInput(),
|
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const res = await apiAction('account.settings.update', { method: 'POST', data });
|
const res = await apiAction('account.settings.update', { method: 'POST', data });
|
||||||
@@ -335,72 +322,6 @@ async function downloadFile(type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBridgeTablesInput() {
|
|
||||||
if (!settingsForm) return [];
|
|
||||||
const raw = settingsForm.bridge_tables?.value || '';
|
|
||||||
return raw
|
|
||||||
.split(/[\s,]+/)
|
|
||||||
.map(part => part.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyBridgePreview(tables) {
|
|
||||||
state.bridgeTables = normalizeTableNames(tables);
|
|
||||||
if (!bridgePreview) return;
|
|
||||||
if (!state.bridgeTables.length) {
|
|
||||||
bridgePreview.innerHTML = '<span class="text-xs text-slate-500">Keine Einschränkung – alle Tabellen erlaubt.</span>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bridgePreview.innerHTML = state.bridgeTables.map(name => `<span class="chip">${escapeHtml(name)}</span>`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateBridgeSettings(ev) {
|
|
||||||
ev?.preventDefault();
|
|
||||||
if (!settingsForm) return;
|
|
||||||
const data = {
|
|
||||||
bridge_url: settingsForm.bridge_url.value.trim(),
|
|
||||||
bridge_token: settingsForm.bridge_token.value.trim(),
|
|
||||||
};
|
|
||||||
if (!data.bridge_url || !data.bridge_token) {
|
|
||||||
toast('Bitte Bridge-URL und Token angeben', false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await apiAction('account.bridge.test', { method: 'POST', data });
|
|
||||||
if (!res?.ok) throw new Error(res?.error || 'Prüfung fehlgeschlagen');
|
|
||||||
const tables = normalizeTableNames(res.tables);
|
|
||||||
applyBridgePreview(tables);
|
|
||||||
if (settingsForm.bridge_tables) {
|
|
||||||
settingsForm.bridge_tables.value = tables.join(', ');
|
|
||||||
}
|
|
||||||
toast('Bridge erfolgreich geprüft', true);
|
|
||||||
} catch (err) {
|
|
||||||
toast(err.message || 'Prüfung fehlgeschlagen', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTableNames(list) {
|
|
||||||
if (!Array.isArray(list)) return [];
|
|
||||||
const seen = new Set();
|
|
||||||
const result = [];
|
|
||||||
for (const entry of list) {
|
|
||||||
let name = '';
|
|
||||||
if (typeof entry === 'string') {
|
|
||||||
name = entry;
|
|
||||||
} else if (entry && typeof entry === 'object') {
|
|
||||||
name = entry.name || entry.table || entry.label || '';
|
|
||||||
}
|
|
||||||
if (typeof name === 'string') {
|
|
||||||
const trimmed = name.trim();
|
|
||||||
if (trimmed && !seen.has(trimmed)) {
|
|
||||||
seen.add(trimmed);
|
|
||||||
result.push(trimmed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
try {
|
try {
|
||||||
const res = await apiAction('account.users.list', { method: 'GET' });
|
const res = await apiAction('account.users.list', { method: 'GET' });
|
||||||
|
|||||||
@@ -876,6 +876,12 @@ class ApiKernel
|
|||||||
case 'downloads.sender':
|
case 'downloads.sender':
|
||||||
$this->handleDownloadFile('sender');
|
$this->handleDownloadFile('sender');
|
||||||
break;
|
break;
|
||||||
|
case 'account.bridge.setup.get':
|
||||||
|
$this->handleAccountBridgeSetupGet();
|
||||||
|
break;
|
||||||
|
case 'account.bridge.setup.save':
|
||||||
|
$this->handleAccountBridgeSetupSave();
|
||||||
|
break;
|
||||||
case 'account.bridge.test':
|
case 'account.bridge.test':
|
||||||
$this->handleAccountBridgeTest();
|
$this->handleAccountBridgeTest();
|
||||||
break;
|
break;
|
||||||
@@ -1272,15 +1278,9 @@ class ApiKernel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings = $this->getCustomerSettings($customerId);
|
|
||||||
$tables = $schema['tables'] ?? [];
|
|
||||||
if (!empty($settings['bridge_tables'])) {
|
|
||||||
$tables = $this->filterSchemaTables($tables, $settings['bridge_tables']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->respond([
|
$this->respond([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'tables' => $tables,
|
'tables' => $schema['tables'] ?? [],
|
||||||
'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
|
'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1466,8 +1466,6 @@ class ApiKernel
|
|||||||
$bridgeToken = trim((string)($this->in['bridge_token'] ?? ''));
|
$bridgeToken = trim((string)($this->in['bridge_token'] ?? ''));
|
||||||
$senderToken = trim((string)($this->in['sender_token'] ?? ''));
|
$senderToken = trim((string)($this->in['sender_token'] ?? ''));
|
||||||
$externalToken = trim((string)($this->in['external_api_token'] ?? ''));
|
$externalToken = trim((string)($this->in['external_api_token'] ?? ''));
|
||||||
$bridgeTablesInput = $this->in['bridge_tables'] ?? null;
|
|
||||||
$bridgeTables = $this->normalizeBridgeTables($bridgeTablesInput);
|
|
||||||
$rotateBridge = !empty($this->in['rotate_bridge_token']);
|
$rotateBridge = !empty($this->in['rotate_bridge_token']);
|
||||||
$rotateSender = !empty($this->in['rotate_sender_token']);
|
$rotateSender = !empty($this->in['rotate_sender_token']);
|
||||||
$rotateExternal = !empty($this->in['rotate_external_token']);
|
$rotateExternal = !empty($this->in['rotate_external_token']);
|
||||||
@@ -1486,7 +1484,6 @@ class ApiKernel
|
|||||||
'bridge_token' => $bridgeToken,
|
'bridge_token' => $bridgeToken,
|
||||||
'sender_token' => $senderToken,
|
'sender_token' => $senderToken,
|
||||||
'external_api_token' => $externalToken,
|
'external_api_token' => $externalToken,
|
||||||
'bridge_tables' => $bridgeTables,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->respond(['ok' => true, 'settings' => $settings]);
|
$this->respond(['ok' => true, 'settings' => $settings]);
|
||||||
@@ -1809,9 +1806,10 @@ class ApiKernel
|
|||||||
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
||||||
|
|
||||||
$settings = $this->ensureSettingsTokens($customerId, $this->getCustomerSettings($customerId));
|
$settings = $this->ensureSettingsTokens($customerId, $this->getCustomerSettings($customerId));
|
||||||
|
$bridgeSetup = $this->getBridgeSetupData($customerId);
|
||||||
$content = $this->loadDownloadTemplate($type);
|
$content = $this->loadDownloadTemplate($type);
|
||||||
if ($type === 'bridge') {
|
if ($type === 'bridge') {
|
||||||
$content = $this->populateBridgeDownload($content, $settings);
|
$content = $this->populateBridgeDownload($content, $settings, $bridgeSetup);
|
||||||
} else {
|
} else {
|
||||||
$content = $this->populateSenderDownload($content, $settings);
|
$content = $this->populateSenderDownload($content, $settings);
|
||||||
}
|
}
|
||||||
@@ -1823,6 +1821,56 @@ class ApiKernel
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleAccountBridgeSetupGet(): void
|
||||||
|
{
|
||||||
|
$user = $this->requireAuth();
|
||||||
|
$this->ensureRole($user, ['owner', 'admin']);
|
||||||
|
$customerId = (int)($user['customer_id'] ?? 0);
|
||||||
|
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
||||||
|
|
||||||
|
$setup = $this->getBridgeSetupData($customerId);
|
||||||
|
$this->respond(['ok' => true, 'setup' => $setup]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleAccountBridgeSetupSave(): void
|
||||||
|
{
|
||||||
|
$user = $this->requireAuth();
|
||||||
|
$this->ensureRole($user, ['owner', 'admin']);
|
||||||
|
$customerId = (int)($user['customer_id'] ?? 0);
|
||||||
|
if ($customerId <= 0) $this->fail('Customer context missing', null, 500);
|
||||||
|
|
||||||
|
$tables = $this->normalizeBridgeTables($this->in['tables'] ?? $this->in['bridge_tables'] ?? []);
|
||||||
|
$mode = strtolower((string)($this->in['mode'] ?? $this->in['db_mode'] ?? 'direct'));
|
||||||
|
$direct = [
|
||||||
|
'host' => trim((string)($this->in['direct_host'] ?? '')),
|
||||||
|
'port' => (int)($this->in['direct_port'] ?? 3306),
|
||||||
|
'database' => trim((string)($this->in['direct_database'] ?? $this->in['direct_db'] ?? '')),
|
||||||
|
'user' => trim((string)($this->in['direct_user'] ?? '')),
|
||||||
|
'password' => (string)($this->in['direct_password'] ?? ''),
|
||||||
|
'charset' => trim((string)($this->in['direct_charset'] ?? '')) ?: 'utf8mb4',
|
||||||
|
];
|
||||||
|
$config = [
|
||||||
|
'file' => trim((string)($this->in['config_file'] ?? '')),
|
||||||
|
'base' => (string)($this->in['config_base'] ?? ''),
|
||||||
|
'host_key' => (string)($this->in['config_host_key'] ?? ''),
|
||||||
|
'port_key' => (string)($this->in['config_port_key'] ?? ''),
|
||||||
|
'database_key' => (string)($this->in['config_database_key'] ?? ''),
|
||||||
|
'user_key' => (string)($this->in['config_user_key'] ?? ''),
|
||||||
|
'password_key' => (string)($this->in['config_password_key'] ?? ''),
|
||||||
|
'charset_key' => (string)($this->in['config_charset_key'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
$setup = $this->sanitizeBridgeSetup([
|
||||||
|
'tables' => $tables,
|
||||||
|
'mode' => $mode,
|
||||||
|
'direct' => $direct,
|
||||||
|
'config' => $config,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stored = $this->saveBridgeSetupData($customerId, $setup);
|
||||||
|
$this->respond(['ok' => true, 'setup' => $stored]);
|
||||||
|
}
|
||||||
|
|
||||||
private function handleAccountBridgeTest(): void
|
private function handleAccountBridgeTest(): void
|
||||||
{
|
{
|
||||||
$user = $this->requireAuth();
|
$user = $this->requireAuth();
|
||||||
@@ -1838,20 +1886,15 @@ class ApiKernel
|
|||||||
if ($bridgeUrl === '' || $bridgeToken === '') {
|
if ($bridgeUrl === '' || $bridgeToken === '') {
|
||||||
$this->fail('Bridge nicht konfiguriert', null, 422);
|
$this->fail('Bridge nicht konfiguriert', null, 422);
|
||||||
}
|
}
|
||||||
$settings = $this->getCustomerSettings($customerId);
|
|
||||||
try {
|
try {
|
||||||
$schema = $this->fetchPlaceholderSchema($bridgeUrl, $bridgeToken, 0);
|
$schema = $this->fetchPlaceholderSchema($bridgeUrl, $bridgeToken, 0);
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
$this->fail('Bridge request failed', $e->getMessage(), 502);
|
$this->fail('Bridge request failed', $e->getMessage(), 502);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$tables = $schema['tables'] ?? [];
|
|
||||||
if (!empty($settings['bridge_tables'])) {
|
|
||||||
$tables = $this->filterSchemaTables($tables, $settings['bridge_tables']);
|
|
||||||
}
|
|
||||||
$this->respond([
|
$this->respond([
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'tables' => $tables,
|
'tables' => $schema['tables'] ?? [],
|
||||||
'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
|
'fetched' => $schema['fetched'] ?? date(DATE_ATOM),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1887,17 +1930,26 @@ class ApiKernel
|
|||||||
return $row ? $this->formatCustomerSettingsRow($row) : [];
|
return $row ? $this->formatCustomerSettingsRow($row) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getBridgeSetupData(int $customerId): array
|
||||||
|
{
|
||||||
|
$settings = $this->getCustomerSettings($customerId);
|
||||||
|
return $settings['bridge_setup'] ?? $this->defaultBridgeSetup();
|
||||||
|
}
|
||||||
|
|
||||||
private function saveCustomerSettings(int $customerId, array $data): array
|
private function saveCustomerSettings(int $customerId, array $data): array
|
||||||
{
|
{
|
||||||
if ($customerId <= 0) return [];
|
if ($customerId <= 0) return [];
|
||||||
$this->ensureCustomerSettingsTableExists();
|
$this->ensureCustomerSettingsTableExists();
|
||||||
$allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'bridge_tables'];
|
$allowed = ['bridge_url', 'bridge_token', 'sender_token', 'external_api_token', 'bridge_tables', 'bridge_setup'];
|
||||||
$fields = array_intersect_key($data, array_flip($allowed));
|
$fields = array_intersect_key($data, array_flip($allowed));
|
||||||
if (!$fields) return $this->getCustomerSettings($customerId);
|
if (!$fields) return $this->getCustomerSettings($customerId);
|
||||||
if (array_key_exists('bridge_tables', $fields)) {
|
if (array_key_exists('bridge_tables', $fields)) {
|
||||||
$normalized = $this->normalizeBridgeTables($fields['bridge_tables']);
|
$normalized = $this->normalizeBridgeTables($fields['bridge_tables']);
|
||||||
$fields['bridge_tables'] = $normalized ? $this->encodeBridgeTables($normalized) : null;
|
$fields['bridge_tables'] = $normalized ? $this->encodeBridgeTables($normalized) : null;
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('bridge_setup', $fields)) {
|
||||||
|
$fields['bridge_setup'] = $this->encodeBridgeSetup($fields['bridge_setup']);
|
||||||
|
}
|
||||||
$fields['customer_id'] = $customerId;
|
$fields['customer_id'] = $customerId;
|
||||||
$columns = array_keys($fields);
|
$columns = array_keys($fields);
|
||||||
$insertCols = implode(',', array_map(fn($c) => "`$c`", $columns));
|
$insertCols = implode(',', array_map(fn($c) => "`$c`", $columns));
|
||||||
@@ -1917,6 +1969,12 @@ class ApiKernel
|
|||||||
return $this->getCustomerSettings($customerId);
|
return $this->getCustomerSettings($customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function saveBridgeSetupData(int $customerId, array $setup): array
|
||||||
|
{
|
||||||
|
$settings = $this->saveCustomerSettings($customerId, ['bridge_setup' => $setup]);
|
||||||
|
return $settings['bridge_setup'] ?? $this->defaultBridgeSetup();
|
||||||
|
}
|
||||||
|
|
||||||
private function ensureSettingsTokens(int $customerId, array $settings): array
|
private function ensureSettingsTokens(int $customerId, array $settings): array
|
||||||
{
|
{
|
||||||
if ($customerId <= 0) return $settings;
|
if ($customerId <= 0) return $settings;
|
||||||
@@ -1940,6 +1998,11 @@ class ApiKernel
|
|||||||
} else {
|
} else {
|
||||||
$row['bridge_tables'] = [];
|
$row['bridge_tables'] = [];
|
||||||
}
|
}
|
||||||
|
if (array_key_exists('bridge_setup', $row)) {
|
||||||
|
$row['bridge_setup'] = $this->decodeBridgeSetup($row['bridge_setup']);
|
||||||
|
} else {
|
||||||
|
$row['bridge_setup'] = $this->defaultBridgeSetup();
|
||||||
|
}
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1982,25 +2045,104 @@ class ApiKernel
|
|||||||
return $this->normalizeBridgeTables($str);
|
return $this->normalizeBridgeTables($str);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function filterSchemaTables(array $tables, array $allowed): array
|
private function encodeBridgeSetup($setup)
|
||||||
{
|
{
|
||||||
if (empty($allowed)) return $tables;
|
if (is_array($setup)) {
|
||||||
$allowedLower = array_map('strtolower', $allowed);
|
$setup = $this->sanitizeBridgeSetup($setup);
|
||||||
$filtered = [];
|
return json_encode($setup, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
foreach ($tables as $entry) {
|
|
||||||
if (is_array($entry)) {
|
|
||||||
$name = strtolower((string)($entry['name'] ?? $entry['table'] ?? $entry['label'] ?? ''));
|
|
||||||
if ($name !== '' && in_array($name, $allowedLower, true)) {
|
|
||||||
$filtered[] = $entry;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$name = strtolower((string)$entry);
|
|
||||||
if ($name !== '' && in_array($name, $allowedLower, true)) {
|
|
||||||
$filtered[] = $entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $filtered;
|
if (is_string($setup)) {
|
||||||
|
return $setup;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function decodeBridgeSetup($stored): array
|
||||||
|
{
|
||||||
|
if (is_array($stored)) {
|
||||||
|
return $this->sanitizeBridgeSetup($stored);
|
||||||
|
}
|
||||||
|
$str = (string)$stored;
|
||||||
|
if ($str === '') {
|
||||||
|
return $this->defaultBridgeSetup();
|
||||||
|
}
|
||||||
|
$decoded = json_decode($str, true);
|
||||||
|
if (is_array($decoded)) {
|
||||||
|
return $this->sanitizeBridgeSetup($decoded);
|
||||||
|
}
|
||||||
|
return $this->defaultBridgeSetup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function defaultBridgeSetup(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tables' => [],
|
||||||
|
'mode' => 'direct',
|
||||||
|
'direct' => [
|
||||||
|
'host' => '',
|
||||||
|
'port' => 3306,
|
||||||
|
'database' => '',
|
||||||
|
'user' => '',
|
||||||
|
'password' => '',
|
||||||
|
'charset' => 'utf8mb4',
|
||||||
|
],
|
||||||
|
'config' => [
|
||||||
|
'file' => '',
|
||||||
|
'base' => '',
|
||||||
|
'host_key' => '',
|
||||||
|
'port_key' => '',
|
||||||
|
'database_key' => '',
|
||||||
|
'user_key' => '',
|
||||||
|
'password_key' => '',
|
||||||
|
'charset_key' => '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeBridgeSetup(?array $input): array
|
||||||
|
{
|
||||||
|
$defaults = $this->defaultBridgeSetup();
|
||||||
|
if (!is_array($input)) {
|
||||||
|
return $defaults;
|
||||||
|
}
|
||||||
|
$mode = strtolower((string)($input['mode'] ?? 'direct'));
|
||||||
|
if (!in_array($mode, ['direct', 'config'], true)) {
|
||||||
|
$mode = 'direct';
|
||||||
|
}
|
||||||
|
$tables = $this->normalizeBridgeTables($input['tables'] ?? []);
|
||||||
|
$direct = $input['direct'] ?? [];
|
||||||
|
$config = $input['config'] ?? [];
|
||||||
|
$sanitizePath = function ($value) {
|
||||||
|
$value = trim((string)$value);
|
||||||
|
if ($value === '') return '';
|
||||||
|
return preg_replace('/[^a-zA-Z0-9_\.\-]/', '', $value) ?: '';
|
||||||
|
};
|
||||||
|
$result = [
|
||||||
|
'tables' => $tables,
|
||||||
|
'mode' => $mode,
|
||||||
|
'direct' => [
|
||||||
|
'host' => trim((string)($direct['host'] ?? $defaults['direct']['host'])),
|
||||||
|
'port' => (int)($direct['port'] ?? $defaults['direct']['port']),
|
||||||
|
'database' => trim((string)($direct['database'] ?? $defaults['direct']['database'])),
|
||||||
|
'user' => trim((string)($direct['user'] ?? $defaults['direct']['user'])),
|
||||||
|
'password' => (string)($direct['password'] ?? $defaults['direct']['password']),
|
||||||
|
'charset' => trim((string)($direct['charset'] ?? $defaults['direct']['charset'])) ?: 'utf8mb4',
|
||||||
|
],
|
||||||
|
'config' => [
|
||||||
|
'file' => trim((string)($config['file'] ?? '')),
|
||||||
|
'base' => $sanitizePath($config['base'] ?? ''),
|
||||||
|
'host_key' => $sanitizePath($config['host_key'] ?? ''),
|
||||||
|
'port_key' => $sanitizePath($config['port_key'] ?? ''),
|
||||||
|
'database_key' => $sanitizePath($config['database_key'] ?? ''),
|
||||||
|
'user_key' => $sanitizePath($config['user_key'] ?? ''),
|
||||||
|
'password_key' => $sanitizePath($config['password_key'] ?? ''),
|
||||||
|
'charset_key' => $sanitizePath($config['charset_key'] ?? ''),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if ($result['direct']['port'] <= 0) {
|
||||||
|
$result['direct']['port'] = 3306;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function customerSettingsTable(): string
|
private function customerSettingsTable(): string
|
||||||
@@ -2056,6 +2198,9 @@ SQL;
|
|||||||
if (!in_array('bridge_tables', $columns, true)) {
|
if (!in_array('bridge_tables', $columns, true)) {
|
||||||
$missing[] = 'ADD COLUMN `bridge_tables` text DEFAULT NULL';
|
$missing[] = 'ADD COLUMN `bridge_tables` text DEFAULT NULL';
|
||||||
}
|
}
|
||||||
|
if (!in_array('bridge_setup', $columns, true)) {
|
||||||
|
$missing[] = 'ADD COLUMN `bridge_setup` longtext DEFAULT NULL';
|
||||||
|
}
|
||||||
|
|
||||||
if (!$missing) {
|
if (!$missing) {
|
||||||
return;
|
return;
|
||||||
@@ -2380,13 +2525,13 @@ SQL;
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private function populateBridgeDownload(string $content, array $settings): string
|
private function populateBridgeDownload(string $content, array $settings, array $setup): string
|
||||||
{
|
{
|
||||||
$token = (string)($settings['bridge_token'] ?? '');
|
$token = (string)($settings['bridge_token'] ?? '');
|
||||||
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', $token, $content);
|
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', $token, $content);
|
||||||
|
|
||||||
$tables = [];
|
$tables = array_values(array_filter(array_map('strval', $setup['tables'] ?? [])));
|
||||||
if (!empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) {
|
if (!$tables && !empty($settings['bridge_tables']) && is_array($settings['bridge_tables'])) {
|
||||||
$tables = array_values(array_filter(array_map('strval', $settings['bridge_tables'])));
|
$tables = array_values(array_filter(array_map('strval', $settings['bridge_tables'])));
|
||||||
}
|
}
|
||||||
$tablesExport = $this->exportPhpArray($tables);
|
$tablesExport = $this->exportPhpArray($tables);
|
||||||
@@ -2397,9 +2542,148 @@ SQL;
|
|||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$mode = strtolower((string)($setup['mode'] ?? 'direct'));
|
||||||
|
if ($mode === 'direct') {
|
||||||
|
$dsn = $this->buildBridgeDsn($setup['direct'] ?? []);
|
||||||
|
if ($dsn !== '') {
|
||||||
|
$dsnValue = var_export($dsn, true);
|
||||||
|
$content = preg_replace("/'dsn'\\s*=>\\s*'[^']*',/", "'dsn' => {$dsnValue},", $content, 1);
|
||||||
|
}
|
||||||
|
$userValue = var_export((string)($setup['direct']['user'] ?? ''), true);
|
||||||
|
$passValue = var_export((string)($setup['direct']['password'] ?? ''), true);
|
||||||
|
$content = preg_replace("/'user'\\s*=>\\s*'[^']*',/", "'user' => {$userValue},", $content, 1);
|
||||||
|
$content = preg_replace("/'pass'\\s*=>\\s*'[^']*',/", "'pass' => {$passValue},", $content, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$snippet = $this->buildBridgeSetupSnippet($setup);
|
||||||
|
if (strpos($content, '// {{BRIDGE_DB_SETUP}}') !== false) {
|
||||||
|
$content = str_replace('// {{BRIDGE_DB_SETUP}}', $snippet, $content);
|
||||||
|
} else {
|
||||||
|
$content .= "\n" . $snippet;
|
||||||
|
}
|
||||||
|
|
||||||
return $content;
|
return $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildBridgeDsn(array $direct): string
|
||||||
|
{
|
||||||
|
$host = trim((string)($direct['host'] ?? ''));
|
||||||
|
$db = trim((string)($direct['database'] ?? ''));
|
||||||
|
if ($host === '' || $db === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$port = (int)($direct['port'] ?? 3306);
|
||||||
|
$charset = trim((string)($direct['charset'] ?? 'utf8mb4')) ?: 'utf8mb4';
|
||||||
|
$parts = ["mysql:host={$host}"];
|
||||||
|
if ($port > 0) {
|
||||||
|
$parts[] = 'port=' . $port;
|
||||||
|
}
|
||||||
|
$parts[] = 'dbname=' . $db;
|
||||||
|
$parts[] = 'charset=' . $charset;
|
||||||
|
return implode(';', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildBridgeSetupSnippet(array $setup): string
|
||||||
|
{
|
||||||
|
$mode = strtolower((string)($setup['mode'] ?? 'direct'));
|
||||||
|
if ($mode !== 'config') {
|
||||||
|
return "// Bridge DB Setup: direkte Angaben aus dem EmailTemplate-Backend.\n";
|
||||||
|
}
|
||||||
|
$config = $setup['config'] ?? [];
|
||||||
|
$file = trim((string)($config['file'] ?? ''));
|
||||||
|
if ($file === '') {
|
||||||
|
return "// Bridge DB Setup: Bitte im EmailTemplate-Backend eine Konfigurationsdatei angeben.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = trim((string)($config['base'] ?? ''));
|
||||||
|
$paths = [
|
||||||
|
'host' => $this->bridgeCombinePath($base, $config['host_key'] ?? ''),
|
||||||
|
'port' => $this->bridgeCombinePath($base, $config['port_key'] ?? ''),
|
||||||
|
'database' => $this->bridgeCombinePath($base, $config['database_key'] ?? ''),
|
||||||
|
'user' => $this->bridgeCombinePath($base, $config['user_key'] ?? ''),
|
||||||
|
'password' => $this->bridgeCombinePath($base, $config['password_key'] ?? ''),
|
||||||
|
'charset' => $this->bridgeCombinePath($base, $config['charset_key'] ?? ''),
|
||||||
|
];
|
||||||
|
$defaults = [
|
||||||
|
'host' => $this->bridgeCombinePath($base, 'host'),
|
||||||
|
'port' => $this->bridgeCombinePath($base, 'port'),
|
||||||
|
'database' => $this->bridgeCombinePath($base, 'database'),
|
||||||
|
'user' => $this->bridgeCombinePath($base, 'user'),
|
||||||
|
'password' => $this->bridgeCombinePath($base, 'password'),
|
||||||
|
'charset' => $this->bridgeCombinePath($base, 'charset'),
|
||||||
|
];
|
||||||
|
foreach ($paths as $key => $value) {
|
||||||
|
if ($value === '') {
|
||||||
|
$paths[$key] = $defaults[$key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileExpr = $this->bridgeConfigFileExpression($file);
|
||||||
|
$baseExport = var_export($base, true);
|
||||||
|
$lines = [];
|
||||||
|
$lines[] = '/** Bridge DB Setup: automatisch generiertes Mapping */';
|
||||||
|
$lines[] = '$bridgeConfigFile = ' . $fileExpr . ';';
|
||||||
|
$lines[] = '$bridgeConfigSource = is_file($bridgeConfigFile) ? include $bridgeConfigFile : null;';
|
||||||
|
$lines[] = 'if (is_array($bridgeConfigSource)) {';
|
||||||
|
$lines[] = ' $bridgeConfigData = $bridgeConfigSource;';
|
||||||
|
$lines[] = " if ({$baseExport} !== '') {";
|
||||||
|
$lines[] = " $bridgeConfigData = bridge_array_get($bridgeConfigSource, {$baseExport}, $bridgeConfigData);";
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = ' if (!is_array($bridgeConfigData)) {';
|
||||||
|
$lines[] = ' $bridgeConfigData = (array)$bridgeConfigData;';
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = ' $bridgeDbHost = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['host'], true) . ', \'\');';
|
||||||
|
$lines[] = ' $bridgeDbName = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['database'], true) . ', \'\');';
|
||||||
|
$lines[] = ' $bridgeDbUser = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['user'], true) . ', \'\');';
|
||||||
|
$lines[] = ' $bridgeDbPass = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['password'], true) . ', \'\');';
|
||||||
|
$lines[] = ' $bridgeDbCharset = (string)bridge_array_get($bridgeConfigData, ' . var_export($paths['charset'], true) . ', \'utf8mb4\');';
|
||||||
|
$lines[] = ' $bridgeDbPort = (int)bridge_array_get($bridgeConfigData, ' . var_export($paths['port'], true) . ', 3306);';
|
||||||
|
$lines[] = ' if ($bridgeDbHost !== \'\' && $bridgeDbName !== \'\') {';
|
||||||
|
$lines[] = ' $bridgeDsnParts = ["mysql:host={$bridgeDbHost}"];';
|
||||||
|
$lines[] = ' if ($bridgeDbPort > 0) {';
|
||||||
|
$lines[] = ' $bridgeDsnParts[] = "port={$bridgeDbPort}";';
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = ' $bridgeDbCharset = $bridgeDbCharset ?: \'utf8mb4\';';
|
||||||
|
$lines[] = ' $bridgeDsnParts[] = "dbname={$bridgeDbName}";';
|
||||||
|
$lines[] = ' $bridgeDsnParts[] = "charset={$bridgeDbCharset}";';
|
||||||
|
$lines[] = ' $bridgeConfig[\'db\'][\'dsn\'] = implode(\';\', $bridgeDsnParts);';
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = ' if ($bridgeDbUser !== \'\') {';
|
||||||
|
$lines[] = ' $bridgeConfig[\'db\'][\'user\'] = $bridgeDbUser;';
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = ' if ($bridgeDbPass !== \'\') {';
|
||||||
|
$lines[] = ' $bridgeConfig[\'db\'][\'pass\'] = $bridgeDbPass;';
|
||||||
|
$lines[] = ' }';
|
||||||
|
$lines[] = '}';
|
||||||
|
|
||||||
|
return implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bridgeConfigFileExpression(string $file): string
|
||||||
|
{
|
||||||
|
if ($file === '') {
|
||||||
|
return var_export('', true);
|
||||||
|
}
|
||||||
|
if ($file[0] === '/' || preg_match('~^[A-Za-z]:\\\\~', $file)) {
|
||||||
|
return var_export($file, true);
|
||||||
|
}
|
||||||
|
$normalized = '/' . ltrim($file, '/');
|
||||||
|
return '__DIR__ . ' . var_export($normalized, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function bridgeCombinePath(string $base, string $key): string
|
||||||
|
{
|
||||||
|
$base = trim($base, '.');
|
||||||
|
$key = trim($key, '.');
|
||||||
|
if ($base !== '' && $key !== '') {
|
||||||
|
return $base . '.' . $key;
|
||||||
|
}
|
||||||
|
if ($base !== '') {
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
private function populateSenderDownload(string $content, array $settings): string
|
private function populateSenderDownload(string $content, array $settings): string
|
||||||
{
|
{
|
||||||
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', (string)($settings['sender_token'] ?? ''), $content);
|
$content = str_replace('REPLACE_WITH_SHARED_TOKEN', (string)($settings['sender_token'] ?? ''), $content);
|
||||||
|
|||||||
Reference in New Issue
Block a user