Merge commit '6aedf21bf6dc495fdda89abadbd383fb3ea9e15e'

This commit is contained in:
2026-06-30 19:52:57 +02:00
8 changed files with 152 additions and 16 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openbureau-cms-api",
"version": "0.6.0",
"version": "0.7.0",
"private": true,
"type": "module",
"description": "Headless CMS backend für OPENBUREAU — schreibt Supabase-Posts in Hugo-content/, baut und serviert die Site.",
+2
View File
@@ -13,6 +13,7 @@ import profile from './routes/profile.js';
import users from './routes/users.js';
import stats from './routes/stats.js';
import system from './routes/system.js';
import account from './routes/account.js';
import history from './routes/history.js';
import authRoute from './routes/auth.js';
import { requireAuth, requireAdmin } from './auth.js';
@@ -100,6 +101,7 @@ app.route('/api/profile', profile);
app.route('/api/users', users);
app.route('/api/stats', stats);
app.route('/api/system', system);
app.route('/api/account', account);
// Content-Modell der Site fürs Admin (schema-getriebenes Rendern). Jede:r
// eingeloggte Nutzer:in darf es lesen — der Editor braucht es zum Aufbauen.
app.get('/api/schema', (c) => c.json({
+38
View File
@@ -0,0 +1,38 @@
import { Hono } from 'hono';
import { config } from '../config.js';
import { supabase, supabaseAuth } from '../supabase.js';
import * as local from '../auth-local.js';
// Self-service for the logged-in user (after requireAuth). Currently: change own
// password — local provider edits the user file; supabase verifies the current
// password then updates via the admin API.
const account = new Hono();
account.post('/password', async (c) => {
let body = {};
try { body = await c.req.json(); } catch { /* 400 below */ }
const { current, next } = body;
if (!next || String(next).length < 6) return c.json({ error: 'Neues Passwort zu kurz (min. 6 Zeichen).' }, 400);
const email = (c.get('email') || '').toLowerCase();
const userId = c.get('user')?.id;
if (config.auth === 'local') {
const u = (await local.loadUsers()).find((x) => (x.email || '').toLowerCase() === email);
if (!u || !(await local.verifyPassword(current || '', u.password))) {
return c.json({ error: 'Aktuelles Passwort ist falsch.' }, 403);
}
await local.updateUser(u.id, { password: next });
return c.json({ ok: true });
}
// supabase: verify current via the dedicated auth client, then update via admin.
if (!supabaseAuth || !supabase) return c.json({ error: 'Auth nicht verfügbar' }, 500);
const { error: e1 } = await supabaseAuth.auth.signInWithPassword({ email, password: current || '' });
await supabaseAuth.auth.signOut().catch(() => {}); // Session sofort wieder lösen
if (e1) return c.json({ error: 'Aktuelles Passwort ist falsch.' }, 403);
const { error: e2 } = await supabase.auth.admin.updateUserById(userId, { password: next });
if (e2) return c.json({ error: e2.message }, 400);
return c.json({ ok: true });
});
export default account;
+38 -13
View File
@@ -1,21 +1,39 @@
import { Hono } from 'hono';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { config } from '../config.js';
import { manager } from '../plugins.js';
import { requireAdmin } from '../auth.js';
// System-/Diagnose-Info für das Admin-Panel: welche core-Version läuft, welche
// Plugins aktiv sind, welches Content-Modell die Site fährt. Rein lesend, Admin.
// Reine Builder-Funktion (testbar) — die Route hängt nur Version + Hugo dran.
const execFileP = promisify(execFile);
let _version = null;
async function coreVersion() {
if (_version) return _version;
try { _version = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8')).version || '0.0.0'; }
catch { _version = '0.0.0'; }
return _version;
}
let _hugo = null;
async function hugoVersion() {
if (_hugo) return _hugo;
try { const { stdout } = await execFileP('hugo', ['version']); _hugo = (stdout.match(/v[0-9][0-9.]*[^\s]*/) || ['?'])[0]; }
catch { _hugo = '?'; }
return _hugo;
}
// System-/Diagnose-Info fürs Admin-Panel. Reiner Builder (testbar); die Route hängt
// nur die zur Laufzeit ermittelten Versionen dran.
export function systemInfo({ version, hugo }) {
return {
core: { name: 'openbureau-core', version },
site: config.site || null,
auth: config.auth || 'supabase',
admins: config.admins || [],
plugins: manager.plugins.map((p) => ({
name: p.name,
routes: (p.routes || []).length,
migrations: (p.migrations || []).length,
name: p.name, routes: (p.routes || []).length, migrations: (p.migrations || []).length,
})),
collections: config.collections.map((c) => ({
kind: c.kind, label: c.label, statKey: c.statKey || null, fields: (c.fields || []).length,
@@ -26,13 +44,20 @@ export function systemInfo({ version, hugo }) {
const system = new Hono();
system.use('*', requireAdmin);
system.get('/', async (c) => {
let version = '0.0.0';
try {
const pkg = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
version = pkg.version || version;
} catch { /* package.json nicht lesbar → 0.0.0 */ }
return c.json(systemInfo({ version, hugo: '0.161.1+extended' }));
system.get('/', async (c) => c.json(systemInfo({ version: await coreVersion(), hugo: await hugoVersion() })));
// Update-Check (token-frei): laufende Core-Version (Image, /app/package.json) vs. die
// im Repo VENDORTE Version (SITE_DIR/cms/core/api/package.json). Ist die vendorte neuer,
// wurde core aktualisiert aber noch nicht neu gebaut → "Rebuild nötig" (Update-Skript).
system.get('/update', async (c) => {
const running = await coreVersion();
const siteDir = process.env.SITE_DIR || '/site';
const vendorPath = path.join(siteDir, process.env.CORE_VENDOR_PATH || 'cms/core', 'api/package.json');
let vendored = null;
try { vendored = JSON.parse(await readFile(vendorPath, 'utf8')).version || null; }
catch { /* nicht als Subtree eingebunden → kein Vergleich möglich */ }
return c.json({ running, vendored, available: !!(vendored && vendored !== running) });
});
export default system;
+1
View File
@@ -17,6 +17,7 @@ test('systemInfo reflects core version, plugins and content model', () => {
assert.equal(info.site, 'openbureau');
assert.equal(info.auth, 'supabase'); // openbureau.config.js sets auth: 'supabase'
assert.equal(info.hugo, '0.161.1+extended');
assert.deepEqual(info.admins, ['karim@gabrielevarano.ch']);
// dialog plugin is active with its routes + the one migration
const dialog = info.plugins.find((p) => p.name === 'dialog');
assert.ok(dialog, 'dialog plugin listed');