deploy
This commit is contained in:
0
docs/Umsetzungsanweisung/Old-Nexus/public/.gitkeep
Executable file
0
docs/Umsetzungsanweisung/Old-Nexus/public/.gitkeep
Executable file
32
docs/Umsetzungsanweisung/Old-Nexus/public/.htaccess
Executable file
32
docs/Umsetzungsanweisung/Old-Nexus/public/.htaccess
Executable file
@@ -0,0 +1,32 @@
|
||||
# -------------------------------------------------
|
||||
# Apache Front Controller Setup (public/.htaccess)
|
||||
# -------------------------------------------------
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Sicherheit: keine Directory Listings
|
||||
Options -Indexes
|
||||
|
||||
# -------------------------------------------------
|
||||
# 1) Assets DIREKT ausliefern
|
||||
# -------------------------------------------------
|
||||
RewriteRule ^assets/ - [L]
|
||||
|
||||
# -------------------------------------------------
|
||||
# 2) Legacy-Pfad blockieren (frueher public/page)
|
||||
# -------------------------------------------------
|
||||
RewriteRule ^page/ - [F,L]
|
||||
|
||||
# -------------------------------------------------
|
||||
# 3) Alles andere an den Front Controller
|
||||
# -------------------------------------------------
|
||||
RewriteRule ^ index.php [L]
|
||||
|
||||
# -------------------------------------------------
|
||||
# 4) (Optional) Zusätzliche Sicherheits-Header
|
||||
# -------------------------------------------------
|
||||
<IfModule mod_headers.c>
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||
</IfModule>
|
||||
1800
docs/Umsetzungsanweisung/Old-Nexus/public/assets/css/app.css
Normal file
1800
docs/Umsetzungsanweisung/Old-Nexus/public/assets/css/app.css
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/Umsetzungsanweisung/Old-Nexus/public/assets/images/kusche-logo.png
Executable file
BIN
docs/Umsetzungsanweisung/Old-Nexus/public/assets/images/kusche-logo.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/Umsetzungsanweisung/Old-Nexus/public/assets/images/logo.png
Normal file
BIN
docs/Umsetzungsanweisung/Old-Nexus/public/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 247 KiB |
455
docs/Umsetzungsanweisung/Old-Nexus/public/assets/js/app.js
Executable file
455
docs/Umsetzungsanweisung/Old-Nexus/public/assets/js/app.js
Executable file
@@ -0,0 +1,455 @@
|
||||
document.documentElement.classList.add('js');
|
||||
|
||||
function readThemePreference(key, fallback) {
|
||||
try {
|
||||
return localStorage.getItem(key) || fallback;
|
||||
} catch (error) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const moduleName = document.documentElement.dataset.module || '';
|
||||
const mainThemeMode = readThemePreference('nexus.theme', 'day');
|
||||
const mainThemeAccent = readThemePreference('nexus.accent', 'logo');
|
||||
const moduleThemeMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : mainThemeMode;
|
||||
const moduleThemeAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : mainThemeAccent;
|
||||
|
||||
function normalizeThemeValue(value, allowed, fallback) {
|
||||
return allowed.includes(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function storedTheme() {
|
||||
const rawMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : readThemePreference('nexus.theme', 'day');
|
||||
const rawAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : readThemePreference('nexus.accent', 'logo');
|
||||
const mode = moduleName ? normalizeThemeValue(rawMode, ['inherit', 'day', 'night'], 'inherit') : normalizeThemeValue(rawMode, ['day', 'night'], 'day');
|
||||
const accent = moduleName ? normalizeThemeValue(rawAccent, ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'], 'inherit') : normalizeThemeValue(rawAccent, ['logo', 'pink', 'cyan', 'orange', 'green'], 'logo');
|
||||
return { mode, accent };
|
||||
}
|
||||
|
||||
function applyTheme(mode, accent, persist = true) {
|
||||
const allowedModes = moduleName ? ['inherit', 'day', 'night'] : ['day', 'night'];
|
||||
const allowedAccents = moduleName ? ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'] : ['logo', 'pink', 'cyan', 'orange', 'green'];
|
||||
const normalizedMode = normalizeThemeValue(mode, allowedModes, moduleName ? 'inherit' : 'day');
|
||||
const normalizedAccent = normalizeThemeValue(accent, allowedAccents, moduleName ? 'inherit' : 'logo');
|
||||
const effectiveMode = normalizedMode === 'inherit' ? mainThemeMode : normalizedMode;
|
||||
const effectiveAccent = normalizedAccent === 'inherit' ? mainThemeAccent : normalizedAccent;
|
||||
document.documentElement.dataset.theme = effectiveMode;
|
||||
document.documentElement.dataset.accent = effectiveAccent;
|
||||
try {
|
||||
if (persist) {
|
||||
if (moduleName) {
|
||||
if (normalizedMode === 'inherit') {
|
||||
localStorage.removeItem(`nexus.module.${moduleName}.theme`);
|
||||
} else {
|
||||
localStorage.setItem(`nexus.module.${moduleName}.theme`, normalizedMode);
|
||||
}
|
||||
if (normalizedAccent === 'inherit') {
|
||||
localStorage.removeItem(`nexus.module.${moduleName}.accent`);
|
||||
} else {
|
||||
localStorage.setItem(`nexus.module.${moduleName}.accent`, normalizedAccent);
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem('nexus.theme', normalizedMode);
|
||||
localStorage.setItem('nexus.accent', normalizedAccent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore blocked storage; the current page still receives the theme.
|
||||
}
|
||||
return { mode: normalizedMode, accent: normalizedAccent };
|
||||
}
|
||||
|
||||
const initialTheme = applyTheme(moduleThemeMode, moduleThemeAccent, false);
|
||||
|
||||
const themeModeSelect = document.querySelector('[data-theme-mode]');
|
||||
const themeAccentSelect = document.querySelector('[data-theme-accent]');
|
||||
|
||||
if (themeModeSelect) {
|
||||
themeModeSelect.value = initialTheme.mode;
|
||||
themeModeSelect.addEventListener('change', () => {
|
||||
const current = storedTheme();
|
||||
applyTheme(themeModeSelect.value, current.accent);
|
||||
});
|
||||
}
|
||||
|
||||
if (themeAccentSelect) {
|
||||
themeAccentSelect.value = initialTheme.accent;
|
||||
themeAccentSelect.addEventListener('change', () => {
|
||||
const current = storedTheme();
|
||||
applyTheme(current.mode, themeAccentSelect.value);
|
||||
});
|
||||
}
|
||||
|
||||
for (const element of document.querySelectorAll('[data-reveal]')) {
|
||||
element.classList.add('reveal');
|
||||
}
|
||||
|
||||
const setupTabs = document.querySelectorAll('[data-setup-tab-target]');
|
||||
if (setupTabs.length > 0) {
|
||||
const setupPanels = document.querySelectorAll('.setup-db-panel');
|
||||
const setupControls = document.querySelectorAll('.setup-db-panel input, .setup-db-panel select, .setup-db-panel textarea');
|
||||
for (const control of setupControls) {
|
||||
if (control.required) {
|
||||
control.dataset.setupRequired = 'true';
|
||||
}
|
||||
}
|
||||
|
||||
const activateSetupTab = (targetId) => {
|
||||
const dbPanelsRoot = document.querySelector('[data-setup-db-panels]');
|
||||
const dbPanelsHidden = dbPanelsRoot ? dbPanelsRoot.hidden : false;
|
||||
for (const tab of setupTabs) {
|
||||
const isActive = tab.dataset.setupTabTarget === targetId;
|
||||
tab.classList.toggle('is-active', isActive);
|
||||
tab.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
}
|
||||
for (const panel of setupPanels) {
|
||||
const isActive = !dbPanelsHidden && panel.id === targetId;
|
||||
panel.hidden = !isActive;
|
||||
for (const control of panel.querySelectorAll('input, select, textarea')) {
|
||||
if (control.dataset.setupRequired === 'true') {
|
||||
control.required = isActive;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const tab of setupTabs) {
|
||||
tab.addEventListener('click', () => {
|
||||
activateSetupTab(tab.dataset.setupTabTarget);
|
||||
});
|
||||
}
|
||||
|
||||
const activeSetupTab = document.querySelector('[data-setup-tab-target].is-active') || setupTabs[0];
|
||||
activateSetupTab(activeSetupTab.dataset.setupTabTarget);
|
||||
}
|
||||
|
||||
window.NexusModal = (() => {
|
||||
const openModals = new Set();
|
||||
|
||||
const syncBodyState = () => {
|
||||
document.body.classList.toggle('has-modal-open', openModals.size > 0);
|
||||
};
|
||||
|
||||
const create = (root, options = {}) => {
|
||||
if (!root) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modal = {
|
||||
root,
|
||||
isOpen: () => root.classList.contains('is-open'),
|
||||
open() {
|
||||
root.classList.add('is-open');
|
||||
root.setAttribute('aria-hidden', 'false');
|
||||
openModals.add(root);
|
||||
syncBodyState();
|
||||
if (typeof options.onOpen === 'function') {
|
||||
options.onOpen();
|
||||
}
|
||||
const focusTarget = options.initialFocus ? root.querySelector(options.initialFocus) : null;
|
||||
if (focusTarget instanceof HTMLElement) {
|
||||
window.setTimeout(() => focusTarget.focus(), 20);
|
||||
}
|
||||
},
|
||||
close() {
|
||||
root.classList.remove('is-open');
|
||||
root.setAttribute('aria-hidden', 'true');
|
||||
openModals.delete(root);
|
||||
syncBodyState();
|
||||
if (typeof options.onClose === 'function') {
|
||||
options.onClose();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!root.dataset.modalBound) {
|
||||
root.addEventListener('click', (event) => {
|
||||
if (event.target === root) {
|
||||
modal.close();
|
||||
}
|
||||
});
|
||||
root.dataset.modalBound = '1';
|
||||
}
|
||||
|
||||
return modal;
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
const active = Array.from(openModals).pop();
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
active.classList.remove('is-open');
|
||||
active.setAttribute('aria-hidden', 'true');
|
||||
openModals.delete(active);
|
||||
syncBodyState();
|
||||
});
|
||||
|
||||
return { create };
|
||||
})();
|
||||
|
||||
(() => {
|
||||
const root = document.getElementById('nexus-debug-root');
|
||||
const dataNode = document.getElementById('nexus-debug-data');
|
||||
if (!root || !dataNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = { enabled: false, entries: [] };
|
||||
try {
|
||||
const parsed = JSON.parse(dataNode.textContent || '{}');
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
payload = parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
payload = { enabled: false, entries: [] };
|
||||
}
|
||||
|
||||
const enabled = !!payload.enabled && document.body.dataset.nexusDebugEnabled === '1';
|
||||
const isAdmin = document.body.dataset.nexusDebugAdmin === '1';
|
||||
const entries = Array.isArray(payload.entries) ? payload.entries.slice(0, 250) : [];
|
||||
let sequence = entries.length;
|
||||
|
||||
const debugBus = window.__nexusDebugBus || { enabled: false, listener: null };
|
||||
debugBus.enabled = enabled;
|
||||
window.__nexusDebugBus = debugBus;
|
||||
|
||||
if (!isAdmin || !enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bugSvg = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4.5h6l1.2 2.2 2.5-1.4 1 1.7-2.4 1.4A6.9 6.9 0 0 1 19 13h2.5v2H19a6.9 6.9 0 0 1-.7 2.9l2.4 1.4-1 1.7-2.5-1.4L15 21.5H9l-1.2-2.2-2.5 1.4-1-1.7 2.4-1.4A6.9 6.9 0 0 1 6 15H3.5v-2H6a6.9 6.9 0 0 1 .7-2.9L4.3 8.7l1-1.7 2.5 1.4L9 4.5Zm1.2 2L9 8.8V15a3 3 0 0 0 6 0V8.8l-1.2-2.3h-3.6ZM10.5 11a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z" fill="currentColor"/></svg>';
|
||||
const copySvg = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 9.75A2.25 2.25 0 0 1 11.25 7.5h7.5A2.25 2.25 0 0 1 21 9.75v8.25a2.25 2.25 0 0 1-2.25 2.25h-7.5A2.25 2.25 0 0 1 9 18V9.75Zm-6 4.5V6A2.25 2.25 0 0 1 5.25 3.75h7.5A2.25 2.25 0 0 1 15 6v.75h-3.75A3.75 3.75 0 0 0 7.5 10.5v7.5h-2.25A2.25 2.25 0 0 1 3 15.75v-1.5Z" fill="currentColor"/></svg>';
|
||||
|
||||
root.innerHTML = `
|
||||
<button type="button" class="nexus-debug-bug" aria-label="Debug öffnen">
|
||||
${bugSvg}
|
||||
<span class="nexus-debug-badge">0</span>
|
||||
</button>
|
||||
<section class="nexus-debug-popup" aria-hidden="true">
|
||||
<div class="nexus-debug-popup__head">
|
||||
<div>
|
||||
<div class="nexus-debug-popup__eyebrow">Nexus Debug</div>
|
||||
<h2>Nexus Debug</h2>
|
||||
</div>
|
||||
<button type="button" class="module-button module-button--secondary module-button--small" data-debug-close>Schließen</button>
|
||||
</div>
|
||||
<div class="nexus-debug-popup__toolbar">
|
||||
<div class="muted">Projektweiter Debug-Stream der aktuellen Admin-Sitzung.</div>
|
||||
<div class="nexus-debug-popup__actions">
|
||||
<button type="button" class="module-button module-button--secondary module-button--small" data-debug-reload>Neu laden</button>
|
||||
<button type="button" class="module-button module-button--secondary module-button--small" data-debug-copy>Kopieren</button>
|
||||
<button type="button" class="module-button module-button--secondary module-button--small" data-debug-clear>Leeren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nexus-debug-popup__body" data-debug-list></div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
const bugButton = root.querySelector('.nexus-debug-bug');
|
||||
const badge = root.querySelector('.nexus-debug-badge');
|
||||
const popup = root.querySelector('.nexus-debug-popup');
|
||||
const closeButton = root.querySelector('[data-debug-close]');
|
||||
const reloadButton = root.querySelector('[data-debug-reload]');
|
||||
const copyButton = root.querySelector('[data-debug-copy]');
|
||||
const clearButton = root.querySelector('[data-debug-clear]');
|
||||
const list = root.querySelector('[data-debug-list]');
|
||||
|
||||
const updateBadge = () => {
|
||||
if (!badge) {
|
||||
return;
|
||||
}
|
||||
badge.textContent = String(entries.length);
|
||||
badge.hidden = entries.length === 0;
|
||||
};
|
||||
|
||||
const normalizeEntry = (entry) => {
|
||||
sequence += 1;
|
||||
return {
|
||||
id: entry && entry.id ? entry.id : `nexus-debug-${sequence}`,
|
||||
source: entry && entry.source ? String(entry.source) : 'nexus',
|
||||
type: entry && entry.type ? String(entry.type) : 'debug',
|
||||
label: entry && entry.label ? String(entry.label) : '',
|
||||
at: entry && (entry.at || entry.time) ? String(entry.at || entry.time) : new Date().toISOString(),
|
||||
payload: entry && typeof entry === 'object' ? entry : { value: entry },
|
||||
};
|
||||
};
|
||||
|
||||
const renderEntries = () => {
|
||||
if (!list) {
|
||||
return;
|
||||
}
|
||||
updateBadge();
|
||||
if (!entries.length) {
|
||||
list.innerHTML = '<div class="nexus-debug-empty">Noch keine Debug-Einträge vorhanden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = entries.map((entry, index) => {
|
||||
const title = entry.label || entry.type || `Eintrag ${index + 1}`;
|
||||
const payloadText = JSON.stringify(entry.payload, null, 2);
|
||||
return `
|
||||
<details class="nexus-debug-entry"${index === 0 ? ' open' : ''}>
|
||||
<summary>
|
||||
<span class="nexus-debug-entry__title">
|
||||
<strong>${escapeHtml(title)}</strong>
|
||||
<span class="nexus-debug-entry__meta">${escapeHtml(entry.source)} · ${escapeHtml(entry.at)}</span>
|
||||
</span>
|
||||
<button type="button" class="module-button module-button--secondary module-button--small nexus-debug-entry__copy" data-entry-copy="${escapeHtml(entry.id)}" aria-label="Eintrag kopieren" title="Eintrag kopieren">${copySvg}</button>
|
||||
</summary>
|
||||
<pre>${escapeHtml(payloadText)}</pre>
|
||||
</details>
|
||||
`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const escapeHtml = (value) => String(value ?? '')
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
|
||||
const copyText = async (text, successLabel, errorLabel) => {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.setAttribute('readonly', 'readonly');
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
appendEntry({
|
||||
source: 'nexus',
|
||||
type: 'debug.copy.success',
|
||||
label: successLabel,
|
||||
});
|
||||
} catch (error) {
|
||||
appendEntry({
|
||||
source: 'nexus',
|
||||
type: 'debug.copy.error',
|
||||
label: errorLabel,
|
||||
message: error && error.message ? error.message : 'Kopieren fehlgeschlagen.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openPopup = () => {
|
||||
popup?.classList.add('is-open');
|
||||
popup?.setAttribute('aria-hidden', 'false');
|
||||
};
|
||||
|
||||
const closePopup = () => {
|
||||
popup?.classList.remove('is-open');
|
||||
popup?.setAttribute('aria-hidden', 'true');
|
||||
};
|
||||
|
||||
const appendEntry = (entry) => {
|
||||
entries.unshift(normalizeEntry(entry));
|
||||
if (entries.length > 250) {
|
||||
entries.length = 250;
|
||||
}
|
||||
renderEntries();
|
||||
};
|
||||
|
||||
const reloadEntries = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/debug/entries', {
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
const nextEntries = Array.isArray(data?.data?.entries) ? data.data.entries.map(normalizeEntry) : [];
|
||||
entries.splice(0, entries.length, ...nextEntries);
|
||||
renderEntries();
|
||||
} catch (error) {
|
||||
appendEntry({
|
||||
source: 'nexus',
|
||||
type: 'debug.reload.error',
|
||||
label: 'Reload fehlgeschlagen',
|
||||
message: error && error.message ? error.message : 'Debug-Einträge konnten nicht geladen werden.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const clearEntries = async () => {
|
||||
try {
|
||||
await fetch('/api/debug/clear', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignore clear failures; local UI is still reset below.
|
||||
}
|
||||
entries.splice(0, entries.length);
|
||||
renderEntries();
|
||||
};
|
||||
|
||||
const copyAllEntries = async () => {
|
||||
const content = entries
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((entry) => JSON.stringify(entry.payload, null, 2))
|
||||
.join('\n\n');
|
||||
if (!content) {
|
||||
appendEntry({
|
||||
source: 'nexus',
|
||||
type: 'debug.copy.empty',
|
||||
label: 'Keine Debug-Einträge zum Kopieren',
|
||||
});
|
||||
return;
|
||||
}
|
||||
await copyText(content, 'Debug-Einträge kopiert', 'Debug-Einträge konnten nicht kopiert werden');
|
||||
};
|
||||
|
||||
const previousListener = typeof debugBus.listener === 'function' ? debugBus.listener : null;
|
||||
debugBus.listener = (entry) => {
|
||||
if (previousListener) {
|
||||
previousListener(entry);
|
||||
}
|
||||
appendEntry(entry);
|
||||
};
|
||||
|
||||
bugButton?.addEventListener('click', () => {
|
||||
if (popup?.classList.contains('is-open')) {
|
||||
closePopup();
|
||||
} else {
|
||||
openPopup();
|
||||
}
|
||||
});
|
||||
closeButton?.addEventListener('click', closePopup);
|
||||
reloadButton?.addEventListener('click', reloadEntries);
|
||||
copyButton?.addEventListener('click', copyAllEntries);
|
||||
clearButton?.addEventListener('click', clearEntries);
|
||||
list?.addEventListener('click', (event) => {
|
||||
const button = event.target instanceof HTMLElement ? event.target.closest('[data-entry-copy]') : null;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const entryId = button.getAttribute('data-entry-copy') || '';
|
||||
const entry = entries.find((item) => String(item.id) === entryId);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
const content = JSON.stringify(entry.payload, null, 2);
|
||||
copyText(content, `Eintrag ${entry.type || entry.id} kopiert`, `Eintrag ${entry.type || entry.id} konnte nicht kopiert werden`);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
closePopup();
|
||||
}
|
||||
});
|
||||
|
||||
renderEntries();
|
||||
})();
|
||||
389
docs/Umsetzungsanweisung/Old-Nexus/public/index.php
Executable file
389
docs/Umsetzungsanweisung/Old-Nexus/public/index.php
Executable file
@@ -0,0 +1,389 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Modules\MiningChecker\Support\ApiException as MiningApiException;
|
||||
use Modules\MiningChecker\Support\DebugState as MiningDebugState;
|
||||
|
||||
// boot application (config, autoload, services)
|
||||
require_once __DIR__ . '/../config/fileload.php';
|
||||
|
||||
$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
|
||||
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
|
||||
$uriPath = trim($uriPath, '/');
|
||||
$projectRoot = dirname(__DIR__);
|
||||
$auth = app()->auth();
|
||||
|
||||
// OIDC Auth
|
||||
$publicPaths = [
|
||||
'auth/login',
|
||||
'auth/callback',
|
||||
'auth/logout',
|
||||
'auth/keycloak/login',
|
||||
'auth/keycloak/callback',
|
||||
'auth/keycloak/logout',
|
||||
'auth/me',
|
||||
'module/pi_control/terminal_info',
|
||||
];
|
||||
$requiresGlobalAuth = in_array($uriPath, ['settings', 'settings/widgets', 'settings/search-engines', 'settings/apps', 'users', 'modules', 'modules/install', 'modules/sql-import', 'debug', 'exports/database.sql', 'dashboard', 'dashboards', 'integrations', 'page-modules'], true)
|
||||
|| str_starts_with($uriPath, 'modules/setup/')
|
||||
|| str_starts_with($uriPath, 'modules/access/');
|
||||
if (str_starts_with($uriPath, 'page-modules/view/')) {
|
||||
$requiresGlobalAuth = true;
|
||||
}
|
||||
if (defined('APP_AUTH_ENABLED') && APP_AUTH_ENABLED && $requiresGlobalAuth && !in_array($uriPath, $publicPaths, true)) {
|
||||
$user = auth_user();
|
||||
if (!$user) {
|
||||
header('Location: /auth/login', true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Sicherheitscheck
|
||||
if (str_contains($uriPath, '..')) {
|
||||
http_response_code(400);
|
||||
exit('Bad request');
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/login') {
|
||||
$returnTo = (string)($_GET['return_to'] ?? '/');
|
||||
$auth->login($returnTo);
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/callback') {
|
||||
$uriPath = 'auth/callback';
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/keycloak/logout') {
|
||||
$uriPath = 'auth/logout';
|
||||
}
|
||||
|
||||
if ($uriPath === 'auth/me') {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'authenticated' => $auth->isAuthenticated(),
|
||||
'user' => $auth->user(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'exports/database.sql') {
|
||||
require_admin();
|
||||
$pdo = app()->basePdo() ?: app()->pdo();
|
||||
if (!$pdo instanceof PDO) {
|
||||
http_response_code(500);
|
||||
exit('Keine Datenbankverbindung fuer den Export verfuegbar.');
|
||||
}
|
||||
|
||||
$filename = 'nexus-export-' . gmdate('Ymd-His') . '.sql';
|
||||
$sql = (new \App\SqlDataExporter())->export($pdo, 'nexus');
|
||||
header('Content-Type: application/sql; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
echo $sql;
|
||||
exit;
|
||||
}
|
||||
|
||||
if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatches)) {
|
||||
$moduleName = $moduleAuthMatches[1];
|
||||
$moduleMeta = app()->modules()->get($moduleName);
|
||||
if ($moduleMeta === null) {
|
||||
http_response_code(404);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'module_not_found'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if (!$auth->isAuthenticated()) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'auth_required'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if (!auth_is_admin()) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['error' => 'forbidden'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
echo json_encode(['data' => ($moduleMeta['auth'] ?? ['required' => false, 'users' => [], 'groups' => []])], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
|
||||
$input = json_decode((string)file_get_contents('php://input'), true);
|
||||
if (!is_array($input)) {
|
||||
$input = [];
|
||||
}
|
||||
echo json_encode(['data' => app()->modules()->saveAuth($moduleName, $input)], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'api/debug/entries') {
|
||||
require_admin();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'data' => [
|
||||
'enabled' => nexus_debug_enabled(),
|
||||
'entries' => nexus_debug_entries(),
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($uriPath === 'api/debug/clear' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') {
|
||||
require_admin();
|
||||
nexus_debug_clear();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'data' => [
|
||||
'cleared' => true,
|
||||
'entries' => [],
|
||||
],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) {
|
||||
$moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]];
|
||||
if (!$auth->canAccessModule($moduleMeta)) {
|
||||
http_response_code($auth->isAuthenticated() ? 403 : 401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
|
||||
'login_url' => '/auth/login',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $projectRoot . '/modules/mining-checker/bootstrap.php';
|
||||
|
||||
try {
|
||||
(new Modules\MiningChecker\Api\Router($projectRoot . '/modules/mining-checker'))->handle($apiMatches[1] ?? '');
|
||||
} catch (MiningApiException $exception) {
|
||||
$debugTrace = MiningDebugState::export();
|
||||
http_response_code($exception->statusCode());
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $exception->getMessage(),
|
||||
'context' => $exception->context(),
|
||||
'debug' => $debugTrace !== [] ? $debugTrace : null,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$debugTrace = MiningDebugState::export();
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => 'Unerwarteter Mining-Checker Fehler.',
|
||||
'context' => ['message' => $exception->getMessage()],
|
||||
'debug' => $debugTrace !== [] ? $debugTrace : null,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('~^api/fx-rates(?:/(.*))?$~', $uriPath, $apiMatches)) {
|
||||
$moduleMeta = app()->modules()->get('fx-rates') ?? ['auth' => ['required' => false]];
|
||||
if (!$auth->canAccessModule($moduleMeta)) {
|
||||
http_response_code($auth->isAuthenticated() ? 403 : 401);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => $auth->isAuthenticated() ? 'forbidden' : 'auth_required',
|
||||
'login_url' => '/auth/login',
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once $projectRoot . '/modules/fx-rates/bootstrap.php';
|
||||
|
||||
try {
|
||||
$service = module_fn('fx-rates', 'service');
|
||||
(new \Modules\FxRates\Api\Router($service))->handle($apiMatches[1] ?? '');
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode([
|
||||
'error' => 'Unerwarteter FX-Module Fehler.',
|
||||
'context' => ['message' => $exception->getMessage()],
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('~^module-assets/([a-zA-Z0-9_-]+)/(.*)$~', $uriPath, $assetMatches)) {
|
||||
$module = $assetMatches[1];
|
||||
$relativeAssetPath = trim($assetMatches[2], '/');
|
||||
if ($relativeAssetPath === '' || str_contains($relativeAssetPath, '..')) {
|
||||
http_response_code(400);
|
||||
exit('Bad request');
|
||||
}
|
||||
|
||||
$assetFile = $projectRoot . '/modules/' . $module . '/assets/' . $relativeAssetPath;
|
||||
if (!is_file($assetFile)) {
|
||||
http_response_code(404);
|
||||
exit('Asset not found');
|
||||
}
|
||||
|
||||
$extension = strtolower(pathinfo($assetFile, PATHINFO_EXTENSION));
|
||||
$contentType = match ($extension) {
|
||||
'css' => 'text/css; charset=utf-8',
|
||||
'js' => 'application/javascript; charset=utf-8',
|
||||
'json' => 'application/json; charset=utf-8',
|
||||
'png' => 'image/png',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'webp' => 'image/webp',
|
||||
'svg' => 'image/svg+xml',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
|
||||
header('Content-Type: ' . $contentType);
|
||||
readfile($assetFile);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Basispfad fuer Landingpages
|
||||
$pagesBase = realpath(__DIR__ . '/../partials/landingpages') ?: (__DIR__ . '/../partials/landingpages');
|
||||
$page404 = $pagesBase . '/errorpages/404.php';
|
||||
|
||||
// Spezialrouten für Module
|
||||
if (str_starts_with($uriPath, 'modules/install')) {
|
||||
$target = $pagesBase . '/modules/install.php';
|
||||
} elseif (str_starts_with($uriPath, 'modules/setup/')) {
|
||||
$setupPath = trim(substr($uriPath, strlen('modules/setup/')), '/');
|
||||
$setupParts = $setupPath === '' ? [] : explode('/', $setupPath, 2);
|
||||
$_GET['module'] = trim((string) ($setupParts[0] ?? ''));
|
||||
if (isset($setupParts[1]) && trim((string) $setupParts[1]) !== '') {
|
||||
$_GET['section'] = trim((string) $setupParts[1]);
|
||||
}
|
||||
$target = $pagesBase . '/modules/setup.php';
|
||||
} elseif (str_starts_with($uriPath, 'modules/access/')) {
|
||||
$_GET['module'] = trim(substr($uriPath, strlen('modules/access/')), '/');
|
||||
$target = $pagesBase . '/modules/access.php';
|
||||
} elseif ($uriPath === 'modules/sql-import') {
|
||||
$target = $pagesBase . '/modules/sql_import.php';
|
||||
} elseif ($uriPath === 'auth/login') {
|
||||
$target = $pagesBase . '/auth/login.php';
|
||||
} elseif ($uriPath === 'auth/callback') {
|
||||
$target = $pagesBase . '/auth/callback.php';
|
||||
} elseif ($uriPath === 'auth/logout') {
|
||||
$target = $pagesBase . '/auth/logout.php';
|
||||
} elseif ($uriPath === 'settings') {
|
||||
$target = $pagesBase . '/users/settings.php';
|
||||
} elseif ($uriPath === 'settings/widgets') {
|
||||
$target = $pagesBase . '/users/settings_widgets.php';
|
||||
} elseif ($uriPath === 'settings/search-engines') {
|
||||
$target = $pagesBase . '/users/settings_search_engines.php';
|
||||
} elseif ($uriPath === 'settings/apps') {
|
||||
$target = $pagesBase . '/users/settings_apps.php';
|
||||
} elseif ($uriPath === 'users') {
|
||||
$target = $pagesBase . '/users/index.php';
|
||||
} elseif ($uriPath === 'dashboard') {
|
||||
$target = $pagesBase . '/dashboard.php';
|
||||
} elseif ($uriPath === 'dashboards') {
|
||||
$target = $pagesBase . '/dashboards.php';
|
||||
} elseif ($uriPath === 'integrations') {
|
||||
$target = $pagesBase . '/integrations.php';
|
||||
} elseif ($uriPath === 'page-modules') {
|
||||
$target = $pagesBase . '/page_modules.php';
|
||||
} elseif (preg_match('~^page-modules/view/(\d+)$~', $uriPath, $pageModuleMatch)) {
|
||||
$_GET['id'] = (string) $pageModuleMatch[1];
|
||||
$target = $pagesBase . '/page_modules_view.php';
|
||||
} elseif ($uriPath === 'debug') {
|
||||
$target = $pagesBase . '/retool/debug.php';
|
||||
} elseif (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
|
||||
$module = $m[1];
|
||||
$page = isset($m[2]) && $m[2] !== '' ? trim($m[2], '/') : 'index';
|
||||
$moduleMeta = app()->modules()->get($module);
|
||||
if ($moduleMeta !== null) {
|
||||
$auth->requireModuleAccess($moduleMeta);
|
||||
}
|
||||
$modulePage = app()->modules()->resolvePage($module, $page);
|
||||
$moduleBootstrap = $projectRoot . '/modules/' . $module . '/bootstrap.php';
|
||||
if (is_file($moduleBootstrap)) {
|
||||
require_once $moduleBootstrap;
|
||||
}
|
||||
if ($modulePage) {
|
||||
$target = $modulePage;
|
||||
} else {
|
||||
http_response_code(404);
|
||||
$target = $page404;
|
||||
}
|
||||
} elseif ($uriPath === '' || $uriPath === 'index' || $uriPath === 'index.php') {
|
||||
$target = $pagesBase . '/index.php';
|
||||
} else {
|
||||
$base = $pagesBase . '/' . $uriPath;
|
||||
// 1) Verzeichnis mit index.php
|
||||
if (is_dir($base) && is_file($base . '/index.php')) {
|
||||
$target = $base . '/index.php';
|
||||
}
|
||||
// 2) Datei
|
||||
elseif (is_file($base . '.php')) {
|
||||
|
||||
$target = $base . '.php';
|
||||
}
|
||||
// 3) 404
|
||||
elseif (is_file($base)) {
|
||||
|
||||
$target = $base;
|
||||
}
|
||||
// 3) 404
|
||||
else {
|
||||
http_response_code(404);
|
||||
$target = $page404;
|
||||
}
|
||||
}
|
||||
// ------------------------------------
|
||||
// Layout-Regel
|
||||
// ------------------------------------
|
||||
$skipLayout = false;
|
||||
$targetReal = realpath($target);
|
||||
$retoolBase = realpath($pagesBase . '/retool/raw');
|
||||
|
||||
// Beispiel: alles unter landingpages/retool/* ohne Layout
|
||||
if ($targetReal && $retoolBase && str_starts_with($targetReal, $retoolBase)) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
|
||||
// ------------------------------------
|
||||
// Ausgabe
|
||||
// ------------------------------------
|
||||
// Erst Inhalt laden (ohne Ausgabe), damit Header/Redirects vor HTML funktionieren
|
||||
ob_start();
|
||||
try {
|
||||
require $target;
|
||||
$content = ob_get_clean();
|
||||
} catch (\App\ModuleConfigException $e) {
|
||||
ob_end_clean();
|
||||
http_response_code(412);
|
||||
$moduleName = $e->module();
|
||||
$module = app()->modules()->get($moduleName);
|
||||
$title = $module['title'] ?? $moduleName;
|
||||
$setupUrl = '/modules/setup/' . rawurlencode($moduleName);
|
||||
$content = '<div class="card">' .
|
||||
'<div class="pill">' . e($title) . '</div>' .
|
||||
'<h1 style="margin-top:.75rem;">Setup erforderlich</h1>' .
|
||||
'<p class="muted">' . e($e->getMessage()) . '</p>' .
|
||||
'<div style="margin-top:1rem;"><a class="nav-link" href="' . e($setupUrl) . '">Zum Setup</a></div>' .
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Wenn bereits Header gesendet wurden (z. B. eigener Redirect/Content-Type), Layout überspringen
|
||||
if (headers_sent()) {
|
||||
$skipLayout = true;
|
||||
}
|
||||
|
||||
if (!$skipLayout) {
|
||||
tpl('layout_start', 'structure');
|
||||
}
|
||||
|
||||
echo $content;
|
||||
|
||||
if (!$skipLayout) {
|
||||
tpl('layout_end', 'structure');
|
||||
}
|
||||
Reference in New Issue
Block a user