CoreWiki 1.0
This commit is contained in:
@@ -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