security: clean repository without media files and sensitive data

- Removed area/ directory with 816MB of media files
- Removed sensitive FTP credentials from Git history
- Implemented .env.upload system for secure deployments
- Added comprehensive .gitignore for future protection

This commit represents a clean slate with all sensitive data removed.
This commit is contained in:
Johannes
2025-09-07 11:05:29 +02:00
commit b4758b4f26
61 changed files with 23829 additions and 0 deletions

27
backend/public/.htaccess Executable file
View File

@@ -0,0 +1,27 @@
RewriteEngine On
# API Routes zu index.php weiterleiten
RewriteCond %{REQUEST_URI} ^/api/
RewriteRule ^(.*)$ index.php [QSA,L]
# React Router: Alle anderen Requests zu index.html (außer existierende Dateien)
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !^/area/
RewriteRule . index.html [L]
# Optional: Gzip Komprimierung
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
</IfModule>
# Optional: Browser Caching
<IfModule mod_expires.c>
ExpiresActive on
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
</IfModule>

46
backend/public/.nginx Normal file
View File

@@ -0,0 +1,46 @@
# nginx Konfiguration für React + PHP Backend
# Diese Datei sollte als .nginx im public/ Verzeichnis liegen
# API-Routen an PHP weiterleiten
location /api/ {
try_files $uri /index.php?$args;
}
# Area-Dateien an PHP weiterleiten (für statische Asset-Auslieferung)
location /area/ {
try_files $uri /index.php?$args;
}
# Statische React Assets direkt ausliefern
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Access-Control-Allow-Origin "*";
try_files $uri =404;
}
# React Assets (JSON, ICO, etc.)
location ~* \.(json|ico|txt)$ {
expires 1d;
add_header Access-Control-Allow-Origin "*";
try_files $uri =404;
}
# PHP-Dateien verarbeiten
location ~ \.php$ {
include fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files $uri =404;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
fastcgi_param HTTPS "on";
fastcgi_param SERVER_PORT 443;
fastcgi_pass 127.0.0.1:9000;
}
# React SPA Fallback - alle anderen Routen auf index.html weiterleiten
location / {
try_files $uri $uri/ /index.html;
}

View File

@@ -0,0 +1,13 @@
{
"files": {
"main.css": "/static/css/main.7b221a61.css",
"main.js": "/static/js/main.f30d7548.js",
"index.html": "/index.html",
"main.7b221a61.css.map": "/static/css/main.7b221a61.css.map",
"main.f30d7548.js.map": "/static/js/main.f30d7548.js.map"
},
"entrypoints": [
"static/css/main.7b221a61.css",
"static/js/main.f30d7548.js"
]
}

17
backend/public/files.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
$dir = __DIR__ . '/../area/Paramount/Testprojekt/ads';
$rootPath = realpath($dir);
if (is_dir($dir) && is_readable($dir)) {
$files = scandir($dir);
echo json_encode([
'success' => true,
'files' => $files,
'rootPath' => $rootPath
]);
} else {
echo json_encode([
'success' => false,
'error' => 'Ordner nicht vorhanden oder keine Leserechte',
'rootPath' => $rootPath
]);
}

View File

@@ -0,0 +1 @@
<!doctype html><html lang="de"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>adspreview</title><script defer="defer" src="/static/js/main.f30d7548.js"></script><link href="/static/css/main.7b221a61.css" rel="stylesheet"></head><body><div id="root"></div></body></html>

33
backend/public/index.php Executable file
View File

@@ -0,0 +1,33 @@
<?php
// CORS-Header für alle API-Routen setzen
if (preg_match('#^/api/#', $_SERVER['REQUEST_URI'])) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
}
// Statische Auslieferung von /area/...
if (preg_match('#^/area/#', $_SERVER['REQUEST_URI'])) {
$relPath = str_replace('/area', '', parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
// URL-decode für Sonderzeichen wie +
$relPath = urldecode($relPath);
$file = realpath(__DIR__ . '/../../area' . $relPath);
$base = realpath(__DIR__ . '/../../area');
if ($file && is_file($file) && strpos($file, $base) === 0) {
$mime = mime_content_type($file);
header('Access-Control-Allow-Origin: *');
header('Content-Type: ' . $mime);
readfile($file);
exit;
}
}
// Einstiegspunkt für die PHP-API
require_once __DIR__ . '/../src/Core/Application.php';
$app = new Application();
$app->run();

View File

@@ -0,0 +1,2 @@
body,html{height:100%;width:100%}input::-ms-clear,input::-ms-reveal{display:none}*,:after,:before{box-sizing:border-box}html{-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0);font-family:sans-serif;line-height:1.15}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:initial;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{font-weight:500;margin-bottom:.5em;margin-top:0}p{margin-bottom:1em;margin-top:0}abbr[data-original-title],abbr[title]{border-bottom:0;cursor:help;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}address{font-style:normal;line-height:inherit;margin-bottom:1em}input[type=number],input[type=password],input[type=text],textarea{-webkit-appearance:none}dl,ol,ul{margin-bottom:1em;margin-top:0}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}code,kbd,pre,samp{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:1em}pre{margin-bottom:1em;margin-top:0;overflow:auto}figure{margin:0 0 1em}img{border-style:none;vertical-align:middle}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{caption-side:bottom;padding-bottom:.3em;padding-top:.75em;text-align:left}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:inherit;line-height:inherit;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{border:0;margin:0;min-width:0;padding:0}legend{color:inherit;display:block;font-size:1.5em;line-height:inherit;margin-bottom:.5em;max-width:100%;padding:0;white-space:normal;width:100%}progress{vertical-align:initial}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:none;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{background-color:#feffe6;padding:.2em}.ant-tabs{border-radius:0;padding:0}.ant-tabs>.ant-tabs-nav{margin-bottom:0;padding:9px 32px;position:sticky;top:0;z-index:100}.ant-tabs-content{overflow:auto;padding:0 32px}
/*# sourceMappingURL=main.7b221a61.css.map*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,157 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @remix-run/router v1.19.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router DOM v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.26.2
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**!
* Sortable 1.15.6
* @author RubaXa <trash@rubaxa.org>
* @author owenm <owen23355@gmail.com>
* @license MIT
*/
/**![arrow-left](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg3MiA0NzRIMjg2LjlsMzUwLjItMzA0YzUuNi00LjkgMi4yLTE0LTUuMi0xNGgtODguNWMtMy45IDAtNy42IDEuNC0xMC41IDMuOUwxNTUgNDg3LjhhMzEuOTYgMzEuOTYgMCAwMDAgNDguM0w1MzUuMSA4NjZjMS41IDEuMyAzLjMgMiA1LjIgMmg5MS41YzcuNCAwIDEwLjgtOS4yIDUuMi0xNEwyODYuOSA1NTBIODcyYzQuNCAwIDgtMy42IDgtOHYtNjBjMC00LjQtMy42LTgtOC04eiIgLz48L3N2Zz4=) */
/**![bars](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkxMiAxOTJIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHptMCAyODRIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHptMCAyODRIMzI4Yy00LjQgMC04IDMuNi04IDh2NTZjMCA0LjQgMy42IDggOCA4aDU4NGM0LjQgMCA4LTMuNiA4LTh2LTU2YzAtNC40LTMuNi04LTgtOHpNMTA0IDIyOGE1NiA1NiAwIDEwMTEyIDAgNTYgNTYgMCAxMC0xMTIgMHptMCAyODRhNTYgNTYgMCAxMDExMiAwIDU2IDU2IDAgMTAtMTEyIDB6bTAgMjg0YTU2IDU2IDAgMTAxMTIgMCA1NiA1NiAwIDEwLTExMiAweiIgLz48L3N2Zz4=) */
/**![check-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0xOTMuNSAzMDEuN2wtMjEwLjYgMjkyYTMxLjggMzEuOCAwIDAxLTUxLjcgMEwzMTguNSA0ODQuOWMtMy44LTUuMyAwLTEyLjcgNi41LTEyLjdoNDYuOWMxMC4yIDAgMTkuOSA0LjkgMjUuOSAxMy4zbDcxLjIgOTguOCAxNTcuMi0yMThjNi04LjMgMTUuNi0xMy4zIDI1LjktMTMuM0g2OTljNi41IDAgMTAuMyA3LjQgNi41IDEyLjd6IiAvPjwvc3ZnPg==) */
/**![check](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkxMiAxOTBoLTY5LjljLTkuOCAwLTE5LjEgNC41LTI1LjEgMTIuMkw0MDQuNyA3MjQuNSAyMDcgNDc0YTMyIDMyIDAgMDAtMjUuMS0xMi4ySDExMmMtNi43IDAtMTAuNCA3LjctNi4zIDEyLjlsMjczLjkgMzQ3YzEyLjggMTYuMiAzNy40IDE2LjIgNTAuMyAwbDQ4OC40LTYxOC45YzQuMS01LjEuNC0xMi44LTYuMy0xMi44eiIgLz48L3N2Zz4=) */
/**![close-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdmlld0JveD0iNjQgNjQgODk2IDg5NiIgZm9jdXNhYmxlPSJmYWxzZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNTEyIDY0YzI0Ny40IDAgNDQ4IDIwMC42IDQ0OCA0NDhTNzU5LjQgOTYwIDUxMiA5NjAgNjQgNzU5LjQgNjQgNTEyIDI2NC42IDY0IDUxMiA2NHptMTI3Ljk4IDI3NC44MmgtLjA0bC0uMDguMDZMNTEyIDQ2Ni43NSAzODQuMTQgMzM4Ljg4Yy0uMDQtLjA1LS4wNi0uMDYtLjA4LS4wNmEuMTIuMTIgMCAwMC0uMDcgMGMtLjAzIDAtLjA1LjAxLS4wOS4wNWwtNDUuMDIgNDUuMDJhLjIuMiAwIDAwLS4wNS4wOS4xMi4xMiAwIDAwMCAuMDd2LjAyYS4yNy4yNyAwIDAwLjA2LjA2TDQ2Ni43NSA1MTIgMzM4Ljg4IDYzOS44NmMtLjA1LjA0LS4wNi4wNi0uMDYuMDhhLjEyLjEyIDAgMDAwIC4wN2MwIC4wMy4wMS4wNS4wNS4wOWw0NS4wMiA0NS4wMmEuMi4yIDAgMDAuMDkuMDUuMTIuMTIgMCAwMC4wNyAwYy4wMiAwIC4wNC0uMDEuMDgtLjA1TDUxMiA1NTcuMjVsMTI3Ljg2IDEyNy44N2MuMDQuMDQuMDYuMDUuMDguMDVhLjEyLjEyIDAgMDAuMDcgMGMuMDMgMCAuMDUtLjAxLjA5LS4wNWw0NS4wMi00NS4wMmEuMi4yIDAgMDAuMDUtLjA5LjEyLjEyIDAgMDAwLS4wN3YtLjAyYS4yNy4yNyAwIDAwLS4wNS0uMDZMNTU3LjI1IDUxMmwxMjcuODctMTI3Ljg2Yy4wNC0uMDQuMDUtLjA2LjA1LS4wOGEuMTIuMTIgMCAwMDAtLjA3YzAtLjAzLS4wMS0uMDUtLjA1LS4wOWwtNDUuMDItNDUuMDJhLjIuMiAwIDAwLS4wOS0uMDUuMTIuMTIgMCAwMC0uMDcgMHoiIC8+PC9zdmc+) */
/**![close](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdmlld0JveD0iNjQgNjQgODk2IDg5NiIgZm9jdXNhYmxlPSJmYWxzZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNzk5Ljg2IDE2Ni4zMWMuMDIgMCAuMDQuMDIuMDguMDZsNTcuNjkgNTcuN2MuMDQuMDMuMDUuMDUuMDYuMDhhLjEyLjEyIDAgMDEwIC4wNmMwIC4wMy0uMDIuMDUtLjA2LjA5TDU2OS45MyA1MTJsMjg3LjcgMjg3LjdjLjA0LjA0LjA1LjA2LjA2LjA5YS4xMi4xMiAwIDAxMCAuMDdjMCAuMDItLjAyLjA0LS4wNi4wOGwtNTcuNyA1Ny42OWMtLjAzLjA0LS4wNS4wNS0uMDcuMDZhLjEyLjEyIDAgMDEtLjA3IDBjLS4wMyAwLS4wNS0uMDItLjA5LS4wNkw1MTIgNTY5LjkzbC0yODcuNyAyODcuN2MtLjA0LjA0LS4wNi4wNS0uMDkuMDZhLjEyLjEyIDAgMDEtLjA3IDBjLS4wMiAwLS4wNC0uMDItLjA4LS4wNmwtNTcuNjktNTcuN2MtLjA0LS4wMy0uMDUtLjA1LS4wNi0uMDdhLjEyLjEyIDAgMDEwLS4wN2MwLS4wMy4wMi0uMDUuMDYtLjA5TDQ1NC4wNyA1MTJsLTI4Ny43LTI4Ny43Yy0uMDQtLjA0LS4wNS0uMDYtLjA2LS4wOWEuMTIuMTIgMCAwMTAtLjA3YzAtLjAyLjAyLS4wNC4wNi0uMDhsNTcuNy01Ny42OWMuMDMtLjA0LjA1LS4wNS4wNy0uMDZhLjEyLjEyIDAgMDEuMDcgMGMuMDMgMCAuMDUuMDIuMDkuMDZMNTEyIDQ1NC4wN2wyODcuNy0yODcuN2MuMDQtLjA0LjA2LS4wNS4wOS0uMDZhLjEyLjEyIDAgMDEuMDcgMHoiIC8+PC9zdmc+) */
/**![copy](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTgzMiA2NEgyOTZjLTQuNCAwLTggMy42LTggOHY1NmMwIDQuNCAzLjYgOCA4IDhoNDk2djY4OGMwIDQuNCAzLjYgOCA4IDhoNTZjNC40IDAgOC0zLjYgOC04Vjk2YzAtMTcuNy0xNC4zLTMyLTMyLTMyek03MDQgMTkySDE5MmMtMTcuNyAwLTMyIDE0LjMtMzIgMzJ2NTMwLjdjMCA4LjUgMy40IDE2LjYgOS40IDIyLjZsMTczLjMgMTczLjNjMi4yIDIuMiA0LjcgNCA3LjQgNS41djEuOWg0LjJjMy41IDEuMyA3LjIgMiAxMSAySDcwNGMxNy43IDAgMzItMTQuMyAzMi0zMlYyMjRjMC0xNy43LTE0LjMtMzItMzItMzJ6TTM1MCA4NTYuMkwyNjMuOSA3NzBIMzUwdjg2LjJ6TTY2NCA4ODhINDE0Vjc0NmMwLTIyLjEtMTcuOS00MC00MC00MEgyMzJWMjY0aDQzMnY2MjR6IiAvPjwvc3ZnPg==) */
/**![edit](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTI1Ny43IDc1MmMyIDAgNC0uMiA2LS41TDQzMS45IDcyMmMyLS40IDMuOS0xLjMgNS4zLTIuOGw0MjMuOS00MjMuOWE5Ljk2IDkuOTYgMCAwMDAtMTQuMUw2OTQuOSAxMTQuOWMtMS45LTEuOS00LjQtMi45LTcuMS0yLjlzLTUuMiAxLTcuMSAyLjlMMjU2LjggNTM4LjhjLTEuNSAxLjUtMi40IDMuMy0yLjggNS4zbC0yOS41IDE2OC4yYTMzLjUgMzMuNSAwIDAwOS40IDI5LjhjNi42IDYuNCAxNC45IDkuOSAyMy44IDkuOXptNjcuNC0xNzQuNEw2ODcuOCAyMTVsNzMuMyA3My4zLTM2Mi43IDM2Mi42LTg4LjkgMTUuNyAxNS42LTg5ek04ODAgODM2SDE0NGMtMTcuNyAwLTMyIDE0LjMtMzIgMzJ2MzZjMCA0LjQgMy42IDggOCA4aDc4NGM0LjQgMCA4LTMuNiA4LTh2LTM2YzAtMTcuNy0xNC4zLTMyLTMyLTMyeiIgLz48L3N2Zz4=) */
/**![ellipsis](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE3NiA1MTFhNTYgNTYgMCAxMDExMiAwIDU2IDU2IDAgMTAtMTEyIDB6bTI4MCAwYTU2IDU2IDAgMTAxMTIgMCA1NiA1NiAwIDEwLTExMiAwem0yODAgMGE1NiA1NiAwIDEwMTEyIDAgNTYgNTYgMCAxMC0xMTIgMHoiIC8+PC9zdmc+) */
/**![enter](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg2NCAxNzBoLTYwYy00LjQgMC04IDMuNi04IDh2NTE4SDMxMHYtNzNjMC02LjctNy44LTEwLjUtMTMtNi4zbC0xNDEuOSAxMTJhOCA4IDAgMDAwIDEyLjZsMTQxLjkgMTEyYzUuMyA0LjIgMTMgLjQgMTMtNi4zdi03NWg0OThjMzUuMyAwIDY0LTI4LjcgNjQtNjRWMTc4YzAtNC40LTMuNi04LTgtOHoiIC8+PC9zdmc+) */
/**![exclamation-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0tMzIgMjMyYzAtNC40IDMuNi04IDgtOGg0OGM0LjQgMCA4IDMuNiA4IDh2MjcyYzAgNC40LTMuNiA4LTggOGgtNDhjLTQuNCAwLTgtMy42LTgtOFYyOTZ6bTMyIDQ0MGE0OC4wMSA0OC4wMSAwIDAxMC05NiA0OC4wMSA0OC4wMSAwIDAxMCA5NnoiIC8+PC9zdmc+) */
/**![eye-invisible](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTk0Mi4yIDQ4Ni4yUTg4OS40NyAzNzUuMTEgODE2LjcgMzA1bC01MC44OCA1MC44OEM4MDcuMzEgMzk1LjUzIDg0My40NSA0NDcuNCA4NzQuNyA1MTIgNzkxLjUgNjg0LjIgNjczLjQgNzY2IDUxMiA3NjZxLTcyLjY3IDAtMTMzLjg3LTIyLjM4TDMyMyA3OTguNzVRNDA4IDgzOCA1MTIgODM4cTI4OC4zIDAgNDMwLjItMzAwLjNhNjAuMjkgNjAuMjkgMCAwMDAtNTEuNXptLTYzLjU3LTMyMC42NEw4MzYgMTIyLjg4YTggOCAwIDAwLTExLjMyIDBMNzE1LjMxIDIzMi4yUTYyNC44NiAxODYgNTEyIDE4NnEtMjg4LjMgMC00MzAuMiAzMDAuM2E2MC4zIDYwLjMgMCAwMDAgNTEuNXE1Ni42OSAxMTkuNCAxMzYuNSAxOTEuNDFMMTEyLjQ4IDgzNWE4IDggMCAwMDAgMTEuMzFMMTU1LjE3IDg4OWE4IDggMCAwMDExLjMxIDBsNzEyLjE1LTcxMi4xMmE4IDggMCAwMDAtMTEuMzJ6TTE0OS4zIDUxMkMyMzIuNiAzMzkuOCAzNTAuNyAyNTggNTEyIDI1OGM1NC41NCAwIDEwNC4xMyA5LjM2IDE0OS4xMiAyOC4zOWwtNzAuMyA3MC4zYTE3NiAxNzYgMCAwMC0yMzguMTMgMjM4LjEzbC04My40MiA4My40MkMyMjMuMSA2MzcuNDkgMTgzLjMgNTgyLjI4IDE0OS4zIDUxMnptMjQ2LjcgMGExMTIuMTEgMTEyLjExIDAgMDExNDYuMi0xMDYuNjlMNDAxLjMxIDU0Ni4yQTExMiAxMTIgMCAwMTM5NiA1MTJ6IiAvPjxwYXRoIGQ9Ik01MDggNjI0Yy0zLjQ2IDAtNi44Ny0uMTYtMTAuMjUtLjQ3bC01Mi44MiA1Mi44MmExNzYuMDkgMTc2LjA5IDAgMDAyMjcuNDItMjI3LjQybC01Mi44MiA1Mi44MmMuMzEgMy4zOC40NyA2Ljc5LjQ3IDEwLjI1YTExMS45NCAxMTEuOTQgMCAwMS0xMTIgMTEyeiIgLz48L3N2Zz4=) */
/**![eye](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTk0Mi4yIDQ4Ni4yQzg0Ny40IDI4Ni41IDcwNC4xIDE4NiA1MTIgMTg2Yy0xOTIuMiAwLTMzNS40IDEwMC41LTQzMC4yIDMwMC4zYTYwLjMgNjAuMyAwIDAwMCA1MS41QzE3Ni42IDczNy41IDMxOS45IDgzOCA1MTIgODM4YzE5Mi4yIDAgMzM1LjQtMTAwLjUgNDMwLjItMzAwLjMgNy43LTE2LjIgNy43LTM1IDAtNTEuNXpNNTEyIDc2NmMtMTYxLjMgMC0yNzkuNC04MS44LTM2Mi43LTI1NEMyMzIuNiAzMzkuOCAzNTAuNyAyNTggNTEyIDI1OGMxNjEuMyAwIDI3OS40IDgxLjggMzYyLjcgMjU0Qzc5MS41IDY4NC4yIDY3My40IDc2NiA1MTIgNzY2em0tNC00MzBjLTk3LjIgMC0xNzYgNzguOC0xNzYgMTc2czc4LjggMTc2IDE3NiAxNzYgMTc2LTc4LjggMTc2LTE3Ni03OC44LTE3Ni0xNzYtMTc2em0wIDI4OGMtNjEuOSAwLTExMi01MC4xLTExMi0xMTJzNTAuMS0xMTIgMTEyLTExMiAxMTIgNTAuMSAxMTIgMTEyLTUwLjEgMTEyLTExMiAxMTJ6IiAvPjwvc3ZnPg==) */
/**![info-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0zMiA2NjRjMCA0LjQtMy42IDgtOCA4aC00OGMtNC40IDAtOC0zLjYtOC04VjQ1NmMwLTQuNCAzLjYtOCA4LThoNDhjNC40IDAgOCAzLjYgOCA4djI3MnptLTMyLTM0NGE0OC4wMSA0OC4wMSAwIDAxMC05NiA0OC4wMSA0OC4wMSAwIDAxMCA5NnoiIC8+PC9zdmc+) */
/**![left](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcyNCAyMTguM1YxNDFjMC02LjctNy43LTEwLjQtMTIuOS02LjNMMjYwLjMgNDg2LjhhMzEuODYgMzEuODYgMCAwMDAgNTAuM2w0NTAuOCAzNTIuMWM1LjMgNC4xIDEyLjkuNCAxMi45LTYuM3YtNzcuM2MwLTQuOS0yLjMtOS42LTYuMS0xMi42bC0zNjAtMjgxIDM2MC0yODEuMWMzLjgtMyA2LjEtNy43IDYuMS0xMi42eiIgLz48L3N2Zz4=) */
/**![loading](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTk4OCA1NDhjLTE5LjkgMC0zNi0xNi4xLTM2LTM2IDAtNTkuNC0xMS42LTExNy0zNC42LTE3MS4zYTQ0MC40NSA0NDAuNDUgMCAwMC05NC4zLTEzOS45IDQzNy43MSA0MzcuNzEgMCAwMC0xMzkuOS05NC4zQzYyOSA4My42IDU3MS40IDcyIDUxMiA3MmMtMTkuOSAwLTM2LTE2LjEtMzYtMzZzMTYuMS0zNiAzNi0zNmM2OS4xIDAgMTM2LjIgMTMuNSAxOTkuMyA0MC4zQzc3Mi4zIDY2IDgyNyAxMDMgODc0IDE1MGM0NyA0NyA4My45IDEwMS44IDEwOS43IDE2Mi43IDI2LjcgNjMuMSA0MC4yIDEzMC4yIDQwLjIgMTk5LjMuMSAxOS45LTE2IDM2LTM1LjkgMzZ6IiAvPjwvc3ZnPg==) */
/**![lock](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTgzMiA0NjRoLTY4VjI0MGMwLTcwLjctNTcuMy0xMjgtMTI4LTEyOEgzODhjLTcwLjcgMC0xMjggNTcuMy0xMjggMTI4djIyNGgtNjhjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjM4NGMwIDE3LjcgMTQuMyAzMiAzMiAzMmg2NDBjMTcuNyAwIDMyLTE0LjMgMzItMzJWNDk2YzAtMTcuNy0xNC4zLTMyLTMyLTMyek0zMzIgMjQwYzAtMzAuOSAyNS4xLTU2IDU2LTU2aDI0OGMzMC45IDAgNTYgMjUuMSA1NiA1NnYyMjRIMzMyVjI0MHptNDYwIDYwMEgyMzJWNTM2aDU2MHYzMDR6TTQ4NCA3MDF2NTNjMCA0LjQgMy42IDggOCA4aDQwYzQuNCAwIDgtMy42IDgtOHYtNTNhNDguMDEgNDguMDEgMCAxMC01NiAweiIgLz48L3N2Zz4=) */
/**![logout](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg2OCA3MzJoLTcwLjNjLTQuOCAwLTkuMyAyLjEtMTIuMyA1LjgtNyA4LjUtMTQuNSAxNi43LTIyLjQgMjQuNWEzNTMuODQgMzUzLjg0IDAgMDEtMTEyLjcgNzUuOUEzNTIuOCAzNTIuOCAwIDAxNTEyLjQgODY2Yy00Ny45IDAtOTQuMy05LjQtMTM3LjktMjcuOGEzNTMuODQgMzUzLjg0IDAgMDEtMTEyLjctNzUuOSAzNTMuMjggMzUzLjI4IDAgMDEtNzYtMTEyLjVDMTY3LjMgNjA2LjIgMTU4IDU1OS45IDE1OCA1MTJzOS40LTk0LjIgMjcuOC0xMzcuOGMxNy44LTQyLjEgNDMuNC04MCA3Ni0xMTIuNXM3MC41LTU4LjEgMTEyLjctNzUuOWM0My42LTE4LjQgOTAtMjcuOCAxMzcuOS0yNy44IDQ3LjkgMCA5NC4zIDkuMyAxMzcuOSAyNy44IDQyLjIgMTcuOCA4MC4xIDQzLjQgMTEyLjcgNzUuOSA3LjkgNy45IDE1LjMgMTYuMSAyMi40IDI0LjUgMyAzLjcgNy42IDUuOCAxMi4zIDUuOEg4NjhjNi4zIDAgMTAuMi03IDYuNy0xMi4zQzc5OCAxNjAuNSA2NjMuOCA4MS42IDUxMS4zIDgyIDI3MS43IDgyLjYgNzkuNiAyNzcuMSA4MiA1MTYuNCA4NC40IDc1MS45IDI3Ni4yIDk0MiA1MTIuNCA5NDJjMTUyLjEgMCAyODUuNy03OC44IDM2Mi4zLTE5Ny43IDMuNC01LjMtLjQtMTIuMy02LjctMTIuM3ptODguOS0yMjYuM0w4MTUgMzkzLjdjLTUuMy00LjItMTMtLjQtMTMgNi4zdjc2SDQ4OGMtNC40IDAtOCAzLjYtOCA4djU2YzAgNC40IDMuNiA4IDggOGgzMTR2NzZjMCA2LjcgNy44IDEwLjUgMTMgNi4zbDE0MS45LTExMmE4IDggMCAwMDAtMTIuNnoiIC8+PC9zdmc+) */
/**![plus](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTQ4MiAxNTJoNjBxOCAwIDggOHY3MDRxMCA4LTggOGgtNjBxLTggMC04LThWMTYwcTAtOCA4LTh6IiAvPjxwYXRoIGQ9Ik0xOTIgNDc0aDY3MnE4IDAgOCA4djYwcTAgOC04IDhIMTYwcS04IDAtOC04di02MHEwLTggOC04eiIgLz48L3N2Zz4=) */
/**![question-circle](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTUxMiA2NEMyNjQuNiA2NCA2NCAyNjQuNiA2NCA1MTJzMjAwLjYgNDQ4IDQ0OCA0NDggNDQ4LTIwMC42IDQ0OC00NDhTNzU5LjQgNjQgNTEyIDY0em0wIDgyMGMtMjA1LjQgMC0zNzItMTY2LjYtMzcyLTM3MnMxNjYuNi0zNzIgMzcyLTM3MiAzNzIgMTY2LjYgMzcyIDM3Mi0xNjYuNiAzNzItMzcyIDM3MnoiIC8+PHBhdGggZD0iTTYyMy42IDMxNi43QzU5My42IDI5MC40IDU1NCAyNzYgNTEyIDI3NnMtODEuNiAxNC41LTExMS42IDQwLjdDMzY5LjIgMzQ0IDM1MiAzODAuNyAzNTIgNDIwdjcuNmMwIDQuNCAzLjYgOCA4IDhoNDhjNC40IDAgOC0zLjYgOC04VjQyMGMwLTQ0LjEgNDMuMS04MCA5Ni04MHM5NiAzNS45IDk2IDgwYzAgMzEuMS0yMiA1OS42LTU2LjEgNzIuNy0yMS4yIDguMS0zOS4yIDIyLjMtNTIuMSA0MC45LTEzLjEgMTktMTkuOSA0MS44LTE5LjkgNjQuOVY2MjBjMCA0LjQgMy42IDggOCA4aDQ4YzQuNCAwIDgtMy42IDgtOHYtMjIuN2E0OC4zIDQ4LjMgMCAwMTMwLjktNDQuOGM1OS0yMi43IDk3LjEtNzQuNyA5Ny4xLTEzMi41LjEtMzkuMy0xNy4xLTc2LTQ4LjMtMTAzLjN6TTQ3MiA3MzJhNDAgNDAgMCAxMDgwIDAgNDAgNDAgMCAxMC04MCAweiIgLz48L3N2Zz4=) */
/**![reload](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkwOS4xIDIwOS4zbC01Ni40IDQ0LjFDNzc1LjggMTU1LjEgNjU2LjIgOTIgNTIxLjkgOTIgMjkwIDkyIDEwMi4zIDI3OS41IDEwMiA1MTEuNSAxMDEuNyA3NDMuNyAyODkuOCA5MzIgNTIxLjkgOTMyYzE4MS4zIDAgMzM1LjgtMTE1IDM5NC42LTI3Ni4xIDEuNS00LjItLjctOC45LTQuOS0xMC4zbC01Ni43LTE5LjVhOCA4IDAgMDAtMTAuMSA0LjhjLTEuOCA1LTMuOCAxMC01LjkgMTQuOS0xNy4zIDQxLTQyLjEgNzcuOC03My43IDEwOS40QTM0NC43NyAzNDQuNzcgMCAwMTY1NS45IDgyOWMtNDIuMyAxNy45LTg3LjQgMjctMTMzLjggMjctNDYuNSAwLTkxLjUtOS4xLTEzMy44LTI3QTM0MS41IDM0MS41IDAgMDEyNzkgNzU1LjJhMzQyLjE2IDM0Mi4xNiAwIDAxLTczLjctMTA5LjRjLTE3LjktNDIuNC0yNy04Ny40LTI3LTEzMy45czkuMS05MS41IDI3LTEzMy45YzE3LjMtNDEgNDIuMS03Ny44IDczLjctMTA5LjQgMzEuNi0zMS42IDY4LjQtNTYuNCAxMDkuMy03My44IDQyLjMtMTcuOSA4Ny40LTI3IDEzMy44LTI3IDQ2LjUgMCA5MS41IDkuMSAxMzMuOCAyN2EzNDEuNSAzNDEuNSAwIDAxMTA5LjMgNzMuOGM5LjkgOS45IDE5LjIgMjAuNCAyNy44IDMxLjRsLTYwLjIgNDdhOCA4IDAgMDAzIDE0LjFsMTc1LjYgNDNjNSAxLjIgOS45LTIuNiA5LjktNy43bC44LTE4MC45Yy0uMS02LjYtNy44LTEwLjMtMTMtNi4yeiIgLz48L3N2Zz4=) */
/**![right](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTc2NS43IDQ4Ni44TDMxNC45IDEzNC43QTcuOTcgNy45NyAwIDAwMzAyIDE0MXY3Ny4zYzAgNC45IDIuMyA5LjYgNi4xIDEyLjZsMzYwIDI4MS4xLTM2MCAyODEuMWMtMy45IDMtNi4xIDcuNy02LjEgMTIuNlY4ODNjMCA2LjcgNy43IDEwLjQgMTIuOSA2LjNsNDUwLjgtMzUyLjFhMzEuOTYgMzEuOTYgMCAwMDAtNTAuNHoiIC8+PC9zdmc+) */
/**![rotate-left](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHN0eWxlIC8+PC9kZWZzPjxwYXRoIGQ9Ik02NzIgNDE4SDE0NGMtMTcuNyAwLTMyIDE0LjMtMzIgMzJ2NDE0YzAgMTcuNyAxNC4zIDMyIDMyIDMyaDUyOGMxNy43IDAgMzItMTQuMyAzMi0zMlY0NTBjMC0xNy43LTE0LjMtMzItMzItMzJ6bS00NCA0MDJIMTg4VjQ5NGg0NDB2MzI2eiIgLz48cGF0aCBkPSJNODE5LjMgMzI4LjVjLTc4LjgtMTAwLjctMTk2LTE1My42LTMxNC42LTE1NC4ybC0uMi02NGMwLTYuNS03LjYtMTAuMS0xMi42LTYuMWwtMTI4IDEwMWMtNCAzLjEtMy45IDkuMSAwIDEyLjNMNDkyIDMxOC42YzUuMSA0IDEyLjcuNCAxMi42LTYuMXYtNjMuOWMxMi45LjEgMjUuOS45IDM4LjggMi41IDQyLjEgNS4yIDgyLjEgMTguMiAxMTkgMzguNyAzOC4xIDIxLjIgNzEuMiA0OS43IDk4LjQgODQuMyAyNy4xIDM0LjcgNDYuNyA3My43IDU4LjEgMTE1LjhhMzI1Ljk1IDMyNS45NSAwIDAxNi41IDE0MC45aDc0LjljMTQuOC0xMDMuNi0xMS4zLTIxMy04MS0zMDIuM3oiIC8+PC9zdmc+) */
/**![rotate-right](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHN0eWxlIC8+PC9kZWZzPjxwYXRoIGQ9Ik00ODAuNSAyNTEuMmMxMy0xLjYgMjUuOS0yLjQgMzguOC0yLjV2NjMuOWMwIDYuNSA3LjUgMTAuMSAxMi42IDYuMUw2NjAgMjE3LjZjNC0zLjIgNC05LjIgMC0xMi4zbC0xMjgtMTAxYy01LjEtNC0xMi42LS40LTEyLjYgNi4xbC0uMiA2NGMtMTE4LjYuNS0yMzUuOCA1My40LTMxNC42IDE1NC4yQTM5OS43NSAzOTkuNzUgMCAwMDEyMy41IDYzMWg3NC45Yy0uOS01LjMtMS43LTEwLjctMi40LTE2LjEtNS4xLTQyLjEtMi4xLTg0LjEgOC45LTEyNC44IDExLjQtNDIuMiAzMS04MS4xIDU4LjEtMTE1LjggMjcuMi0zNC43IDYwLjMtNjMuMiA5OC40LTg0LjMgMzctMjAuNiA3Ni45LTMzLjYgMTE5LjEtMzguOHoiIC8+PHBhdGggZD0iTTg4MCA0MThIMzUyYy0xNy43IDAtMzIgMTQuMy0zMiAzMnY0MTRjMCAxNy43IDE0LjMgMzIgMzIgMzJoNTI4YzE3LjcgMCAzMi0xNC4zIDMyLTMyVjQ1MGMwLTE3LjctMTQuMy0zMi0zMi0zMnptLTQ0IDQwMkgzOTZWNDk0aDQ0MHYzMjZ6IiAvPjwvc3ZnPg==) */
/**![search](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkwOS42IDg1NC41TDY0OS45IDU5NC44QzY5MC4yIDU0Mi43IDcxMiA0NzkgNzEyIDQxMmMwLTgwLjItMzEuMy0xNTUuNC04Ny45LTIxMi4xLTU2LjYtNTYuNy0xMzItODcuOS0yMTIuMS04Ny45cy0xNTUuNSAzMS4zLTIxMi4xIDg3LjlDMTQzLjIgMjU2LjUgMTEyIDMzMS44IDExMiA0MTJjMCA4MC4xIDMxLjMgMTU1LjUgODcuOSAyMTIuMUMyNTYuNSA2ODAuOCAzMzEuOCA3MTIgNDEyIDcxMmM2NyAwIDEzMC42LTIxLjggMTgyLjctNjJsMjU5LjcgMjU5LjZhOC4yIDguMiAwIDAwMTEuNiAwbDQzLjYtNDMuNWE4LjIgOC4yIDAgMDAwLTExLjZ6TTU3MC40IDU3MC40QzUyOCA2MTIuNyA0NzEuOCA2MzYgNDEyIDYzNnMtMTE2LTIzLjMtMTU4LjQtNjUuNkMyMTEuMyA1MjggMTg4IDQ3MS44IDE4OCA0MTJzMjMuMy0xMTYuMSA2NS42LTE1OC40QzI5NiAyMTEuMyAzNTIuMiAxODggNDEyIDE4OHMxMTYuMSAyMy4yIDE1OC40IDY1LjZTNjM2IDM1Mi4yIDYzNiA0MTJzLTIzLjMgMTE2LjEtNjUuNiAxNTguNHoiIC8+PC9zdmc+) */
/**![setting](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkyNC44IDYyNS43bC02NS41LTU2YzMuMS0xOSA0LjctMzguNCA0LjctNTcuOHMtMS42LTM4LjgtNC43LTU3LjhsNjUuNS01NmEzMi4wMyAzMi4wMyAwIDAwOS4zLTM1LjJsLS45LTIuNmE0NDMuNzQgNDQzLjc0IDAgMDAtNzkuNy0xMzcuOWwtMS44LTIuMWEzMi4xMiAzMi4xMiAwIDAwLTM1LjEtOS41bC04MS4zIDI4LjljLTMwLTI0LjYtNjMuNS00NC05OS43LTU3LjZsLTE1LjctODVhMzIuMDUgMzIuMDUgMCAwMC0yNS44LTI1LjdsLTIuNy0uNWMtNTIuMS05LjQtMTA2LjktOS40LTE1OSAwbC0yLjcuNWEzMi4wNSAzMi4wNSAwIDAwLTI1LjggMjUuN2wtMTUuOCA4NS40YTM1MS44NiAzNTEuODYgMCAwMC05OSA1Ny40bC04MS45LTI5LjFhMzIgMzIgMCAwMC0zNS4xIDkuNWwtMS44IDIuMWE0NDYuMDIgNDQ2LjAyIDAgMDAtNzkuNyAxMzcuOWwtLjkgMi42Yy00LjUgMTIuNS0uOCAyNi41IDkuMyAzNS4ybDY2LjMgNTYuNmMtMy4xIDE4LjgtNC42IDM4LTQuNiA1Ny4xIDAgMTkuMiAxLjUgMzguNCA0LjYgNTcuMUw5OSA2MjUuNWEzMi4wMyAzMi4wMyAwIDAwLTkuMyAzNS4ybC45IDIuNmMxOC4xIDUwLjQgNDQuOSA5Ni45IDc5LjcgMTM3LjlsMS44IDIuMWEzMi4xMiAzMi4xMiAwIDAwMzUuMSA5LjVsODEuOS0yOS4xYzI5LjggMjQuNSA2My4xIDQzLjkgOTkgNTcuNGwxNS44IDg1LjRhMzIuMDUgMzIuMDUgMCAwMDI1LjggMjUuN2wyLjcuNWE0NDkuNCA0NDkuNCAwIDAwMTU5IDBsMi43LS41YTMyLjA1IDMyLjA1IDAgMDAyNS44LTI1LjdsMTUuNy04NWEzNTAgMzUwIDAgMDA5OS43LTU3LjZsODEuMyAyOC45YTMyIDMyIDAgMDAzNS4xLTkuNWwxLjgtMi4xYzM0LjgtNDEuMSA2MS42LTg3LjUgNzkuNy0xMzcuOWwuOS0yLjZjNC41LTEyLjMuOC0yNi4zLTkuMy0zNXpNNzg4LjMgNDY1LjljMi41IDE1LjEgMy44IDMwLjYgMy44IDQ2LjFzLTEuMyAzMS0zLjggNDYuMWwtNi42IDQwLjEgNzQuNyA2My45YTM3MC4wMyAzNzAuMDMgMCAwMS00Mi42IDczLjZMNzIxIDcwMi44bC0zMS40IDI1LjhjLTIzLjkgMTkuNi01MC41IDM1LTc5LjMgNDUuOGwtMzguMSAxNC4zLTE3LjkgOTdhMzc3LjUgMzc3LjUgMCAwMS04NSAwbC0xNy45LTk3LjItMzcuOC0xNC41Yy0yOC41LTEwLjgtNTUtMjYuMi03OC43LTQ1LjdsLTMxLjQtMjUuOS05My40IDMzLjJjLTE3LTIyLjktMzEuMi00Ny42LTQyLjYtNzMuNmw3NS41LTY0LjUtNi41LTQwYy0yLjQtMTQuOS0zLjctMzAuMy0zLjctNDUuNSAwLTE1LjMgMS4yLTMwLjYgMy43LTQ1LjVsNi41LTQwLTc1LjUtNjQuNWMxMS4zLTI2LjEgMjUuNi01MC43IDQyLjYtNzMuNmw5My40IDMzLjIgMzEuNC0yNS45YzIzLjctMTkuNSA1MC4yLTM0LjkgNzguNy00NS43bDM3LjktMTQuMyAxNy45LTk3LjJjMjguMS0zLjIgNTYuOC0zLjIgODUgMGwxNy45IDk3IDM4LjEgMTQuM2MyOC43IDEwLjggNTUuNCAyNi4yIDc5LjMgNDUuOGwzMS40IDI1LjggOTIuOC0zMi45YzE3IDIyLjkgMzEuMiA0Ny42IDQyLjYgNzMuNkw3ODEuOCA0MjZsNi41IDM5Ljl6TTUxMiAzMjZjLTk3LjIgMC0xNzYgNzguOC0xNzYgMTc2czc4LjggMTc2IDE3NiAxNzYgMTc2LTc4LjggMTc2LTE3Ni03OC44LTE3Ni0xNzYtMTc2em03OS4yIDI1NS4yQTExMS42IDExMS42IDAgMDE1MTIgNjE0Yy0yOS45IDAtNTgtMTEuNy03OS4yLTMyLjhBMTExLjYgMTExLjYgMCAwMTQwMCA1MDJjMC0yOS45IDExLjctNTggMzIuOC03OS4yQzQ1NCA0MDEuNiA0ODIuMSAzOTAgNTEyIDM5MGMyOS45IDAgNTggMTEuNiA3OS4yIDMyLjhBMTExLjYgMTExLjYgMCAwMTYyNCA1MDJjMCAyOS45LTExLjcgNTgtMzIuOCA3OS4yeiIgLz48L3N2Zz4=) */
/**![swap](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg0Ny45IDU5MkgxNTJjLTQuNCAwLTggMy42LTggOHY2MGMwIDQuNCAzLjYgOCA4IDhoNjA1LjJMNjEyLjkgODUxYy00LjEgNS4yLS40IDEzIDYuMyAxM2g3Mi41YzQuOSAwIDkuNS0yLjIgMTIuNi02LjFsMTY4LjgtMjE0LjFjMTYuNS0yMSAxLjYtNTEuOC0yNS4yLTUxLjh6TTg3MiAzNTZIMjY2LjhsMTQ0LjMtMTgzYzQuMS01LjIuNC0xMy02LjMtMTNoLTcyLjVjLTQuOSAwLTkuNSAyLjItMTIuNiA2LjFMMTUwLjkgMzgwLjJjLTE2LjUgMjEtMS42IDUxLjggMjUuMSA1MS44aDY5NmM0LjQgMCA4LTMuNiA4LTh2LTYwYzAtNC40LTMuNi04LTgtOHoiIC8+PC9zdmc+) */
/**![user](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg1OC41IDc2My42YTM3NCAzNzQgMCAwMC04MC42LTExOS41IDM3NS42MyAzNzUuNjMgMCAwMC0xMTkuNS04MC42Yy0uNC0uMi0uOC0uMy0xLjItLjVDNzE5LjUgNTE4IDc2MCA0NDQuNyA3NjAgMzYyYzAtMTM3LTExMS0yNDgtMjQ4LTI0OFMyNjQgMjI1IDI2NCAzNjJjMCA4Mi43IDQwLjUgMTU2IDEwMi44IDIwMS4xLS40LjItLjguMy0xLjIuNS00NC44IDE4LjktODUgNDYtMTE5LjUgODAuNmEzNzUuNjMgMzc1LjYzIDAgMDAtODAuNiAxMTkuNUEzNzEuNyAzNzEuNyAwIDAwMTM2IDkwMS44YTggOCAwIDAwOCA4LjJoNjBjNC40IDAgNy45LTMuNSA4LTcuOCAyLTc3LjIgMzMtMTQ5LjUgODcuOC0yMDQuMyA1Ni43LTU2LjcgMTMyLTg3LjkgMjEyLjItODcuOXMxNTUuNSAzMS4yIDIxMi4yIDg3LjlDNzc5IDc1Mi43IDgxMCA4MjUgODEyIDkwMi4yYy4xIDQuNCAzLjYgNy44IDggNy44aDYwYTggOCAwIDAwOC04LjJjLTEtNDcuOC0xMC45LTk0LjMtMjkuNS0xMzguMnpNNTEyIDUzNGMtNDUuOSAwLTg5LjEtMTcuOS0xMjEuNi01MC40UzM0MCA0MDcuOSAzNDAgMzYyYzAtNDUuOSAxNy45LTg5LjEgNTAuNC0xMjEuNlM0NjYuMSAxOTAgNTEyIDE5MHM4OS4xIDE3LjkgMTIxLjYgNTAuNFM2ODQgMzE2LjEgNjg0IDM2MmMwIDQ1LjktMTcuOSA4OS4xLTUwLjQgMTIxLjZTNTU3LjkgNTM0IDUxMiA1MzR6IiAvPjwvc3ZnPg==) */
/**![zoom-in](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYzNyA0NDNINTE5VjMwOWMwLTQuNC0zLjYtOC04LThoLTYwYy00LjQgMC04IDMuNi04IDh2MTM0SDMyNWMtNC40IDAtOCAzLjYtOCA4djYwYzAgNC40IDMuNiA4IDggOGgxMTh2MTM0YzAgNC40IDMuNiA4IDggOGg2MGM0LjQgMCA4LTMuNiA4LThWNTE5aDExOGM0LjQgMCA4LTMuNiA4LTh2LTYwYzAtNC40LTMuNi04LTgtOHptMjg0IDQyNEw3NzUgNzIxYzEyMi4xLTE0OC45IDExMy42LTM2OS41LTI2LTUwOS0xNDgtMTQ4LjEtMzg4LjQtMTQ4LjEtNTM3IDAtMTQ4LjEgMTQ4LjYtMTQ4LjEgMzg5IDAgNTM3IDEzOS41IDEzOS42IDM2MC4xIDE0OC4xIDUwOSAyNmwxNDYgMTQ2YzMuMiAyLjggOC4zIDIuOCAxMSAwbDQzLTQzYzIuOC0yLjcgMi44LTcuOCAwLTExek02OTYgNjk2Yy0xMTguOCAxMTguNy0zMTEuMiAxMTguNy00MzAgMC0xMTguNy0xMTguOC0xMTguNy0zMTEuMiAwLTQzMCAxMTguOC0xMTguNyAzMTEuMi0xMTguNyA0MzAgMCAxMTguNyAxMTguOCAxMTguNyAzMTEuMiAwIDQzMHoiIC8+PC9zdmc+) */
/**![zoom-out](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIGZpbGw9IiNjYWNhY2EiIHZpZXdCb3g9IjY0IDY0IDg5NiA4OTYiIGZvY3VzYWJsZT0iZmFsc2UiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYzNyA0NDNIMzI1Yy00LjQgMC04IDMuNi04IDh2NjBjMCA0LjQgMy42IDggOCA4aDMxMmM0LjQgMCA4LTMuNiA4LTh2LTYwYzAtNC40LTMuNi04LTgtOHptMjg0IDQyNEw3NzUgNzIxYzEyMi4xLTE0OC45IDExMy42LTM2OS41LTI2LTUwOS0xNDgtMTQ4LjEtMzg4LjQtMTQ4LjEtNTM3IDAtMTQ4LjEgMTQ4LjYtMTQ4LjEgMzg5IDAgNTM3IDEzOS41IDEzOS42IDM2MC4xIDE0OC4xIDUwOSAyNmwxNDYgMTQ2YzMuMiAyLjggOC4zIDIuOCAxMSAwbDQzLTQzYzIuOC0yLjcgMi44LTcuOCAwLTExek02OTYgNjk2Yy0xMTguOCAxMTguNy0zMTEuMiAxMTguNy00MzAgMC0xMTguNy0xMTguOC0xMTguNy0zMTEuMiAwLTQzMCAxMTguOC0xMTguNyAzMTEuMi0xMTguNyA0MzAgMCAxMTguNyAxMTguOCAxMTguNyAzMTEuMiAwIDQzMHoiIC8+PC9zdmc+) */

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,30 @@
<?php
require_once __DIR__ . '/../Services/AuthService.php';
class AuthController {
public static function login() {
$input = json_decode(file_get_contents('php://input'), true);
$username = $input['username'] ?? null;
$password = $input['password'] ?? null;
$result = AuthService::authenticate($username, $password);
if ($result['success']) {
http_response_code(200);
echo json_encode([
'success' => true,
'token' => $result['token'],
'role' => $result['role'],
'message' => 'Login erfolgreich.'
]);
} else {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => [
'code' => 'AUTH_FAILED',
'message' => $result['message']
]
]);
}
}
}

View File

@@ -0,0 +1,197 @@
<?php
require_once __DIR__ . '/../Services/AuthService.php';
require_once __DIR__ . '/../Services/JsonStorageService.php';
class EntityController {
private static function authorize(string $requiredRole) {
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
if (!$user || strtolower($user['role']) !== strtolower($requiredRole)) {
http_response_code(403);
echo json_encode(['success' => false, 'error' => ['code'=>'FORBIDDEN','message'=>'Nicht berechtigt.']]);
exit;
}
return $user;
}
public static function list(string $type) {
$user = self::authorize('admin');
// Datei-Namen-Mapping: 'users' speichert in admins.json
$storageType = $type === 'users' ? 'admins' : $type;
$items = JsonStorageService::read($storageType);
// Für Client-Listen: Prüfe disallowedClients des aktuellen Admins
if ($type === 'clients') {
$adminData = self::getAdminData($user['username']);
$disallowedClients = $adminData['disallowedClients'] ?? [];
// Filtere nicht erlaubte Clients aus
$filteredItems = [];
foreach ($items as $clientKey => $clientData) {
if (!in_array($clientKey, $disallowedClients)) {
$filteredItems[$clientKey] = $clientData;
}
}
$items = $filteredItems;
}
// Für User-Listen: Entferne Passwörter aus der Response
if ($type === 'users') {
foreach ($items as &$item) {
unset($item['password']); // Passwörter nie an Frontend senden
}
}
http_response_code(200);
echo json_encode(['success'=>true, strtolower($type)=>$items]);
}
// Hilfsfunktion: Admin-Daten laden
private static function getAdminData($username) {
$adminFile = __DIR__ . '/../../storage/data/admins.json';
if (file_exists($adminFile)) {
$admins = json_decode(file_get_contents($adminFile), true);
foreach ($admins as $admin) {
if ($admin['username'] === $username) {
return $admin;
}
}
}
return [];
}
public static function create(string $type) {
self::authorize('admin');
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input) || empty($input['username']) || empty($input['role'])) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>['code'=>'INVALID_INPUT','message'=>'Ungültige Eingabedaten.']]);
return;
}
$storageType = $type === 'users' ? 'admins' : $type;
$items = JsonStorageService::read($storageType);
$newId = empty($items) ? 1 : max(array_column($items,'id'))+1;
$new = [
'id'=>$newId,
'username'=>$input['username'],
'role'=>$input['role'],
'email'=>$input['email']??''
];
// Passwort hashen falls angegeben
if (!empty($input['password'])) {
$new['password'] = password_hash($input['password'], PASSWORD_BCRYPT);
}
// disallowedClients für Admin-Benutzer hinzufügen
if ($input['role'] === 'admin' && isset($input['disallowedClients'])) {
$new['disallowedClients'] = is_array($input['disallowedClients']) ? $input['disallowedClients'] : [];
}
$items[] = $new;
JsonStorageService::write($storageType, $items);
http_response_code(201);
echo json_encode(['success'=>true, strtolower($type)=>$new]);
}
public static function update(string $type, int $id) {
self::authorize('admin');
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input) || empty($input['username']) || empty($input['role'])) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>['code'=>'INVALID_INPUT','message'=>'Ungültige Eingabedaten.']]);
return;
}
$storageType = $type === 'users' ? 'admins' : $type;
$items = JsonStorageService::read($storageType);
$found = false;
foreach ($items as &$item) {
if ($item['id']==$id) {
$item['username']=$input['username'];
$item['role']=$input['role'];
$item['email']=$input['email']??'';
// Passwort aktualisieren falls angegeben
if (!empty($input['password'])) {
$item['password'] = password_hash($input['password'], PASSWORD_BCRYPT);
}
// disallowedClients für Admin-Benutzer aktualisieren
if ($input['role'] === 'admin' && isset($input['disallowedClients'])) {
$item['disallowedClients'] = is_array($input['disallowedClients']) ? $input['disallowedClients'] : [];
} elseif ($input['role'] !== 'admin') {
// Entferne disallowedClients wenn User kein Admin mehr ist
unset($item['disallowedClients']);
}
$found = true;
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['success'=>false,'error'=>['code'=>'NOT_FOUND','message'=>'Eintrag nicht gefunden.']]);
return;
}
JsonStorageService::write($storageType, $items);
http_response_code(200);
echo json_encode(['success'=>true,strtolower($type)=>$item]);
}
public static function delete(string $type, int $id) {
self::authorize('admin');
$storageType = $type === 'users' ? 'admins' : $type;
$items = JsonStorageService::read($storageType);
$found = false;
$out = [];
foreach ($items as $item) {
if ($item['id']==$id) { $found=true; continue; }
$out[]=$item;
}
if (!$found) {
http_response_code(404);
echo json_encode(['success'=>false,'error'=>['code'=>'NOT_FOUND','message'=>'Eintrag nicht gefunden.']]);
return;
}
JsonStorageService::write($storageType, $out);
http_response_code(200);
echo json_encode(['success'=>true]);
}
public static function updatePassword(string $type, int $id) {
self::authorize('admin');
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input) || empty($input['password'])) {
http_response_code(400);
echo json_encode(['success'=>false,'error'=>['code'=>'INVALID_INPUT','message'=>'Passwort ist erforderlich.']]);
return;
}
$storageType = $type === 'users' ? 'admins' : $type;
$items = JsonStorageService::read($storageType);
$found = false;
foreach ($items as &$item) {
if ($item['id']==$id) {
$item['password'] = password_hash($input['password'], PASSWORD_BCRYPT);
$found = true;
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode(['success'=>false,'error'=>['code'=>'NOT_FOUND','message'=>'Benutzer nicht gefunden.']]);
return;
}
JsonStorageService::write($storageType, $items);
http_response_code(200);
echo json_encode(['success'=>true, 'message'=>'Passwort erfolgreich aktualisiert.']);
}
}

View File

@@ -0,0 +1,792 @@
<?php
require_once __DIR__ . '/../../vendor/getid3/getid3.php';
class ProjectsController {
// Bestimmt den korrekten Pfad zum area-Ordner basierend auf der Umgebung
private static function getAreaPath() {
// Prüfe ob wir in der Development-Struktur sind (backend/src/Api)
$devPath = __DIR__ . '/../../../area';
if (is_dir($devPath)) {
return '/../../../area';
}
// Prüfe ob wir in der Deployment-Struktur sind (src/Api)
$deployPath = __DIR__ . '/../../area';
if (is_dir($deployPath)) {
return '/../../area';
}
// Fallback auf Development-Pfad
return '/../../../area';
}
// Zentrale Liste der zu ignorierenden Dateien
private static function getIgnoredFiles() {
return [
'.DS_Store', // macOS System-Datei
'Thumbs.db', // Windows Thumbnail-Cache
'desktop.ini', // Windows Desktop-Konfiguration
'.gitignore', // Git-Konfiguration
'.gitkeep', // Git-Platzhalter
'config.yaml', // Konfigurationsdateien
'config.yml',
'setup.yaml',
'setup.yml',
'.htaccess', // Apache-Konfiguration
'index.php', // PHP-Index (außer in HTML-Ordnern)
'web.config', // IIS-Konfiguration
'.env', // Environment-Variablen
'.env.local',
'README.md', // Dokumentation
'readme.txt',
'license.txt',
'LICENSE'
];
}
// Prüft, ob der eingeloggte Client auf den gewünschten Bereich zugreifen darf
private static function requireClientAccess($clientDir) {
$user = self::requireAuth();
// Admins haben grundsätzlich Zugriff, aber prüfe disallowedClients
if (isset($user['role']) && $user['role'] === 'admin') {
$adminData = self::getAdminData($user['username']);
$disallowedClients = $adminData['disallowedClients'] ?? [];
if (in_array($clientDir, $disallowedClients)) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => "Zugriff auf Client '{$clientDir}' ist für diesen Administrator nicht erlaubt."
]
]);
exit;
}
return $user;
}
if ($user['role'] === 'client' && $user['dir'] !== $clientDir) {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nicht berechtigt für diesen Bereich.'
]
]);
exit;
}
return $user;
}
// JWT-basierte Authentifizierungsprüfung für alle API-Methoden
private static function requireAuth() {
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? null;
if (!$authHeader || !preg_match('/^Bearer\s+(.*)$/i', $authHeader, $matches)) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'UNAUTHORIZED',
'message' => 'Kein gültiges Auth-Token übergeben.'
]
]);
exit;
}
$jwt = $matches[1];
require_once __DIR__ . '/../Services/AuthService.php';
$user = AuthService::verifyJWT($jwt);
if (!$user) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'UNAUTHORIZED',
'message' => 'Token ungültig oder abgelaufen.'
]
]);
exit;
}
// Optional: User-Infos für spätere Nutzung zurückgeben
return $user;
}
// Prüft ob eine Datei ignoriert werden soll
private static function shouldIgnoreFile($filename) {
$ignoredFiles = self::getIgnoredFiles();
return in_array(strtolower($filename), array_map('strtolower', $ignoredFiles));
}
public static function listForClient($clientDir) {
self::requireClientAccess($clientDir);
$base = realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir);
// Lade clients.json, um den Anzeigenamen des Clients zu bekommen
$loginsFile = __DIR__ . '/../../../storage/data/clients.json';
$clientDisplay = $clientDir;
if (file_exists($loginsFile)) {
$logins = json_decode(file_get_contents($loginsFile), true);
foreach ($logins as $login) {
if (isset($login['dir']) && $login['dir'] === $clientDir) {
$clientDisplay = $login['dir'];
break;
}
}
}
if (!$base || !is_dir($base)) {
http_response_code(404);
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'Kundenordner nicht gefunden.'
]
]);
return;
}
// Lade project_order.json falls vorhanden
$projectOrderFile = $base . '/project_order.json';
$projectOrder = [];
if (file_exists($projectOrderFile)) {
$orderContent = file_get_contents($projectOrderFile);
$decoded = json_decode($orderContent, true);
if (is_array($decoded)) {
$projectOrder = $decoded;
}
}
$projects = [];
$foundProjects = [];
// Sammle alle verfügbaren Projekte
foreach (scandir($base) as $entry) {
if ($entry === '.' || $entry === '..') continue;
$path = $base . '/' . $entry;
if (is_dir($path)) {
$setupDir = $path . '/setup';
$poster = null;
$logo = null;
if (is_dir($setupDir)) {
foreach (scandir($setupDir) as $file) {
if (!$poster && stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) {
$poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file);
}
if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) {
$logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($entry) . '/setup/' . rawurlencode($file);
}
if ($poster && $logo) break;
}
}
$isHE = (stripos($entry, 'HE_') === 0);
$foundProjects[$entry] = [
'name' => $entry,
'path' => $entry,
'poster' => $poster,
'logo' => $logo,
'isHE' => $isHE,
'client' => $clientDisplay
];
}
}
// Sortiere nach project_order.json falls vorhanden
if (!empty($projectOrder)) {
// Erst die Projekte in der definierten Reihenfolge
foreach ($projectOrder as $orderedProject) {
if (isset($foundProjects[$orderedProject])) {
$projects[] = $foundProjects[$orderedProject];
unset($foundProjects[$orderedProject]);
}
}
// Neue Projekte (nicht in order-Liste) kommen an den ANFANG
$remainingProjects = array_values($foundProjects);
usort($remainingProjects, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
// Neue Projekte VOR die sortierten einfügen
$projects = array_merge($remainingProjects, $projects);
} else {
// Fallback: Alphabetische Sortierung
$projects = array_values($foundProjects);
usort($projects, function($a, $b) {
return strcmp($a['name'], $b['name']);
});
}
echo json_encode([
'success' => true,
'projects' => $projects
]);
}
// Speichert die Projekt-Reihenfolge in project_order.json
public static function saveProjectOrder($clientDir) {
self::requireClientAccess($clientDir);
$clientDir = rawurldecode($clientDir);
// JSON-Body lesen
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!isset($data['order']) || !is_array($data['order'])) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => [
'code' => 'INVALID_DATA',
'message' => 'Ungültige Daten. "order" Array erwartet.'
]
]);
return;
}
$base = realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir);
if (!$base || !is_dir($base)) {
http_response_code(404);
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'Kundenordner nicht gefunden.'
]
]);
return;
}
$projectOrderFile = $base . '/project_order.json';
// Validiere dass alle Projekte im order Array auch wirklich existieren
$existingProjects = [];
foreach (scandir($base) as $entry) {
if ($entry === '.' || $entry === '..') continue;
if (is_dir($base . '/' . $entry)) {
$existingProjects[] = $entry;
}
}
foreach ($data['order'] as $projectName) {
if (!in_array($projectName, $existingProjects)) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => [
'code' => 'INVALID_PROJECT',
'message' => "Projekt '$projectName' existiert nicht."
]
]);
return;
}
}
// Speichere die neue Reihenfolge
$result = file_put_contents($projectOrderFile, json_encode($data['order'], JSON_PRETTY_PRINT));
if ($result === false) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => [
'code' => 'WRITE_ERROR',
'message' => 'Fehler beim Speichern der project_order.json.'
]
]);
return;
}
echo json_encode([
'success' => true,
'message' => 'Projekt-Reihenfolge gespeichert.'
]);
}
public static function adsFolders($clientDir, $projectName) {
self::requireClientAccess($clientDir);
$clientDir = rawurldecode($clientDir);
$projectName = rawurldecode($projectName);
$adsPath = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads';
$folders = [];
if (is_dir($adsPath)) {
foreach (scandir($adsPath) as $entry) {
if ($entry === '.' || $entry === '..') continue;
if (is_dir($adsPath . '/' . $entry)) {
$folders[] = $entry;
}
}
}
header('Content-Type: application/json');
echo json_encode(['folders' => $folders]);
}
public static function adsSubfolders($clientDir, $projectName, ...$adsFolders) {
self::requireClientAccess($clientDir);
$adsFolders = array_map('rawurldecode', $adsFolders);
$base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads';
if (!empty($adsFolders)) {
$base .= '/' . implode('/', $adsFolders);
}
$real = realpath($base);
$folders = [];
if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) {
foreach (scandir($real) as $entry) {
if ($entry === '.' || $entry === '..') continue;
if (is_dir($real . '/' . $entry)) {
$folders[] = $entry;
}
}
}
header('Content-Type: application/json');
echo json_encode(['folders' => $folders]);
}
// Liefert die Sub-Subfolder eines Subfolders in ads
public static function adsSubSubfolders($clientDir, $projectName, $adsFolder, $subFolder) {
self::requireClientAccess($clientDir);
$adsFolder = rawurldecode($adsFolder);
$subFolder = rawurldecode($subFolder);
$base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads/' . $adsFolder . '/' . $subFolder;
$real = realpath($base);
$folders = [];
if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) {
foreach (scandir($real) as $entry) {
if ($entry === '.' || $entry === '..') continue;
if (is_dir($real . '/' . $entry)) {
$folders[] = $entry;
}
}
}
header('Content-Type: application/json');
echo json_encode(['folders' => $folders]);
}
// Liefert die Dateien eines beliebigen Unterordners in ads (beliebige Tiefe)
public static function adsFolderFiles($clientDir, $projectName, ...$adsFolders) {
self::requireClientAccess($clientDir);
$adsFolders = array_map('rawurldecode', $adsFolders);
$base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads';
if (!empty($adsFolders)) {
$base .= '/' . implode('/', $adsFolders);
}
$real = realpath($base);
$files = [];
if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) {
foreach (scandir($real) as $entry) {
if ($entry === '.' || $entry === '..') continue;
$full = $real . '/' . $entry;
if (is_file($full)) {
// Ignoriere bestimmte Dateien
if (self::shouldIgnoreFile($entry)) {
continue;
}
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
$type = 'other';
if (in_array($ext, ['jpg','jpeg','png','gif','webp','svg'])) $type = 'image';
elseif (in_array($ext, ['mp4','mov','avi','webm'])) $type = 'video';
elseif (in_array($ext, ['html','htm'])) $type = 'html';
$urlParts = array_map('rawurlencode', $adsFolders);
$files[] = [
'name' => $entry,
'type' => $type,
'url' => "/area/$clientDir/$projectName/ads/" . implode('/', $urlParts) . (count($urlParts) ? '/' : '') . rawurlencode($entry)
];
}
}
}
header('Content-Type: application/json');
echo json_encode(['files' => $files]);
}
// Liefert sowohl Ordner als auch Dateien eines beliebigen Unterordners in ads (beliebige Tiefe)
public static function adsFolderContent($clientDir, $projectName, ...$adsFolders) {
self::requireClientAccess($clientDir);
$adsFolders = array_map('rawurldecode', $adsFolders);
$base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads';
if (!empty($adsFolders)) {
$base .= '/' . implode('/', $adsFolders);
}
$real = realpath($base);
$folders = [];
$files = [];
if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) {
foreach (scandir($real) as $entry) {
if ($entry === '.' || $entry === '..') continue;
$full = $real . '/' . $entry;
if (is_dir($full)) {
$folders[] = $entry;
} elseif (is_file($full)) {
// Ignoriere bestimmte Dateien
if (self::shouldIgnoreFile($entry)) {
continue;
}
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
$type = 'other';
if (in_array($ext, ['jpg','jpeg','png','gif','webp','svg'])) $type = 'image';
elseif (in_array($ext, ['mp4','mov','avi','webm'])) $type = 'video';
elseif (in_array($ext, ['html','htm'])) $type = 'html';
$urlParts = array_map('rawurlencode', $adsFolders);
$files[] = [
'name' => $entry,
'type' => $type,
'url' => "/area/$clientDir/$projectName/ads/" . implode('/', $urlParts) . (count($urlParts) ? '/' : '') . rawurlencode($entry)
];
}
}
}
header('Content-Type: application/json');
echo json_encode([
'folders' => $folders,
'files' => $files
]);
}
// Liefert config.yaml aus einem beliebigen Ordnerpfad
public static function getConfig($clientDir, $projectName, ...$adsFolders) {
self::requireClientAccess($clientDir);
$adsFolders = array_map('rawurldecode', $adsFolders);
$base = __DIR__ . self::getAreaPath() . '/' . $clientDir . '/' . $projectName . '/ads';
if (!empty($adsFolders)) {
$base .= '/' . implode('/', $adsFolders);
}
$real = realpath($base);
if ($real && is_dir($real) && strpos($real, realpath(__DIR__ . self::getAreaPath() . '/' . $clientDir)) === 0) {
$configFile = $real . '/config.yaml';
if (file_exists($configFile)) {
header('Content-Type: text/plain; charset=utf-8');
readfile($configFile);
return;
}
}
http_response_code(404);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'config.yaml nicht gefunden'
]
]);
}
// Liefert die Details eines Projekts (inkl. Logo, Poster etc.)
public static function projectDetails($clientDir, $projectName) {
self::requireClientAccess($clientDir);
$clientDir = rawurldecode($clientDir);
$projectName = rawurldecode($projectName);
$setupDir = __DIR__ . self::getAreaPath() . "/$clientDir/$projectName/setup";
$poster = null;
$logo = null;
if (is_dir($setupDir)) {
foreach (scandir($setupDir) as $file) {
if ($file === '.' || $file === '..') continue;
if (stripos($file, 'poster') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) {
$poster = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file);
}
if (!$logo && stripos($file, 'logo') === 0 && preg_match('/\.(jpg|jpeg|png|webp)$/i', $file)) {
$logo = '/area/' . rawurlencode($clientDir) . '/' . rawurlencode($projectName) . '/setup/' . rawurlencode($file);
}
if ($poster && $logo) break;
}
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'data' => [
'poster' => $poster,
'logo' => $logo,
]
]);
}
// Rekursive Übersicht aller Ads als strukturierter Baum
public static function adsOverview($clientDir, $projectName) {
self::requireClientAccess($clientDir);
$clientDir = rawurldecode($clientDir);
$projectName = rawurldecode($projectName);
$adsRoot = __DIR__ . self::getAreaPath() . "/$clientDir/$projectName/ads";
$baseUrl = $_ENV['BACKEND_URL'] ?? (isset($_SERVER['HTTP_HOST']) ? (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] : '');
// Hilfsfunktion für schöne Tab-Titel
function beautifyTabName($name) {
// Ersetze _ und - durch Leerzeichen, dann jedes Wort groß
$name = str_replace(['_', '-'], ' ', $name);
$name = mb_strtolower($name, 'UTF-8');
$name = preg_replace_callback('/\b\w/u', function($m) { return mb_strtoupper($m[0], 'UTF-8'); }, $name);
return $name;
}
function scanAdsFolder($absPath, $relPath, $baseUrl, $clientDir, $projectName) {
$result = [];
if (!is_dir($absPath)) return $result;
$entries = array_diff(scandir($absPath), ['.', '..']);
foreach ($entries as $entry) {
$entryAbs = "$absPath/$entry";
$entryRel = $relPath ? "$relPath/$entry" : $entry;
if (is_dir($entryAbs)) {
$children = scanAdsFolder($entryAbs, $entryRel, $baseUrl, $clientDir, $projectName);
$depth = substr_count($entryRel, '/');
$type = match($depth) {
0 => 'category',
1 => 'subcategory',
2 => 'ad',
default => 'folder'
};
$node = [
'name' => $entry,
'title' => beautifyTabName($entry),
'type' => $type,
];
if ($type === 'ad') {
// Prüfe, ob ein Unterordner mit 'html' im Namen existiert
$htmlFolder = null;
foreach (scandir($entryAbs) as $sub) {
if ($sub === '.' || $sub === '..') continue;
if (is_dir("$entryAbs/$sub") && stripos($sub, 'html') !== false) {
$htmlFolder = $sub;
break;
}
}
// Dateien sammeln, aber config.yaml (und config.yml) ignorieren
$node['files'] = array_values(array_filter($children, function($child) {
if (in_array($child['type'], ['category','subcategory','ad','folder'])) return false;
if (isset($child['name']) && preg_match('/^config\.ya?ml$/i', $child['name'])) return false;
return true;
}));
// Spezialfall: HTML-Ordner gefunden
if ($htmlFolder) {
$htmlAbs = "$entryAbs/$htmlFolder";
$htmlIndex = "$htmlAbs/index.html";
// Suche config.yaml eine Ebene über dem HTML-Ordner (im Ad-Ordner)
$configYaml = "$entryAbs/config.yaml";
$meta = [];
if (is_file($configYaml)) {
$yaml = file_get_contents($configYaml);
$width = 0;
$height = 0;
$lines = preg_split('/\r?\n/', $yaml);
foreach ($lines as $line) {
if ($width === 0 && preg_match('/width\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $wMatch)) {
$val = str_replace([',', ' '], '', $wMatch[1]);
$width = (int)floatval($val);
}
if ($height === 0 && preg_match('/height\s*:\s*["\']?([\d,.]+)\s*([a-zA-Z%]*)["\']?/i', $line, $hMatch)) {
$val = str_replace([',', ' '], '', $hMatch[1]);
$height = (int)floatval($val);
}
}
$meta['width'] = $width;
$meta['height'] = $height;
} else {
// Fallback: Versuche Dimensionen aus dem Ad-Ordnernamen zu extrahieren (z.B. 800x250)
$adFolderName = basename($entryAbs);
if (preg_match('/(\d{2,5})[xX](\d{2,5})/', $adFolderName, $matches)) {
$meta['width'] = (int)$matches[1];
$meta['height'] = (int)$matches[2];
// file_put_contents(__DIR__.'/debug.log', "Fallback: Dimensionen aus Ordnername erkannt width={$meta['width']} height={$meta['height']} ($adFolderName)\n", FILE_APPEND);
}
}
if (is_file($htmlIndex)) {
$url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') . rawurlencode($entry) . "/" . rawurlencode($htmlFolder) . "/index.html";
$fileMeta = [
'name' => 'index.html',
'type' => 'html',
'url' => $url,
'size' => is_file($htmlIndex) ? filesize($htmlIndex) : null,
'width' => (isset($meta['width'])) ? $meta['width'] : 123,
'height' => (isset($meta['height'])) ? $meta['height'] : 456
];
// Füge index.html nur hinzu, wenn sie nicht schon in files ist
$already = false;
foreach ($node['files'] as $f) {
if ($f['name'] === 'index.html') { $already = true; break; }
}
if (!$already) {
$node['files'][] = $fileMeta;
}
}
}
} else {
$node['children'] = array_filter($children, fn($c) => in_array($c['type'], ['category','subcategory','ad','folder']));
}
$result[] = $node;
} else {
// Datei
$ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION));
$type = match(true) {
in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp']) => 'image',
in_array($ext, ['mp4', 'mov', 'webm']) => 'video',
$ext === 'html' => 'html',
default => 'file'
};
$url = "$baseUrl/area/" . rawurlencode($clientDir) . "/" . rawurlencode($projectName) . "/ads/" . ($relPath ? str_replace('%2F', '/', rawurlencode($relPath)) . '/' : '') . rawurlencode($entry);
$meta = [];
$fileAbs = $entryAbs;
// Dateigröße
$meta['size'] = is_file($fileAbs) ? filesize($fileAbs) : null;
// Dimensionen für Bilder
if ($type === 'image') {
$imgInfo = @getimagesize($fileAbs);
if ($imgInfo) {
$meta['width'] = $imgInfo[0];
$meta['height'] = $imgInfo[1];
}
}
// Dimensionen und Dauer für Videos (nur wenn ffprobe verfügbar)
if ($type === 'video' && is_file($fileAbs)) {
$ffprobePath = shell_exec('which ffprobe');
$ffprobeUsed = false;
if ($ffprobePath) {
$ffprobe = trim($ffprobePath);
if ($ffprobe !== '') {
$cmd = escapeshellcmd($ffprobe) . ' -v error -select_streams v:0 -show_entries stream=width,height,duration -of default=noprint_wrappers=1:nokey=1 ' . escapeshellarg($fileAbs);
$out = shell_exec($cmd);
if ($out) {
$lines = explode("\n", trim($out));
if (count($lines) >= 3) {
$meta['width'] = (int)$lines[0];
$meta['height'] = (int)$lines[1];
$meta['duration'] = (float)$lines[2];
$ffprobeUsed = true;
}
}
}
}
// Fallback auf getID3, falls ffprobe nicht verfügbar oder fehlgeschlagen
if (!$ffprobeUsed) {
try {
$getID3 = new \getID3();
$info = $getID3->analyze($fileAbs);
if (!empty($info['video']['resolution_x']) && !empty($info['video']['resolution_y'])) {
$meta['width'] = (int)$info['video']['resolution_x'];
$meta['height'] = (int)$info['video']['resolution_y'];
}
if (!empty($info['playtime_seconds'])) {
$meta['duration'] = (float)$info['playtime_seconds'];
}
} catch (\Exception $e) {
// Fehler ignorieren, Metadaten bleiben leer
}
}
}
$result[] = array_merge([
'name' => $entry,
'title' => beautifyTabName($entry),
'type' => $type,
'url' => $url
], $meta);
}
}
usort($result, function($a, $b) {
if (($a['type'] === 'file') !== ($b['type'] === 'file')) {
return $a['type'] === 'file' ? 1 : -1;
}
return strnatcasecmp($a['name'], $b['name']);
});
return $result;
}
$tree = scanAdsFolder($adsRoot, '', $baseUrl, $clientDir, $projectName);
header('Content-Type: application/json');
echo json_encode($tree, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
}
// Projekt→Client Mapping für Admin Smart URL Resolution
public static function getProjectClientMapping() {
// Nur Admins dürfen diese API verwenden
$user = self::requireAuth();
if (!isset($user['role']) || $user['role'] !== 'admin') {
http_response_code(403);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nur Administratoren können das Projekt-Client-Mapping abrufen.'
]
]);
exit;
}
// Lade Admin-Daten für Client-Berechtigungen
$adminData = self::getAdminData($user['username']);
$disallowedClients = $adminData['disallowedClients'] ?? [];
$areaPath = __DIR__ . self::getAreaPath();
$mapping = [];
// Durchsuche alle Client-Ordner
if (is_dir($areaPath)) {
$clientDirs = scandir($areaPath);
foreach ($clientDirs as $clientDir) {
if ($clientDir === '.' || $clientDir === '..' || !is_dir($areaPath . '/' . $clientDir)) {
continue;
}
// Prüfe ob Admin Zugriff auf diesen Client hat
if (in_array($clientDir, $disallowedClients)) {
continue; // Client ist für diesen Admin nicht erlaubt
}
$clientPath = $areaPath . '/' . $clientDir;
if (is_dir($clientPath)) {
$projects = scandir($clientPath);
foreach ($projects as $project) {
if ($project === '.' || $project === '..' || !is_dir($clientPath . '/' . $project)) {
continue;
}
// Ignoriere spezielle Dateien
if (in_array($project, ['project_order.json', 'logins.json'])) {
continue;
}
// Füge Projekt→Client Mapping hinzu
$mapping[$project] = $clientDir;
}
}
}
}
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'mapping' => $mapping,
'adminInfo' => [
'username' => $user['username'],
'disallowedClients' => $disallowedClients
]
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
// Hilfsfunktion: Admin-Daten laden
private static function getAdminData($username) {
$adminFile = __DIR__ . '/../../storage/data/admins.json';
if (file_exists($adminFile)) {
$admins = json_decode(file_get_contents($adminFile), true);
foreach ($admins as $admin) {
if ($admin['username'] === $username) {
return $admin;
}
}
}
return [];
}
}

View File

@@ -0,0 +1,184 @@
<?php
require_once __DIR__ . '/../Services/AuthService.php';
class UserController {
public static function currentUser() {
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
if ($user) {
http_response_code(200);
echo json_encode([
'success' => true,
'user' => $user
]);
} else {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => [
'code' => 'INVALID_TOKEN',
'message' => 'Token ungültig oder abgelaufen.'
]
]);
}
}
// Gibt alle Benutzer als Array zurück (z.B. aus storage/data/admins.json)
public static function listAll() {
$file = __DIR__ . '/../../storage/data/admins.json';
if (!file_exists($file)) {
http_response_code(200);
echo json_encode([
'success' => true,
'users' => []
]);
return;
}
$json = file_get_contents($file);
$users = json_decode($json, true);
if (!is_array($users)) $users = [];
http_response_code(200);
echo json_encode([
'success' => true,
'users' => $users
]);
}
// Legt einen neuen Benutzer an
public static function create() {
$file = __DIR__ . '/../../storage/data/admins.json';
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input) || empty($input['username']) || empty($input['role'])) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => [
'code' => 'INVALID_INPUT',
'message' => 'Ungültige Eingabedaten.'
]
]);
return;
}
// Bestehende Benutzer laden
$users = [];
if (file_exists($file)) {
$json = file_get_contents($file);
$users = json_decode($json, true);
if (!is_array($users)) $users = [];
}
// Neue ID generieren
$newId = 1;
if (!empty($users)) {
$ids = array_column($users, 'id');
$newId = max($ids) + 1;
}
$newUser = [
'id' => $newId,
'username' => $input['username'],
'role' => $input['role'],
'email' => $input['email'] ?? '',
// Optional: Passwort-Hash, weitere Felder
];
$users[] = $newUser;
// Speichern
file_put_contents($file, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
http_response_code(201);
echo json_encode([
'success' => true,
'user' => $newUser
]);
}
// Bearbeitet einen bestehenden Benutzer
public static function update($id) {
$file = __DIR__ . '/../../storage/data/admins.json';
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input) || empty($input['username']) || empty($input['role'])) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => [
'code' => 'INVALID_INPUT',
'message' => 'Ungültige Eingabedaten.'
]
]);
return;
}
// Bestehende Benutzer laden
$users = [];
if (file_exists($file)) {
$json = file_get_contents($file);
$users = json_decode($json, true);
if (!is_array($users)) $users = [];
}
$found = false;
foreach ($users as &$user) {
if ($user['id'] == $id) {
$user['username'] = $input['username'];
$user['role'] = $input['role'];
$user['email'] = $input['email'] ?? '';
// Optional: weitere Felder wie Passwort etc.
$found = true;
break;
}
}
if (!$found) {
http_response_code(404);
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'Benutzer nicht gefunden.'
]
]);
return;
}
// Speichern
file_put_contents($file, json_encode($users, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
http_response_code(200);
echo json_encode([
'success' => true,
'user' => $user
]);
}
// Löscht einen bestehenden Benutzer
public static function delete($id) {
$file = __DIR__ . '/../../storage/data/admins.json';
$users = [];
if (file_exists($file)) {
$json = file_get_contents($file);
$users = json_decode($json, true);
if (!is_array($users)) $users = [];
}
$found = false;
$newUsers = [];
foreach ($users as $user) {
if ($user['id'] == $id) {
$found = true;
continue; // Benutzer überspringen (löschen)
}
$newUsers[] = $user;
}
if (!$found) {
http_response_code(404);
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'Benutzer nicht gefunden.'
]
]);
return;
}
// Speichern
file_put_contents($file, json_encode($newUsers, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
http_response_code(200);
echo json_encode([
'success' => true
]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
require_once __DIR__ . '/Router.php';
class Application {
public function run() {
header('Content-Type: application/json');
Router::handle();
}
}

258
backend/src/Core/Router.php Normal file
View File

@@ -0,0 +1,258 @@
<?php
// Explizit Header setzen vor jeder Ausgabe
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
// Preflight OPTIONS Requests explizit beantworten und beenden
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
class Router {
public static function handle() {
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
if ($uri === '/api/auth/login' && $method === 'POST') {
require_once __DIR__ . '/../Api/AuthController.php';
AuthController::login();
return;
}
if ($uri === '/api/auth/user' && $method === 'GET') {
require_once __DIR__ . '/../Api/UserController.php';
UserController::currentUser();
return;
}
// Admin: Benutzerliste
if ($uri === '/api/admin/users' && $method === 'GET') {
require_once __DIR__ . '/../Services/AuthService.php';
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
if ($user && $user['role'] === 'admin') {
require_once __DIR__ . '/../Api/UserController.php';
UserController::listAll();
return;
} else {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nicht berechtigt.'
]
]);
return;
}
}
// Admin: Benutzer anlegen
if ($uri === '/api/admin/users' && $method === 'POST') {
require_once __DIR__ . '/../Services/AuthService.php';
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
if ($user && $user['role'] === 'admin') {
require_once __DIR__ . '/../Api/UserController.php';
UserController::create();
return;
} else {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nicht berechtigt.'
]
]);
return;
}
}
// Parametrisierte Admin CRUD für Users & Clients
if (preg_match('#^/api/admin/(users|clients)(?:/(\d+))?$#', $uri, $m)) {
require_once __DIR__ . '/../Api/EntityController.php';
$type = $m[1];
$id = isset($m[2]) ? (int)$m[2] : null;
switch ($method) {
case 'GET':
EntityController::list($type);
break;
case 'POST':
EntityController::create($type);
break;
case 'PUT':
EntityController::update($type, $id);
break;
case 'DELETE':
EntityController::delete($type, $id);
break;
}
return;
}
// Passwort-Update Route
if (preg_match('#^/api/admin/(users|clients)/(\d+)/password$#', $uri, $m) && $method === 'PUT') {
require_once __DIR__ . '/../Api/EntityController.php';
$type = $m[1];
$id = (int)$m[2];
EntityController::updatePassword($type, $id);
return;
}
// Projekt→Client Mapping für Admin Smart URL Resolution
if ($uri === '/api/admin/project-client-mapping' && $method === 'GET') {
require_once __DIR__ . '/../Api/ProjectsController.php';
ProjectsController::getProjectClientMapping();
return;
}
if ($uri === '/api/projects' && $method === 'GET') {
require_once __DIR__ . '/../Services/AuthService.php';
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
file_put_contents(__DIR__ . '/user_debug.log', date('c') . ' AUTH: ' . var_export($auth, true) . ' TOKEN: ' . var_export($token, true) . PHP_EOL, FILE_APPEND);
if ($user && $user['role'] === 'client' && !empty($user['dir'])) {
require_once __DIR__ . '/../Api/ProjectsController.php';
ProjectsController::listForClient($user['dir']);
return;
} else {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nicht berechtigt oder kein Client.'
]
]);
return;
}
}
// Admin: Benutzer löschen
if (preg_match('#^/api/admin/users/(\d+)$#', $uri, $m) && $method === 'DELETE') {
require_once __DIR__ . '/../Services/AuthService.php';
$headers = getallheaders();
$auth = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$token = str_replace('Bearer ', '', $auth);
$user = AuthService::verifyJWT($token);
if ($user && $user['role'] === 'admin') {
require_once __DIR__ . '/../Api/UserController.php';
UserController::delete((int)$m[1]);
return;
} else {
http_response_code(403);
echo json_encode([
'success' => false,
'error' => [
'code' => 'FORBIDDEN',
'message' => 'Nicht berechtigt.'
]
]);
return;
}
}
// Test-Route für area-Zugriff
if (preg_match('#^/api/test/([^/]+)$#', $uri, $m)) {
$value = urldecode($m[1]);
$base = __DIR__ . "/../../area/$value";
$real = realpath($base);
$result = [
'requested' => $base,
'realpath' => $real,
'is_dir' => $real && is_dir($real),
'is_readable' => $real && is_readable($real),
'files' => $real && is_dir($real) ? scandir($real) : null
];
header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return;
}
// Projekt-Reihenfolge speichern
if (preg_match('#^/api/projects/([^/]+)/project-order$#', $uri, $m) && $method === 'POST') {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
ProjectsController::saveProjectOrder($clientDir);
return;
}
// Einzelnes Projekt-Objekt (inkl. Logo, Poster etc.)
if (preg_match('#^/api/projects/([^/]+)/([^/]+)$#', $uri, $m) && $method === 'GET') {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
$projectName = urldecode($m[2]);
ProjectsController::projectDetails($clientDir, $projectName);
return;
}
// Rekursive Ads-Übersicht als Baumstruktur (muss VOR der generischen /ads-Route stehen!)
if (preg_match('#^/api/projects/([^/]+)/([^/]+)/ads-overview$#', $uri, $m)) {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
$projectName = urldecode($m[2]);
ProjectsController::adsOverview($clientDir, $projectName);
return;
}
// Endpunkt für config.yaml in beliebiger Tiefe: /ads/.../config.yaml
if (preg_match('#^/api/projects/([^/]+)/([^/]+)/ads/(.+)/config\.yaml$#', $uri, $m)) {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
$projectName = urldecode($m[2]);
$folders = array_map('urldecode', explode('/', $m[3]));
ProjectsController::getConfig($clientDir, $projectName, ...$folders);
return;
}
// Endpunkt für Dateien in beliebiger Tiefe: /ads/.../files
if (preg_match('#^/api/projects/([^/]+)/([^/]+)/ads/(.+)/files$#', $uri, $m)) {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
$projectName = urldecode($m[2]);
$folders = array_map('urldecode', explode('/', $m[3]));
ProjectsController::adsFolderFiles($clientDir, $projectName, ...$folders);
return;
}
// Endpunkt für Unterordner in beliebiger Tiefe: /ads/... (aber nicht /files)
if (preg_match('#^/api/projects/([^/]+)/([^/]+)/ads(?:/(.+))?$#', $uri, $m) && (empty($m[3]) || !preg_match('#/files$#', $uri))) {
require_once __DIR__ . '/../Api/ProjectsController.php';
$clientDir = urldecode($m[1]);
$projectName = urldecode($m[2]);
if (empty($m[3])) {
// Nur Tab-Ordner für /ads
ProjectsController::adsFolders($clientDir, $projectName);
} else {
// Sowohl Ordner als auch Dateien für tiefere Pfade
$folders = array_map('urldecode', explode('/', $m[3]));
ProjectsController::adsFolderContent($clientDir, $projectName, ...$folders);
}
return;
}
// 404
http_response_code(404);
echo json_encode([
'success' => false,
'error' => [
'code' => 'NOT_FOUND',
'message' => 'Route nicht gefunden.'
]
]);
}
}

View File

@@ -0,0 +1,12 @@
<?php
function loadEnv($file) {
if (!file_exists($file)) return;
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
if (strpos($line, '=') === false) continue;
list($name, $value) = array_map('trim', explode('=', $line, 2));
putenv("$name=$value");
$_ENV[$name] = $value;
}
}

View File

@@ -0,0 +1,70 @@
<?php
class AuthService {
private static function base64url_encode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public static function verifyJWT($jwt) {
if (!$jwt) return null;
$parts = explode('.', $jwt);
if (count($parts) !== 3) return null;
list($header, $payload, $signature) = $parts;
$valid = self::base64url_encode(hash_hmac('sha256', "$header.$payload", self::$jwtSecret, true));
if (!hash_equals($valid, $signature)) return null;
$data = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
if (!$data || ($data['exp'] ?? 0) < time()) return null;
// User-Infos je nach Rolle ergänzen
if ($data['role'] === 'admin') {
return [
'username' => $data['id'],
'role' => 'admin'
];
} elseif ($data['role'] === 'client') {
return [
'client' => $data['dir'],
'role' => 'client',
'dir' => $data['dir']
];
}
return null;
}
private static $adminFile = __DIR__ . '/../../storage/data/admins.json';
private static $clientFile = __DIR__ . '/../../storage/data/clients.json';
private static $jwtSecret = 'adspreview_secret_2025';
public static function authenticate($username, $password) {
if ($username) {
// Admin-Login
$admins = json_decode(file_get_contents(self::$adminFile), true);
foreach ($admins as $admin) {
if ($admin['username'] === $username && password_verify($password, $admin['password'])) {
$token = self::generateJWT($admin['username'], '', 'admin');
return ['success' => true, 'token' => $token, 'role' => 'admin'];
}
}
return ['success' => false, 'message' => 'Falscher Benutzername oder Passwort.'];
} else {
// Client-Login (nur Passwort)
$clients = json_decode(file_get_contents(self::$clientFile), true);
foreach ($clients as $key => $client) {
if ($client['pass'] === md5($password)) {
$token = self::generateJWT($key, $client['dir'], 'client');
return ['success' => true, 'token' => $token, 'role' => 'client'];
}
}
return ['success' => false, 'message' => 'Falsches Passwort.'];
}
}
private static function generateJWT($id, $dir, $role) {
$header = self::base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = self::base64url_encode(json_encode([
'id' => $id,
'dir' => $dir,
'role' => $role,
'exp' => time() + 60 * 60 * 12 // 12 Stunden
]));
$signature = hash_hmac('sha256', "$header.$payload", self::$jwtSecret, true);
$signature = self::base64url_encode($signature);
return "$header.$payload.$signature";
}
}

View File

@@ -0,0 +1,23 @@
<?php
class JsonStorageService {
private static function getFilePath(string $type): string {
return __DIR__ . '/../../storage/data/' . $type . '.json';
}
public static function read(string $type): array {
$file = self::getFilePath($type);
if (!file_exists($file)) {
return [];
}
$json = file_get_contents($file);
$data = json_decode($json, true);
return is_array($data) ? $data : [];
}
public static function write(string $type, array $data): bool {
$file = self::getFilePath($type);
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return file_put_contents($file, $json) !== false;
}
}

11
backend/storage/.htaccess Normal file
View File

@@ -0,0 +1,11 @@
# Zugriff auf Storage-Dateien verbieten
<Files "*.json">
Order allow,deny
Deny from all
</Files>
# Zugriff auf Log-Dateien verbieten
<Files "*.log">
Order allow,deny
Deny from all
</Files>

View File

@@ -0,0 +1,20 @@
[
{
"id": 1,
"username": "admin",
"password": "$2a$12$s8EiG.SOcQm\/avdEwsX0ve9QXqW7KbKpKwQOLEZPTY1mIzY5nCUVm",
"role": "admin",
"email": "",
"disallowedClients": []
},
{
"id": 2,
"username": "Johannes",
"role": "admin",
"email": "j.kluge@duwfamily.de",
"disallowedClients": [
"Paramount"
],
"password": "$2y$10$T059TgvbwRwmSOiqqhlrLeMeaxWAYZd4mT9ot\/tCTG.ldUi4pvk.K"
}
]

View File

@@ -0,0 +1,318 @@
{
"AllScreens": {
"pass": "282362f6d534e4106b25a9400a355ace",
"dir": "AllScreens",
"in": "Hallo! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"Arthaus": {
"pass": "f978da4380dda2b14eb0e14fed27479d",
"dir": "Arthaus",
"in": "Hallo Arthaus! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"Cromatics": {
"pass": "90bebd2f0dc248887ba9d779a95c78f3",
"dir": "Cromatics",
"in": "Hallo Cromatics! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"DCM": {
"pass": "64651a9d82a5ac27476ff2d1d26022b4",
"dir": "DCM",
"in": "Hi DCM, toll dass ihr hier vorbeischaut!",
"out": "Kommt bald mal wieder!"
},
"Delasocial": {
"pass": "2faa7c30137f77c7ac8ddeee3a0afcc7",
"dir": "Delasocial",
"in": "Hallo Delasocial! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"Disney": {
"pass": "9f234b3b8a14d247e8c8b7c566b7c5c1",
"dir": "Disney",
"in": "Hey Disney! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"DUW": {
"pass": "c3875d07f44c422f3b3bc019c23e16ae",
"dir": "DruckUndWerte",
"in": "Aloha und willkommen allerseits!",
"out": "Aloha und bis später!"
},
"DUW_ex": {
"pass": "9f917c4ee8fbfd6ea6337a9c66a5f28b",
"dir": "DruckUndWerte_extern",
"in": "Hallo, schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"DOHR": {
"pass": "7c2090fa35a78ad8b97903bc957e10d3",
"dir": "DOHR",
"in": "Hallo, schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"FARBFILM": {
"pass": "431dc785c3b06782a16792f53601b560",
"dir": "Farbfilm",
"in": "Hallo Farbfilm! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"FODI": {
"pass": "aba74476945b8c5695da91ef7d228591",
"dir": "Forever Digital",
"in": "Hallo Forever Digital! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"FREELANCER": {
"pass": "3ff47a2d7c0f78a49c8774990d82ccdd",
"dir": "Freelancer",
"in": "Hallo Freelancer! Schön, dass du da bist.",
"out": "Danke für deinen Besuch, bis später!"
},
"HERZ": {
"pass": "97456273d78bfb02a13f7ae3fc39d968",
"dir": "Herzkampf",
"in": "Hallo Martin! Sehr cool, dass du vorbeischaust.",
"out": "Danke für deinen Besuch, bis später!"
},
"HONKYTONK": {
"pass": "3f739972a5ed0418b23a3b657022529a",
"dir": "Honky_Tonk",
"in": "Hallo Honky Tonk! Sehr cool, dass du vorbeischaust.",
"out": "Danke für deinen Besuch, bis später!"
},
"IAK": {
"pass": "b683e7e38b1b31d6ba7d52e5707eacf7",
"dir": "IAK_Projekte",
"in": "Hallo IAK! Sehr cool, dass du vorbeischaust.",
"out": "Danke für deinen Besuch, bis später!"
},
"KOCH": {
"pass": "c18c81318d17d0a77f6264877360cfa6",
"dir": "Koch",
"in": "Hallo Koch Media. Sehr cool, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"KREATIVORTE_SACHSEN": {
"pass": "40be028f0c67ad526f95915404d921dd",
"dir": "Kreativorte_Sachsen",
"in": "Hallo Kreatives Sachsen. Sehr cool, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"LEONINE": {
"pass": "79cfab624c6f02057146c1cdadff2edf",
"dir": "Leonine",
"in": "Hallo Leonine Studios. Sehr cool, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"MAWI": {
"pass": "3f2c3763d1eaf85c32a95f12f129e482",
"dir": "Mawi",
"in": "Hallo MAWI",
"out": "Bis bald mal wieder!"
},
"BIO": {
"pass": "292c525c1552ff3a76e8c774fcdc3c6d",
"dir": "Neue_Bioskop",
"in": "Hallo Neue_Bioskop",
"out": "Bis bald mal wieder!"
},
"GEWAND": {
"pass": "9976c5c3ee90a05e19bdf99f094c4cb6",
"dir": "Gewandhaus",
"in": "Hallo Gewandhaus Orchester",
"out": "Bis bald mal wieder!"
},
"OEMG": {
"pass": "c921775f278ccf9b30259e2ccc9c8316",
"dir": "Oekomarktgemeinschaft",
"in": "Hey ÖMG! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"PARA": {
"pass": "fc8b22f41628b89ce77d2e705d17344c",
"dir": "Paramount",
"in": "Hey Paramount! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"PARA_EXT": {
"pass": "fb4103032c7d126cba199df4283104af",
"dir": "Paramount_extern",
"in": "Hallo! Schön, dass Sie da sind.",
"out": "Danke für Ihren Besuch, bis später!"
},
"PLAION": {
"pass": "ba186491e569565b13b797779792a830",
"dir": "Plaion",
"in": "Hallo Plaion! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"PORT": {
"pass": "118c9af69a42383387e8ce6ab22867d7",
"dir": "PortAuPrince",
"in": "Hallo Port Au Prince! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"PREVIEW": {
"pass": "5ebeb6065f64f2346dbb00ab789cf001",
"dir": "Preview",
"in": "Hey Watcher! Schön, dass Du da bist.",
"out": "Danke für Euren Besuch, bis später!"
},
"SHOWAD": {
"pass": "fc5bebaa11dfefe697de7270f3f50962",
"dir": "ShowAd",
"in": "Willkommen ShowAd",
"out": "Danke für Euren Besuch, bis später!"
},
"SMH": {
"pass": "fe34aa295723a5886cc6b880772d22e9",
"dir": "Stadtmarketing_Halle",
"in": "Willkommen Stadtmarketing Halle",
"out": "Danke für Euren Besuch, bis später!"
},
"SQUAREONE": {
"pass": "12f85fdf12a50de1f8f209c9080ea937",
"dir": "Square_One_Entertainment",
"in": "Hey SquareOne Entertainment! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"STADT": {
"pass": "80be0458f1cb1f7ee8c2b979c2b5f52a",
"dir": "Stadtgespraech",
"in": "Hallo Stadtgespräch! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"STC": {
"pass": "2483e14219cce6fe63d8ac91afc92618",
"dir": "STC",
"in": "Hallo Studiocanal! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"STC_EXT": {
"pass": "cca944a0fe3c28c377d9056846b9348e",
"dir": "STC_extern",
"in": "Hallo! Schön, dass Du da bist.",
"out": "Danke für Deinen Besuch, bis später!"
},
"Studiocanal": {
"pass": "cea12e6c71d1264a896d8db81d2c0fdc",
"dir": "Studiocanal",
"in": "Hallo Studiocanal! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"THEO_Werbeagentur": {
"pass": "267e8c911ca96f658543693e2e5f19ac",
"dir": "THEO_Werbeagentur",
"in": "Hallo THEO! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"Tobis": {
"pass": "b7cff13b0234c8b67a0ad80f3b58381c",
"dir": "Tobis",
"in": "Hallo TOBIS! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"TPDOMO": {
"pass": "83f0daabd67bff33021f8ce2b19244b3",
"dir": "TorpedoMoto",
"in": "Nie zufrieden.",
"out": "We are friends of creative and digital marketing ;)"
},
"Universal_Music": {
"pass": "68a4f8d71c5a11701957f1276a74b217",
"dir": "Universal_Music",
"in": "Hallo Universal Music! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WARN": {
"pass": "7cb825305140bd57e6475ac54711c4f0",
"dir": "Warner",
"in": "Hallo Warner Bros.! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WARNERMUSIC": {
"pass": "aca10ece993488f9b58b7f392c45b205",
"dir": "WarnerMusic",
"in": "Hallo Warner Bros.! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WEIN": {
"pass": "2bf36d400f27b62790464572f2e6d16b",
"dir": "WeinRieder",
"in": "Hey Wein.Rieder! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WTK": {
"pass": "eb35c17c47b8ea645be204aba44cae3d",
"dir": "Weltkino",
"in": "Hey Weltkino! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"PLA": {
"pass": "ba186491e569565b13b797779792a830",
"dir": "Plaion",
"in": "Hey Plaion! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WB": {
"pass": "a10a7b46a6e2be8c070e3305a300492e",
"dir": "Wildbunch",
"in": "Hey Wildbunch! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
},
"WOODWALKERS": {
"pass": "9163f2b67e738edf854cbfda80aef9af",
"dir": "Woodwalkers",
"in": "Hallo Studiocanal! Schön, dass ihr da seid.",
"out": "Danke für Euren Besuch, bis später!"
}
}

View File

@@ -0,0 +1,7 @@
{
"Viewer_Sample": {
"pass": "fc8b22f41628b89ce8102e705d1734kf",
"dir": "Paramount",
"tab": "Beispiele"
}
}