cms: Autoren-Verwaltung (Admin), Cover-Upload, einheitliche Feldhöhen

- Admin-only Seite „Autor:innen": Nutzer anlegen/Passwort setzen/löschen via
  GoTrue-Admin-API (/api/users, requireAdmin). /api/me liefert isAdmin → Nav
  zeigt den Punkt nur Admins.
- Cover-Bild: Upload-Knopf + Thumbnail (Bilder im Beitrag gingen schon über den
  WYSIWYG-Editor).
- Editor-Metazeile: einzeilige Felder + Dropdowns einheitlich 38px hoch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 12:40:31 +02:00
parent 10d803b7b3
commit f42a69c7ed
6 changed files with 159 additions and 3 deletions
+6
View File
@@ -19,3 +19,9 @@ export async function requireAuth(c, next) {
c.set('isAdmin', ADMINS.includes(email));
await next();
}
// Nur Admins (nach requireAuth einsetzen).
export async function requireAdmin(c, next) {
if (!c.get('isAdmin')) return c.json({ error: 'Nur für Admins' }, 403);
await next();
}
+3
View File
@@ -7,6 +7,7 @@ import preview from './routes/preview.js';
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 { requireAuth } from './auth.js';
const SITE_DIR = process.env.SITE_DIR || '/site';
@@ -19,11 +20,13 @@ const app = new Hono();
app.get('/api/health', (c) => c.json({ ok: true, hugo: '0.161.1+extended' }));
// Alles unter /api/* (ausser /health oben) braucht ein gültiges Supabase-Token.
app.use('/api/*', requireAuth);
app.get('/api/me', (c) => c.json({ email: c.get('email'), isAdmin: c.get('isAdmin') }));
app.route('/api/content', content);
app.route('/api/preview', preview);
app.route('/api/publish', publish);
app.route('/api/upload', upload);
app.route('/api/profile', profile);
app.route('/api/users', users);
// --- Admin-SPA (im Container mitgebaut, unter /admin serviert) ---
app.get('/admin', (c) => c.redirect('/admin/'));
+46
View File
@@ -0,0 +1,46 @@
import { Hono } from 'hono';
import { supabase } from '../supabase.js';
import { requireAdmin } from '../auth.js';
// Autoren-/Nutzerverwaltung über die GoTrue-Admin-API (Service-Key). Nur Admins.
const ADMINS = (process.env.ADMIN_EMAILS || '')
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
const users = new Hono();
users.use('*', requireAdmin);
users.get('/', async (c) => {
const { data, error } = await supabase.auth.admin.listUsers();
if (error) return c.json({ error: error.message }, 500);
const list = (data?.users || []).map((u) => ({
id: u.id,
email: u.email,
created_at: u.created_at,
isAdmin: ADMINS.includes((u.email || '').toLowerCase()),
}));
return c.json(list);
});
users.post('/', async (c) => {
const { email, password } = 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 (error) return c.json({ error: error.message }, 400);
return c.json({ ok: true, id: data.user.id });
});
users.put('/:id', async (c) => {
const { password } = await c.req.json();
if (!password) return c.json({ error: 'Passwort nötig' }, 400);
const { error } = await supabase.auth.admin.updateUserById(c.req.param('id'), { password });
if (error) return c.json({ error: error.message }, 400);
return c.json({ ok: true });
});
users.delete('/:id', async (c) => {
const { error } = await supabase.auth.admin.deleteUser(c.req.param('id'));
if (error) return c.json({ error: error.message }, 400);
return c.json({ ok: true });
});
export default users;