diff --git a/public/api (Kopie).php b/public/api (Kopie).php deleted file mode 100644 index 60e81b1..0000000 --- a/public/api (Kopie).php +++ /dev/null @@ -1,525 +0,0 @@ -false,'error'=>$msg,'detail'=>$detail], $code); -} - -function load_config(): array { - $paths = [ - __DIR__ . '/../inc/config.php', - __DIR__ . '/inc/config.php', - __DIR__ . '/config.php', - ]; - - foreach ($paths as $p) { - if (is_file($p)) { - $conf = @include $p; - if (is_array($conf)) return $conf; - } - } - fail('Invalid config.php', 'config.php not found or not returning array', 500); -} - -function cors(array $conf): void { - $cors = $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') respond(['ok'=>true]); -} - -function get_input(): array { - $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; - return $data; -} - -function val(array $in, $keys, $default=null) { - if (!is_array($keys)) $keys = [$keys]; - foreach ($keys as $k) if (array_key_exists($k,$in)) return $in[$k]; - return $default; -} - -/** - * Inline-Styling für E-Mail-Templates (nutzt TijsVerkoyen/CssToInlineStyles) - * - * Diese Funktion steht nur bereit, wird aber noch nicht verwendet. - * @param string $html - * @param string|null $css - * @return string - */ -function inline_css(string $html, ?string $css = null): string { - // 1. Klasse existiert nicht - if (!class_exists('\TijsVerkoyen\CssToInlineStyles\CssToInlineStyles')) { - return $html; - } - - // 2. Direkte Instanziierung und Aufruf in einer einzigen Zeile - $result = (new \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles($html, $css))->convert(); - - return $result; -} - -/* ===================== DB helpers ===================== */ -function pdo_templates(array $conf): PDO { - if (!isset($conf['templates']) || !is_array($conf['templates'])) { - fail('Missing templates DB config', null, 500); - } - $c = $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); -} - -function verify_password(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 ($legacy === 'plain')return hash_equals($stored, $input); - if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored); - return hash_equals($stored, $input); -} - -/* ============ schema helpers ============ */ -function table_columns(PDO $pdo, string $table): array { - $cols = []; - $stmt = $pdo->query("SHOW COLUMNS FROM `$table`"); - foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field']; - return $cols; -} - -function primary_key(PDO $pdo, string $table): ?string { - $stmt = $pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'"); - $stmt->execute(); - $row = $stmt->fetch(); - return $row['Column_name'] ?? null; -} - -function first_existing(array $columns, array $candidates): ?string { - foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c; - return null; -} - -/* ===================== Boot ===================== */ -try { - $conf = load_config(); - cors($conf); - // if (session_status() === PHP_SESSION_NONE) session_start(); - if (!empty($conf['auth']['cookie'])) { - $c = $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); - } - - $in = get_input(); - - /* ---- Compat: ?resource=blocks&action=list -> blocks.list ---- */ - $action = val($in, 'action', ''); - $resource = val($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; - } - - /* ---- Multi-tenant ---- */ - $multi = $conf['multi'] ?? []; - $tenantCol = $multi['tenant_col'] ?? null; - $mapSess = $multi['map_session_to'] ?? 'id'; // 'id'|'email'|'name' - - $tenantWhere = function(array $session) use ($tenantCol, $mapSess) { - 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]]; - }; - - $tenantAssign = function(array $session, array $columns) use ($tenantCol, $mapSess) { - if (!$tenantCol || !in_array($tenantCol, $columns, true)) return []; - $val = $session[$mapSess] ?? null; - return ($val===null || $val==='') ? [] : [$tenantCol => $val]; - }; - - $requireAuth = function() { - if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401); - return $_SESSION['auth']; - }; - - /* ---- config tables/columns ---- */ - $tables = $conf['tables'] ?? []; - $tableMap = [ - 'templates' => $tables['templates'] ?? 'emailtemplate_templates', - 'sections' => $tables['sections'] ?? 'emailtemplate_sections', - 'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks', - 'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets', - ]; - - $colsDefault = [ - 'id' => 'id', - 'name' => 'name', - 'desc' => 'description', - 'cat' => 'category', - 'upd' => 'updated_at', - ]; - - $pdo = pdo_templates($conf); - - /* helper: resolve id column for a table */ - $resolveIdCol = function(string $kind) use ($conf, $colsDefault, $tableMap, $pdo) { - $t = $tableMap[$kind]; - $cfg = $conf['columns'][$kind] ?? []; - $cols = table_columns($pdo, $t); - $idCol = $cfg['id'] ?? (in_array('id', $cols, true) ? 'id' : primary_key($pdo, $t)); - if (!$idCol) $idCol = 'id'; // fallback - return [$idCol, $cols]; - }; - - /* helper: accept many id aliases */ - $pullId = function(array $src) { - $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; - }; - - /* ===================== Router ===================== */ - switch ($action) { - case 'health': - respond(['ok'=>true,'time'=>date('c')]); - - /* ---------- AUTH ---------- */ - case 'auth.login': { - $identifier = trim((string)val($in, ['username','user','email','login'], '')); - $password = (string)val($in, ['password','pass','pwd'], ''); - if ($identifier === '' || $password === '') fail('username/password required', null, 422); - $authDb = $conf['auth']['db'] ?? []; - $table = $authDb['table'] ?? 'emailtemplate_users'; - $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]; - - $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1"); - - $stmt->execute([':u'=>$identifier]); - $row = $stmt->fetch(); - if (!$row) fail('Invalid credentials', null, 401); - if ($colStatus && isset($row[$colStatus])) { - if (!in_array($row[$colStatus], $activeValues, true)) { - fail('Account inactive', null, 403); - } - } - - $stored = (string)($row[$colPass] ?? ''); - if ($stored === '' || !verify_password($password, $stored, $authDb)) { - 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)); - respond(['ok'=>true,'user'=>$_SESSION['auth'],'token'=>$token]); - } - - case 'auth.me': - if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401); - respond(['ok'=>true,'user'=>$_SESSION['auth']]); - case 'auth.logout': - $_SESSION = []; - if (session_id() !== '') session_destroy(); - - respond(['ok'=>true]); - - /* ---------- LIST (mit Parent-Filtern) ---------- */ - case 'templates.list': - case 'sections.list': - case 'blocks.list': - case 'snippets.list': { - $auth = $requireAuth(); - $kind = explode('.', $action)[0]; - $t = $tableMap[$kind]; - [$idCol, $allCols] = $resolveIdCol($kind); - $cfg = $conf['columns'][$kind] ?? []; - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - $q = trim((string)val($in,'q','')); - $limit = max(1, (int)val($in,'limit', 500)); - $offset = max(0, (int)val($in,'offset',0)); - $where = ' WHERE 1=1 '; - $params = []; - 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.'%'; - } - - // Parent-Filtern (falls Spalten existieren) - $parentFilters = [ - 'template_id' => val($in, ['template_id','tpl_id'], null), - 'section_id' => val($in, ['section_id','sec_id'], null), - 'block_id' => val($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; } - } - - // Mandant - [$tw,$tp] = $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 = $pdo->prepare($sql); - 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]; - $out[] = $item; - } - respond(['ok'=>true,'kind'=>$kind,'items'=>$out,'data'=>$out,'count'=>count($out),'offset'=>$offset,'limit'=>$limit]); - } - - /* ---------- GET (JETZT mit top-level html/content) ---------- */ - case 'templates.get': - case 'sections.get': - case 'blocks.get': - case 'snippets.get': { - $auth = $requireAuth(); - $kind = explode('.', $action)[0]; - $t = $tableMap[$kind]; - [$idCol, $allCols] = $resolveIdCol($kind); - $id = $pullId($in); - if ($id === null || $id === '') fail('id required', null, 422); - [$tw,$tp] = $tenantWhere($auth); - $sql = "SELECT * FROM `$t` WHERE `$idCol` = :id".$tw." LIMIT 1"; - $stmt = $pdo->prepare($sql); - $stmt->bindValue(':id', $id); - foreach ($tp as $k=>$v) $stmt->bindValue($k,$v); - $stmt->execute(); - $row = $stmt->fetch(); - if (!$row) fail('Not found', ['kind'=>$kind,'id'=>$id], 404); - $rowOut = ['id' => $row[$idCol] ?? $id] + $row; - - // NEU: Spalten für HTML/JSON erkennen und top-level ausgeben - $htmlCol = first_existing($allCols, ['html','body','markup']); - $jsonCol = first_existing($allCols, ['content_json','json','content','structure_json']); - $topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null; - $topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null; - - respond([ - 'ok'=>true, - 'kind'=>$kind, - 'id'=>$rowOut['id'], - 'item'=>$rowOut, - 'data'=>$rowOut, - 'html'=>$topHtml, // <— wichtig für Vorschau - 'content'=>$topContent // optional (z. B. GrapesJS JSON) - ]); - } - - /* ---------- CREATE ---------- */ - case 'templates.create': - case 'sections.create': - case 'blocks.create': - case 'snippets.create': { - $auth = $requireAuth(); - $kind = explode('.', $action)[0]; - $t = $tableMap[$kind]; - [$idCol, $allCols] = $resolveIdCol($kind); - $cfg = $conf['columns'][$kind] ?? []; - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - $name = trim((string)val($in, ['name','title'], '')); - if ($name === '') fail('name required', null, 422); - $desc = (string)val($in, ['description','desc'], null); - $cat = (string)val($in, ['category','cat'], null); - $html = (string)val($in, ['html','body','markup'], null); - $json = val($in, ['content_json','json','content','structure_json'], null); - $settings = val($in, ['settings_json','settings'], null); - $templateId = val($in, ['template_id','tpl_id'], null); - $sectionId = val($in, ['section_id','sec_id'], null); - $blockId = val($in, ['block_id','blk_id'], null); - $data = [ $nameCol => $name ]; - if ($desc !== null && $descCol) $data[$descCol] = $desc; - if ($cat !== null && $catCol) $data[$catCol] = $cat; - $c = first_existing($allCols, ['html','body','markup']); if ($c && $html !== null) $data[$c]=$html; - $c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($c && $json !== null) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - $c = first_existing($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 + $tenantAssign($_SESSION['auth'] ?? [], $allCols); - $now = date('Y-m-d H:i:s'); - $createdCol = first_existing($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 = $pdo->prepare($sql); - foreach ($data as $k=>$v) $stmt->bindValue(":$k", $v); - $stmt->execute(); - $newId = $pdo->lastInsertId(); - $out = ['id'=>$newId,'name'=>$name]; - if ($desc !== null) $out['desc']=$desc; - if ($cat !== null) $out['category']=$cat; - respond(['ok'=>true,'kind'=>$kind,'id'=>$newId,'item'=>$out,'data'=>$out]); - } - - /* ---------- UPDATE ---------- */ - case 'templates.update': - case 'sections.update': - case 'blocks.update': - case 'snippets.update': { - $auth = $requireAuth(); - $kind = explode('.', $action)[0]; - $t = $tableMap[$kind]; - [$idCol, $allCols] = $resolveIdCol($kind); - $cfg = $conf['columns'][$kind] ?? []; - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - $id = $pullId($in); - if ($id === null || $id === '') fail('id required', null, 422); - $data = []; - $name = val($in, ['name','title'], null); - $desc = val($in, ['description','desc'], null); - $cat = val($in, ['category','cat'], null); - $html = val($in, ['html','body','markup'], null); - $json = val($in, ['content_json','json','content','structure_json'], null); - $settings = val($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; - $c = first_existing($allCols, ['html','body','markup']); if ($html !== null && $c) $data[$c]=(string)$html; - $c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($json !== null && $c) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - $c = first_existing($allCols, ['settings_json','settings']); if ($settings !== null && $c) $data[$c]= is_string($settings)?$settings:json_encode($settings, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - $tpl = val($in, ['template_id','tpl_id'], null); if ($tpl !== null && in_array('template_id',$allCols,true)) $data['template_id']=$tpl; - $sec = val($in, ['section_id','sec_id'], null); if ($sec !== null && in_array('section_id',$allCols,true)) $data['section_id']=$sec; - $blk = val($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) fail('nothing to update', null, 422); - [$tw,$tp] = $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 = $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(); - respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'updated'=>array_keys($data)]); - } - - /* ---------- DELETE ---------- */ - case 'templates.delete': - case 'sections.delete': - case 'blocks.delete': - case 'snippets.delete': { - $auth = $requireAuth(); - $kind = explode('.', $action)[0]; - $t = $tableMap[$kind]; - [$idCol, $allCols] = $resolveIdCol($kind); - $id = $pullId($in); - if ($id === null || $id === '') fail('id required', null, 422); - [$tw,$tp] = $tenantWhere($auth); - $sql = "DELETE FROM `$t` WHERE `$idCol` = :__id".$tw." LIMIT 1"; - $stmt = $pdo->prepare($sql); - $stmt->bindValue(':__id', $id); - foreach ($tp as $k=>$v) $stmt->bindValue($k,$v); - $stmt->execute(); - respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'deleted'=>true]); - } - - /* ---------- Platzhalter für kommende Features ---------- */ - case 'render.preview': - fail('Not implemented', 'Preview wird im Feature-Patch geliefert', 501); - case 'templates.test_send': - fail('Not implemented', 'Testversand wird im Feature-Patch geliefert', 501); - case 'export.render': - fail('Not implemented', 'Export-API wird im Feature-Patch geliefert', 501); - default: - fail('Unknown action', $action ?: 'missing', 404); - } -} catch (Throwable $e) { - fail('Server error', get_class($e).': '.$e->getMessage(), 500); -} diff --git a/public/api-original.php b/public/api-original.php deleted file mode 100644 index cfb6653..0000000 --- a/public/api-original.php +++ /dev/null @@ -1,1017 +0,0 @@ -false,'error'=>$msg,'detail'=>$detail], $code); - -} - -function load_config(): array { - - $paths = [ - - __DIR__ . '/../inc/config.php', - - __DIR__ . '/inc/config.php', - - __DIR__ . '/config.php', - - ]; - - foreach ($paths as $p) { - - if (is_file($p)) { - - $conf = @include $p; - - if (is_array($conf)) return $conf; - - } - - } - - fail('Invalid config.php', 'config.php not found or not returning array', 500); - -} - -function cors(array $conf): void { - - $cors = $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') respond(['ok'=>true]); - -} - -function get_input(): array { - - $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; - - return $data; - -} - -function val(array $in, $keys, $default=null) { - - if (!is_array($keys)) $keys = [$keys]; - - foreach ($keys as $k) if (array_key_exists($k,$in)) return $in[$k]; - - return $default; - -} - - - -/* ===================== DB helpers ===================== */ - -function pdo_templates(array $conf): PDO { - - if (!isset($conf['templates']) || !is_array($conf['templates'])) { - - fail('Missing templates DB config', null, 500); - - } - - $c = $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); - -} - -function verify_password(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 ($legacy === 'plain')return hash_equals($stored, $input); - - if (password_get_info($stored)['algo'] !== 0) return password_verify($input, $stored); - - return hash_equals($stored, $input); - -} - - - -/* ============ schema helpers ============ */ - -function table_columns(PDO $pdo, string $table): array { - - $cols = []; - - $stmt = $pdo->query("SHOW COLUMNS FROM `$table`"); - - foreach ($stmt->fetchAll() as $r) $cols[] = $r['Field']; - - return $cols; - -} - -function primary_key(PDO $pdo, string $table): ?string { - - $stmt = $pdo->prepare("SHOW KEYS FROM `$table` WHERE Key_name = 'PRIMARY'"); - - $stmt->execute(); - - $row = $stmt->fetch(); - - return $row['Column_name'] ?? null; - -} - -function first_existing(array $columns, array $candidates): ?string { - - foreach ($candidates as $c) if (in_array($c, $columns, true)) return $c; - - return null; - -} - - - -/* ===================== Boot ===================== */ - -try { - - $conf = load_config(); - - cors($conf); - - if (session_status() === PHP_SESSION_NONE) session_start(); - - if (!empty($conf['auth']['cookie'])) { - - $c = $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); - - } - - - - $in = get_input(); - - - - /* ---- Compat: ?resource=blocks&action=list -> blocks.list ---- */ - - $action = val($in, 'action', ''); - - $resource = val($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; - - } - - - - /* ---- Multi-tenant ---- */ - - $multi = $conf['multi'] ?? []; - - $tenantCol = $multi['tenant_col'] ?? null; - - $mapSess = $multi['map_session_to'] ?? 'id'; // 'id'|'email'|'name' - - - - $tenantWhere = function(array $session) use ($tenantCol, $mapSess) { - - 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]]; - - }; - - $tenantAssign = function(array $session, array $columns) use ($tenantCol, $mapSess) { - - if (!$tenantCol || !in_array($tenantCol, $columns, true)) return []; - - $val = $session[$mapSess] ?? null; - - return ($val===null || $val==='') ? [] : [$tenantCol => $val]; - - }; - - $requireAuth = function() { - - if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401); - - return $_SESSION['auth']; - - }; - - - - /* ---- config tables/columns ---- */ - - $tables = $conf['tables'] ?? []; - - $tableMap = [ - - 'templates' => $tables['templates'] ?? 'emailtemplate_templates', - - 'sections' => $tables['sections'] ?? 'emailtemplate_sections', - - 'blocks' => $tables['blocks'] ?? 'emailtemplate_blocks', - - 'snippets' => $tables['snippets'] ?? 'emailtemplate_snippets', - - ]; - - $colsDefault = [ - - 'id' => 'id', - - 'name' => 'name', - - 'desc' => 'description', - - 'cat' => 'category', - - 'upd' => 'updated_at', - - ]; - - - - $pdo = pdo_templates($conf); - - - - /* helper: resolve id column for a table */ - - $resolveIdCol = function(string $kind) use ($conf, $colsDefault, $tableMap, $pdo) { - - $t = $tableMap[$kind]; - - $cfg = $conf['columns'][$kind] ?? []; - - $cols = table_columns($pdo, $t); - - $idCol = $cfg['id'] ?? (in_array('id', $cols, true) ? 'id' : primary_key($pdo, $t)); - - if (!$idCol) $idCol = 'id'; // fallback - - return [$idCol, $cols]; - - }; - - - - /* helper: accept many id aliases */ - - $pullId = function(array $src) { - - $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; - - }; - - - - /* ===================== Router ===================== */ - - switch ($action) { - - case 'health': - - respond(['ok'=>true,'time'=>date('c')]); - - - - /* ---------- AUTH ---------- */ - - case 'auth.login': { - - $identifier = trim((string)val($in, ['username','user','email','login'], '')); - - $password = (string)val($in, ['password','pass','pwd'], ''); - - if ($identifier === '' || $password === '') fail('username/password required', null, 422); - - - - $authDb = $conf['auth']['db'] ?? []; - - $table = $authDb['table'] ?? 'emailtemplate_users'; - - $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]; - - - - $stmt = $pdo->prepare("SELECT * FROM `$table` WHERE `$colUser` = :u LIMIT 1"); - - $stmt->execute([':u'=>$identifier]); - - $row = $stmt->fetch(); - - if (!$row) fail('Invalid credentials', null, 401); - - - - if ($colStatus && isset($row[$colStatus])) { - - if (!in_array($row[$colStatus], $activeValues, true)) { - - fail('Account inactive', null, 403); - - } - - } - - - - $stored = (string)($row[$colPass] ?? ''); - - if ($stored === '' || !verify_password($password, $stored, $authDb)) { - - 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)); - - respond(['ok'=>true,'user'=>$_SESSION['auth'],'token'=>$token]); - - } - - - - case 'auth.me': - - if (empty($_SESSION['auth'])) fail('Not authenticated', null, 401); - - respond(['ok'=>true,'user'=>$_SESSION['auth']]); - - - - case 'auth.logout': - - $_SESSION = []; - - if (session_id() !== '') session_destroy(); - - respond(['ok'=>true]); - - - - /* ---------- LIST (mit Parent-Filtern) ---------- */ - - case 'templates.list': - - case 'sections.list': - - case 'blocks.list': - - case 'snippets.list': { - - $auth = $requireAuth(); - - - - $kind = explode('.', $action)[0]; - - $t = $tableMap[$kind]; - - [$idCol, $allCols] = $resolveIdCol($kind); - - $cfg = $conf['columns'][$kind] ?? []; - - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - - - - $q = trim((string)val($in,'q','')); - - $limit = max(1, (int)val($in,'limit', 500)); - - $offset = max(0, (int)val($in,'offset',0)); - - - - $where = ' WHERE 1=1 '; - - $params = []; - - - - 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.'%'; - - } - - - - // Parent-Filtern (falls Spalten existieren) - - $parentFilters = [ - - 'template_id' => val($in, ['template_id','tpl_id'], null), - - 'section_id' => val($in, ['section_id','sec_id'], null), - - 'block_id' => val($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; } - - } - - - - // Mandant - - [$tw,$tp] = $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 = $pdo->prepare($sql); - - 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]; - - $out[] = $item; - - } - - respond(['ok'=>true,'kind'=>$kind,'items'=>$out,'data'=>$out,'count'=>count($out),'offset'=>$offset,'limit'=>$limit]); - - } - - - - /* ---------- GET (JETZT mit top-level html/content) ---------- */ - - case 'templates.get': - - case 'sections.get': - - case 'blocks.get': - - case 'snippets.get': { - - $auth = $requireAuth(); - - - - $kind = explode('.', $action)[0]; - - $t = $tableMap[$kind]; - - [$idCol, $allCols] = $resolveIdCol($kind); - - - - $id = $pullId($in); - - if ($id === null || $id === '') fail('id required', null, 422); - - - - [$tw,$tp] = $tenantWhere($auth); - - $sql = "SELECT * FROM `$t` WHERE `$idCol` = :id".$tw." LIMIT 1"; - - $stmt = $pdo->prepare($sql); - - $stmt->bindValue(':id', $id); - - foreach ($tp as $k=>$v) $stmt->bindValue($k,$v); - - $stmt->execute(); - - $row = $stmt->fetch(); - - if (!$row) fail('Not found', ['kind'=>$kind,'id'=>$id], 404); - - - - $rowOut = ['id' => $row[$idCol] ?? $id] + $row; - - - - // NEU: Spalten für HTML/JSON erkennen und top-level ausgeben - - $htmlCol = first_existing($allCols, ['html','body','markup']); - - $jsonCol = first_existing($allCols, ['content_json','json','content','structure_json']); - - $topHtml = ($htmlCol && isset($row[$htmlCol])) ? (string)$row[$htmlCol] : null; - - $topContent = ($jsonCol && isset($row[$jsonCol])) ? $row[$jsonCol] : null; - - - - respond([ - - 'ok'=>true, - - 'kind'=>$kind, - - 'id'=>$rowOut['id'], - - 'item'=>$rowOut, - - 'data'=>$rowOut, - - 'html'=>$topHtml, // <— wichtig für Vorschau - - 'content'=>$topContent // optional (z. B. GrapesJS JSON) - - ]); - - } - - - - /* ---------- CREATE ---------- */ - - case 'templates.create': - - case 'sections.create': - - case 'blocks.create': - - case 'snippets.create': { - - $auth = $requireAuth(); - - - - $kind = explode('.', $action)[0]; - - $t = $tableMap[$kind]; - - [$idCol, $allCols] = $resolveIdCol($kind); - - $cfg = $conf['columns'][$kind] ?? []; - - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - - - - $name = trim((string)val($in, ['name','title'], '')); - - if ($name === '') fail('name required', null, 422); - - - - $desc = (string)val($in, ['description','desc'], null); - - $cat = (string)val($in, ['category','cat'], null); - - $html = (string)val($in, ['html','body','markup'], null); - - $json = val($in, ['content_json','json','content','structure_json'], null); - - $settings = val($in, ['settings_json','settings'], null); - - - - $templateId = val($in, ['template_id','tpl_id'], null); - - $sectionId = val($in, ['section_id','sec_id'], null); - - $blockId = val($in, ['block_id','blk_id'], null); - - - - $data = [ $nameCol => $name ]; - - if ($desc !== null && $descCol) $data[$descCol] = $desc; - - if ($cat !== null && $catCol) $data[$catCol] = $cat; - - - - $c = first_existing($allCols, ['html','body','markup']); if ($c && $html !== null) $data[$c]=$html; - - $c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($c && $json !== null) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - - $c = first_existing($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 + $tenantAssign($_SESSION['auth'] ?? [], $allCols); - - - - $now = date('Y-m-d H:i:s'); - - $createdCol = first_existing($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 = $pdo->prepare($sql); - - foreach ($data as $k=>$v) $stmt->bindValue(":$k", $v); - - $stmt->execute(); - - $newId = $pdo->lastInsertId(); - - - - $out = ['id'=>$newId,'name'=>$name]; - - if ($desc !== null) $out['desc']=$desc; - - if ($cat !== null) $out['category']=$cat; - - respond(['ok'=>true,'kind'=>$kind,'id'=>$newId,'item'=>$out,'data'=>$out]); - - } - - - - /* ---------- UPDATE ---------- */ - - case 'templates.update': - - case 'sections.update': - - case 'blocks.update': - - case 'snippets.update': { - - $auth = $requireAuth(); - - - - $kind = explode('.', $action)[0]; - - $t = $tableMap[$kind]; - - [$idCol, $allCols] = $resolveIdCol($kind); - - $cfg = $conf['columns'][$kind] ?? []; - - $nameCol = $cfg['name'] ?? (in_array('name',$allCols,true) ? 'name' : $idCol); - - $descCol = $cfg['desc'] ?? first_existing($allCols, ['description','desc','descr']); - - $catCol = $cfg['cat'] ?? first_existing($allCols, ['category','cat']); - - $updCol = $cfg['upd'] ?? first_existing($allCols, ['updated_at','updated','updatedAt']); - - - - $id = $pullId($in); - - if ($id === null || $id === '') fail('id required', null, 422); - - - - $data = []; - - $name = val($in, ['name','title'], null); - - $desc = val($in, ['description','desc'], null); - - $cat = val($in, ['category','cat'], null); - - $html = val($in, ['html','body','markup'], null); - - $json = val($in, ['content_json','json','content','structure_json'], null); - - $settings = val($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; - - $c = first_existing($allCols, ['html','body','markup']); if ($html !== null && $c) $data[$c]=(string)$html; - - $c = first_existing($allCols, ['content_json','json','content','structure_json']); if ($json !== null && $c) $data[$c]= is_string($json)?$json:json_encode($json, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - - $c = first_existing($allCols, ['settings_json','settings']); if ($settings !== null && $c) $data[$c]= is_string($settings)?$settings:json_encode($settings, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - - - - $tpl = val($in, ['template_id','tpl_id'], null); if ($tpl !== null && in_array('template_id',$allCols,true)) $data['template_id']=$tpl; - - $sec = val($in, ['section_id','sec_id'], null); if ($sec !== null && in_array('section_id',$allCols,true)) $data['section_id']=$sec; - - $blk = val($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) fail('nothing to update', null, 422); - - - - [$tw,$tp] = $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 = $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(); - - - - respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'updated'=>array_keys($data)]); - - } - - - - /* ---------- DELETE ---------- */ - - case 'templates.delete': - - case 'sections.delete': - - case 'blocks.delete': - - case 'snippets.delete': { - - $auth = $requireAuth(); - - - - $kind = explode('.', $action)[0]; - - $t = $tableMap[$kind]; - - [$idCol, $allCols] = $resolveIdCol($kind); - - - - $id = $pullId($in); - - if ($id === null || $id === '') fail('id required', null, 422); - - - - [$tw,$tp] = $tenantWhere($auth); - - $sql = "DELETE FROM `$t` WHERE `$idCol` = :__id".$tw." LIMIT 1"; - - $stmt = $pdo->prepare($sql); - - $stmt->bindValue(':__id', $id); - - foreach ($tp as $k=>$v) $stmt->bindValue($k,$v); - - $stmt->execute(); - - - - respond(['ok'=>true,'kind'=>$kind,'id'=>$id,'deleted'=>true]); - - } - - - - /* ---------- Platzhalter für kommende Features ---------- */ - - case 'render.preview': - - fail('Not implemented', 'Preview wird im Feature-Patch geliefert', 501); - - case 'templates.test_send': - - fail('Not implemented', 'Testversand wird im Feature-Patch geliefert', 501); - - case 'export.render': - - fail('Not implemented', 'Export-API wird im Feature-Patch geliefert', 501); - - - - default: - - fail('Unknown action', $action ?: 'missing', 404); - - } - - - -} catch (Throwable $e) { - - fail('Server error', get_class($e).': '.$e->getMessage(), 500); - -} diff --git a/public/api.php.txt b/public/api.php.txt deleted file mode 100644 index 255b234..0000000 --- a/public/api.php.txt +++ /dev/null @@ -1,492 +0,0 @@ - false, - 'error' => 'internal', - 'type' => get_class($e), - 'msg' => $e->getMessage(), - 'file' => basename($e->getFile()), - 'line' => $e->getLine(), - ], JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - exit; -}); - -/* ------------------------- Helpers ------------------------- */ -function json_out(array $data, int $code = 200): void { - http_response_code($code); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode($data, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); - exit; -} -function in_json(): array { - $raw = file_get_contents('php://input') ?: ''; - if ($raw === '') return []; - $j = json_decode($raw, true); - return is_array($j) ? $j : []; -} - -/* ------------------------- Config laden ------------------------- */ -// Parent /inc/config.php (wie von dir beschrieben) -$cfgFile = dirname(__DIR__) . '/inc/config.php'; -if (!is_file($cfgFile)) { - json_out(['ok'=>false,'error'=>'config_missing','hint'=>'config.php nicht gefunden','path'=>$cfgFile], 500); -} -$CFG = include $cfgFile; -if (!is_array($CFG)) { - json_out(['ok'=>false,'error'=>'config_invalid','hint'=>'config.php muss ein Array zurückgeben'], 500); -} -$ENV = $CFG['env'] ?? 'prod'; - -/* ------------------------- PDO-Factories ------------------------- */ -function pdo_from_cfg(?array $dbc): ?PDO { - if (!$dbc) return null; - $dsn = sprintf( - 'mysql:host=%s;dbname=%s;charset=%s', - $dbc['db_host'] ?? 'localhost', - $dbc['db_name'] ?? '', - $dbc['db_charset']?? 'utf8mb4' - ); - return new PDO($dsn, $dbc['db_user'] ?? '', $dbc['db_pass'] ?? '', [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); -} -$pdoTpl = pdo_from_cfg($CFG['templates'] ?? null); // Template-Daten -$pdoCust = pdo_from_cfg(($CFG['customers'] ?? null) ?: ($CFG['templates'] ?? null)); // Kunden/Users - -$TPL_DB = $CFG['templates']['db_name'] ?? null; // für information_schema - -function has_column(PDO $pdo, ?string $db, string $table, string $col): bool { - if (!$db) return false; - $st = $pdo->prepare("SELECT 1 FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA=:db AND TABLE_NAME=:t AND COLUMN_NAME=:c - LIMIT 1"); - $st->execute([':db'=>$db, ':t'=>$table, ':c'=>$col]); - return (bool)$st->fetchColumn(); -} - -/* ------------------------- Auth (Helper oder Fallback) ------------------------- */ -$authFile = dirname(__DIR__) . '/inc/auth_helpers.php'; -$useHelpers = is_file($authFile); -if ($useHelpers) { - require_once $authFile; // stellt auth_start_session(), auth_require(), auth_logout(), require_role() bereit -} else { - // interner Fallback – kompatibel zu deinen Erwartungen - function auth_start_session(array $CFG): void { - $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); - session_set_cookie_params([ - 'httponly' => true, - 'samesite' => 'Lax', - 'secure' => $secure, - 'path' => rtrim(dirname($_SERVER['SCRIPT_NAME']),'/').'/' - ]); - session_name('et_session'); - if (session_status() !== PHP_SESSION_ACTIVE) session_start(); - } - function auth_require(array $CFG): void { - auth_start_session($CFG); - if (empty($_SESSION['user'])) { - json_out(['ok'=>false,'error'=>'unauthorized'], 401); - } - } - function auth_logout(array $CFG): void { - auth_start_session($CFG); - $_SESSION = []; - if (ini_get('session.use_cookies')) { - $p = session_get_cookie_params(); - setcookie(session_name(), '', time()-42000, $p['path'], $p['domain'] ?? '', $p['secure'], $p['httponly']); - } - session_destroy(); - } - function require_role(array $CFG, array $roles): void { - auth_start_session($CFG); - $r = $_SESSION['user']['role'] ?? null; - if (!$r || !in_array($r, $roles, true)) json_out(['ok'=>false,'error'=>'forbidden'], 403); - } -} - -/* ------------------------- Routing + Legacy-Mapping ------------------------- */ -$action = $_GET['action'] ?? $_POST['action'] ?? null; - -// Alt: ?resource=blocks&action=list|get|create|update|delete|sync -if (!empty($_GET['resource'])) { - $res = (string)$_GET['resource']; - $act = (string)($_GET['action'] ?? ''); - $allowed = ['templates','sections','blocks','snippets','assets','template_items','section_items']; - if (in_array($res, $allowed, true)) { - if ($act === 'list') $action = $res.'.list'; - if ($act === 'get') $action = $res.'.get'; - if ($act === 'create') $action = $res.'.create'; - if ($act === 'update') $action = $res.'.update'; - if ($act === 'delete') $action = $res.'.delete'; - if ($act === 'sync') $action = $res.'.sync'; - } -} - -/* ------------------------- Meta/Health ------------------------- */ -if ($action === 'health' || $action === 'ping') json_out(['ok'=>true,'env'=>$ENV,'time'=>date('c')]); -if ($action === 'version') json_out(['ok'=>true,'version'=>'2025-09-05','env'=>$ENV]); - -/* ------------------------- Diagnose (leichtgewichtig) ------------------------- */ -if ($action === 'debug.diag') { - $diag = [ - 'php' => PHP_VERSION, - 'pdo' => extension_loaded('pdo'), - 'pdo_mysql'=> extension_loaded('pdo_mysql'), - 'cfg' => ['templates'=>!!$pdoTpl, 'customers'=>!!$pdoCust], - ]; - json_out(['ok'=>true,'diag'=>$diag]); -} - -/* ------------------------- STAGING: User-Debug ------------------------- */ -if (in_array($action, ['debug.users','debug.users.check','debug.users.setpass','debug.users.peek'], true)) { - if ($ENV !== 'staging') json_out(['ok'=>false,'error'=>'forbidden'], 403); - if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500); - - if ($action === 'debug.users') { - $email = isset($_GET['email']) ? trim((string)$_GET['email']) : ''; - if ($email !== '') { - $st = $pdoCust->prepare("SELECT id, customer_id, email, role, is_active, created_at, updated_at - FROM customer_users WHERE email=:email"); - $st->execute([':email'=>$email]); - $rows = $st->fetchAll(); - } else { - $st = $pdoCust->query("SELECT id, customer_id, email, role, is_active, created_at, updated_at - FROM customer_users ORDER BY id DESC LIMIT 50"); - $rows = $st->fetchAll(); - } - json_out(['ok'=>true,'items'=>$rows]); - } - - if ($action === 'debug.users.check') { - $in = in_json(); - $email = trim((string)($in['email'] ?? '')); - $pass = (string)($in['password'] ?? ''); - if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400); - $st = $pdoCust->prepare("SELECT id, customer_id, email, password_hash, role, is_active FROM customer_users - WHERE email=:email LIMIT 1"); - $st->execute([':email'=>$email]); - $u = $st->fetch(); - if (!$u) json_out(['ok'=>true,'exists'=>false,'password_match'=>false]); - json_out(['ok'=>true,'exists'=>true,'password_match'=>password_verify($pass,$u['password_hash'])]); - } - - if ($action === 'debug.users.setpass') { - $in = in_json(); - $email = trim((string)($in['email'] ?? '')); - $pass = (string)($in['password'] ?? ''); - if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_params'], 400); - $st = $pdoCust->prepare("SELECT id FROM customer_users WHERE email=:email LIMIT 1"); - $st->execute([':email'=>$email]); - $u = $st->fetch(); - if (!$u) json_out(['ok'=>false,'error'=>'user_not_found'], 404); - $hash = password_hash($pass, PASSWORD_DEFAULT); - $upd = $pdoCust->prepare("UPDATE customer_users SET password_hash=:h, is_active=1 WHERE id=:id"); - $upd->execute([':h'=>$hash, ':id'=>$u['id']]); - json_out(['ok'=>true,'set'=>true]); - } - - if ($action === 'debug.users.peek') { - $email = isset($_GET['email']) ? trim((string)$_GET['email']) : ''; - if ($email==='') json_out(['ok'=>false,'error'=>'missing_email'], 400); - $st = $pdoCust->prepare("SELECT id, customer_id, email, LENGTH(password_hash) len - FROM customer_users WHERE email=:email"); - $st->execute([':email'=>$email]); - json_out(['ok'=>true,'user'=>$st->fetch()]); - } -} - -/* ------------------------- AUTH: Login / Logout / Me ------------------------- */ -if ($action === 'auth.login') { - $in = in_json(); - $email = trim(strtolower((string)($in['email'] ?? ''))); - $pass = (string)($in['password'] ?? ''); - if ($email==='' || $pass==='') json_out(['ok'=>false,'error'=>'missing_credentials'], 400); - if (!$pdoCust) json_out(['ok'=>false,'error'=>'customers_db_not_configured'], 500); - - // Mehrfachkunden mit gleicher Mail erlauben → best match über password_verify - $st = $pdoCust->prepare("SELECT cu.id, cu.customer_id, cu.email, cu.password_hash, cu.role, cu.is_active, - c.slug AS customer_slug, c.plan, c.status - FROM customer_users cu - JOIN customers c ON c.id = cu.customer_id - WHERE cu.email=:email"); - $st->execute([':email'=>$email]); - $rows = $st->fetchAll(); - - $match = null; - foreach ($rows as $r) { - if ((int)$r['is_active'] === 1 && !empty($r['password_hash']) && password_verify($pass, $r['password_hash'])) { - $match = $r; break; - } - } - if (!$match) json_out(['ok'=>false,'error'=>'invalid_credentials'], 401); - if (($match['status'] ?? 'active') !== 'active') json_out(['ok'=>false,'error'=>'customer_inactive'], 403); - - auth_start_session($CFG); - $_SESSION['user'] = [ - 'id' => (int)$match['id'], - 'email' => $match['email'], - 'role' => $match['role'], - 'customer_id' => (int)$match['customer_id'], - 'customer_slug' => $match['customer_slug'], - 'plan' => $match['plan'] ?? null, - ]; - json_out(['ok'=>true,'user'=>$_SESSION['user']]); -} - -if ($action === 'auth.logout') { auth_logout($CFG); json_out(['ok'=>true]); } - -if ($action === 'auth.me') { - auth_start_session($CFG); - json_out(['ok'=>!empty($_SESSION['user']), 'user'=>$_SESSION['user'] ?? null]); -} - -/* ------------------------- ab hier: geschützt ------------------------- */ -$public = ['auth.login','auth.logout','auth.me','health','ping','version','debug.diag','debug.users','debug.users.check','debug.users.setpass','debug.users.peek']; -if (!in_array($action, $public, true)) auth_require($CFG); - -$customerId = (int)($_SESSION['user']['customer_id'] ?? 0); - -/* ------------------------- Templates ------------------------- */ -if ($action === 'templates.list') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $st = $pdoTpl->prepare("SELECT id, name, updated_at - FROM emailtemplate_templates - WHERE customer_id = :cid - ORDER BY updated_at DESC, id DESC - LIMIT 1000"); - $st->execute([':cid'=>$customerId]); - json_out(['ok'=>true,'items'=>$st->fetchAll()]); -} - -if ($action === 'templates.get') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html'); - $cols = $hasHtml ? "id, customer_id, name, html, updated_at" : "id, customer_id, name, NULL AS html, updated_at"; - $st = $pdoTpl->prepare("SELECT $cols FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1"); - $st->execute([':id'=>$id, ':cid'=>$customerId]); - $row = $st->fetch(); - if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404); - json_out(['ok'=>true,'item'=>$row]); -} - -if ($action === 'templates.create') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in = in_json(); - $name = trim((string)($in['name'] ?? '')); - $html = (string)($in['html'] ?? ''); - if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400); - $hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html'); - - if ($hasHtml) { - $st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,html,created_at,updated_at) - VALUES (:cid,:name,:html,NOW(),NOW())"); - $st->execute([':cid'=>$customerId, ':name'=>$name, ':html'=>$html]); - } else { - $st = $pdoTpl->prepare("INSERT INTO emailtemplate_templates (customer_id,name,created_at,updated_at) - VALUES (:cid,:name,NOW(),NOW())"); - $st->execute([':cid'=>$customerId, ':name'=>$name]); - } - json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]); -} - -if ($action === 'templates.update') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in = in_json(); - $id = (int)($in['id'] ?? 0); - $name = array_key_exists('name',$in) ? trim((string)$in['name']) : null; - $html = array_key_exists('html',$in) ? (string)$in['html'] : null; - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - - $hasHtml = has_column($pdoTpl, $TPL_DB, 'emailtemplate_templates', 'html'); - $sets=[]; $p=[':id'=>$id, ':cid'=>$customerId]; - if ($name!==null) { $sets[]="name=:name"; $p[':name']=$name; } - if ($hasHtml && $html!==null) { $sets[]="html=:html"; $p[':html']=$html; } - if (!$sets) json_out(['ok'=>false,'error'=>'nothing_to_update'], 400); - - $sql = "UPDATE emailtemplate_templates SET ".implode(',',$sets).", updated_at=NOW() WHERE id=:id AND customer_id=:cid"; - $st = $pdoTpl->prepare($sql); - $st->execute($p); - json_out(['ok'=>true,'updated'=>$st->rowCount()]); -} - -if ($action === 'templates.delete') { - require_role($CFG, ['owner','admin']); - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in = in_json(); $id=(int)($in['id'] ?? 0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $pdoTpl->beginTransaction(); - try { - $pdoTpl->prepare("DELETE FROM emailtemplate_template_items WHERE template_id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]); - $pdoTpl->prepare("DELETE FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid")->execute([':id'=>$id, ':cid'=>$customerId]); - $pdoTpl->commit(); - json_out(['ok'=>true]); - } catch (Throwable $e) { $pdoTpl->rollBack(); throw $e; } -} - -/* ------------------------- Sections ------------------------- */ -if ($action === 'sections.list') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $templateId = isset($_GET['template_id']) ? (int)$_GET['template_id'] : 0; - if ($templateId>0) { - $st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at - FROM emailtemplate_sections - WHERE customer_id=:cid AND template_id=:tid - ORDER BY z_index ASC, id ASC - LIMIT 1000"); - $st->execute([':cid'=>$customerId, ':tid'=>$templateId]); - } else { - $st = $pdoTpl->prepare("SELECT id, template_id, name, z_index, type, updated_at - FROM emailtemplate_sections - WHERE customer_id=:cid - ORDER BY template_id ASC, z_index ASC, id ASC - LIMIT 1000"); - $st->execute([':cid'=>$customerId]); - } - json_out(['ok'=>true,'items'=>$st->fetchAll()]); -} - -if ($action === 'sections.get') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $st = $pdoTpl->prepare("SELECT id, customer_id, template_id, name, z_index, type, updated_at - FROM emailtemplate_sections - WHERE id=:id AND customer_id=:cid - LIMIT 1"); - $st->execute([':id'=>$id, ':cid'=>$customerId]); - $row = $st->fetch(); - if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404); - json_out(['ok'=>true,'item'=>$row]); -} - -/* ------------------------- Blocks ------------------------- */ -if ($action === 'blocks.list') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $st = $pdoTpl->prepare("SELECT id, name, category, updated_at - FROM emailtemplate_blocks - WHERE customer_id=:cid - ORDER BY name ASC - LIMIT 1000"); - $st->execute([':cid'=>$customerId]); - json_out(['ok'=>true,'items'=>$st->fetchAll()]); -} - -if ($action === 'blocks.get') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $st = $pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at - FROM emailtemplate_blocks - WHERE id=:id AND customer_id=:cid - LIMIT 1"); - $st->execute([':id'=>$id, ':cid'=>$customerId]); - $row = $st->fetch(); - if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404); - json_out(['ok'=>true,'item'=>$row]); -} - -if ($action === 'blocks.create') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in=in_json(); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??'')); - if ($name==='') json_out(['ok'=>false,'error'=>'name_required'], 400); - $st=$pdoTpl->prepare("INSERT INTO emailtemplate_blocks (customer_id,name,category,created_at,updated_at) - VALUES (:cid,:name,COALESCE(NULLIF(:cat,''),'Default'),NOW(),NOW())"); - $st->execute([':cid'=>$customerId, ':name'=>$name, ':cat'=>$cat]); - json_out(['ok'=>true,'id'=>(int)$pdoTpl->lastInsertId()]); -} - -if ($action === 'blocks.update') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in=in_json(); $id=(int)($in['id']??0); $name=trim((string)($in['name']??'')); $cat=trim((string)($in['category']??'')); - if ($id<=0 || $name==='') json_out(['ok'=>false,'error'=>'invalid_params'], 400); - $st=$pdoTpl->prepare("UPDATE emailtemplate_blocks - SET name=:name, category=COALESCE(NULLIF(:cat,''),category), updated_at=NOW() - WHERE id=:id AND customer_id=:cid"); - $st->execute([':name'=>$name, ':cat'=>$cat, ':id'=>$id, ':cid'=>$customerId]); - json_out(['ok'=>true,'updated'=>$st->rowCount()]); -} - -if ($action === 'blocks.delete') { - require_role($CFG, ['owner','admin']); - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in=in_json(); $id=(int)($in['id']??0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $st=$pdoTpl->prepare("DELETE FROM emailtemplate_blocks WHERE id=:id AND customer_id=:cid"); - $st->execute([':id'=>$id, ':cid'=>$customerId]); - json_out(['ok'=>true,'deleted'=>$st->rowCount()]); -} - -/* ------------------------- Snippets ------------------------- */ -if ($action === 'snippets.list') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $st=$pdoTpl->prepare("SELECT id, name, category, updated_at - FROM emailtemplate_snippets - WHERE customer_id=:cid - ORDER BY name ASC - LIMIT 1000"); - $st->execute([':cid'=>$customerId]); - json_out(['ok'=>true,'items'=>$st->fetchAll()]); -} - -if ($action === 'snippets.get') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $id = isset($_GET['id']) ? (int)$_GET['id'] : (int)($_POST['id'] ?? 0); - if ($id<=0) json_out(['ok'=>false,'error'=>'missing_id'], 400); - $st=$pdoTpl->prepare("SELECT id, customer_id, name, category, updated_at - FROM emailtemplate_snippets - WHERE id=:id AND customer_id=:cid - LIMIT 1"); - $st->execute([':id'=>$id, ':cid'=>$customerId]); - $row=$st->fetch(); - if (!$row) json_out(['ok'=>false,'error'=>'not_found'], 404); - json_out(['ok'=>true,'item'=>$row]); -} - -/* ------------------------- Assets (READ) ------------------------- */ -if ($action === 'assets.list') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $st=$pdoTpl->prepare("SELECT id, name, type, mime_type, size_bytes, public_url, updated_at - FROM emailtemplate_assets - WHERE customer_id=:cid - ORDER BY updated_at DESC, id DESC - LIMIT 200"); - $st->execute([':cid'=>$customerId]); - json_out(['ok'=>true,'items'=>$st->fetchAll()]); -} - -/* ------------------------- Editor-Referenzen (Placeholders) ------------------------- */ -if ($action === 'template_items.sync') { json_out(['ok'=>true]); } -if ($action === 'section_items.sync') { json_out(['ok'=>true]); } - -/* ------------------------- Render (Fallback) ------------------------- */ -if ($action === 'render.preview') { - if (!$pdoTpl) json_out(['ok'=>false,'error'=>'templates_db_not_configured'], 500); - $in = in_json(); - $templateId = (int)($in['template_id'] ?? 0); - if ($templateId<=0) json_out(['ok'=>false,'error'=>'template_id_required'], 400); - $st = $pdoTpl->prepare("SELECT id, name FROM emailtemplate_templates WHERE id=:id AND customer_id=:cid LIMIT 1"); - $st->execute([':id'=>$templateId, ':cid'=>$customerId]); - $tpl = $st->fetch(); - if (!$tpl) json_out(['ok'=>false,'error'=>'not_found'], 404); - $html = "\n
Preview okay.
"; - json_out(['ok'=>true, 'template'=>$tpl, 'html'=>$html]); -} - -/* ------------------------- Fallback ------------------------- */ -json_out(['ok'=>false,'error'=>'unknown_action','action'=>$action], 404); - diff --git a/public/version.php b/public/version.php deleted file mode 100644 index 62a2de0..0000000 --- a/public/version.php +++ /dev/null @@ -1,3 +0,0 @@ -