CoreWiki 1.0
This commit is contained in:
+12
@@ -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
|
||||||
+12
@@ -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
|
||||||
@@ -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://<server-ip>:8443` erreichbar.
|
||||||
|
|
||||||
|
*Default Login:* `admin` / `admin` (Bitte nach dem ersten Login sofort im User-Tab oder per Profil ändern!)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
RewriteEngine On
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||||
+440
@@ -0,0 +1,440 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CoreWiki</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
||||||
|
mermaid.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' });
|
||||||
|
window.mermaid = mermaid;
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = { theme: { extend: { colors: { darkbg: '#0f172a', darkcard: '#1e293b', darkborder: '#334155', accent: '#3b82f6' } } } }
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.prose h1 { font-size: 2.25rem; font-weight: bold; margin-bottom: 1rem; color: white; border-bottom: 1px solid #334155; padding-bottom: 0.5rem;}
|
||||||
|
.prose h2 { font-size: 1.5rem; font-weight: bold; margin-top: 1.5rem; margin-bottom: 0.75rem; color: white;}
|
||||||
|
.prose p { margin-bottom: 1rem; line-height: 1.6; color: #cbd5e1; }
|
||||||
|
.prose code { background: #0f172a; padding: 0.2rem 0.4rem; border-radius: 4px; font-family: monospace; color: #60a5fa; }
|
||||||
|
.prose pre { background: #0f172a; padding: 1rem; border-radius: 8px; overflow-x: auto; margin-bottom: 1rem; border: 1px solid #334155; }
|
||||||
|
.prose pre code { background: transparent; padding: 0; color: #e2e8f0; }
|
||||||
|
.prose img { max-width: 100%; border-radius: 8px; border: 1px solid #334155; margin: 1rem 0;}
|
||||||
|
.prose a { color: #3b82f6; text-decoration: none; } .prose a:hover { text-decoration: underline; }
|
||||||
|
.prose ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; color: #cbd5e1;}
|
||||||
|
textarea::-webkit-scrollbar { width: 8px; } textarea::-webkit-scrollbar-thumb { background-color: #334155; border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-darkbg text-gray-300 font-sans h-screen flex flex-col overflow-hidden">
|
||||||
|
|
||||||
|
<!-- LOGIN SCREEN -->
|
||||||
|
<div id="loginView" class="flex-1 flex items-center justify-center hidden">
|
||||||
|
<div class="bg-darkcard border border-darkborder rounded-xl p-8 shadow-2xl w-full max-w-sm">
|
||||||
|
<h2 class="text-2xl font-bold text-white mb-6 text-center">CoreSuite Login</h2>
|
||||||
|
<div id="loginError" class="text-red-500 text-sm text-center mb-4 hidden">Login fehlgeschlagen.</div>
|
||||||
|
<form id="loginForm" class="space-y-4">
|
||||||
|
<div><label class="block text-xs font-medium text-gray-500 mb-1 uppercase tracking-wider">Benutzername</label><input type="text" id="username" class="w-full bg-darkbg border border-darkborder focus:border-accent rounded p-2.5 text-white outline-none" required></div>
|
||||||
|
<div><label class="block text-xs font-medium text-gray-500 mb-1 uppercase tracking-wider">Passwort</label><input type="password" id="password" class="w-full bg-darkbg border border-darkborder focus:border-accent rounded p-2.5 text-white outline-none" required></div>
|
||||||
|
<button type="submit" class="w-full bg-accent hover:bg-blue-500 text-white font-bold py-3 rounded mt-2 transition-colors">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MAIN APP -->
|
||||||
|
<div id="appView" class="flex-1 flex flex-col h-full hidden">
|
||||||
|
<header class="bg-darkcard border-b border-darkborder h-16 flex items-center justify-between px-6 shrink-0 z-10 shadow-sm">
|
||||||
|
<div class="flex items-center gap-3 w-64 cursor-pointer" onclick="openArticle(1)">
|
||||||
|
<div class="bg-accent p-1.5 rounded-lg text-white"><span class="text-lg">📚</span></div>
|
||||||
|
<span class="text-xl font-bold text-white tracking-wider">CoreWiki</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-6 justify-end">
|
||||||
|
<button onclick="createNewArticle()" class="bg-accent hover:bg-blue-500 text-white px-4 py-1.5 rounded-lg text-sm font-bold transition-colors flex items-center gap-2 shadow-lg shadow-accent/20">
|
||||||
|
<span>+ Neuer Artikel</span>
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-3 border-l border-darkborder pl-6">
|
||||||
|
<button onclick="openModal('pwdModal')" class="text-gray-400 hover:text-white px-2" title="Passwort ändern">🔑</button>
|
||||||
|
<button id="navAdmin" onclick="openModal('adminModal')" class="text-gray-400 hover:text-white px-2 hidden" title="System Settings">⚙️</button>
|
||||||
|
<div class="text-right ml-2 border-l border-darkborder pl-4">
|
||||||
|
<p class="text-xs text-gray-500">Angemeldet</p>
|
||||||
|
<p class="text-sm text-white font-bold" id="userNameDisplay"></p>
|
||||||
|
</div>
|
||||||
|
<button onclick="logout()" class="text-gray-500 hover:text-red-500 ml-2" title="Abmelden">✖</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- 3-SPALTEN LAYOUT -->
|
||||||
|
<div class="flex-1 flex overflow-hidden">
|
||||||
|
<!-- LINKE SPALTE: Katalog -->
|
||||||
|
<aside class="w-72 bg-darkcard border-r border-darkborder flex flex-col shrink-0">
|
||||||
|
<div class="p-5 flex-1 overflow-y-auto">
|
||||||
|
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-4">Bibliotheken</h3>
|
||||||
|
<nav class="space-y-4" id="categoryNav"></nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- MITTLERE SPALTE: Content & Editor -->
|
||||||
|
<main class="flex-1 bg-darkbg overflow-y-auto relative flex flex-col">
|
||||||
|
<div class="max-w-4xl w-full mx-auto p-10 flex-1 flex flex-col">
|
||||||
|
<div id="readView" class="flex-1 flex flex-col">
|
||||||
|
<div class="border-b border-darkborder pb-4 mb-6 flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 text-xs text-accent font-bold mb-2"><span id="readCategory"></span></div>
|
||||||
|
<h1 class="text-3xl font-bold text-white mb-1" id="readTitle">Lade...</h1>
|
||||||
|
<p class="text-xs text-gray-500">Von <span id="readAuthor"></span> am <span id="readDate"></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick="deleteArticle()" id="btnDelete" class="bg-darkbg border border-red-500/50 text-red-500 hover:bg-red-500 hover:text-white px-3 py-1.5 rounded text-xs transition-colors hidden">Löschen</button>
|
||||||
|
<button onclick="toggleEditMode()" id="btnEdit" class="bg-darkcard border border-darkborder hover:border-accent text-white px-4 py-1.5 rounded text-sm transition-colors hidden">✏️ Bearbeiten</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article id="readContent" class="prose flex-1 pb-10">Lade Inhalt...</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="editView" class="flex-1 flex flex-col hidden">
|
||||||
|
<div class="border-b border-darkborder pb-4 mb-6 flex justify-between items-center">
|
||||||
|
<div class="flex-1 flex gap-4 mr-4">
|
||||||
|
<select id="editCategory" class="bg-darkcard border border-darkborder rounded p-2 text-white text-sm outline-none focus:border-accent w-48"></select>
|
||||||
|
<input type="text" id="editTitle" class="flex-1 bg-darkcard border border-darkborder rounded p-2 text-white font-bold outline-none focus:border-accent" placeholder="Artikel Titel...">
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button onclick="toggleEditMode()" class="text-gray-400 hover:text-white px-4 py-1.5 rounded text-sm transition-colors">Abbrechen</button>
|
||||||
|
<button onclick="saveArticle()" class="bg-accent hover:bg-blue-500 text-white px-4 py-1.5 rounded text-sm font-bold transition-colors shadow-lg shadow-accent/20">💾 Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-500/10 border border-accent/30 text-accent text-xs p-2 rounded mb-4 flex items-center justify-between">
|
||||||
|
<span>Markdown & HTML unterstützt. <b>Bilder per Drag & Drop!</b> Netzpläne via ```mermaid</span>
|
||||||
|
</div>
|
||||||
|
<textarea id="editContent" class="flex-1 w-full bg-darkcard border border-darkborder rounded-lg p-4 text-gray-300 font-mono text-sm outline-none focus:border-accent resize-none mb-10" placeholder="Schreibe deinen Artikel hier in Markdown..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- RECHTE SPALTE: Historie & Updates -->
|
||||||
|
<aside class="w-64 bg-darkcard border-l border-darkborder p-5 flex flex-col shrink-0">
|
||||||
|
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">Zuletzt aktualisiert</h3>
|
||||||
|
<div id="latestNav" class="space-y-1 overflow-y-auto mb-8 pb-4 border-b border-darkborder"></div>
|
||||||
|
|
||||||
|
<h3 class="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3">Zuletzt gelesen (Du)</h3>
|
||||||
|
<div id="historyNav" class="space-y-1 overflow-y-auto"></div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CORETEMPLATE MODALS -->
|
||||||
|
<div id="pwdModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
|
||||||
|
<div class="bg-darkcard border border-darkborder rounded-xl p-6 shadow-2xl w-full max-w-sm">
|
||||||
|
<div class="flex justify-between items-center mb-4"><h2 class="text-lg font-bold text-white">Passwort ändern</h2><button onclick="closeModal('pwdModal')" class="text-gray-400 hover:text-white">✕</button></div>
|
||||||
|
<div id="pwMsg" class="text-xs mb-3 text-green-400 hidden"></div>
|
||||||
|
<form id="passwordForm" class="space-y-4">
|
||||||
|
<input type="password" id="oldPassword" placeholder="Altes Passwort" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
|
||||||
|
<input type="password" id="newPassword" placeholder="Neues Passwort (min. 4)" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
|
||||||
|
<input type="password" id="newPasswordConfirm" placeholder="Passwort wiederholen" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white outline-none focus:border-accent">
|
||||||
|
<button type="submit" class="w-full bg-accent text-white font-bold py-2 rounded mt-2">Ändern</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="adminModal" class="fixed inset-0 bg-black/80 hidden z-50 flex items-center justify-center backdrop-blur-sm">
|
||||||
|
<div class="bg-darkcard border border-darkborder rounded-xl shadow-2xl w-full max-w-5xl flex flex-col h-[85vh]">
|
||||||
|
<div class="p-5 border-b border-darkborder flex justify-between items-center bg-darkbg rounded-t-xl">
|
||||||
|
<h2 class="text-xl font-bold text-white">System Administration</h2>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button onclick="switchAdminTab('categories')" id="btn-admin-categories" class="text-accent font-bold">Kategorien</button>
|
||||||
|
<button onclick="switchAdminTab('users')" id="btn-admin-users" class="text-gray-400 hover:text-white">Benutzer</button>
|
||||||
|
<button onclick="switchAdminTab('settings')" id="btn-admin-settings" class="text-gray-400 hover:text-white">Settings</button>
|
||||||
|
<button onclick="switchAdminTab('logs')" id="btn-admin-logs" class="text-gray-400 hover:text-white">Audit Log</button>
|
||||||
|
<button onclick="closeModal('adminModal')" class="text-gray-500 hover:text-red-500 ml-4 border-l border-darkborder pl-4">✕ Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 overflow-y-auto flex-1 bg-darkbg">
|
||||||
|
<!-- CATEGORIES TAB -->
|
||||||
|
<div id="admin-tab-categories" class="space-y-6">
|
||||||
|
<div class="bg-darkcard p-4 rounded border border-darkborder">
|
||||||
|
<form id="catForm" class="flex gap-4 items-end">
|
||||||
|
<input type="hidden" id="catId" value="0">
|
||||||
|
<div class="w-32">
|
||||||
|
<label class="block text-xs text-gray-500 mb-1">Icon</label>
|
||||||
|
<select id="catIcon" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white">
|
||||||
|
<option value="📁">📁 Ordner</option>
|
||||||
|
<option value="💻">💻 Code</option>
|
||||||
|
<option value="🖥️">🖥️ Server</option>
|
||||||
|
<option value="🌐">🌐 Netzwerk</option>
|
||||||
|
<option value="📝">📝 Notizen</option>
|
||||||
|
<option value="⚙️">⚙️ Configs</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Kategorie-Name</label><input type="text" id="catName" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Unterkategorie von...</label><select id="catParentId" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="">-- Keine (Hauptordner) --</option></select></div>
|
||||||
|
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
|
||||||
|
<button type="button" onclick="resetCatForm()" class="bg-darkbg border border-darkborder text-gray-400 hover:text-white font-bold py-2 px-4 rounded">X</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="adminCatList" class="space-y-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- USERS TAB -->
|
||||||
|
<div id="admin-tab-users" class="hidden space-y-6">
|
||||||
|
<div class="bg-darkcard p-4 rounded border border-darkborder">
|
||||||
|
<form id="userForm" class="flex gap-4 items-end">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Benutzername</label><input type="text" id="newUsername" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Passwort</label><input type="password" id="newUserPass" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
|
||||||
|
<div class="w-32"><label class="block text-xs text-gray-500 mb-1">Rolle</label><select id="newUserRole" class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"><option value="user">Author</option><option value="admin">Admin</option></select></div>
|
||||||
|
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Anlegen</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">ID</th><th class="p-3">User</th><th class="p-3">Rolle</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="usersTableBody" class="divide-y divide-darkborder"></tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SETTINGS TAB -->
|
||||||
|
<div id="admin-tab-settings" class="hidden space-y-6">
|
||||||
|
<form id="settingsForm" class="flex gap-4 items-end bg-darkcard p-4 rounded border border-darkborder">
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Config Key</label><input type="text" id="configKey" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
|
||||||
|
<div class="flex-1"><label class="block text-xs text-gray-500 mb-1">Value</label><input type="text" id="configValue" required class="w-full bg-darkbg border border-darkborder rounded p-2 text-white"></div>
|
||||||
|
<button type="submit" class="bg-accent text-white font-bold py-2 px-6 rounded">Speichern</button>
|
||||||
|
</form>
|
||||||
|
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Key</th><th class="p-3">Value</th><th class="p-3 text-right">Aktion</th></tr></thead><tbody id="settingsTableBody" class="divide-y divide-darkborder"></tbody></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- LOGS TAB -->
|
||||||
|
<div id="admin-tab-logs" class="hidden">
|
||||||
|
<table class="w-full text-left text-sm text-gray-400"><thead class="bg-darkcard border-b border-darkborder text-xs uppercase"><tr><th class="p-3">Zeitstempel</th><th class="p-3">User</th><th class="p-3">Aktion</th><th class="p-3">Ziel</th></tr></thead><tbody id="logTableBody" class="divide-y divide-darkborder"></tbody></table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentArticleId = 0; let isAdmin = false;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', checkSession);
|
||||||
|
|
||||||
|
function checkSession() {
|
||||||
|
fetch('/api/me').then(res=>res.json()).then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('loginView').classList.add('hidden');
|
||||||
|
document.getElementById('appView').classList.remove('hidden');
|
||||||
|
document.getElementById('userNameDisplay').innerText = data.username;
|
||||||
|
isAdmin = (data.role === 'admin');
|
||||||
|
if (isAdmin) document.getElementById('navAdmin').classList.remove('hidden');
|
||||||
|
loadCategories(); loadLatest(); loadHistory(); openArticle(1);
|
||||||
|
} else {
|
||||||
|
document.getElementById('appView').classList.add('hidden');
|
||||||
|
document.getElementById('loginView').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('loginForm').addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault(); const fd = new FormData();
|
||||||
|
fd.append('username', document.getElementById('username').value);
|
||||||
|
fd.append('password', document.getElementById('password').value);
|
||||||
|
fetch('/api/login', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if (data.success) { document.getElementById('loginError').classList.add('hidden'); checkSession(); }
|
||||||
|
else { document.getElementById('loginError').classList.remove('hidden'); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function logout() { fetch('/api/logout').then(() => { location.reload(); }); }
|
||||||
|
|
||||||
|
function loadCategories() {
|
||||||
|
fetch('/api/categories').then(res=>res.json()).then(data => {
|
||||||
|
if(!data.success) return;
|
||||||
|
const nav = document.getElementById('categoryNav'); const sel = document.getElementById('editCategory');
|
||||||
|
const adminList = document.getElementById('adminCatList'); const adminSel = document.getElementById('catParentId');
|
||||||
|
nav.innerHTML = ''; sel.innerHTML = '';
|
||||||
|
if(adminList) adminList.innerHTML = ''; if(adminSel) adminSel.innerHTML = '<option value="">-- Keine (Hauptordner) --</option>';
|
||||||
|
|
||||||
|
let allCats = data.data;
|
||||||
|
|
||||||
|
// ROBUSTHEITS-CHECK FÜR LEERES WIKI
|
||||||
|
if(allCats.length === 0) {
|
||||||
|
nav.innerHTML = '<p class="text-xs text-gray-500 italic p-2 leading-relaxed">Noch keine Bibliotheken vorhanden.<br>Lege oben rechts unter ⚙️ deine erste Kategorie an!</p>';
|
||||||
|
sel.innerHTML = '<option value="1">Standard Kategorie</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainCats = allCats.filter(c => !c.parent_id);
|
||||||
|
|
||||||
|
allCats.forEach(c => {
|
||||||
|
let safeName = (c.name || 'Unbenannt').replace(/'/g, "\\'");
|
||||||
|
let safeIcon = c.icon || '📁';
|
||||||
|
let safeParent = c.parent_id || '';
|
||||||
|
|
||||||
|
sel.innerHTML += `<option value="${c.id}">${safeIcon} ${c.name || 'Unbenannt'}</option>`;
|
||||||
|
if(adminSel) adminSel.innerHTML += `<option value="${c.id}">${safeIcon} ${c.name || 'Unbenannt'}</option>`;
|
||||||
|
|
||||||
|
if(adminList) {
|
||||||
|
let parentLabel = c.parent_id ? ' <span class="text-xs text-gray-500 ml-2">(Unterkategorie)</span>' : '';
|
||||||
|
adminList.innerHTML += `<div class="flex justify-between items-center bg-darkbg p-2 rounded mb-1 text-sm border border-darkborder"><span class="font-bold text-white">${safeIcon} ${c.name || 'Unbenannt'}${parentLabel}</span> <div><button onclick="editCategory(${c.id}, '${safeName}', '${safeIcon}', '${safeParent}')" class="text-blue-400 hover:text-white mr-3">✏️</button><button onclick="deleteCategory(${c.id})" class="text-red-500 hover:text-white">🗑️</button></div></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainCats.forEach(cat => { nav.innerHTML += buildCategoryTree(cat, allCats); });
|
||||||
|
}).catch(err => console.error(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCategoryTree(cat, allCats) {
|
||||||
|
let children = allCats.filter(c => c.parent_id == cat.id);
|
||||||
|
let safeName = cat.name || 'Unbenannt'; let safeIcon = cat.icon || '📁';
|
||||||
|
let html = `<div><div class="flex items-center gap-2 text-white font-bold mb-2"><span>${safeIcon}</span> ${safeName}</div><ul class="space-y-1 border-l border-darkborder ml-3 pl-3">`;
|
||||||
|
|
||||||
|
let arts = cat.articles || [];
|
||||||
|
if(arts.length === 0 && children.length === 0) html += `<li class="text-xs text-gray-500 italic">Leer</li>`;
|
||||||
|
|
||||||
|
arts.forEach(art => {
|
||||||
|
let activeCls = (art.id == currentArticleId) ? 'text-accent font-bold' : 'text-gray-400 hover:text-white';
|
||||||
|
html += `<li onclick="openArticle(${art.id})" class="text-sm cursor-pointer truncate ${activeCls} transition-colors">${art.title}</li>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
children.forEach(child => { html += `<li class="mt-3">` + buildCategoryTree(child, allCats) + `</li>`; });
|
||||||
|
return html + `</ul></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadLatest() {
|
||||||
|
fetch('/api/latest').then(res=>res.json()).then(data => {
|
||||||
|
if(!data.success) return;
|
||||||
|
const nav = document.getElementById('latestNav'); nav.innerHTML = '';
|
||||||
|
if(data.data.length === 0) nav.innerHTML = '<div class="text-xs text-gray-500 italic">Noch keine Updates</div>';
|
||||||
|
data.data.forEach(a => { nav.innerHTML += `<div onclick="openArticle(${a.id})" class="text-sm cursor-pointer p-2 hover:bg-darkbg rounded transition-colors border-l-2 border-transparent hover:border-blue-500"><div class="font-medium text-white truncate">${a.title}</div><div class="text-xs text-gray-500">Von ${a.author_name}</div></div>`; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistory() {
|
||||||
|
fetch('/api/history').then(res=>res.json()).then(data => {
|
||||||
|
if(!data.success) return;
|
||||||
|
const nav = document.getElementById('historyNav'); nav.innerHTML = '';
|
||||||
|
if(data.data.length === 0) nav.innerHTML = '<div class="text-xs text-gray-500 italic">Noch keine Historie</div>';
|
||||||
|
data.data.forEach(h => { nav.innerHTML += `<div onclick="openArticle(${h.id})" class="text-sm cursor-pointer p-2 hover:bg-darkbg rounded transition-colors border-l-2 border-transparent hover:border-accent"><div class="font-medium text-gray-300 truncate">${h.title}</div><div class="text-xs text-gray-500">${h.viewed_at.substring(0,16)}</div></div>`; });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openArticle(id) {
|
||||||
|
fetch('/api/article?id=' + id).then(res=>res.json()).then(data => {
|
||||||
|
// FALLBACK WENN DB KOMPLETT LEER IST
|
||||||
|
if(!data.success) {
|
||||||
|
document.getElementById('readView').classList.remove('hidden');
|
||||||
|
document.getElementById('editView').classList.add('hidden');
|
||||||
|
document.getElementById('readCategory').innerText = 'System Info';
|
||||||
|
document.getElementById('readTitle').innerText = 'CoreWiki ist einsatzbereit!';
|
||||||
|
document.getElementById('readAuthor').innerText = '-';
|
||||||
|
document.getElementById('readDate').innerText = '-';
|
||||||
|
document.getElementById('readContent').innerHTML = '<p class="text-gray-300 bg-darkcard p-4 rounded-lg border border-darkborder border-l-4 border-l-accent">Das Wiki ist noch leer. Klicke oben rechts auf das <b>Zahnrad ⚙️ -> Kategorien</b>, um deine erste Bibliothek (z.B. "Infrastruktur") anzulegen. Klicke danach auf <b>+ Neuer Artikel</b>, um zu starten!</p>';
|
||||||
|
document.getElementById('btnDelete').classList.add('hidden');
|
||||||
|
document.getElementById('btnEdit').classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btnEdit').classList.remove('hidden');
|
||||||
|
currentArticleId = id; const art = data.data;
|
||||||
|
document.getElementById('readView').classList.remove('hidden'); document.getElementById('editView').classList.add('hidden');
|
||||||
|
document.getElementById('readCategory').innerText = art.category_name || 'Allgemein'; document.getElementById('readTitle').innerText = art.title;
|
||||||
|
document.getElementById('readAuthor').innerText = art.author_name; document.getElementById('readDate').innerText = art.updated_at.substring(0,10);
|
||||||
|
|
||||||
|
const finalHtml = marked.parse(art.content_md || '').replace(/<pre><code class="language-mermaid">([\s\S]*?)<\/code><\/pre>/g, '<div class="mermaid">$1</div>');
|
||||||
|
document.getElementById('readContent').innerHTML = finalHtml;
|
||||||
|
|
||||||
|
if (isAdmin && id != 1) document.getElementById('btnDelete').classList.remove('hidden'); else document.getElementById('btnDelete').classList.add('hidden');
|
||||||
|
if(window.mermaid) setTimeout(() => { window.mermaid.run({ querySelector: '.mermaid' }); }, 100);
|
||||||
|
loadCategories(); loadHistory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewArticle() {
|
||||||
|
currentArticleId = 0; document.getElementById('editTitle').value = ''; document.getElementById('editContent').value = '';
|
||||||
|
document.getElementById('readView').classList.add('hidden'); document.getElementById('editView').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleEditMode() {
|
||||||
|
if(document.getElementById('editView').classList.contains('hidden')) {
|
||||||
|
fetch('/api/article?id=' + currentArticleId).then(res=>res.json()).then(data => {
|
||||||
|
if(!data.success) return;
|
||||||
|
document.getElementById('editTitle').value = data.data.title; document.getElementById('editCategory').value = data.data.category_id;
|
||||||
|
document.getElementById('editContent').value = data.data.content_md;
|
||||||
|
document.getElementById('readView').classList.add('hidden'); document.getElementById('editView').classList.remove('hidden');
|
||||||
|
});
|
||||||
|
} else { if(currentArticleId === 0) openArticle(1); else openArticle(currentArticleId); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveArticle() {
|
||||||
|
const fd = new FormData(); fd.append('id', currentArticleId); fd.append('category_id', document.getElementById('editCategory').value);
|
||||||
|
fd.append('title', document.getElementById('editTitle').value); fd.append('content_md', document.getElementById('editContent').value);
|
||||||
|
fetch('/api/article', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) { openArticle(data.id); loadLatest(); } else alert('Fehler'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteArticle() {
|
||||||
|
if(!confirm('Artikel wirklich löschen?')) return;
|
||||||
|
const fd = new FormData(); fd.append('id', currentArticleId);
|
||||||
|
fetch('/api/article/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) { openArticle(1); loadLatest(); } else alert('Fehler'); });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('catForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault(); const fd = new FormData();
|
||||||
|
fd.append('id', document.getElementById('catId').value); fd.append('name', document.getElementById('catName').value);
|
||||||
|
fd.append('icon', document.getElementById('catIcon').value); fd.append('parent_id', document.getElementById('catParentId').value);
|
||||||
|
fetch('/api/categories/save', { method: 'POST', body: fd }).then(res=>res.json()).then(data => {
|
||||||
|
if(data.success) { resetCatForm(); loadCategories(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
function resetCatForm() { document.getElementById('catForm').reset(); document.getElementById('catId').value = '0'; }
|
||||||
|
function editCategory(id, name, icon, parentId) { document.getElementById('catId').value = id; document.getElementById('catName').value = name; document.getElementById('catIcon').value = icon; document.getElementById('catParentId').value = parentId; }
|
||||||
|
function deleteCategory(id) {
|
||||||
|
if(!confirm('Kategorie löschen? (Artikel werden in Hauptkategorie verschoben)')) return;
|
||||||
|
const fd = new FormData(); fd.append('id', id);
|
||||||
|
fetch('/api/categories/delete', { method: 'POST', body: fd }).then(res=>res.json()).then(data => { if(data.success) loadCategories(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = document.getElementById('editContent');
|
||||||
|
editor.addEventListener('dragover', (e) => { e.preventDefault(); editor.classList.add('border-accent'); });
|
||||||
|
editor.addEventListener('dragleave', (e) => { e.preventDefault(); editor.classList.remove('border-accent'); });
|
||||||
|
editor.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault(); editor.classList.remove('border-accent');
|
||||||
|
const file = e.dataTransfer.files[0]; if (!file || !file.type.startsWith('image/')) return;
|
||||||
|
const cursorPos = editor.selectionStart;
|
||||||
|
editor.value = editor.value.substring(0, cursorPos) + `\n\n` + editor.value.substring(cursorPos);
|
||||||
|
const fd = new FormData(); fd.append('image', file);
|
||||||
|
const res = await fetch('/api/upload', {method: 'POST', body: fd}).then(r=>r.json());
|
||||||
|
if(res.success) editor.value = editor.value.replace(``, ``);
|
||||||
|
else editor.value = editor.value.replace(``, `*(Upload fehlgeschlagen)*`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function openModal(id) {
|
||||||
|
document.getElementById(id).classList.remove('hidden');
|
||||||
|
if(id === 'adminModal') { loadUsers(); loadSettings(); loadAuditLogs(); loadCategories(); }
|
||||||
|
}
|
||||||
|
function closeModal(id) { document.getElementById(id).classList.add('hidden'); }
|
||||||
|
|
||||||
|
function switchAdminTab(tab) {
|
||||||
|
['categories', 'users', 'settings', 'logs'].forEach(t => {
|
||||||
|
document.getElementById('admin-tab-'+t).classList.add('hidden');
|
||||||
|
document.getElementById('btn-admin-'+t).className = 'text-gray-400 hover:text-white';
|
||||||
|
});
|
||||||
|
document.getElementById('admin-tab-'+tab).classList.remove('hidden');
|
||||||
|
document.getElementById('btn-admin-'+tab).className = 'text-accent font-bold';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('passwordForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault(); const msg = document.getElementById('pwMsg');
|
||||||
|
if (document.getElementById('newPassword').value !== document.getElementById('newPasswordConfirm').value) { msg.style.color = '#ef4444'; msg.innerText = 'Passwörter stimmen nicht überein!'; msg.classList.remove('hidden'); return; }
|
||||||
|
const fd = new FormData(); fd.append('old_password', document.getElementById('oldPassword').value); fd.append('new_password', document.getElementById('newPassword').value);
|
||||||
|
fetch('/api/password', { method: 'POST', body: fd }).then(res => res.json()).then(data => {
|
||||||
|
msg.classList.remove('hidden');
|
||||||
|
if (data.success) { msg.style.color = '#10b981'; msg.innerText = 'Passwort geändert!'; document.getElementById('passwordForm').reset(); setTimeout(() => closeModal('pwdModal'), 2000); }
|
||||||
|
else { msg.style.color = '#ef4444'; msg.innerText = data.error; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadAuditLogs() { fetch('/api/logs').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('logTableBody'); tbody.innerHTML = ''; data.data.forEach(log => { tbody.innerHTML += `<tr><td class="p-3">${log.timestamp}</td><td class="p-3 font-bold">${log.username}</td><td class="p-3 text-white">${log.action}</td><td class="p-3">${log.target || '-'}</td></tr>`; }); } }); }
|
||||||
|
function loadSettings() { fetch('/api/settings').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('settingsTableBody'); tbody.innerHTML = ''; data.data.forEach(setting => { tbody.innerHTML += `<tr><td class="p-3 font-bold">${setting.config_key}</td><td class="p-3">${setting.config_value}</td><td class="p-3 text-right"><button onclick="deleteSetting('${setting.config_key}')" class="text-red-500 hover:text-white">✖</button></td></tr>`; }); } }); }
|
||||||
|
function loadUsers() { fetch('/api/users').then(res => res.json()).then(data => { if (data.success) { const tbody = document.getElementById('usersTableBody'); tbody.innerHTML = ''; data.data.forEach(u => { let roleBadge = u.role === 'admin' ? '<span class="bg-red-500/20 text-red-500 px-2 py-1 rounded text-xs">Admin</span>' : '<span class="bg-blue-500/20 text-accent px-2 py-1 rounded text-xs">Author</span>'; tbody.innerHTML += `<tr><td class="p-3">${u.id}</td><td class="p-3 font-bold text-white">${u.username}</td><td class="p-3">${roleBadge}</td><td class="p-3 text-right"><button onclick="deleteUser(${u.id})" class="text-red-500 hover:text-white">🗑️</button></td></tr>`; }); } }); }
|
||||||
|
function deleteSetting(key) { if (!confirm(`Einstellung "${key}" löschen?`)) return; const fd = new FormData(); fd.append('config_key', key); fetch('/api/settings/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadSettings(); }); }
|
||||||
|
function deleteUser(id) { if (!confirm('Benutzer löschen?')) return; const fd = new FormData(); fd.append('id', id); fetch('/api/users/delete', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) loadUsers(); else alert(data.error); }); }
|
||||||
|
document.getElementById('settingsForm').addEventListener('submit', function(e) { e.preventDefault(); const fd = new FormData(); fd.append('config_key', document.getElementById('configKey').value); fd.append('config_value', document.getElementById('configValue').value); fetch('/api/settings', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('configKey').value = ''; document.getElementById('configValue').value = ''; loadSettings(); } }); });
|
||||||
|
document.getElementById('userForm').addEventListener('submit', function(e) { e.preventDefault(); const fd = new FormData(); fd.append('username', document.getElementById('newUsername').value); fd.append('password', document.getElementById('newUserPass').value); fd.append('role', document.getElementById('newUserRole').value); fetch('/api/users', { method: 'POST', body: fd }).then(res => res.json()).then(data => { if (data.success) { document.getElementById('newUsername').value = ''; document.getElementById('newUserPass').value = ''; loadUsers(); } else alert(data.error); }); });
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
function handleLogin() {
|
||||||
|
global $pdo; @session_start();
|
||||||
|
$username = trim($_POST['username'] ?? '');
|
||||||
|
$password =$_POST['password'] ?? '';
|
||||||
|
|
||||||
|
$stmt =$pdo->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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
$dbPath = '/var/www/data/wiki.sqlite';$uploadDir = '/var/www/data/uploads';
|
||||||
|
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = new PDO("sqlite:" . $dbPath);
|
||||||
|
$pdo->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()]));
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
ini_set('display_errors', 0); error_reporting(E_ALL); ini_set('log_errors', 1); ini_set('error_log', '/var/www/data/php_errors.log');
|
||||||
|
set_error_handler(function($errno, $errstr,$errfile, $errline) { error_log("Error [$errno]: $errstr in$errfile on line $errline"); if (error_reporting() & $errno) { throw new ErrorException($errstr, 0,$errno, $errfile,$errline); } });
|
||||||
|
set_exception_handler(function($e) { error_log("Exception: " . $e->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 "<h1 style='color:white; font-family:sans-serif; text-align:center; margin-top:50px;'>500 - Systemfehler</h1>"; } exit; });
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
require_once 'database.php';
|
||||||
|
require_once 'auth.php';
|
||||||
|
require_once 'wiki.php';
|
||||||
|
require_once 'router.php';
|
||||||
|
?>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
$request =$_SERVER['REQUEST_URI'];
|
||||||
|
$path = parse_url($request, PHP_URL_PATH);
|
||||||
|
$method =$_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($path === '/' or$path === '') {
|
||||||
|
header('Content-Type: text/html'); readfile('app.html'); exit;
|
||||||
|
} elseif ($path === '/api/login' and$method === 'POST') { handleLogin();
|
||||||
|
} elseif ($path === '/api/logout') { handleLogout();
|
||||||
|
} elseif ($path === '/api/status' and$method === 'GET') { echo json_encode(['success' => 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']);
|
||||||
|
}
|
||||||
|
?>
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
function handleGetCategories() {
|
||||||
|
global $pdo; @session_start(); if (!isset($_SESSION['user_id'])) exit;
|
||||||
|
$cats =$pdo->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;
|
||||||
|
}
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user