From 2958524ef2da3bcaffc6204f5bfec36fbf743ba5 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 27 Aug 2026 13:43:40 +0200 Subject: [PATCH] CoreWiki 1.0 --- .gitignore | 12 ++ Dockerfile | 12 ++ README.md | 55 ++++++ data/.gitkeep | 0 docker-compose.yaml | 11 ++ src/.htaccess | 4 + src/app.html | 440 ++++++++++++++++++++++++++++++++++++++++++++ src/auth.php | 111 +++++++++++ src/database.php | 33 ++++ src/index.php | 11 ++ src/router.php | 42 +++++ src/wiki.php | 102 ++++++++++ 12 files changed, 833 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 data/.gitkeep create mode 100644 docker-compose.yaml create mode 100644 src/.htaccess create mode 100644 src/app.html create mode 100644 src/auth.php create mode 100644 src/database.php create mode 100644 src/index.php create mode 100644 src/router.php create mode 100644 src/wiki.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cbf59d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# OS & Editor Files +.DS_Store +.vscode/ +.idea/ + +# CoreWiki Data Directory +# Ignoriert alles im data-Ordner (Datenbank, Uploads, Logs) +data/* + +# Behält aber den data-Ordner selbst im Repo (wichtig für den Docker-Mount!) +!data/.gitkeep +!data/.gitignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..915b614 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM php:8.2-apache +RUN a2enmod rewrite ssl +RUN sed -i 's/AllowOverride None/AllowOverride All/g' /etc/apache2/apache2.conf +RUN apt-get update && apt-get install -y libsqlite3-dev openssl \ + && docker-php-ext-install pdo pdo_sqlite \ + && apt-get clean && rm -rf /var/lib/apt/lists/* +RUN openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/ssl/private/ssl-cert-snakeoil.key \ + -out /etc/ssl/certs/ssl-cert-snakeoil.pem \ + -subj "/C=DE/ST=Brandenburg/L=Gruenheide/O=Tessmann Digital/CN=localhost" +RUN a2ensite default-ssl +WORKDIR /var/www/html \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c769877 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# 📚 CoreWiki + +**Das Zero-Bloat Wissens- und Dokumentationsportal von Tessmann Digital.** + +CoreWiki ist ein extrem schlankes, pfeilschnelles Wiki-System, das speziell für IT-Consultants, Systemadministratoren und Entwickler gebaut wurde. Anstatt auf überladene Enterprise-Frameworks zu setzen, nutzt CoreWiki natives Vanilla PHP, eine leichtgewichtige SQLite-Datenbank und das TailwindCSS Framework für ein sauberes Dark-Mode UI. + +## ✨ Features + +* **Zero-Friction Architektur:** Keine Ladezeiten, keine externen Datenbank-Server (MariaDB/MySQL) nötig. Alles liegt sicher in einer lokalen SQLite-Datei. +* **Natives Markdown:** Artikel werden in reinem Markdown geschrieben und in Echtzeit geparst. +* **Infrastruktur als Code (Mermaid.js):** Erstelle komplexe Netzpläne, Stern-Topologien und Flussdiagramme direkt durch Code-Blöcke (` ```mermaid `) – ohne externe Grafikprogramme. +* **Drag & Drop Media:** Bilder und Screenshots können direkt in den Editor gezogen werden und werden sicher im lokalen Volume abgelegt. +* **Dynamische Kategorien:** Unendlich tief verschachtelbare Kategorie-Bäume für eine saubere Ordnerstruktur mit festen Icons (📁, 💻, 🖥️, 🌐, 📝, ⚙️). +* **Integriertes Rechtesystem:** Volle Administrationsoberfläche (CoreTemplate Basis) inkl. Audit-Logs, Settings-Manager und User-Rollen (Admin/Author). +* **Automatische Historie:** Verfolge die global zuletzt aktualisierten Artikel und behalte deine persönliche Lese-Historie in der Seitenleiste im Blick. + +## 🛠️ Tech Stack + +* **Backend:** Vanilla PHP 8.2 (Apache) +* **Datenbank:** SQLite3 +* **Frontend:** HTML5, Vanilla JavaScript, TailwindCSS (CDN) +* **Parser:** marked.js (Markdown), Mermaid.js (Diagramme) + +## 📂 Verzeichnisstruktur + +```text +/ +├── docker-compose.yml +├── Dockerfile +├── README.md +├── data/ # Persistenter Speicher (SQLite DB, Uploads, Logs) +└── src/ # Der gesamte PHP App-Code + ├── index.php + ├── database.php + ├── wiki.php + ├── auth.php + ├── router.php + └── app.html +``` + +## 🚀 Deployment (Docker) + +1. Repository klonen. +2. Berechtigungen für den `data` Ordner setzen (verhindert Error 500 durch Apache-Schreibrechte): + ```bash + sudo chown -R 33:33 ./data + # oder alternativ für Testumgebungen: sudo chmod -R 777 ./data + ``` +3. Container starten: + ```bash + docker compose up -d --build + ``` +4. Das Wiki ist nun unter `https://:8443` erreichbar. + +*Default Login:* `admin` / `admin` (Bitte nach dem ersten Login sofort im User-Tab oder per Profil ändern!) \ No newline at end of file diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..032abf6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,11 @@ +version: '3.8' +services: + corewiki: + build: . + container_name: corewiki_app + ports: + - "8443:443" + volumes: + - ./src:/var/www/html + - ./data:/var/www/data + restart: unless-stopped \ No newline at end of file diff --git a/src/.htaccess b/src/.htaccess new file mode 100644 index 0000000..307cde9 --- /dev/null +++ b/src/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [QSA,L] \ No newline at end of file diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..981722d --- /dev/null +++ b/src/app.html @@ -0,0 +1,440 @@ + + + + + + CoreWiki + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/auth.php b/src/auth.php new file mode 100644 index 0000000..0a510ad --- /dev/null +++ b/src/auth.php @@ -0,0 +1,111 @@ +prepare("SELECT * FROM users WHERE username = ?"); + $stmt->execute([$username]); + $user =$stmt->fetch(); + + if ($user && password_verify($password,$user['password_hash'])) { + $_SESSION['user_id'] =$user['id']; + $_SESSION['username'] =$user['username']; + $_SESSION['role'] =$user['role']; + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$user['id'], $user['username'], 'User Login',$_SERVER['REMOTE_ADDR']]); + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false]); + } + exit; +} + +function handleLogout() { + global $pdo; @session_start(); + if (isset($_SESSION['user_id'])) {$pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'],$_SESSION['username'], 'User Logout', 'System']); + } + session_destroy(); echo json_encode(['success' => true]); exit; +} + +function handleGetLogs() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt =$pdo->query("SELECT * FROM audit_logs ORDER BY id DESC LIMIT 50"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleGetSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt =$pdo->query("SELECT * FROM settings ORDER BY config_key ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleSaveSettings() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $key = trim($_POST['config_key'] ?? ''); $value = trim($_POST['config_value'] ?? ''); + if ($key === '') { echo json_encode(['success' => false, 'error' => 'Schlüssel darf nicht leer sein.']); exit; } + $pdo->prepare("REPLACE INTO settings (config_key, config_value) VALUES (?, ?)")->execute([$key,$value]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting Update', "Key: $key"]); + echo json_encode(['success' => true]); exit; +} + +function handleDeleteSetting() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $key = trim($_POST['config_key'] ?? ''); + $pdo->prepare("DELETE FROM settings WHERE config_key = ?")->execute([$key]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'Setting gelöscht', "Key: $key"]); + echo json_encode(['success' => true]); exit; +} + +function handleChangePassword() { + global $pdo; @session_start(); + $userId =$_SESSION['user_id'] ?? 0; + if (!$userId) { http_response_code(401); exit; } + $oldPass =$_POST['old_password'] ?? ''; $newPass =$_POST['new_password'] ?? ''; + $stmt =$pdo->prepare("SELECT password_hash FROM users WHERE id = ?"); $stmt->execute([$userId]); $user =$stmt->fetch(); + if ($user && password_verify($oldPass,$user['password_hash'])) { + if (strlen($newPass) < 4) { echo json_encode(['success' => false, 'error' => 'Min 4 Zeichen.']); exit; } + $newHash = password_hash($newPass, PASSWORD_DEFAULT); + $pdo->prepare("UPDATE users SET password_hash = ? WHERE id = ?")->execute([$newHash, $userId]);$pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$userId,$_SESSION['username'], 'Passwort geändert', 'Self-Service']); + echo json_encode(['success' => true]); + } else { + echo json_encode(['success' => false, 'error' => 'Altes Passwort falsch.']); + } + exit; +} + +function handleGetUsers() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $stmt =$pdo->query("SELECT id, username, role FROM users ORDER BY id ASC"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleCreateUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $username = trim($_POST['username'] ?? ''); $password =$_POST['password'] ?? ''; $role =$_POST['role'] === 'admin' ? 'admin' : 'user'; + try { + $hash = password_hash($password, PASSWORD_DEFAULT); + $pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")->execute([$username, $hash,$role]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User erstellt', "User: $username"]); + echo json_encode(['success' => true]); + } catch (PDOException $e) { + echo json_encode(['success' => false, 'error' => 'Fehler (evtl. existiert der Name schon).']); + } + exit; +} + +function handleDeleteUser() { + global $pdo; @session_start(); + if (($_SESSION['role'] ?? '') !== 'admin') { http_response_code(403); exit; } + $deleteId =$_POST['id'] ?? 0; + if ($deleteId ==$_SESSION['user_id']) { echo json_encode(['success' => false, 'error' => 'Du kannst dich nicht selbst löschen.']); exit; } + $pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$deleteId]); + $pdo->prepare("INSERT INTO audit_logs (user_id, username, action, target) VALUES (?, ?, ?, ?)")->execute([$_SESSION['user_id'], $_SESSION['username'], 'User gelöscht', "User ID: $deleteId"]); + echo json_encode(['success' => true]); exit; +} +?> \ No newline at end of file diff --git a/src/database.php b/src/database.php new file mode 100644 index 0000000..ac53198 --- /dev/null +++ b/src/database.php @@ -0,0 +1,33 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); + $pdo->setAttribute(PDO::ATTR_TIMEOUT, 5);$pdo->exec("PRAGMA journal_mode = WAL;"); + $pdo->exec("PRAGMA busy_timeout = 5000;"); + + // === CoreTemplate Basis Tabellen === + $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user')"); + $pdo->exec("CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, username TEXT, action TEXT, target TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP)"); + $pdo->exec("CREATE TABLE IF NOT EXISTS settings (config_key TEXT PRIMARY KEY, config_value TEXT NOT NULL)"); + + // === CoreWiki Tabellen === + $pdo->exec("CREATE TABLE IF NOT EXISTS categories (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, icon TEXT, sort_order INTEGER DEFAULT 0)"); + + // NEU: Parent-ID für Unterkategorien patchen, falls die Tabelle schon existiert + try { $pdo->exec("ALTER TABLE categories ADD COLUMN parent_id INTEGER DEFAULT NULL"); } catch (PDOException $e) {} + + $pdo->exec("CREATE TABLE IF NOT EXISTS articles (id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER, title TEXT, content_md TEXT, author_id INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); + $pdo->exec("CREATE TABLE IF NOT EXISTS user_history (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, article_id INTEGER, viewed_at DATETIME DEFAULT CURRENT_TIMESTAMP)"); + + // Initialer Admin & Default-Content + $stmt =$pdo->query("SELECT COUNT(*) FROM users"); + if ($stmt->fetchColumn() == 0) { + $hash = password_hash('admin', PASSWORD_DEFAULT);$pdo->prepare("INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)")->execute(['admin', $hash, 'admin']);$pdo->exec("INSERT INTO categories (name, icon, sort_order) VALUES ('Server Configs', '🖥️', 1), ('Web Programmierung', '💻', 2), ('Infrastruktur', '⚙️', 3)"); + $defaultMd = "# Willkommen im CoreWiki!\n\nDein neues, pfeilschnelles Dokumentationsportal.\n\n## Dein erster Netzplan\n```mermaid\ngraph TD\n WAN((Internet)) --> FW[OPNsense VM]\n FW --> PBS[Strato PBS]\n```\n\n*(Klicke oben rechts auf Bearbeiten, um loszulegen!)*"; + $pdo->prepare("INSERT INTO articles (category_id, title, content_md, author_id) VALUES (?, ?, ?, ?)")->execute([1, 'Willkommen im CoreWiki', $defaultMd, 1]); } } catch (PDOException$e) { + die(json_encode(['success' => false, 'error' => 'Datenbankfehler: ' . $e->getMessage()])); +} +?> \ No newline at end of file diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..b2ffcc4 --- /dev/null +++ b/src/index.php @@ -0,0 +1,11 @@ +getMessage() . " in " . $e->getFile() . " on line " . $e->getLine());$request = $_SERVER['REQUEST_URI'] ?? ''; if (strpos($request, '/api/') === 0) { http_response_code(500); header('Content-Type: application/json'); echo json_encode(['success' => false, 'error' => 'Interner Systemfehler. Bitte Logs prüfen.']); } else { http_response_code(500); echo "

500 - Systemfehler

"; } exit; }); +header('Content-Type: application/json'); + +require_once 'database.php'; +require_once 'auth.php'; +require_once 'wiki.php'; +require_once 'router.php'; +?> \ No newline at end of file diff --git a/src/router.php b/src/router.php new file mode 100644 index 0000000..e6f5880 --- /dev/null +++ b/src/router.php @@ -0,0 +1,42 @@ + true, 'status' => 'online', 'system' => 'CoreWiki']); +// CoreTemplate Routes +} elseif ($path === '/api/logs' and$method === 'GET') { handleGetLogs(); +} elseif ($path === '/api/settings' and$method === 'GET') { handleGetSettings(); +} elseif ($path === '/api/settings' and$method === 'POST') { handleSaveSettings(); +} elseif ($path === '/api/settings/delete' and$method === 'POST') { handleDeleteSetting(); +} elseif ($path === '/api/password' and$method === 'POST') { handleChangePassword(); +} elseif ($path === '/api/users' and$method === 'GET') { handleGetUsers(); +} elseif ($path === '/api/users' and$method === 'POST') { handleCreateUser(); +} elseif ($path === '/api/users/delete' and$method === 'POST') { handleDeleteUser(); +// CoreWiki Routes +} elseif ($path === '/api/categories' and$method === 'GET') { handleGetCategories(); +} elseif ($path === '/api/categories/save' and$method === 'POST') { handleSaveCategory(); +} elseif ($path === '/api/categories/delete' and$method === 'POST') { handleDeleteCategory(); +} elseif ($path === '/api/article' and$method === 'GET') { handleGetArticle(); +} elseif ($path === '/api/article' and$method === 'POST') { handleSaveArticle(); +} elseif ($path === '/api/article/delete' and$method === 'POST') { handleDeleteArticle(); +} elseif ($path === '/api/latest' and$method === 'GET') { handleGetLatestUpdates(); // NEU +} elseif ($path === '/api/history' and$method === 'GET') { handleGetHistory(); +} elseif ($path === '/api/upload' and$method === 'POST') { handleImageUpload(); +// Image Provider +} elseif (strpos($path, '/api/uploads/') === 0) { + $file = '/var/www/data/uploads/' . basename($path); + if (file_exists($file)) { header('Content-Type: ' . mime_content_type($file)); readfile($file); exit; } else { http_response_code(404); exit; } +} elseif ($path === '/api/me' and$method === 'GET') { + @session_start(); + if (isset($_SESSION['user_id'])) { echo json_encode(['success' => true, 'username' => $_SESSION['username'], 'role' =>$_SESSION['role']]); } + else { http_response_code(401); echo json_encode(['success' => false, 'error' => 'Not authenticated']); } + exit; +} else { + http_response_code(404); echo json_encode(['success' => false, 'error' => 'Endpoint not found']); +} +?> \ No newline at end of file diff --git a/src/wiki.php b/src/wiki.php new file mode 100644 index 0000000..24cc120 --- /dev/null +++ b/src/wiki.php @@ -0,0 +1,102 @@ +query("SELECT * FROM categories ORDER BY sort_order ASC, name ASC")->fetchAll(); + foreach ($cats as &$cat) { + $stmt =$pdo->prepare("SELECT id, title FROM articles WHERE category_id = ? ORDER BY title ASC"); + $stmt->execute([$cat['id']]); + $cat['articles'] =$stmt->fetchAll(); + } + echo json_encode(['success' => true, 'data' => $cats]); exit; +} + +// NEU: Kategorien speichern (Erstellen & Bearbeiten) +function handleSaveCategory() { + global $pdo; @session_start(); if (($_SESSION['role']??'') !== 'admin') exit; + $id =$_POST['id'] ?? 0; + $name = trim($_POST['name'] ?? ''); + $icon = trim($_POST['icon'] ?? '📁'); + $parentId =$_POST['parent_id'] ?? ''; + if ($parentId === '')$parentId = null; + if ($name === '') { echo json_encode(['success' => false]); exit; } + + if ($id == 0) { + $pdo->prepare("INSERT INTO categories (name, icon, parent_id) VALUES (?, ?, ?)")->execute([$name, $icon,$parentId]); + } else { + $pdo->prepare("UPDATE categories SET name = ?, icon = ?, parent_id = ? WHERE id = ?")->execute([$name,$icon, $parentId,$id]); + } + echo json_encode(['success' => true]); exit; +} + +// NEU: Kategorie löschen +function handleDeleteCategory() { + global $pdo; @session_start(); if (($_SESSION['role']??'') !== 'admin') exit; + $id =$_POST['id'] ?? 0; + // Fallback: Artikel aus gelöschter Kategorie kommen in die Root-Kategorie 1 + $pdo->prepare("UPDATE articles SET category_id = 1 WHERE category_id = ?")->execute([$id]); + $pdo->prepare("UPDATE categories SET parent_id = NULL WHERE parent_id = ?")->execute([$id]); + $pdo->prepare("DELETE FROM categories WHERE id = ?")->execute([$id]); + echo json_encode(['success' => true]); exit; +} + +function handleGetArticle() { + global $pdo; @session_start(); if (!isset($_SESSION['user_id'])) exit; + $id =$_GET['id'] ?? 0; + $stmt =$pdo->prepare("SELECT a.*, c.name as category_name, u.username as author_name FROM articles a LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN users u ON a.author_id = u.id WHERE a.id = ?"); + $stmt->execute([$id]); + $article =$stmt->fetch(); + if ($article) { + $pdo->prepare("DELETE FROM user_history WHERE user_id = ? AND article_id = ?")->execute([$_SESSION['user_id'], $id]);$pdo->prepare("INSERT INTO user_history (user_id, article_id) VALUES (?, ?)")->execute([$_SESSION['user_id'],$id]); + echo json_encode(['success' => true, 'data' => $article]); + } else { echo json_encode(['success' => false]); } + exit; +} + +function handleSaveArticle() { + global $pdo; @session_start(); if (!isset($_SESSION['user_id'])) exit; + $id =$_POST['id'] ?? 0; + $catId =$_POST['category_id'] ?? 1; + $title = trim($_POST['title'] ?? 'Neuer Artikel'); + $content = trim($_POST['content_md'] ?? ''); + + if ($id == 0) {$pdo->prepare("INSERT INTO articles (category_id, title, content_md, author_id) VALUES (?, ?, ?, ?)")->execute([$catId,$title, $content,$_SESSION['user_id']]); + $id =$pdo->lastInsertId(); + } else { + $pdo->prepare("UPDATE articles SET category_id = ?, title = ?, content_md = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?")->execute([$catId,$title, $content,$id]); + } + echo json_encode(['success' => true, 'id' => $id]); exit; +} + +function handleDeleteArticle() { + global $pdo; @session_start(); if (($_SESSION['role']??'') !== 'admin') exit; + $pdo->prepare("DELETE FROM articles WHERE id = ?")->execute([$_POST['id'] ?? 0]); + $pdo->prepare("DELETE FROM user_history WHERE article_id = ?")->execute([$_POST['id'] ?? 0]); + echo json_encode(['success' => true]); exit; +} + +// NEU: Zuletzt global aktualisiert +function handleGetLatestUpdates() { + global $pdo; @session_start(); if (!isset($_SESSION['user_id'])) exit; + $stmt =$pdo->query("SELECT a.id, a.title, u.username as author_name FROM articles a LEFT JOIN users u ON a.author_id = u.id ORDER BY a.updated_at DESC LIMIT 5"); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleGetHistory() { + global $pdo; @session_start(); if (!isset($_SESSION['user_id'])) exit; + $stmt =$pdo->prepare("SELECT h.viewed_at, a.id, a.title FROM user_history h JOIN articles a ON h.article_id = a.id WHERE h.user_id = ? ORDER BY h.viewed_at DESC LIMIT 5"); + $stmt->execute([$_SESSION['user_id']]); + echo json_encode(['success' => true, 'data' => $stmt->fetchAll()]); exit; +} + +function handleImageUpload() { + @session_start(); if (!isset($_SESSION['user_id'])) exit; + if (!isset($_FILES['image']) or$_FILES['image']['error'] !== UPLOAD_ERR_OK) { echo json_encode(['success' => false]); exit; } + $ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION); + $filename = uniqid('img_') . '.' .$ext; + $target = '/var/www/data/uploads/' .$filename; + if (move_uploaded_file($_FILES['image']['tmp_name'],$target)) { + echo json_encode(['success' => true, 'url' => '/api/uploads/' . $filename]); + } else { echo json_encode(['success' => false]); } + exit; +} +?> \ No newline at end of file