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:
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "ecobot-frontend",
|
||||
"name": "d4rkbot-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ecobot-frontend",
|
||||
"name": "d4rkbot-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fontsource/barlow-condensed": "^5.3.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ecobot-frontend",
|
||||
"name": "d4rkbot-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet, apiPut, apiPost } from '../api.js';
|
||||
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
|
||||
|
||||
const API_SCOPES = [
|
||||
{ id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' },
|
||||
{ id: 'dm', label: 'dm', hint: 'Direktnachrichten senden' },
|
||||
{ id: 'roles', label: 'roles', hint: 'Rollen geben/nehmen' },
|
||||
{ id: 'read', label: 'read', hint: 'Member-Infos & Statistiken lesen' },
|
||||
];
|
||||
|
||||
/** Sekunden → "2d 4h 13m" */
|
||||
function fmtUptime(s) {
|
||||
@@ -19,6 +26,11 @@ export default function Settings({ me }) {
|
||||
const [form, setForm] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
// API-Keys
|
||||
const [keys, setKeys] = useState([]);
|
||||
const [newKeyName, setNewKeyName] = useState('');
|
||||
const [newKeyScopes, setNewKeyScopes] = useState(['message']);
|
||||
const [createdKey, setCreatedKey] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!me.admin) return;
|
||||
@@ -28,8 +40,28 @@ export default function Settings({ me }) {
|
||||
setForm(d.settings);
|
||||
})
|
||||
.catch((e) => setError(e.status));
|
||||
apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {});
|
||||
}, [me.admin]);
|
||||
|
||||
async function createKey() {
|
||||
if (!newKeyName.trim() || newKeyScopes.length === 0) return;
|
||||
try {
|
||||
const res = await apiPost('/api/apikeys', { name: newKeyName.trim(), scopes: newKeyScopes });
|
||||
setCreatedKey(res);
|
||||
setNewKeyName('');
|
||||
setKeys((await apiGet('/api/apikeys')).keys);
|
||||
} catch {
|
||||
flash('✗ Key-Erstellung fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeKey(id) {
|
||||
if (!window.confirm('Diesen API-Key widerrufen? Skripte mit diesem Key verlieren sofort den Zugriff.')) return;
|
||||
await apiDelete(`/api/apikeys/${id}`).catch(() => {});
|
||||
setKeys((await apiGet('/api/apikeys')).keys);
|
||||
if (createdKey?.id === id) setCreatedKey(null);
|
||||
}
|
||||
|
||||
function flash(msg) {
|
||||
setFeedback(msg);
|
||||
setTimeout(() => setFeedback(null), 3000);
|
||||
@@ -105,6 +137,29 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Allgemein */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Allgemein</h2>
|
||||
<div className="field">
|
||||
<label>Öffentliche URL <span className="field-hint">Basis für Buttons, RSS und OAuth-Redirect (Redirect auch im Dev-Portal eintragen)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://bot.d4rkst3r.de"
|
||||
value={form.public_url}
|
||||
onChange={(e) => setForm({ ...form, public_url: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Gitea-URL <span className="field-hint">Basis für die Issue-API (/bug)</span></label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://git.d4rkst3r.de"
|
||||
value={form.gitea_url}
|
||||
onChange={(e) => setForm({ ...form, gitea_url: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Kanäle */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Kanäle</h2>
|
||||
@@ -227,6 +282,64 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API-Keys */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// API-Keys (/api/v1)</h2>
|
||||
|
||||
{keys.length > 0 && (
|
||||
<div className="key-list">
|
||||
{keys.map((k) => (
|
||||
<div className="key-row" key={k.id}>
|
||||
<span className="key-name">{k.name}</span>
|
||||
<span className="key-scopes">{k.scopes}</span>
|
||||
<span className="key-used">
|
||||
{k.last_used_at ? `zuletzt: ${k.last_used_at.slice(0, 16)}` : 'nie benutzt'}
|
||||
</span>
|
||||
<button className="card-delete" title="Key widerrufen" onClick={() => revokeKey(k.id)}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{createdKey && (
|
||||
<div className="key-created">
|
||||
<p>Key <strong>{createdKey.name}</strong> erstellt — <strong>jetzt kopieren</strong>, er wird nie wieder angezeigt:</p>
|
||||
<code>{createdKey.key}</code>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="field">
|
||||
<label>Neuer Key <span className="field-hint">Name des Skripts/Dienstes + Scopes</span></label>
|
||||
<div className="field-row">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="z. B. devlog.py, platform, fivem"
|
||||
value={newKeyName}
|
||||
onChange={(e) => setNewKeyName(e.target.value)}
|
||||
/>
|
||||
<button className="btn" onClick={createKey}>Erstellen</button>
|
||||
</div>
|
||||
<div className="scope-row">
|
||||
{API_SCOPES.map((s) => (
|
||||
<label className="scope-pick" key={s.id} title={s.hint}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newKeyScopes.includes(s.id)}
|
||||
onChange={(e) =>
|
||||
setNewKeyScopes(
|
||||
e.target.checked
|
||||
? [...newKeyScopes, s.id]
|
||||
: newKeyScopes.filter((x) => x !== s.id)
|
||||
)
|
||||
}
|
||||
/>
|
||||
{s.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Commit-Feed */}
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Commit-Feed</h2>
|
||||
|
||||
@@ -503,6 +503,44 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
||||
.toggle input:checked::after { transform: translateX(16px); background: var(--neon); }
|
||||
.toggle > span:first-of-type { color: var(--text); font-size: .95rem; }
|
||||
|
||||
/* API-Keys */
|
||||
.key-list { margin-bottom: 1.2rem; border: 1px solid rgba(255, 255, 255, .06); }
|
||||
.key-row {
|
||||
display: flex; align-items: center; gap: 1rem;
|
||||
padding: .5rem .8rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, .05);
|
||||
font-size: .9rem;
|
||||
}
|
||||
.key-row:last-child { border-bottom: none; }
|
||||
.key-name { font-weight: 600; min-width: 8rem; }
|
||||
.key-scopes {
|
||||
font-family: var(--mono); font-size: .65rem; letter-spacing: .1em;
|
||||
color: var(--neon); flex: 1;
|
||||
}
|
||||
.key-used { font-family: var(--mono); font-size: .62rem; color: var(--muted2); white-space: nowrap; }
|
||||
.key-created {
|
||||
border: 1px solid rgba(0, 255, 136, .3);
|
||||
background: rgba(0, 255, 136, .04);
|
||||
padding: .8rem 1rem;
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.key-created p { margin: 0 0 .5rem; }
|
||||
.key-created code {
|
||||
display: block;
|
||||
font-family: var(--mono); font-size: .8rem;
|
||||
color: var(--success);
|
||||
background: #000; padding: .5rem .7rem;
|
||||
user-select: all; word-break: break-all;
|
||||
}
|
||||
.scope-row { display: flex; gap: 1.2rem; margin-top: .5rem; flex-wrap: wrap; }
|
||||
.scope-pick {
|
||||
display: flex; align-items: center; gap: .4rem;
|
||||
font-family: var(--mono); font-size: .7rem; letter-spacing: .1em;
|
||||
color: var(--muted); cursor: pointer;
|
||||
}
|
||||
.scope-pick input { accent-color: var(--neon); }
|
||||
|
||||
.settings-actions { display: flex; align-items: center; gap: 1rem; }
|
||||
.btn-save { background: var(--neon); color: #0a0a0a; border-color: var(--neon); font-weight: 600; }
|
||||
.btn-save:hover:not(:disabled) { background: var(--text); border-color: var(--text); }
|
||||
|
||||
Reference in New Issue
Block a user