admin: Übersicht-Dashboard, aufgewertete Nutzerverwaltung, Wiki-Autoren
- Neue /api/stats (Admin, read-only): Inhalte/Nutzer/Dialog-Kennzahlen - Übersicht-View als Admin-Dashboard: Stat-Karten (klickbar) + Schnellzugriff - Nutzerverwaltung: Avatar-Initiale, angelegt/zuletzt-aktiv, Rolle beim Anlegen, Inline-Passwort (statt prompt), Filter, Rollen-Badge; API liefert last_sign_in_at - Wiki im Editor anlegbar: Typ 'Wiki-Seite' + Gruppe-Feld → content/wiki/<slug>.md; files.js klassifiziert wiki als eigene 'kind' (eigene Sidebar-Gruppe) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,9 @@ function classify(rel) {
|
||||
if (parts[0] === 'library' && parts.length === 3) {
|
||||
return { kind: 'beitrag', section: parts[1] };
|
||||
}
|
||||
if (parts[0] === 'wiki') {
|
||||
return { kind: 'wiki', section: 'wiki' };
|
||||
}
|
||||
return { kind: 'seite', section: null };
|
||||
}
|
||||
|
||||
@@ -82,8 +85,8 @@ export async function listEntries() {
|
||||
url: urlFor(rel),
|
||||
});
|
||||
}
|
||||
// Beiträge zuerst, dann Seiten, dann Rubriken; je nach Datum/Titel.
|
||||
const order = { beitrag: 0, seite: 1, rubrik: 2 };
|
||||
// Beiträge zuerst, dann Wiki, Seiten, Rubriken; je nach Datum/Titel.
|
||||
const order = { beitrag: 0, wiki: 1, seite: 2, rubrik: 3 };
|
||||
items.sort((a, b) =>
|
||||
(order[a.kind] - order[b.kind]) ||
|
||||
(b.date || '').localeCompare(a.date || '') ||
|
||||
|
||||
@@ -11,6 +11,7 @@ import publish from './routes/publish.js';
|
||||
import upload from './routes/upload.js';
|
||||
import profile from './routes/profile.js';
|
||||
import users from './routes/users.js';
|
||||
import stats from './routes/stats.js';
|
||||
import { listComments, createComment, deleteComment, login } from './routes/comments.js';
|
||||
import history from './routes/history.js';
|
||||
import {
|
||||
@@ -91,6 +92,7 @@ app.route('/api/publish', publish);
|
||||
app.route('/api/upload', upload);
|
||||
app.route('/api/profile', profile);
|
||||
app.route('/api/users', users);
|
||||
app.route('/api/stats', stats);
|
||||
|
||||
// --- Admin-SPA (im Container mitgebaut, unter /admin serviert) ---
|
||||
app.get('/admin', (c) => c.redirect('/admin/'));
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Hono } from 'hono';
|
||||
import { supabase } from '../supabase.js';
|
||||
import { listEntries } from '../files.js';
|
||||
import { requireAdmin, roleOf } from '../auth.js';
|
||||
|
||||
// Kennzahlen für die Admin-Übersicht. Nur Admins; rein lesend.
|
||||
const stats = new Hono();
|
||||
stats.use('*', requireAdmin);
|
||||
|
||||
stats.get('/', async (c) => {
|
||||
// Inhalte aus dem Dateisystem zählen.
|
||||
const content = { beitraege: 0, entwuerfe: 0, wiki: 0, seiten: 0, rubriken: 0 };
|
||||
try {
|
||||
for (const e of await listEntries()) {
|
||||
if (e.kind === 'beitrag') { content.beitraege++; if (e.draft) content.entwuerfe++; }
|
||||
else if (e.kind === 'wiki') content.wiki++;
|
||||
else if (e.kind === 'rubrik') content.rubriken++;
|
||||
else content.seiten++;
|
||||
}
|
||||
} catch { /* Filesystem nicht lesbar → 0 */ }
|
||||
|
||||
// Nutzer nach Rolle.
|
||||
const users = { total: 0, admin: 0, editor: 0, user: 0 };
|
||||
try {
|
||||
const { data } = await supabase.auth.admin.listUsers();
|
||||
for (const u of data?.users || []) { users.total++; users[roleOf(u)] = (users[roleOf(u)] || 0) + 1; }
|
||||
} catch { /* GoTrue nicht erreichbar */ }
|
||||
|
||||
// Dialog-Zähler (effizient: head + count, keine Zeilen laden).
|
||||
const count = async (table, filter) => {
|
||||
try {
|
||||
let q = supabase.from(table).select('*', { count: 'exact', head: true });
|
||||
if (filter) q = filter(q);
|
||||
const { count: n } = await q;
|
||||
return n || 0;
|
||||
} catch { return 0; }
|
||||
};
|
||||
const [forums, threads, comments] = await Promise.all([
|
||||
count('forums'),
|
||||
count('threads', (q) => q.eq('deleted', false)),
|
||||
count('comments', (q) => q.eq('deleted', false)),
|
||||
]);
|
||||
|
||||
return c.json({ content, users, dialog: { forums, threads, comments } });
|
||||
});
|
||||
|
||||
export default stats;
|
||||
@@ -18,6 +18,7 @@ users.get('/', async (c) => {
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
created_at: u.created_at,
|
||||
last_sign_in_at: u.last_sign_in_at || null,
|
||||
role,
|
||||
isAdmin: role === 'admin',
|
||||
// Admins aus der .env lassen sich nicht per UI herabstufen.
|
||||
@@ -28,9 +29,12 @@ users.get('/', async (c) => {
|
||||
});
|
||||
|
||||
users.post('/', async (c) => {
|
||||
const { email, password } = await c.req.json();
|
||||
const { email, password, role } = await c.req.json();
|
||||
if (!email || !password) return c.json({ error: 'E-Mail und Passwort nötig' }, 400);
|
||||
const { data, error } = await supabase.auth.admin.createUser({ email, password, email_confirm: true });
|
||||
if (role && !['user', 'editor', 'admin'].includes(role)) return c.json({ error: 'Unbekannte Rolle' }, 400);
|
||||
const payload = { email, password, email_confirm: true };
|
||||
if (role && role !== 'user') payload.app_metadata = { role };
|
||||
const { data, error } = await supabase.auth.admin.createUser(payload);
|
||||
if (error) return c.json({ error: error.message }, 400);
|
||||
return c.json({ ok: true, id: data.user.id });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user