d4rkbot: API v1 mit Key-System, Env-Diät, Rebranding
- API-Keys (SHA-256-Hash, Scopes, last_used) — Verwaltung auf der Setup-Seite, Klartext-Key wird genau einmal angezeigt - /api/v1: message, dm, roles (add/remove), member/:id, stats — Bearer-Auth mit Scope-Prüfung, Embed-Sanitizing, README-Doku mit Python-Beispiel - Env-Diät: PUBLIC_URL + GITEA_URL jetzt Settings (Env nur Fallback), OAuth-Redirect dynamisch; Env enthält nur noch Secrets/Bootstrap - Rebranding ecobot → d4rkbot (Packages, Container, Cookies, README); Volume-Name bleibt ecobot_data (Datenerhalt), Portainer-Stack-Name bleibt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+48
-4
@@ -1,9 +1,12 @@
|
||||
// REST-API fürs Webinterface: Devlogs öffentlich, Commits + Settings nur für den Admin
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats, getSetting, setSetting } from '../db.js';
|
||||
import {
|
||||
listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats,
|
||||
getSetting, setSetting, createApiKey, listApiKeys, deleteApiKey,
|
||||
} from '../db.js';
|
||||
import { config } from '../config.js';
|
||||
import { removeDevlog } from '../bot/devlog-archive.js';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId } from '../runtime-settings.js';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl } from '../runtime-settings.js';
|
||||
import { getSessionUser, isAdmin } from './auth.js';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
@@ -62,7 +65,7 @@ export function registerApiRoutes(app, client) {
|
||||
const title = `Devlog ${dateFmt.format(new Date(d.posted_at))}${project ? ` — ${project}` : ''}`;
|
||||
return ` <item>
|
||||
<title>${esc(title)}</title>
|
||||
<link>${config.publicUrl}/devlogs</link>
|
||||
<link>${publicUrl()}/devlogs</link>
|
||||
<guid isPermaLink="false">${esc(d.message_id)}</guid>
|
||||
<pubDate>${new Date(d.posted_at).toUTCString()}</pubDate>
|
||||
<description>${esc(d.content)}</description>
|
||||
@@ -74,7 +77,7 @@ export function registerApiRoutes(app, client) {
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>D4RKST3R // DEVLOG</title>
|
||||
<link>${config.publicUrl}/devlogs</link>
|
||||
<link>${publicUrl()}/devlogs</link>
|
||||
<description>Entwicklungs-Updates, automatisch archiviert.</description>
|
||||
<language>de</language>
|
||||
${rssItems}
|
||||
@@ -142,6 +145,8 @@ ${rssItems}
|
||||
devlog_threads_enabled: getSetting('devlog_threads_enabled') !== '0',
|
||||
bug_report_repo: getSetting('bug_report_repo') ?? 'D4rkst3r/EcoGame',
|
||||
watchdog_urls: getSetting('watchdog_urls') ?? '',
|
||||
public_url: publicUrl(),
|
||||
gitea_url: getSetting('gitea_url') ?? config.giteaUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -209,11 +214,50 @@ ${rssItems}
|
||||
setSetting(key, String(body[key]).trim());
|
||||
}
|
||||
}
|
||||
// URLs: müssen mit http(s) beginnen
|
||||
for (const key of ['public_url', 'gitea_url']) {
|
||||
if (body[key] === undefined) continue;
|
||||
const value = String(body[key]).trim().replace(/\/$/, '');
|
||||
if (!/^https?:\/\/.+/.test(value)) {
|
||||
return reply.code(400).send({ error: `${key}: muss mit http(s):// beginnen` });
|
||||
}
|
||||
setSetting(key, value);
|
||||
}
|
||||
|
||||
request.log.info('Settings per Web-UI aktualisiert');
|
||||
return { ok: true, settings: currentSettings() };
|
||||
});
|
||||
|
||||
// --- API-Keys (Admin) — für /api/v1/* ---
|
||||
|
||||
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
||||
|
||||
app.get('/api/apikeys', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { keys: listApiKeys() };
|
||||
});
|
||||
|
||||
app.post('/api/apikeys', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
|
||||
const name = String(request.body?.name ?? '').trim();
|
||||
const scopes = (request.body?.scopes ?? []).filter((s) => VALID_SCOPES.includes(s));
|
||||
if (!name || scopes.length === 0) {
|
||||
return reply.code(400).send({ error: 'name und mindestens ein Scope nötig' });
|
||||
}
|
||||
const { id, key } = createApiKey(name, scopes);
|
||||
request.log.info(`API-Key '${name}' erstellt (Scopes: ${scopes.join(',')})`);
|
||||
// Klartext-Key nur in dieser einen Antwort!
|
||||
return { id, key, name, scopes };
|
||||
});
|
||||
|
||||
app.delete('/api/apikeys/:id', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
const deleted = deleteApiKey(Number(request.params.id));
|
||||
request.log.info(`API-Key ${request.params.id} widerrufen: ${deleted}`);
|
||||
return { deleted };
|
||||
});
|
||||
|
||||
// Test-Embed in den konfigurierten Kanal senden (prüft die Kanal-Wahl ohne Push/Devlog)
|
||||
app.post('/api/settings/test/:target', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
|
||||
Reference in New Issue
Block a user