codex update
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<?php
|
||||
|
||||
define('APP_DOMAIN_NAME', 'usbcheck.it');
|
||||
define('APP_PREFIX', 'usbcheck');
|
||||
define('APP_DOMAIN_NAME', 'emailtemplate.it');
|
||||
define('APP_PREFIX', 'emailtemplate');
|
||||
110
config/emailtemplate.conf.php
Normal file
110
config/emailtemplate.conf.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$bootstrapConfig = __DIR__ . '/config.php';
|
||||
if (is_file($bootstrapConfig)) {
|
||||
require_once $bootstrapConfig;
|
||||
}
|
||||
|
||||
$overrides = $GLOBALS['EMAILTEMPLATE_OVERRIDES'] ?? [];
|
||||
|
||||
$env = (string)($overrides['env'] ?? (defined('APP_ENV') ? APP_ENV : (getenv('APP_ENV') ?: 'local')));
|
||||
$baseUrl = (string)($overrides['base_url'] ?? (defined('APP_URL_PRIMARY') ? APP_URL_PRIMARY : (getenv('APP_BASE_URL') ?: '')));
|
||||
$cookieDomainDefault = (string)($overrides['cookie_domain'] ?? ($overrides['auth']['cookie_domain'] ?? (defined('APP_COOKIE_DOMAIN') ? APP_COOKIE_DOMAIN : (defined('APP_DOMAIN_PRIMARY') ? '.' . APP_DOMAIN_PRIMARY : ''))));
|
||||
$sessionNameDefault = (string)($overrides['session_name'] ?? ($overrides['auth']['session_name'] ?? (defined('APP_COOKIE_PREFIX') ? APP_COOKIE_PREFIX . 'session' : 'et_session')));
|
||||
|
||||
$projectDbDefaults = [
|
||||
'db_host' => getenv('DB_HOST') ?: getenv('DB_TPL_HOST') ?: 'localhost',
|
||||
'db_name' => getenv('DB_NAME') ?: getenv('DB_TPL_NAME') ?: 'd044ae9e',
|
||||
'db_user' => getenv('DB_USER') ?: getenv('DB_TPL_USER') ?: 'd044ae9e',
|
||||
'db_pass' => getenv('DB_PASS') ?: getenv('DB_TPL_PASS') ?: '9BVUn)Töcü@ÖVÜfgO8!J',
|
||||
'db_charset' => getenv('DB_CHARSET') ?: 'utf8mb4',
|
||||
'db_port' => (int)(getenv('DB_PORT') ?: 3306),
|
||||
'prefix' => defined('APP_PREFIX') ? APP_PREFIX . '_' : 'emailtemplate_',
|
||||
];
|
||||
|
||||
$legacyOverrides = [];
|
||||
if (isset($overrides['project'])) {
|
||||
$legacyOverrides = $overrides['project'];
|
||||
} elseif (isset($overrides['templates'])) {
|
||||
$legacyOverrides = $overrides['templates'];
|
||||
}
|
||||
$projectDb = array_replace($projectDbDefaults, $legacyOverrides, $overrides['projectdb'] ?? $overrides['dbsettings'] ?? []);
|
||||
|
||||
$cors = $overrides['cors'] ?? (getenv('CORS_ORIGIN') ?: '*');
|
||||
|
||||
$authDefaults = [
|
||||
'session_name' => $sessionNameDefault,
|
||||
'cookie_domain' => $cookieDomainDefault,
|
||||
'cookie_secure' => $env === 'prod',
|
||||
'cookie_httponly' => true,
|
||||
'cookie_samesite' => 'Lax',
|
||||
'cookie' => [
|
||||
'domain' => $cookieDomainDefault,
|
||||
'secure' => $env === 'prod',
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
],
|
||||
'db' => [
|
||||
'table' => 'customer_users',
|
||||
'col_user' => 'email',
|
||||
'col_pass' => 'password_hash',
|
||||
'col_name' => 'name',
|
||||
'col_id' => 'id',
|
||||
'col_status' => 'is_active',
|
||||
'active_values' => ['active','1',1],
|
||||
'legacy' => 'md5',
|
||||
],
|
||||
];
|
||||
$auth = array_replace_recursive($authDefaults, $overrides['auth'] ?? []);
|
||||
|
||||
$smtpDefaults = [
|
||||
'host' => 'smtp.example.com',
|
||||
'port' => 587,
|
||||
'user' => 'smtp-user',
|
||||
'pass' => 'smtp-pass',
|
||||
'secure' => 'tls',
|
||||
'from_email' => 'no-reply@example.com',
|
||||
'from_name' => 'EmailTemplate',
|
||||
];
|
||||
$smtp = array_replace($smtpDefaults, $overrides['smtp'] ?? []);
|
||||
|
||||
$exportDefaults = [
|
||||
'api_keys' => ['dev-key-123', 'noch-ein-key'],
|
||||
];
|
||||
$export = array_replace_recursive($exportDefaults, $overrides['export'] ?? []);
|
||||
|
||||
$multiDefaults = [
|
||||
'tenant_col' => 'customer_id',
|
||||
'map_session_to'=> 'id',
|
||||
];
|
||||
$multi = array_replace($multiDefaults, $overrides['multi'] ?? []);
|
||||
|
||||
$tablesDefaults = [
|
||||
'templates' => 'emailtemplate_templates',
|
||||
'sections' => 'emailtemplate_sections',
|
||||
'blocks' => 'emailtemplate_blocks',
|
||||
'snippets' => 'emailtemplate_snippets',
|
||||
];
|
||||
$tables = array_replace($tablesDefaults, $overrides['tables'] ?? []);
|
||||
|
||||
$columnsDefaults = [
|
||||
'templates' => ['id'=>'id','name'=>'name','desc'=>null,'cat'=>null,'upd'=>'updated_at'],
|
||||
'sections' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'],
|
||||
'blocks' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
'snippets' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
];
|
||||
$columns = array_replace_recursive($columnsDefaults, $overrides['columns'] ?? []);
|
||||
|
||||
return [
|
||||
'projectdb' => $projectDb,
|
||||
'cors' => $cors,
|
||||
'env' => $env,
|
||||
'base_url' => $baseUrl,
|
||||
'auth' => $auth,
|
||||
'smtp' => $smtp,
|
||||
'export' => $export,
|
||||
'multi' => $multi,
|
||||
'tables' => $tables,
|
||||
'columns' => $columns,
|
||||
];
|
||||
@@ -7,6 +7,11 @@
|
||||
// -----------------------------------------------------------
|
||||
require_once __DIR__ . "/config.php";
|
||||
|
||||
$emailtemplateConfigPath = __DIR__ . '/emailtemplate.conf.php';
|
||||
if (is_file($emailtemplateConfigPath)) {
|
||||
$GLOBALS['emailtemplate_config'] = require $emailtemplateConfigPath;
|
||||
}
|
||||
|
||||
|
||||
// Diese Werte später ins Template schieben:
|
||||
$GLOBALS['app_env'] = APP_ENV;
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . "/domaindata.php";
|
||||
|
||||
// Umgebung (optional, aber hilfreich für Debugging / Logik)
|
||||
define('APP_ENV', 'prod'); // oder 'prod', 'local', ...
|
||||
if (!defined('ASSET_VERSION')) {
|
||||
define('ASSET_VERSION', '2024-11-22'); // oder deine aktuelle Version
|
||||
$domaindataPath = __DIR__ . '/domaindata.php';
|
||||
if (!is_file($domaindataPath)) {
|
||||
$domaindataPath = __DIR__ . '/../domaindata.php';
|
||||
}
|
||||
require_once $domaindataPath;
|
||||
|
||||
define('APP_ENV', 'prod');
|
||||
if (!defined('ASSET_VERSION')) {
|
||||
define('ASSET_VERSION', '2024-11-22');
|
||||
}
|
||||
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
|
||||
if (!defined('APP_DOMAIN_PRIMARY')) {
|
||||
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
|
||||
}
|
||||
@@ -24,11 +28,9 @@ if (!defined('APP_URL_FAKECHECK')) {
|
||||
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
|
||||
}
|
||||
|
||||
// Matomo Einstellungen
|
||||
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
|
||||
define('MATOMO_ENABLED', true);
|
||||
define('MATOMO_SITE_ID', 7);
|
||||
$env = 'prod';
|
||||
|
||||
$baseUrl = 'https://' . APP_DOMAIN_NAME;
|
||||
$apiBaseUrl = 'https://api.' . APP_DOMAIN_NAME;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
$DB_HOST = 'localhost';
|
||||
$DB_NAME = 'd0455ede';
|
||||
$DB_USER = 'd0455ede';
|
||||
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||
$DB_NAME = 'd04582a5';
|
||||
$DB_USER = 'd04582a5';
|
||||
$DB_PASS = 'P§RuDQ2öh/(aiyW3ssPx'; // anpassen
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . "/domaindata.php";
|
||||
|
||||
// Umgebung (optional, aber hilfreich für Debugging / Logik)
|
||||
define('APP_ENV', 'staging'); // oder 'prod', 'local', ...
|
||||
$domaindataPath = __DIR__ . '/domaindata.php';
|
||||
if (!is_file($domaindataPath)) {
|
||||
$domaindataPath = __DIR__ . '/../domaindata.php';
|
||||
}
|
||||
require_once $domaindataPath;
|
||||
|
||||
define('APP_ENV', 'staging');
|
||||
|
||||
if (!defined('ASSET_VERSION')) {
|
||||
define('ASSET_VERSION', time()); // oder deine aktuelle Version
|
||||
define('ASSET_VERSION', time());
|
||||
}
|
||||
|
||||
// Domain-Konfiguration (kann pro Umgebung angepasst werden)
|
||||
if (!defined('APP_DOMAIN_PRIMARY')) {
|
||||
define('APP_DOMAIN_PRIMARY', 'staging.' . APP_DOMAIN_NAME);
|
||||
}
|
||||
@@ -27,10 +30,9 @@ if (!defined('APP_URL_FAKECHECK')) {
|
||||
define('APP_URL_FAKECHECK', 'https://' . APP_DOMAIN_FAKECHECK);
|
||||
}
|
||||
|
||||
// Matomo Einstellungen
|
||||
define('MATOMO_URL', 'https://matomo.my-statistics.info/');
|
||||
define('MATOMO_ENABLED', false);
|
||||
define('MATOMO_SITE_ID', 8);
|
||||
|
||||
$baseUrl = 'https://' . APP_DOMAIN_PRIMARY;
|
||||
$apiBaseUrl = 'https://api.' . APP_DOMAIN_PRIMARY;
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
$DB_HOST = 'localhost';
|
||||
$DB_NAME = 'd0455edf';
|
||||
$DB_USER = 'd0455edf';
|
||||
$DB_PASS = 'fF8PhxfCibdLBrSxowIo'; // anpassen
|
||||
$DB_NAME = 'd044ae9e';
|
||||
$DB_USER = 'd044ae9e';
|
||||
$DB_PASS = '9BVUn)Töcü@ÖVÜfgO8!J'; // anpassen
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
|
||||
@@ -1,657 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// 💡 NEUE KORREKTUR: Starte Output Buffering so früh wie möglich, um Whitespace/Errors
|
||||
// von inkludierten Dateien (AuthService.php, config.php) abzufangen.
|
||||
ob_start();
|
||||
|
||||
// Lade den AuthService
|
||||
require_once __DIR__ . '/AuthService.php';
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ApiKernel.php (OPTIMIERT & KORRIGIERT)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
class ApiKernel
|
||||
{
|
||||
// Klassen-Eigenschaften
|
||||
private array $conf;
|
||||
private ?PDO $pdo = null;
|
||||
private array $in;
|
||||
private string $action;
|
||||
private array $tableMap;
|
||||
private AuthService $authService;
|
||||
|
||||
// --- Initialisierung & Konstruktor (Optimiert) ---
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// ob_start() wurde an den Anfang der Datei verschoben und wird hier entfernt.
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->conf = $this->loadConfig();
|
||||
$this->cors();
|
||||
$this->setInput();
|
||||
$this->pdo = $this->getPdoTemplates();
|
||||
$this->resolveAction();
|
||||
$this->resolveTableMap();
|
||||
$this->authService = new AuthService($this->conf, $this->pdo);
|
||||
} catch (Throwable $e) {
|
||||
// Im Fehlerfall ruft fail() die respond() Methode auf, die den Header setzt und den Buffer leert.
|
||||
$this->fail('Initialization error', get_class($e) . ': ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Responder-Methoden (KORRIGIERT) ---
|
||||
|
||||
public function respond($data, int $code = 200): void
|
||||
{
|
||||
// 1. Output-Puffer leeren, um jeglichen unbeabsichtigten Output zu verwerfen (z.B. PHP Notices).
|
||||
if (ob_get_level() > 0) {
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
// 2. 💡 KRITISCHE KORREKTUR: Content-Type Header setzen.
|
||||
// Dies ist der entscheidende Schritt, der dem Browser sagt: "Dies ist JSON!"
|
||||
if (!headers_sent() && !isset($this->conf['no_content_type'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
http_response_code($code);
|
||||
echo is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function fail(string $msg, $detail = null, int $code = 400): void
|
||||
{
|
||||
$this->respond(['ok' => false, 'error' => $msg, 'detail' => $detail], $code);
|
||||
}
|
||||
|
||||
// --- Private Initialisierungs- & Utility-Methoden (Unverändert) ---
|
||||
|
||||
private function loadConfig(): array { /* ... Logik bleibt unverändert ... */
|
||||
$paths = [
|
||||
__DIR__ . '/config.php',
|
||||
__DIR__ . '/../config.php',
|
||||
__DIR__ . '/../../config.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
if (is_file($p)) {
|
||||
$conf = @include $p;
|
||||
if (is_array($conf)) return $conf;
|
||||
}
|
||||
}
|
||||
$this->fail('Invalid config.php', 'config.php not found or not returning array', 500);
|
||||
}
|
||||
private function cors(): void { /* ... Logik bleibt unverändert ... */
|
||||
$cors = $this->conf['cors'] ?? '*';
|
||||
if ($cors) {
|
||||
header('Access-Control-Allow-Origin: ' . $cors);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') $this->respond(['ok' => true]);
|
||||
|
||||
if (!empty($this->conf['auth']['cookie'])) {
|
||||
$c = $this->conf['auth']['cookie'];
|
||||
$params = session_get_cookie_params();
|
||||
$params['lifetime'] = $c['lifetime'] ?? $params['lifetime'];
|
||||
$params['path'] = $c['path'] ?? $params['path'];
|
||||
$params['domain'] = $c['domain'] ?? $params['domain'];
|
||||
$params['secure'] = $c['secure'] ?? $params['secure'];
|
||||
$params['httponly'] = $c['httponly'] ?? $params['httponly'];
|
||||
if (isset($c['samesite'])) $params['samesite'] = $c['samesite'];
|
||||
session_set_cookie_params($params);
|
||||
}
|
||||
}
|
||||
private function setInput(): void { /* ... Logik bleibt unverändert ... */
|
||||
$data = [];
|
||||
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (stripos($ct, 'application/json') !== false) {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw !== false && $raw !== '') {
|
||||
$js = json_decode($raw, true);
|
||||
if (is_array($js)) $data = $js;
|
||||
}
|
||||
}
|
||||
foreach ($_POST as $k => $v) $data[$k] = $v;
|
||||
foreach ($_GET as $k => $v) if (!array_key_exists($k, $data)) $data[$k] = $v;
|
||||
$this->in = $data;
|
||||
}
|
||||
private function getPdoTemplates(): PDO { /* ... Logik bleibt unverändert ... */
|
||||
if (!isset($this->conf['templates']) || !is_array($this->conf['templates'])) {
|
||||
$this->fail('Missing templates DB config', null, 500);
|
||||
}
|
||||
$c = $this->conf['templates'];
|
||||
$host = $c['db_host'] ?? 'localhost';
|
||||
$db = $c['db_name'] ?? ($c['database'] ?? '');
|
||||
$user = $c['db_user'] ?? ($c['username'] ?? '');
|
||||
$pass = $c['db_pass'] ?? ($c['password'] ?? '');
|
||||
$charset = $c['db_charset'] ?? 'utf8mb4';
|
||||
$port = $c['db_port'] ?? 3306;
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
];
|
||||
return new PDO($dsn, $user, $pass, $opt);
|
||||
}
|
||||
private function resolveAction(): void { /* ... Logik bleibt unverändert ... */
|
||||
$action = $this->val($this->in, 'action', '');
|
||||
$resource = $this->val($this->in, 'resource', null);
|
||||
$allowedResources = ['templates', 'sections', 'blocks', 'snippets'];
|
||||
if ($resource && in_array($resource, $allowedResources, true) && strpos((string)$action, '.') === false) {
|
||||
$verb = strtolower((string)$action);
|
||||
if (in_array($verb, ['list', 'get', 'create', 'update', 'delete'], true)) $action = $resource . '.' . $verb;
|
||||
}
|
||||
$this->action = $action;
|
||||
}
|
||||
private function resolveTableMap(): void { /* ... Logik bleibt unverändert ... */
|
||||
$tables = $this->conf['tables'] ?? [];
|
||||
$this->tableMap = [
|
||||
'templates' => $tables['templates'] ?? 'emailtemplate_templates',
|
||||
'sections' => $tables['sections'] ?? 'emailtemplate_sections',
|
||||
'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks',
|
||||
'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets',
|
||||
];
|
||||
}
|
||||
|
||||
private function val(array $in, $keys, $default = null) { /* ... Logik bleibt unverändert ... */
|
||||
if (!is_array($keys)) $keys = [$keys];
|
||||
foreach ($keys as $k) if (array_key_exists($k, $in)) return $in[$k];
|
||||
return $default;
|
||||
}
|
||||
private function firstExisting(array $columns, array $candidates): ?string { /* ... Logik bleibt unverändert ... */
|
||||
foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c;
|
||||
return null;
|
||||
}
|
||||
private function tableColumns(string $table): array { /* ... Logik bleibt unverändert ... */
|
||||
$cols = [];
|
||||
$stmt = $this->pdo->query("SHOW COLUMNS FROM `$table`");
|
||||
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
|
||||
return $cols;
|
||||
}
|
||||
private function primaryKey(string $table): ?string { /* ... Logik bleibt unverändert ... */
|
||||
$stmt = $this->pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
return $row['Column_name'] ?? null;
|
||||
}
|
||||
private function requireAuth(): array { /* ... Logik bleibt unverändert ... */
|
||||
return $this->authService->requireAuth();
|
||||
}
|
||||
private function pullId(array $src) { /* ... Logik bleibt unverändert ... */
|
||||
$aliases = ['id', 'item_id', 'template_id', 'tpl_id', 'section_id', 'sec_id', 'block_id', 'blk_id', 'snippet_id', 'snip_id'];
|
||||
foreach ($aliases as $a) if (isset($src[$a]) && $src[$a] !== '') return $src[$a];
|
||||
return null;
|
||||
}
|
||||
private function tenantWhere(array $session): array { /* ... Logik bleibt unverändert ... */
|
||||
$multi = $this->conf['multi'] ?? [];
|
||||
$tenantCol = $multi['tenant_col'] ?? null;
|
||||
$mapSess = $multi['map_session_to'] ?? 'id';
|
||||
|
||||
if (!$tenantCol) return ['', []];
|
||||
if (!$session) return [' AND 1=0 ', []];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
if ($val === null || $val === '') return [' AND 1=0 ', []];
|
||||
return [" AND `$tenantCol` = :__tenant", [':__tenant' => $val]];
|
||||
}
|
||||
private function tenantAssign(array $session, array $columns): array { /* ... Logik bleibt unverändert ... */
|
||||
$multi = $this->conf['multi'] ?? [];
|
||||
$tenantCol = $multi['tenant_col'] ?? null;
|
||||
$mapSess = $multi['map_session_to'] ?? 'id';
|
||||
|
||||
if (!$tenantCol || !in_array($tenantCol, $columns, true)) return [];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
return ($val === null || $val === '') ? [] : [$tenantCol => $val];
|
||||
}
|
||||
private function resolveIdCol(string $kind): array { /* ... Logik bleibt unverändert ... */
|
||||
$t = $this->tableMap[$kind];
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$cols = $this->tableColumns($t);
|
||||
$idCol = $cfg['id'] ?? ($this->firstExisting($cols, ['id']) ?: $this->primaryKey($t));
|
||||
if (!$idCol) $idCol = 'id';
|
||||
return [$idCol, $cols];
|
||||
}
|
||||
private function parseHtmlToGjsComponents(string $html): array { /* ... Logik bleibt unverändert ... */
|
||||
if (trim($html) === '') return [];
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 'html',
|
||||
'content' => $html,
|
||||
'removable' => true,
|
||||
'draggable' => true,
|
||||
'droppable' => true,
|
||||
'copyable' => true,
|
||||
'selectable' => true,
|
||||
'editable' => false,
|
||||
'traits' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 💡 Bereinigungsmethode
|
||||
private function cleanReferenceComponents(array $components): array {
|
||||
foreach ($components as &$component) {
|
||||
if (is_array($component) && isset($component['type'])) {
|
||||
|
||||
if ($component['type'] === 'library-reference') {
|
||||
if (isset($component['content'])) {
|
||||
$component['content'] = '';
|
||||
}
|
||||
if (isset($component['components'])) {
|
||||
$component['components'] = [];
|
||||
}
|
||||
}
|
||||
if (isset($component['components']) && is_array($component['components'])) {
|
||||
$component['components'] = $this->cleanReferenceComponents($component['components']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $components;
|
||||
}
|
||||
|
||||
|
||||
// =================================================================
|
||||
// 🚀 CRUD HANDLER METHODEN
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von LIST-Anfragen.
|
||||
*/
|
||||
private function handleList(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
$q = trim((string)$this->val($this->in, 'q', ''));
|
||||
$limit = max(1, (int)$this->val($this->in, 'limit', 500));
|
||||
$offset = max(0, (int)$this->val($this->in, 'offset', 0));
|
||||
|
||||
$where = ' WHERE 1=1 ';
|
||||
$params = [];
|
||||
// Suchlogik (q)
|
||||
if ($q !== '') {
|
||||
$parts = ["`$nameCol` LIKE :q"];
|
||||
if ($descCol) $parts[] = "`$descCol` LIKE :q";
|
||||
if ($catCol) $parts[] = "`$catCol` LIKE :q";
|
||||
$where .= " AND (" . implode(' OR ', $parts) . ") ";
|
||||
$params[':q'] = '%' . $q . '%';
|
||||
}
|
||||
|
||||
// Filterlogik (parentFilters)
|
||||
$parentFilters = [
|
||||
'template_id' => $this->val($this->in, ['template_id', 'tpl_id'], null),
|
||||
'section_id' => $this->val($this->in, ['section_id', 'sec_id'], null),
|
||||
'block_id' => $this->val($this->in, ['block_id', 'blk_id'], null),
|
||||
];
|
||||
foreach ($parentFilters as $col => $v) {
|
||||
if ($v === null || $v === '') continue;
|
||||
if (in_array($col, $allCols, true)) { $where .= " AND `$col` = :$col "; $params[":$col"] = $v; }
|
||||
}
|
||||
|
||||
// Tenant-Filter
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$where .= $tw;
|
||||
foreach ($tp as $k => $v) $params[$k] = $v;
|
||||
|
||||
$order = $updCol ? " ORDER BY `$updCol` DESC " : " ORDER BY `$nameCol` ASC ";
|
||||
$sql = "SELECT * FROM `$t` $where $order LIMIT :off,:lim";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
// Bind parameters
|
||||
foreach ($params as $k => $v) $stmt->bindValue($k, $v, is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$item = [
|
||||
'id' => $r[$idCol] ?? null,
|
||||
'name' => $r[$nameCol] ?? null,
|
||||
];
|
||||
if ($descCol && isset($r[$descCol])) $item['desc'] = $r[$descCol];
|
||||
if ($catCol && isset($r[$catCol])) $item['category'] = $r[$catCol];
|
||||
if ($updCol && isset($r[$updCol])) $item['updated_at'] = $r[$updCol];
|
||||
|
||||
// Lade HTML und JSON aus den korrekten Spalten
|
||||
$htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']);
|
||||
if ($htmlCol && isset($r[$htmlCol])) $item['html'] = (string)$r[$htmlCol];
|
||||
$jsonCol = $this->firstExisting($allCols, ['json_content']);
|
||||
if ($jsonCol && isset($r[$jsonCol])) $item['content'] = $r[$jsonCol];
|
||||
|
||||
$out[] = $item;
|
||||
}
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'items' => $out, 'data' => $out, 'count' => count($out), 'offset' => $offset, 'limit' => $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von GET-Anfragen.
|
||||
*/
|
||||
private function handleGet(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$sql = "SELECT * FROM `$t` WHERE `$idCol` = :id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) $this->fail('Not found', ['kind' => $kind, 'id' => $id], 404);
|
||||
$rowOut = ['id' => $row[$idCol] ?? $id] + $row;
|
||||
|
||||
// Lade HTML und JSON aus den korrekten Spalten
|
||||
$htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']);
|
||||
$topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null;
|
||||
$jsonCol = $this->firstExisting($allCols, ['json_content']);
|
||||
$topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null;
|
||||
|
||||
$gjsComponents = [];
|
||||
|
||||
if ($topContent !== null) {
|
||||
$decodedContent = json_decode($topContent, true);
|
||||
if (is_array($decodedContent)) {
|
||||
$gjsComponents = $decodedContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($gjsComponents) && $topHtml !== null) {
|
||||
$gjsComponents = $this->parseHtmlToGjsComponents($topHtml);
|
||||
}
|
||||
|
||||
$this->respond([
|
||||
'ok' => true,
|
||||
'kind' => $kind,
|
||||
'id' => $rowOut['id'],
|
||||
'item' => $rowOut,
|
||||
'data' => $rowOut,
|
||||
'html' => $topHtml,
|
||||
'content' => $topContent,
|
||||
'gjs_components' => $gjsComponents
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von CREATE-Anfragen (inkl. JSON-Bereinigung).
|
||||
*/
|
||||
private function handleCreate(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
|
||||
$name = trim((string)$this->val($this->in, ['name', 'title'], ''));
|
||||
if ($name === '') $this->fail('name required', null, 422);
|
||||
|
||||
$desc = (string)$this->val($this->in, ['description', 'desc'], null);
|
||||
$cat = (string)$this->val($this->in, ['category', 'cat'], null);
|
||||
$html = (string)$this->val($this->in, ['html', 'body', 'markup'], null);
|
||||
$json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null);
|
||||
$settings = $this->val($this->in, ['settings_json', 'settings'], null);
|
||||
$templateId = $this->val($this->in, ['template_id', 'tpl_id'], null);
|
||||
$sectionId = $this->val($this->in, ['section_id', 'sec_id'], null);
|
||||
$blockId = $this->val($this->in, ['block_id', 'blk_id'], null);
|
||||
|
||||
$data = [$nameCol => $name];
|
||||
if ($desc !== null && $descCol) $data[$descCol] = $desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = $cat;
|
||||
|
||||
$htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup']));
|
||||
$jsonDbCol = $this->firstExisting($allCols, ['json_content']);
|
||||
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG START ---
|
||||
|
||||
// 1. JSON-Content behandeln
|
||||
if ($json !== null) {
|
||||
if ($jsonDbCol) {
|
||||
$components = is_string($json) ? json_decode($json, true) : $json;
|
||||
if (is_array($components)) {
|
||||
$components = $this->cleanReferenceComponents($components); // BEREINIGUNG
|
||||
$data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$data[$jsonDbCol] = is_string($json) ? $json : '';
|
||||
}
|
||||
} else {
|
||||
// FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben
|
||||
$this->fail(
|
||||
'JSON content provided but no `json_content` column found',
|
||||
['table' => $t, 'available_cols' => $allCols],
|
||||
422
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. HTML-Content speichern
|
||||
if ($htmlDbCol && $html !== null) {
|
||||
$data[$htmlDbCol] = $html;
|
||||
}
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
|
||||
|
||||
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
|
||||
if ($c && $settings !== null) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($templateId !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $templateId;
|
||||
if ($sectionId !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sectionId;
|
||||
if ($blockId !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blockId;
|
||||
|
||||
$data = $data + $this->tenantAssign($_SESSION['auth'] ?? [], $allCols);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$createdCol = $this->firstExisting($allCols, ['created_at', 'created', 'createdAt']);
|
||||
if ($createdCol) $data[$createdCol] = $now;
|
||||
if ($updCol) $data[$updCol] = $now;
|
||||
|
||||
$fields = array_keys($data);
|
||||
$place = array_map(fn($c) => ":$c", $fields);
|
||||
$sql = "INSERT INTO `$t` (" . implode(',', array_map(fn($c) => "`$c`", $fields)) . ") VALUES (" . implode(',', $place) . ")";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->execute();
|
||||
$newId = $this->pdo->lastInsertId();
|
||||
|
||||
$out = ['id' => $newId, 'name' => $name];
|
||||
if ($desc !== null) $out['desc'] = $desc;
|
||||
if ($cat !== null) $out['category'] = $cat;
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $newId, 'item' => $out, 'data' => $out]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von UPDATE-Anfragen (inkl. JSON-Bereinigung).
|
||||
*/
|
||||
private function handleUpdate(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
$data = [];
|
||||
$name = $this->val($this->in, ['name', 'title'], null);
|
||||
$desc = $this->val($this->in, ['description', 'desc'], null);
|
||||
$cat = $this->val($this->in, ['category', 'cat'], null);
|
||||
$html = $this->val($this->in, ['html', 'body', 'markup'], null);
|
||||
$json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null);
|
||||
$settings = $this->val($this->in, ['settings_json', 'settings'], null);
|
||||
|
||||
if ($name !== null) $data[$nameCol] = (string)$name;
|
||||
if ($desc !== null && $descCol) $data[$descCol] = (string)$desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = (string)$cat;
|
||||
|
||||
$htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup']));
|
||||
$jsonDbCol = $this->firstExisting($allCols, ['json_content']);
|
||||
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG START ---
|
||||
|
||||
// 1. JSON-Content behandeln
|
||||
if ($json !== null) {
|
||||
if ($jsonDbCol) {
|
||||
// Wenn JSON-Spalte existiert, JSON verarbeiten und speichern
|
||||
$components = is_string($json) ? json_decode($json, true) : $json;
|
||||
if (is_array($components)) {
|
||||
$components = $this->cleanReferenceComponents($components); // BEREINIGUNG
|
||||
$data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$data[$jsonDbCol] = is_string($json) ? $json : '';
|
||||
}
|
||||
} else {
|
||||
// FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben
|
||||
$this->fail(
|
||||
'JSON content provided but no `json_content` column found',
|
||||
['table' => $t, 'available_cols' => $allCols],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Den zugehörigen HTML-Output speichern (wird vom Editor immer mitgesendet, wenn JSON da ist)
|
||||
if ($html !== null && $htmlDbCol) {
|
||||
$data[$htmlDbCol] = (string)$html;
|
||||
}
|
||||
} elseif ($html !== null && $htmlDbCol) {
|
||||
// Wenn NUR HTML gesendet wird (für minimale Änderungen), speichern wir nur HTML.
|
||||
$data[$htmlDbCol] = (string)$html;
|
||||
}
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
|
||||
|
||||
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
|
||||
if ($settings !== null && $c) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$tpl = $this->val($this->in, ['template_id', 'tpl_id'], null);
|
||||
if ($tpl !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $tpl;
|
||||
$sec = $this->val($this->in, ['section_id', 'sec_id'], null);
|
||||
if ($sec !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sec;
|
||||
$blk = $this->val($this->in, ['block_id', 'blk_id'], null);
|
||||
if ($blk !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blk;
|
||||
|
||||
if ($updCol) $data[$updCol] = date('Y-m-d H:i:s');
|
||||
if (!$data) $this->fail('nothing to update', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$set = [];
|
||||
foreach (array_keys($data) as $c) $set[] = "`$c` = :$c";
|
||||
$sql = "UPDATE `$t` SET " . implode(',', $set) . " WHERE `$idCol` = :__id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'updated' => array_keys($data)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von DELETE-Anfragen.
|
||||
*/
|
||||
private function handleDelete(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$sql = "DELETE FROM `$t` WHERE `$idCol` = :__id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'deleted' => true]);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 💡 Öffentliche run()-Methode (KORRIGIERT)
|
||||
// =================================================================
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// 💡 KORREKTUR: Der Content-Type Header wird hier entfernt, da er jetzt in respond()
|
||||
// zentralisiert wurde, um sicherzustellen, dass er auch bei Fehlern im Konstruktor oder
|
||||
// im try-Block korrekt gesetzt wird.
|
||||
// header('Content-Type: application/json; charset=utf-8'); // DIESE ZEILE ENTFERNT
|
||||
|
||||
try {
|
||||
// Extrahiere den Ressourcen-Typ und die Operation (z.B. 'templates' und 'list')
|
||||
[$kind, $operation] = explode('.', $this->action, 2) + [1 => ''];
|
||||
|
||||
switch ($this->action) {
|
||||
case 'health':
|
||||
$this->respond(['ok' => true, 'time' => date('c')]);
|
||||
|
||||
/* ---------- AUTH ---------- */
|
||||
case 'auth.login':
|
||||
$result = $this->authService->login($this->in);
|
||||
$this->respond(['ok' => true] + $result);
|
||||
break;
|
||||
case 'auth.me':
|
||||
if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401);
|
||||
$this->respond(['ok' => true, 'user' => $_SESSION['auth']]);
|
||||
break;
|
||||
case 'auth.logout':
|
||||
$this->authService->logout();
|
||||
$this->respond(['ok' => true]);
|
||||
break;
|
||||
|
||||
/* ---------- CRUD HANDLER ---------- */
|
||||
default:
|
||||
if (in_array($kind, ['templates', 'sections', 'blocks', 'snippets'])) {
|
||||
switch ($operation) {
|
||||
case 'list':
|
||||
$this->handleList($kind);
|
||||
break;
|
||||
case 'get':
|
||||
$this->handleGet($kind);
|
||||
break;
|
||||
case 'create':
|
||||
$this->handleCreate($kind);
|
||||
break;
|
||||
case 'update':
|
||||
$this->handleUpdate($kind);
|
||||
break;
|
||||
case 'delete':
|
||||
$this->handleDelete($kind);
|
||||
break;
|
||||
default:
|
||||
$this->fail('Unknown operation for resource: ' . $this->action, null, 404);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$this->fail('Unknown action', $this->action ?: 'missing', 404);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->fail('Server error', get_class($e) . ': ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
require_once __DIR__ . '/../src/ApiKernel.php';
|
||||
|
||||
@@ -1,105 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// AuthService.php: Kapselt die gesamte Authentifizierungslogik.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
class AuthService
|
||||
{
|
||||
private array $conf;
|
||||
private PDO $pdo;
|
||||
|
||||
// Abhängigkeiten (Konfiguration und PDO) werden per Konstruktor übergeben
|
||||
public function __construct(array $conf, PDO $pdo)
|
||||
{
|
||||
$this->conf = $conf;
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
// --- Private Utility Methoden ---
|
||||
|
||||
private function fail(string $msg, $detail = null, int $code = 400): void
|
||||
{
|
||||
// Wir müssen hier direkt antworten, da wir das Fail-Verhalten des Kernels benötigen.
|
||||
// Im ApiKernel werden wir die respond/fail-Methoden als public lassen,
|
||||
// um sie hier injizieren zu können, oder wir lassen sie hier im Global Scope
|
||||
// (WENN Sie die ursprünglichen globalen Funktionen respond/fail wieder zulassen).
|
||||
// Für eine saubere Kapselung injizieren wir die Respond-Logik.
|
||||
// HIER verwenden wir eine einfache JSON-Antwort, da die fail-Methode
|
||||
// normalerweise den gesamten Kernel stoppt. Wir nutzen exit.
|
||||
|
||||
http_response_code($code);
|
||||
echo json_encode(['ok'=>false,'error'=>$msg,'detail'=>$detail], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function verifyPassword(string $input, string $stored, array $authDbConf): bool
|
||||
{
|
||||
if (preg_match('~^\$2[aby]\$~', $stored) || strpos($stored, '$argon2') === 0) return password_verify($input, $stored);
|
||||
$legacy = strtolower($authDbConf['legacy'] ?? '');
|
||||
if ($legacy === 'md5') return hash_equals($stored, md5($input));
|
||||
if ($legacy === 'sha1') return hash_equals($stored, sha1($input));
|
||||
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
|
||||
return hash_equals($stored, $input);
|
||||
}
|
||||
|
||||
// --- Public Service Methoden ---
|
||||
|
||||
public function requireAuth(): array
|
||||
{
|
||||
if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401);
|
||||
return $_SESSION['auth'];
|
||||
}
|
||||
|
||||
public function logout(): bool
|
||||
{
|
||||
$_SESSION = [];
|
||||
if (session_id() !== '') session_destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(array $in): array
|
||||
{
|
||||
$authDb = $this->conf['auth']['db'] ?? [];
|
||||
$colUser = $authDb['col_user'] ?? 'email';
|
||||
$colPass = $authDb['col_pass'] ?? 'password';
|
||||
$colName = $authDb['col_name'] ?? 'name';
|
||||
$colId = $authDb['col_id'] ?? 'id';
|
||||
$colStatus = $authDb['col_status']?? null;
|
||||
$activeValues = $authDb['active_values'] ?? ['active','1',1];
|
||||
$table = $authDb['table'] ?? 'emailtemplate_users';
|
||||
|
||||
$identifier = trim((string)($in['username'] ?? $in['user'] ?? $in['email'] ?? $in['login'] ?? ''));
|
||||
$password = (string)($in['password'] ?? $in['pass'] ?? $in['pwd'] ?? '');
|
||||
|
||||
if ($identifier === '' || $password === '') $this->fail('username/password required', null, 422);
|
||||
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1");
|
||||
$stmt->execute([':u'=>$identifier]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) $this->fail('Invalid credentials', null, 401);
|
||||
|
||||
if ($colStatus && isset($row[$colStatus])) {
|
||||
if (!in_array($row[$colStatus], $activeValues, true)) {
|
||||
$this->fail('Account inactive', null, 403);
|
||||
}
|
||||
}
|
||||
|
||||
$stored = (string)($row[$colPass] ?? '');
|
||||
if ($stored === '' || !$this->verifyPassword($password, $stored, $authDb)) {
|
||||
$this->fail('Invalid credentials', null, 401);
|
||||
}
|
||||
|
||||
$_SESSION['auth'] = [
|
||||
'id' => $row[$colId] ?? null,
|
||||
'name' => $row[$colName] ?? ($row[$colUser] ?? $identifier),
|
||||
'email' => $row[$colUser] ?? $identifier,
|
||||
'at' => time(),
|
||||
];
|
||||
|
||||
$token = base64_encode(hash('sha256', ($_SESSION['auth']['id'] ?? $identifier).'|'.session_id(), true));
|
||||
return ['user'=>$_SESSION['auth'], 'token'=>$token];
|
||||
}
|
||||
}
|
||||
require_once __DIR__ . '/../src/AuthService.php';
|
||||
|
||||
@@ -1,76 +1,5 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
return [
|
||||
'templates' => [
|
||||
'db_host' => getenv('DB_TPL_HOST') ?: 'localhost',
|
||||
'db_name' => getenv('DB_TPL_NAME') ?: 'd044ae9e',
|
||||
'db_user' => getenv('DB_TPL_USER') ?: 'd044ae9e',
|
||||
'db_pass' => getenv('DB_TPL_PASS') ?: '9BVUn)Töcü@ÖVÜfgO8!J',
|
||||
'db_charset' => 'utf8',
|
||||
'prefix' => getenv('DB_TPL_PREFIX') ?: 'emailtemplate_',
|
||||
|
||||
],
|
||||
'project' => [
|
||||
'db_host' => getenv('DB_TPL_HOST') ?: 'w0207fd0.kasserver.com',
|
||||
'db_name' => getenv('DB_TPL_NAME') ?: 'd0444c25',
|
||||
'db_user' => getenv('DB_TPL_USER') ?: 'd0444c25',
|
||||
'db_pass' => getenv('DB_TPL_PASS') ?: '/7ü9+§ÄfkiQvGPr§2Op7',
|
||||
'db_charset' => 'utf8',
|
||||
],
|
||||
'cors' => getenv('CORS_ORIGIN') ?: '*',
|
||||
'env' => 'staging',
|
||||
'base_url' => 'https://staging.emailtemplate.it',
|
||||
'auth' => [
|
||||
'session_name' => 'et_session',
|
||||
'cookie_domain' => 'staging.emailtemplate.it',
|
||||
'cookie_secure' => true,
|
||||
'cookie_httponly'=> true,
|
||||
'cookie_samesite'=> 'Lax',
|
||||
'db' => [
|
||||
'table' => 'customer_users',
|
||||
'col_user' => 'email', // alternativ: 'username'
|
||||
'col_pass' => 'password_hash',
|
||||
'col_name' => 'name', // optional
|
||||
'col_id' => 'id', // optional
|
||||
'col_status' => 'is_active', // optional
|
||||
'active_values'=> ['active','1',1], // optional
|
||||
'legacy' => 'md5' // optional: 'md5' | 'sha1' | 'plain' (sonst bcrypt/argon2)
|
||||
],
|
||||
|
||||
],
|
||||
'smtp' => [
|
||||
'host' => 'smtp.example.com',
|
||||
'port' => 587,
|
||||
'user' => 'smtp-user',
|
||||
'pass' => 'smtp-pass',
|
||||
'secure' => 'tls', // oder 'ssl'
|
||||
'from_email' => 'no-reply@example.com',
|
||||
'from_name' => 'EmailTemplate',
|
||||
],
|
||||
'export' => [
|
||||
'api_keys' => ['dev-key-123', 'noch-ein-key'], // füge hier deine Keys ein
|
||||
],
|
||||
'multi' => [
|
||||
// Spalte in ALLEN Content-Tabellen, die dem Besitzer/Mandanten entspricht:
|
||||
'tenant_col' => 'customer_id', // <— falls es bei dir z. B. 'owner_id' heißt: entsprechend anpassen.
|
||||
// Welche Session-Info darauf gemappt wird:
|
||||
'map_session_to' => 'id', // 'id' (Default) | 'email' | 'name'
|
||||
],
|
||||
|
||||
// optional: abweichende Tabellennamen/Spalten:
|
||||
'tables' => [
|
||||
'templates' => 'emailtemplate_templates',
|
||||
'sections' => 'emailtemplate_sections',
|
||||
'blocks' => 'emailtemplate_blocks',
|
||||
'snippets' => 'emailtemplate_snippets',
|
||||
],
|
||||
'columns' => [
|
||||
// Nur anpassen, wenn deine Spaltennamen abweichen
|
||||
'templates' => ['id'=>'id','name'=>'name','desc'=>null,'cat'=>null,'upd'=>'updated_at'],
|
||||
'sections' => ['id'=>'id','name'=>'name','cat'=>null,'upd'=>'updated_at'],
|
||||
'blocks' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
'snippets' => ['id'=>'id','name'=>'name','cat'=>'category','upd'=>'updated_at'],
|
||||
],
|
||||
];
|
||||
|
||||
// Legacy stub that keeps existing include paths working.
|
||||
return require __DIR__ . '/../config/emailtemplate.conf.php';
|
||||
|
||||
@@ -8,7 +8,7 @@ if (is_file($composerAutoload)) {
|
||||
}
|
||||
|
||||
// 2. Lade die Service-Klasse (API-Kernel)
|
||||
require_once __DIR__ . '/../inc/ApiKernel.php';
|
||||
require_once __DIR__ . '/../src/ApiKernel.php';
|
||||
|
||||
// 3. Erstelle eine Instanz und führe sie aus
|
||||
$api = new ApiKernel();
|
||||
|
||||
@@ -3,13 +3,12 @@ header('Content-Type: text/html; charset=utf-8');
|
||||
$conf = @include __DIR__ . '/../../inc/config.php';
|
||||
function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8'); }
|
||||
|
||||
if (!is_array($conf) || !isset($conf['templates'])) {
|
||||
echo 'Invalid config.php (expected return array with keys templates/project)'; exit;
|
||||
if (!is_array($conf) || !isset($conf['projectdb'])) {
|
||||
echo 'Invalid config.php (expected return array with key projectdb)'; exit;
|
||||
}
|
||||
|
||||
$profile = $_GET['profile'] ?? 'templates';
|
||||
$cfg = ($profile==='project') ? ($conf['project'] ?? null) : $conf['templates'];
|
||||
$prefix = (string)(($profile==='project') ? ($conf['project']['prefix'] ?? '') : ($conf['templates']['prefix'] ?? ''));
|
||||
$cfg = $conf['projectdb'];
|
||||
$prefix = (string)($cfg['prefix'] ?? '');
|
||||
|
||||
$attempts=[]; $pdo=null;
|
||||
$mkPdo=function(array $cfg) use(&$attempts){
|
||||
@@ -57,7 +56,7 @@ if ($pdo){
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<meta charset="utf-8">
|
||||
<title>DB-Doctor (<?=h($profile)?>)</title>
|
||||
<title>DB-Doctor</title>
|
||||
<style>
|
||||
body{font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#f8fafc;color:#0f172a;margin:0;padding:24px;}
|
||||
.nav a{display:inline-block;margin-right:8px;padding:8px 12px;border:1px solid #e5e7eb;border-radius:8px;background:#fff;text-decoration:none;color:#0f172a}
|
||||
@@ -69,12 +68,7 @@ if ($pdo){
|
||||
code{background:#0b1020;color:#e5e7eb;padding:2px 6px;border-radius:6px}
|
||||
</style>
|
||||
|
||||
<h1>DB-Doctor <small style="font-weight:400;color:#475569">(Profil: <?=h($profile)?>)</small></h1>
|
||||
|
||||
<div class="nav">
|
||||
<a href="?profile=templates" class="<?= $profile==='templates'?'active':'' ?>">Templates</a>
|
||||
<a href="?profile=project" class="<?= $profile==='project' ?'active':'' ?>">Project</a>
|
||||
</div>
|
||||
<h1>DB-Doctor</h1>
|
||||
|
||||
<div class="card">
|
||||
<h3>Verbindungsversuche</h3>
|
||||
@@ -115,4 +109,3 @@ if ($pdo){
|
||||
], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES))?></pre>
|
||||
</div>
|
||||
</html>
|
||||
|
||||
|
||||
659
src/ApiKernel.php
Normal file
659
src/ApiKernel.php
Normal file
@@ -0,0 +1,659 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// 💡 NEUE KORREKTUR: Starte Output Buffering so früh wie möglich, um Whitespace/Errors
|
||||
// von inkludierten Dateien (AuthService.php, config.php) abzufangen.
|
||||
ob_start();
|
||||
|
||||
// Lade den AuthService
|
||||
require_once __DIR__ . '/AuthService.php';
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// ApiKernel.php (OPTIMIERT & KORRIGIERT)
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
class ApiKernel
|
||||
{
|
||||
// Klassen-Eigenschaften
|
||||
private array $conf;
|
||||
private ?PDO $pdo = null;
|
||||
private array $in;
|
||||
private string $action;
|
||||
private array $tableMap;
|
||||
private AuthService $authService;
|
||||
|
||||
// --- Initialisierung & Konstruktor (Optimiert) ---
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// ob_start() wurde an den Anfang der Datei verschoben und wird hier entfernt.
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
try {
|
||||
$this->conf = $this->loadConfig();
|
||||
$this->cors();
|
||||
$this->setInput();
|
||||
$this->pdo = $this->getPdoTemplates();
|
||||
$this->resolveAction();
|
||||
$this->resolveTableMap();
|
||||
$this->authService = new AuthService($this->conf, $this->pdo);
|
||||
} catch (Throwable $e) {
|
||||
// Im Fehlerfall ruft fail() die respond() Methode auf, die den Header setzt und den Buffer leert.
|
||||
$this->fail('Initialization error', get_class($e) . ': ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Responder-Methoden (KORRIGIERT) ---
|
||||
|
||||
public function respond($data, int $code = 200): void
|
||||
{
|
||||
// 1. Output-Puffer leeren, um jeglichen unbeabsichtigten Output zu verwerfen (z.B. PHP Notices).
|
||||
if (ob_get_level() > 0) {
|
||||
ob_clean();
|
||||
}
|
||||
|
||||
// 2. 💡 KRITISCHE KORREKTUR: Content-Type Header setzen.
|
||||
// Dies ist der entscheidende Schritt, der dem Browser sagt: "Dies ist JSON!"
|
||||
if (!headers_sent() && !isset($this->conf['no_content_type'])) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
http_response_code($code);
|
||||
echo is_string($data) ? $data : json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function fail(string $msg, $detail = null, int $code = 400): void
|
||||
{
|
||||
$this->respond(['ok' => false, 'error' => $msg, 'detail' => $detail], $code);
|
||||
}
|
||||
|
||||
// --- Private Initialisierungs- & Utility-Methoden (Unverändert) ---
|
||||
|
||||
private function loadConfig(): array { /* ... Logik bleibt unverändert ... */
|
||||
$paths = [
|
||||
__DIR__ . '/../config/emailtemplate.conf.php',
|
||||
__DIR__ . '/../inc/config.php',
|
||||
__DIR__ . '/config.php',
|
||||
__DIR__ . '/../config.php',
|
||||
__DIR__ . '/../../config.php',
|
||||
];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
if (is_file($p)) {
|
||||
$conf = @include $p;
|
||||
if (is_array($conf)) return $conf;
|
||||
}
|
||||
}
|
||||
$this->fail('Invalid config', 'config file not found or not returning array', 500);
|
||||
}
|
||||
private function cors(): void { /* ... Logik bleibt unverändert ... */
|
||||
$cors = $this->conf['cors'] ?? '*';
|
||||
if ($cors) {
|
||||
header('Access-Control-Allow-Origin: ' . $cors);
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, Authorization');
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'OPTIONS') $this->respond(['ok' => true]);
|
||||
|
||||
if (!empty($this->conf['auth']['cookie'])) {
|
||||
$c = $this->conf['auth']['cookie'];
|
||||
$params = session_get_cookie_params();
|
||||
$params['lifetime'] = $c['lifetime'] ?? $params['lifetime'];
|
||||
$params['path'] = $c['path'] ?? $params['path'];
|
||||
$params['domain'] = $c['domain'] ?? $params['domain'];
|
||||
$params['secure'] = $c['secure'] ?? $params['secure'];
|
||||
$params['httponly'] = $c['httponly'] ?? $params['httponly'];
|
||||
if (isset($c['samesite'])) $params['samesite'] = $c['samesite'];
|
||||
session_set_cookie_params($params);
|
||||
}
|
||||
}
|
||||
private function setInput(): void { /* ... Logik bleibt unverändert ... */
|
||||
$data = [];
|
||||
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
|
||||
if (stripos($ct, 'application/json') !== false) {
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw !== false && $raw !== '') {
|
||||
$js = json_decode($raw, true);
|
||||
if (is_array($js)) $data = $js;
|
||||
}
|
||||
}
|
||||
foreach ($_POST as $k => $v) $data[$k] = $v;
|
||||
foreach ($_GET as $k => $v) if (!array_key_exists($k, $data)) $data[$k] = $v;
|
||||
$this->in = $data;
|
||||
}
|
||||
private function getPdoTemplates(): PDO { /* ... Logik bleibt unverändert ... */
|
||||
if (!isset($this->conf['projectdb']) || !is_array($this->conf['projectdb'])) {
|
||||
$this->fail('Missing project DB config', null, 500);
|
||||
}
|
||||
$c = $this->conf['projectdb'];
|
||||
$host = $c['db_host'] ?? 'localhost';
|
||||
$db = $c['db_name'] ?? ($c['database'] ?? '');
|
||||
$user = $c['db_user'] ?? ($c['username'] ?? '');
|
||||
$pass = $c['db_pass'] ?? ($c['password'] ?? '');
|
||||
$charset = $c['db_charset'] ?? 'utf8mb4';
|
||||
$port = $c['db_port'] ?? 3306;
|
||||
$dsn = "mysql:host=$host;port=$port;dbname=$db;charset=$charset";
|
||||
$opt = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
];
|
||||
return new PDO($dsn, $user, $pass, $opt);
|
||||
}
|
||||
private function resolveAction(): void { /* ... Logik bleibt unverändert ... */
|
||||
$action = $this->val($this->in, 'action', '');
|
||||
$resource = $this->val($this->in, 'resource', null);
|
||||
$allowedResources = ['templates', 'sections', 'blocks', 'snippets'];
|
||||
if ($resource && in_array($resource, $allowedResources, true) && strpos((string)$action, '.') === false) {
|
||||
$verb = strtolower((string)$action);
|
||||
if (in_array($verb, ['list', 'get', 'create', 'update', 'delete'], true)) $action = $resource . '.' . $verb;
|
||||
}
|
||||
$this->action = $action;
|
||||
}
|
||||
private function resolveTableMap(): void { /* ... Logik bleibt unverändert ... */
|
||||
$tables = $this->conf['tables'] ?? [];
|
||||
$this->tableMap = [
|
||||
'templates' => $tables['templates'] ?? 'emailtemplate_templates',
|
||||
'sections' => $tables['sections'] ?? 'emailtemplate_sections',
|
||||
'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks',
|
||||
'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets',
|
||||
];
|
||||
}
|
||||
|
||||
private function val(array $in, $keys, $default = null) { /* ... Logik bleibt unverändert ... */
|
||||
if (!is_array($keys)) $keys = [$keys];
|
||||
foreach ($keys as $k) if (array_key_exists($k, $in)) return $in[$k];
|
||||
return $default;
|
||||
}
|
||||
private function firstExisting(array $columns, array $candidates): ?string { /* ... Logik bleibt unverändert ... */
|
||||
foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c;
|
||||
return null;
|
||||
}
|
||||
private function tableColumns(string $table): array { /* ... Logik bleibt unverändert ... */
|
||||
$cols = [];
|
||||
$stmt = $this->pdo->query("SHOW COLUMNS FROM `$table`");
|
||||
foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field'];
|
||||
return $cols;
|
||||
}
|
||||
private function primaryKey(string $table): ?string { /* ... Logik bleibt unverändert ... */
|
||||
$stmt = $this->pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'");
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
return $row['Column_name'] ?? null;
|
||||
}
|
||||
private function requireAuth(): array { /* ... Logik bleibt unverändert ... */
|
||||
return $this->authService->requireAuth();
|
||||
}
|
||||
private function pullId(array $src) { /* ... Logik bleibt unverändert ... */
|
||||
$aliases = ['id', 'item_id', 'template_id', 'tpl_id', 'section_id', 'sec_id', 'block_id', 'blk_id', 'snippet_id', 'snip_id'];
|
||||
foreach ($aliases as $a) if (isset($src[$a]) && $src[$a] !== '') return $src[$a];
|
||||
return null;
|
||||
}
|
||||
private function tenantWhere(array $session): array { /* ... Logik bleibt unverändert ... */
|
||||
$multi = $this->conf['multi'] ?? [];
|
||||
$tenantCol = $multi['tenant_col'] ?? null;
|
||||
$mapSess = $multi['map_session_to'] ?? 'id';
|
||||
|
||||
if (!$tenantCol) return ['', []];
|
||||
if (!$session) return [' AND 1=0 ', []];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
if ($val === null || $val === '') return [' AND 1=0 ', []];
|
||||
return [" AND `$tenantCol` = :__tenant", [':__tenant' => $val]];
|
||||
}
|
||||
private function tenantAssign(array $session, array $columns): array { /* ... Logik bleibt unverändert ... */
|
||||
$multi = $this->conf['multi'] ?? [];
|
||||
$tenantCol = $multi['tenant_col'] ?? null;
|
||||
$mapSess = $multi['map_session_to'] ?? 'id';
|
||||
|
||||
if (!$tenantCol || !in_array($tenantCol, $columns, true)) return [];
|
||||
$val = $session[$mapSess] ?? null;
|
||||
return ($val === null || $val === '') ? [] : [$tenantCol => $val];
|
||||
}
|
||||
private function resolveIdCol(string $kind): array { /* ... Logik bleibt unverändert ... */
|
||||
$t = $this->tableMap[$kind];
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$cols = $this->tableColumns($t);
|
||||
$idCol = $cfg['id'] ?? ($this->firstExisting($cols, ['id']) ?: $this->primaryKey($t));
|
||||
if (!$idCol) $idCol = 'id';
|
||||
return [$idCol, $cols];
|
||||
}
|
||||
private function parseHtmlToGjsComponents(string $html): array { /* ... Logik bleibt unverändert ... */
|
||||
if (trim($html) === '') return [];
|
||||
|
||||
return [
|
||||
[
|
||||
'type' => 'html',
|
||||
'content' => $html,
|
||||
'removable' => true,
|
||||
'draggable' => true,
|
||||
'droppable' => true,
|
||||
'copyable' => true,
|
||||
'selectable' => true,
|
||||
'editable' => false,
|
||||
'traits' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 💡 Bereinigungsmethode
|
||||
private function cleanReferenceComponents(array $components): array {
|
||||
foreach ($components as &$component) {
|
||||
if (is_array($component) && isset($component['type'])) {
|
||||
|
||||
if ($component['type'] === 'library-reference') {
|
||||
if (isset($component['content'])) {
|
||||
$component['content'] = '';
|
||||
}
|
||||
if (isset($component['components'])) {
|
||||
$component['components'] = [];
|
||||
}
|
||||
}
|
||||
if (isset($component['components']) && is_array($component['components'])) {
|
||||
$component['components'] = $this->cleanReferenceComponents($component['components']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $components;
|
||||
}
|
||||
|
||||
|
||||
// =================================================================
|
||||
// 🚀 CRUD HANDLER METHODEN
|
||||
// =================================================================
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von LIST-Anfragen.
|
||||
*/
|
||||
private function handleList(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
$q = trim((string)$this->val($this->in, 'q', ''));
|
||||
$limit = max(1, (int)$this->val($this->in, 'limit', 500));
|
||||
$offset = max(0, (int)$this->val($this->in, 'offset', 0));
|
||||
|
||||
$where = ' WHERE 1=1 ';
|
||||
$params = [];
|
||||
// Suchlogik (q)
|
||||
if ($q !== '') {
|
||||
$parts = ["`$nameCol` LIKE :q"];
|
||||
if ($descCol) $parts[] = "`$descCol` LIKE :q";
|
||||
if ($catCol) $parts[] = "`$catCol` LIKE :q";
|
||||
$where .= " AND (" . implode(' OR ', $parts) . ") ";
|
||||
$params[':q'] = '%' . $q . '%';
|
||||
}
|
||||
|
||||
// Filterlogik (parentFilters)
|
||||
$parentFilters = [
|
||||
'template_id' => $this->val($this->in, ['template_id', 'tpl_id'], null),
|
||||
'section_id' => $this->val($this->in, ['section_id', 'sec_id'], null),
|
||||
'block_id' => $this->val($this->in, ['block_id', 'blk_id'], null),
|
||||
];
|
||||
foreach ($parentFilters as $col => $v) {
|
||||
if ($v === null || $v === '') continue;
|
||||
if (in_array($col, $allCols, true)) { $where .= " AND `$col` = :$col "; $params[":$col"] = $v; }
|
||||
}
|
||||
|
||||
// Tenant-Filter
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$where .= $tw;
|
||||
foreach ($tp as $k => $v) $params[$k] = $v;
|
||||
|
||||
$order = $updCol ? " ORDER BY `$updCol` DESC " : " ORDER BY `$nameCol` ASC ";
|
||||
$sql = "SELECT * FROM `$t` $where $order LIMIT :off,:lim";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
// Bind parameters
|
||||
foreach ($params as $k => $v) $stmt->bindValue($k, $v, is_int($v) ? PDO::PARAM_INT : PDO::PARAM_STR);
|
||||
$stmt->bindValue(':off', $offset, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':lim', $limit, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$item = [
|
||||
'id' => $r[$idCol] ?? null,
|
||||
'name' => $r[$nameCol] ?? null,
|
||||
];
|
||||
if ($descCol && isset($r[$descCol])) $item['desc'] = $r[$descCol];
|
||||
if ($catCol && isset($r[$catCol])) $item['category'] = $r[$catCol];
|
||||
if ($updCol && isset($r[$updCol])) $item['updated_at'] = $r[$updCol];
|
||||
|
||||
// Lade HTML und JSON aus den korrekten Spalten
|
||||
$htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']);
|
||||
if ($htmlCol && isset($r[$htmlCol])) $item['html'] = (string)$r[$htmlCol];
|
||||
$jsonCol = $this->firstExisting($allCols, ['json_content']);
|
||||
if ($jsonCol && isset($r[$jsonCol])) $item['content'] = $r[$jsonCol];
|
||||
|
||||
$out[] = $item;
|
||||
}
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'items' => $out, 'data' => $out, 'count' => count($out), 'offset' => $offset, 'limit' => $limit]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von GET-Anfragen.
|
||||
*/
|
||||
private function handleGet(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$sql = "SELECT * FROM `$t` WHERE `$idCol` = :id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) $this->fail('Not found', ['kind' => $kind, 'id' => $id], 404);
|
||||
$rowOut = ['id' => $row[$idCol] ?? $id] + $row;
|
||||
|
||||
// Lade HTML und JSON aus den korrekten Spalten
|
||||
$htmlCol = $this->firstExisting($allCols, ['html', 'body', 'markup', 'content']);
|
||||
$topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null;
|
||||
$jsonCol = $this->firstExisting($allCols, ['json_content']);
|
||||
$topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null;
|
||||
|
||||
$gjsComponents = [];
|
||||
|
||||
if ($topContent !== null) {
|
||||
$decodedContent = json_decode($topContent, true);
|
||||
if (is_array($decodedContent)) {
|
||||
$gjsComponents = $decodedContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($gjsComponents) && $topHtml !== null) {
|
||||
$gjsComponents = $this->parseHtmlToGjsComponents($topHtml);
|
||||
}
|
||||
|
||||
$this->respond([
|
||||
'ok' => true,
|
||||
'kind' => $kind,
|
||||
'id' => $rowOut['id'],
|
||||
'item' => $rowOut,
|
||||
'data' => $rowOut,
|
||||
'html' => $topHtml,
|
||||
'content' => $topContent,
|
||||
'gjs_components' => $gjsComponents
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von CREATE-Anfragen (inkl. JSON-Bereinigung).
|
||||
*/
|
||||
private function handleCreate(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
|
||||
$name = trim((string)$this->val($this->in, ['name', 'title'], ''));
|
||||
if ($name === '') $this->fail('name required', null, 422);
|
||||
|
||||
$desc = (string)$this->val($this->in, ['description', 'desc'], null);
|
||||
$cat = (string)$this->val($this->in, ['category', 'cat'], null);
|
||||
$html = (string)$this->val($this->in, ['html', 'body', 'markup'], null);
|
||||
$json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null);
|
||||
$settings = $this->val($this->in, ['settings_json', 'settings'], null);
|
||||
$templateId = $this->val($this->in, ['template_id', 'tpl_id'], null);
|
||||
$sectionId = $this->val($this->in, ['section_id', 'sec_id'], null);
|
||||
$blockId = $this->val($this->in, ['block_id', 'blk_id'], null);
|
||||
|
||||
$data = [$nameCol => $name];
|
||||
if ($desc !== null && $descCol) $data[$descCol] = $desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = $cat;
|
||||
|
||||
$htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup']));
|
||||
$jsonDbCol = $this->firstExisting($allCols, ['json_content']);
|
||||
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG START ---
|
||||
|
||||
// 1. JSON-Content behandeln
|
||||
if ($json !== null) {
|
||||
if ($jsonDbCol) {
|
||||
$components = is_string($json) ? json_decode($json, true) : $json;
|
||||
if (is_array($components)) {
|
||||
$components = $this->cleanReferenceComponents($components); // BEREINIGUNG
|
||||
$data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$data[$jsonDbCol] = is_string($json) ? $json : '';
|
||||
}
|
||||
} else {
|
||||
// FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben
|
||||
$this->fail(
|
||||
'JSON content provided but no `json_content` column found',
|
||||
['table' => $t, 'available_cols' => $allCols],
|
||||
422
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. HTML-Content speichern
|
||||
if ($htmlDbCol && $html !== null) {
|
||||
$data[$htmlDbCol] = $html;
|
||||
}
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
|
||||
|
||||
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
|
||||
if ($c && $settings !== null) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
if ($templateId !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $templateId;
|
||||
if ($sectionId !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sectionId;
|
||||
if ($blockId !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blockId;
|
||||
|
||||
$data = $data + $this->tenantAssign($_SESSION['auth'] ?? [], $allCols);
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$createdCol = $this->firstExisting($allCols, ['created_at', 'created', 'createdAt']);
|
||||
if ($createdCol) $data[$createdCol] = $now;
|
||||
if ($updCol) $data[$updCol] = $now;
|
||||
|
||||
$fields = array_keys($data);
|
||||
$place = array_map(fn($c) => ":$c", $fields);
|
||||
$sql = "INSERT INTO `$t` (" . implode(',', array_map(fn($c) => "`$c`", $fields)) . ") VALUES (" . implode(',', $place) . ")";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->execute();
|
||||
$newId = $this->pdo->lastInsertId();
|
||||
|
||||
$out = ['id' => $newId, 'name' => $name];
|
||||
if ($desc !== null) $out['desc'] = $desc;
|
||||
if ($cat !== null) $out['category'] = $cat;
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $newId, 'item' => $out, 'data' => $out]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von UPDATE-Anfragen (inkl. JSON-Bereinigung).
|
||||
*/
|
||||
private function handleUpdate(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$cfg = $this->conf['columns'][$kind] ?? [];
|
||||
$nameCol = $cfg['name'] ?? ($this->firstExisting($allCols, ['name']) ?: $idCol);
|
||||
$descCol = $cfg['desc'] ?? $this->firstExisting($allCols, ['description', 'desc', 'descr']);
|
||||
$catCol = $cfg['cat'] ?? $this->firstExisting($allCols, ['category', 'cat']);
|
||||
$updCol = $cfg['upd'] ?? $this->firstExisting($allCols, ['updated_at', 'updated', 'updatedAt']);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
$data = [];
|
||||
$name = $this->val($this->in, ['name', 'title'], null);
|
||||
$desc = $this->val($this->in, ['description', 'desc'], null);
|
||||
$cat = $this->val($this->in, ['category', 'cat'], null);
|
||||
$html = $this->val($this->in, ['html', 'body', 'markup'], null);
|
||||
$json = $this->val($this->in, ['content_json', 'json', 'content', 'structure_json'], null);
|
||||
$settings = $this->val($this->in, ['settings_json', 'settings'], null);
|
||||
|
||||
if ($name !== null) $data[$nameCol] = (string)$name;
|
||||
if ($desc !== null && $descCol) $data[$descCol] = (string)$desc;
|
||||
if ($cat !== null && $catCol) $data[$catCol] = (string)$cat;
|
||||
|
||||
$htmlDbCol = $this->firstExisting($allCols, ($kind === 'snippets' ? ['content'] : ['html', 'body', 'markup']));
|
||||
$jsonDbCol = $this->firstExisting($allCols, ['json_content']);
|
||||
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG START ---
|
||||
|
||||
// 1. JSON-Content behandeln
|
||||
if ($json !== null) {
|
||||
if ($jsonDbCol) {
|
||||
// Wenn JSON-Spalte existiert, JSON verarbeiten und speichern
|
||||
$components = is_string($json) ? json_decode($json, true) : $json;
|
||||
if (is_array($components)) {
|
||||
$components = $this->cleanReferenceComponents($components); // BEREINIGUNG
|
||||
$data[$jsonDbCol] = json_encode($components, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} else {
|
||||
$data[$jsonDbCol] = is_string($json) ? $json : '';
|
||||
}
|
||||
} else {
|
||||
// FALLBACK: Wenn JSON gesendet wurde, aber keine json_content Spalte existiert, FEHLER ausgeben
|
||||
$this->fail(
|
||||
'JSON content provided but no `json_content` column found',
|
||||
['table' => $t, 'available_cols' => $allCols],
|
||||
422
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Den zugehörigen HTML-Output speichern (wird vom Editor immer mitgesendet, wenn JSON da ist)
|
||||
if ($html !== null && $htmlDbCol) {
|
||||
$data[$htmlDbCol] = (string)$html;
|
||||
}
|
||||
} elseif ($html !== null && $htmlDbCol) {
|
||||
// Wenn NUR HTML gesendet wird (für minimale Änderungen), speichern wir nur HTML.
|
||||
$data[$htmlDbCol] = (string)$html;
|
||||
}
|
||||
// --- LOGIK mit ERWEITERTER PRÜFUNG ENDE ---
|
||||
|
||||
$c = $this->firstExisting($allCols, ['settings_json', 'settings']);
|
||||
if ($settings !== null && $c) $data[$c] = is_string($settings) ? $settings : json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$tpl = $this->val($this->in, ['template_id', 'tpl_id'], null);
|
||||
if ($tpl !== null && in_array('template_id', $allCols, true)) $data['template_id'] = $tpl;
|
||||
$sec = $this->val($this->in, ['section_id', 'sec_id'], null);
|
||||
if ($sec !== null && in_array('section_id', $allCols, true)) $data['section_id'] = $sec;
|
||||
$blk = $this->val($this->in, ['block_id', 'blk_id'], null);
|
||||
if ($blk !== null && in_array('block_id', $allCols, true)) $data['block_id'] = $blk;
|
||||
|
||||
if ($updCol) $data[$updCol] = date('Y-m-d H:i:s');
|
||||
if (!$data) $this->fail('nothing to update', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$set = [];
|
||||
foreach (array_keys($data) as $c) $set[] = "`$c` = :$c";
|
||||
$sql = "UPDATE `$t` SET " . implode(',', $set) . " WHERE `$idCol` = :__id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
|
||||
foreach ($data as $k => $v) $stmt->bindValue(":$k", $v);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'updated' => array_keys($data)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allgemeine Methode zur Handhabung von DELETE-Anfragen.
|
||||
*/
|
||||
private function handleDelete(string $kind): void
|
||||
{
|
||||
$auth = $this->requireAuth();
|
||||
$t = $this->tableMap[$kind];
|
||||
[$idCol, $allCols] = $this->resolveIdCol($kind);
|
||||
$id = $this->pullId($this->in);
|
||||
if ($id === null || $id === '') $this->fail('id required', null, 422);
|
||||
|
||||
[$tw, $tp] = $this->tenantWhere($auth);
|
||||
$sql = "DELETE FROM `$t` WHERE `$idCol` = :__id" . $tw . " LIMIT 1";
|
||||
$stmt = $this->pdo->prepare($sql);
|
||||
$stmt->bindValue(':__id', $id);
|
||||
foreach ($tp as $k => $v) $stmt->bindValue($k, $v);
|
||||
$stmt->execute();
|
||||
$this->respond(['ok' => true, 'kind' => $kind, 'id' => $id, 'deleted' => true]);
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
// 💡 Öffentliche run()-Methode (KORRIGIERT)
|
||||
// =================================================================
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
// 💡 KORREKTUR: Der Content-Type Header wird hier entfernt, da er jetzt in respond()
|
||||
// zentralisiert wurde, um sicherzustellen, dass er auch bei Fehlern im Konstruktor oder
|
||||
// im try-Block korrekt gesetzt wird.
|
||||
// header('Content-Type: application/json; charset=utf-8'); // DIESE ZEILE ENTFERNT
|
||||
|
||||
try {
|
||||
// Extrahiere den Ressourcen-Typ und die Operation (z.B. 'templates' und 'list')
|
||||
[$kind, $operation] = explode('.', $this->action, 2) + [1 => ''];
|
||||
|
||||
switch ($this->action) {
|
||||
case 'health':
|
||||
$this->respond(['ok' => true, 'time' => date('c')]);
|
||||
|
||||
/* ---------- AUTH ---------- */
|
||||
case 'auth.login':
|
||||
$result = $this->authService->login($this->in);
|
||||
$this->respond(['ok' => true] + $result);
|
||||
break;
|
||||
case 'auth.me':
|
||||
if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401);
|
||||
$this->respond(['ok' => true, 'user' => $_SESSION['auth']]);
|
||||
break;
|
||||
case 'auth.logout':
|
||||
$this->authService->logout();
|
||||
$this->respond(['ok' => true]);
|
||||
break;
|
||||
|
||||
/* ---------- CRUD HANDLER ---------- */
|
||||
default:
|
||||
if (in_array($kind, ['templates', 'sections', 'blocks', 'snippets'])) {
|
||||
switch ($operation) {
|
||||
case 'list':
|
||||
$this->handleList($kind);
|
||||
break;
|
||||
case 'get':
|
||||
$this->handleGet($kind);
|
||||
break;
|
||||
case 'create':
|
||||
$this->handleCreate($kind);
|
||||
break;
|
||||
case 'update':
|
||||
$this->handleUpdate($kind);
|
||||
break;
|
||||
case 'delete':
|
||||
$this->handleDelete($kind);
|
||||
break;
|
||||
default:
|
||||
$this->fail('Unknown operation for resource: ' . $this->action, null, 404);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$this->fail('Unknown action', $this->action ?: 'missing', 404);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->fail('Server error', get_class($e) . ': ' . $e->getMessage(), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
105
src/AuthService.php
Normal file
105
src/AuthService.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// AuthService.php: Kapselt die gesamte Authentifizierungslogik.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
class AuthService
|
||||
{
|
||||
private array $conf;
|
||||
private PDO $pdo;
|
||||
|
||||
// Abhängigkeiten (Konfiguration und PDO) werden per Konstruktor übergeben
|
||||
public function __construct(array $conf, PDO $pdo)
|
||||
{
|
||||
$this->conf = $conf;
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
// --- Private Utility Methoden ---
|
||||
|
||||
private function fail(string $msg, $detail = null, int $code = 400): void
|
||||
{
|
||||
// Wir müssen hier direkt antworten, da wir das Fail-Verhalten des Kernels benötigen.
|
||||
// Im ApiKernel werden wir die respond/fail-Methoden als public lassen,
|
||||
// um sie hier injizieren zu können, oder wir lassen sie hier im Global Scope
|
||||
// (WENN Sie die ursprünglichen globalen Funktionen respond/fail wieder zulassen).
|
||||
// Für eine saubere Kapselung injizieren wir die Respond-Logik.
|
||||
// HIER verwenden wir eine einfache JSON-Antwort, da die fail-Methode
|
||||
// normalerweise den gesamten Kernel stoppt. Wir nutzen exit.
|
||||
|
||||
http_response_code($code);
|
||||
echo json_encode(['ok'=>false,'error'=>$msg,'detail'=>$detail], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function verifyPassword(string $input, string $stored, array $authDbConf): bool
|
||||
{
|
||||
if (preg_match('~^\$2[aby]\$~', $stored) || strpos($stored, '$argon2') === 0) return password_verify($input, $stored);
|
||||
$legacy = strtolower($authDbConf['legacy'] ?? '');
|
||||
if ($legacy === 'md5') return hash_equals($stored, md5($input));
|
||||
if ($legacy === 'sha1') return hash_equals($stored, sha1($input));
|
||||
if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored);
|
||||
return hash_equals($stored, $input);
|
||||
}
|
||||
|
||||
// --- Public Service Methoden ---
|
||||
|
||||
public function requireAuth(): array
|
||||
{
|
||||
if (empty($_SESSION['auth'])) $this->fail('Not authenticated', null, 401);
|
||||
return $_SESSION['auth'];
|
||||
}
|
||||
|
||||
public function logout(): bool
|
||||
{
|
||||
$_SESSION = [];
|
||||
if (session_id() !== '') session_destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
public function login(array $in): array
|
||||
{
|
||||
$authDb = $this->conf['auth']['db'] ?? [];
|
||||
$colUser = $authDb['col_user'] ?? 'email';
|
||||
$colPass = $authDb['col_pass'] ?? 'password';
|
||||
$colName = $authDb['col_name'] ?? 'name';
|
||||
$colId = $authDb['col_id'] ?? 'id';
|
||||
$colStatus = $authDb['col_status']?? null;
|
||||
$activeValues = $authDb['active_values'] ?? ['active','1',1];
|
||||
$table = $authDb['table'] ?? 'emailtemplate_users';
|
||||
|
||||
$identifier = trim((string)($in['username'] ?? $in['user'] ?? $in['email'] ?? $in['login'] ?? ''));
|
||||
$password = (string)($in['password'] ?? $in['pass'] ?? $in['pwd'] ?? '');
|
||||
|
||||
if ($identifier === '' || $password === '') $this->fail('username/password required', null, 422);
|
||||
|
||||
$stmt = $this->pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1");
|
||||
$stmt->execute([':u'=>$identifier]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
if (!$row) $this->fail('Invalid credentials', null, 401);
|
||||
|
||||
if ($colStatus && isset($row[$colStatus])) {
|
||||
if (!in_array($row[$colStatus], $activeValues, true)) {
|
||||
$this->fail('Account inactive', null, 403);
|
||||
}
|
||||
}
|
||||
|
||||
$stored = (string)($row[$colPass] ?? '');
|
||||
if ($stored === '' || !$this->verifyPassword($password, $stored, $authDb)) {
|
||||
$this->fail('Invalid credentials', null, 401);
|
||||
}
|
||||
|
||||
$_SESSION['auth'] = [
|
||||
'id' => $row[$colId] ?? null,
|
||||
'name' => $row[$colName] ?? ($row[$colUser] ?? $identifier),
|
||||
'email' => $row[$colUser] ?? $identifier,
|
||||
'at' => time(),
|
||||
];
|
||||
|
||||
$token = base64_encode(hash('sha256', ($_SESSION['auth']['id'] ?? $identifier).'|'.session_id(), true));
|
||||
return ['user'=>$_SESSION['auth'], 'token'=>$token];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user