cms: Rollen + Kollaboration (Admin sieht alles, Autoren nur eigene/geteilte)

- ADMIN_EMAILS (.env) = Admins, sehen/bearbeiten alles
- Autor:innen sehen nur Einträge mit ihrer Mail unter `authors:`; Ersteller wird
  beim Anlegen automatisch Autor
- Kollaboration: Feld „Autor:innen" im Editor → mehrere Mails = gemeinsamer Zugriff
- API erzwingt Zugriff bei list/read/save (403 ohne Recht)
- ADMIN_EMAILS in compose + LXC-Script (fragt Admin-Mail ab)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:22:06 +02:00
parent 35c2a122ae
commit bd4b470877
8 changed files with 86 additions and 16 deletions
+33 -12
View File
@@ -1,28 +1,49 @@
import { Hono } from 'hono';
import { listEntries, readEntry, writeEntry, entryExists } from '../files.js';
import { listEntries, readEntry, writeEntry, entryExists, hasAccess, normAuthors } from '../files.js';
// Dateibasiert: liest/schreibt die echten .md unter content/.
// Dateibasiert + Rechte: Admin sieht/bearbeitet alles, Autor:innen nur Einträge,
// in denen ihre Mail unter `authors` steht.
const content = new Hono();
// Liste aller Einträge (Beiträge, Seiten, Rubriken).
content.get('/', async (c) => {
try { return c.json(await listEntries()); }
catch (e) { return c.json({ error: String(e.message || e) }, 500); }
const email = c.get('email'); const isAdmin = c.get('isAdmin');
try {
let items = await listEntries();
if (!isAdmin) items = items.filter((e) => hasAccess(e.authors, email));
return c.json(items);
} catch (e) { return c.json({ error: String(e.message || e) }, 500); }
});
// Einen Eintrag lesen: /api/content/entry?path=library/software/stack.md
content.get('/entry', async (c) => {
try { return c.json(await readEntry(c.req.query('path'))); }
catch (e) { return c.json({ error: String(e.message || e) }, 400); }
const email = c.get('email'); const isAdmin = c.get('isAdmin');
try {
const entry = await readEntry(c.req.query('path'));
if (!isAdmin && !hasAccess(entry.frontmatter.authors, email)) {
return c.json({ error: 'Kein Zugriff auf diesen Eintrag' }, 403);
}
return c.json(entry);
} catch (e) { return c.json({ error: String(e.message || e) }, 400); }
});
// Anlegen oder überschreiben.
content.put('/entry', async (c) => {
const email = c.get('email'); const isAdmin = c.get('isAdmin');
const { path: rel, frontmatter, body } = await c.req.json();
try {
const created = !(await entryExists(rel));
const saved = await writeEntry(rel, frontmatter, body);
return c.json({ ok: true, path: saved, created });
const exists = await entryExists(rel);
if (exists && !isAdmin) {
const cur = await readEntry(rel);
if (!hasAccess(cur.frontmatter.authors, email)) {
return c.json({ error: 'Kein Zugriff auf diesen Eintrag' }, 403);
}
}
// authors zusammenführen; Ersteller wird beim Anlegen automatisch Autor.
const authors = normAuthors(frontmatter.authors);
if (!exists && email && !authors.some((a) => a.toLowerCase() === email)) {
authors.unshift(email);
}
const fm = { ...frontmatter, authors };
const saved = await writeEntry(rel, fm, body);
return c.json({ ok: true, path: saved, created: !exists });
} catch (e) { return c.json({ error: String(e.message || e) }, 400); }
});