Seiten-Editor: Inhalte pflegen ohne Code-Änderung
Antwort auf den CMS-Wunsch aus der Community — statt eines Forums, das neben aktivem Discord erfahrungsgemäß verwaist, ein schlanker Editor für eigene Seiten: Regeln, Über uns, Mitmachen, FAQ. - neuer Setup-Tab "Seiten" (Scope content): Titel, Adress-Kürzel wird aus dem Titel abgeleitet, Markdown-Editor mit Live-Vorschau daneben - pro Seite wählbar: veröffentlicht oder Entwurf, im Community-Menü oder nur per Direktlink, Reihenfolge - Anzeige unter /<kürzel>, gerendert mit dem vorhandenen Markdown-Parser - Entwürfe sind öffentlich nicht abrufbar — sie brauchen den content-Scope - reservierte Kürzel (devlogs, settings, api, impressum …) werden abgelehnt, damit nichts die festen Routen überschreibt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -516,6 +516,39 @@ export function deleteUserData(userId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Frei anlegbare Seiten (Regeln, Über uns, Mitmachen …) — Inhalte pflegen
|
||||
// ohne Code-Änderung. Rechtstexte laufen bewusst separat.
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS pages (
|
||||
slug TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
published INTEGER NOT NULL DEFAULT 0,
|
||||
in_menu INTEGER NOT NULL DEFAULT 1,
|
||||
sort INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
const upsertPage = db.prepare(`
|
||||
INSERT INTO pages (slug, title, content, published, in_menu, sort, updated_at)
|
||||
VALUES (@slug, @title, @content, @published, @in_menu, @sort, datetime('now'))
|
||||
ON CONFLICT(slug) DO UPDATE SET
|
||||
title = excluded.title, content = excluded.content,
|
||||
published = excluded.published, in_menu = excluded.in_menu,
|
||||
sort = excluded.sort, updated_at = datetime('now')
|
||||
`);
|
||||
const getPageStmt = db.prepare('SELECT * FROM pages WHERE slug = ?');
|
||||
const listPagesStmt = db.prepare('SELECT * FROM pages ORDER BY sort, title');
|
||||
const listPublishedPagesStmt = db.prepare(
|
||||
'SELECT slug, title, in_menu, sort FROM pages WHERE published = 1 ORDER BY sort, title'
|
||||
);
|
||||
const deletePageStmt = db.prepare('DELETE FROM pages WHERE slug = ?');
|
||||
export const savePage = (p) => upsertPage.run(p);
|
||||
export const getPage = (slug) => getPageStmt.get(slug) ?? null;
|
||||
export const listPages = () => listPagesStmt.all();
|
||||
export const listPublishedPages = () => listPublishedPagesStmt.all();
|
||||
export const deletePage = (slug) => deletePageStmt.run(slug).changes > 0;
|
||||
|
||||
// Portal: eigene Dienste (Gitea, Kanban, Cloud …) für die Kachel-Übersicht
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS services (
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
logAudit, listAudit, playerHistory, deleteUserData,
|
||||
saveSsoApp, listSsoApps, deleteSsoApp, getSsoApp,
|
||||
createService, updateService, listServices, deleteService,
|
||||
savePage, getPage, listPages, listPublishedPages, deletePage,
|
||||
} from '../db.js';
|
||||
import crypto from 'node:crypto';
|
||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||
@@ -163,6 +164,69 @@ export function registerApiRoutes(app, client) {
|
||||
}),
|
||||
}));
|
||||
|
||||
// --- Frei angelegte Seiten (Regeln, Über uns, …) ---
|
||||
|
||||
// Adressen, die schon vom Frontend oder Server belegt sind
|
||||
const RESERVED_SLUGS = new Set([
|
||||
'devlogs', 'roadmap', 'server', 'galerie', 'level', 'events', 'changelog',
|
||||
'profil', 'commits', 'settings', 'dashboard', 'impressum', 'datenschutz',
|
||||
'api', 'auth', 'webhooks', 'sso', 'health', 'assets', 'features', 'commands', 'docs',
|
||||
]);
|
||||
|
||||
// Menü-Einträge + veröffentlichte Seiten — öffentlich
|
||||
app.get('/api/pages', async () => ({ pages: listPublishedPages() }));
|
||||
|
||||
app.get('/api/pages/:slug', async (request, reply) => {
|
||||
const page = getPage(String(request.params.slug));
|
||||
if (!page || !page.published) {
|
||||
// Entwürfe darf nur sehen, wer sie auch bearbeiten dürfte
|
||||
const scopes = scopesOf(getSessionUser(request));
|
||||
if (!page || !(scopes.includes('*') || scopes.includes('content'))) {
|
||||
return reply.code(404).send({ error: 'Seite nicht gefunden' });
|
||||
}
|
||||
}
|
||||
return { page: { ...page, published: Boolean(page.published), in_menu: Boolean(page.in_menu) } };
|
||||
});
|
||||
|
||||
// Verwaltung (Scope content) — inklusive Entwürfen
|
||||
app.get('/api/pages/all/list', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
return {
|
||||
pages: listPages().map((p) => ({
|
||||
...p, published: Boolean(p.published), in_menu: Boolean(p.in_menu),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/api/pages/:slug', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
const slug = String(request.params.slug).trim().toLowerCase()
|
||||
.replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
const title = String(request.body?.title ?? '').trim();
|
||||
|
||||
if (!slug || !title) return reply.code(400).send({ error: 'Kürzel und Titel nötig' });
|
||||
if (RESERVED_SLUGS.has(slug)) {
|
||||
return reply.code(400).send({ error: `„${slug}" ist bereits vergeben — bitte anderes Kürzel` });
|
||||
}
|
||||
|
||||
savePage({
|
||||
slug,
|
||||
title: title.slice(0, 80),
|
||||
content: String(request.body?.content ?? '').slice(0, 50_000),
|
||||
published: request.body?.published ? 1 : 0,
|
||||
in_menu: request.body?.in_menu === false ? 0 : 1,
|
||||
sort: Number(request.body?.sort) || 0,
|
||||
});
|
||||
logAudit(getSessionUser(request), 'seite gespeichert', `${title} (/${slug})`);
|
||||
return { ok: true, slug, pages: listPages() };
|
||||
});
|
||||
|
||||
app.delete('/api/pages/:slug', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
logAudit(getSessionUser(request), 'seite gelöscht', String(request.params.slug));
|
||||
return { deleted: deletePage(String(request.params.slug)), pages: listPages() };
|
||||
});
|
||||
|
||||
// Dienste-Kacheln fürs Portal — öffentlich
|
||||
app.get('/api/services', async () => ({ services: listServices() }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user