Portal-Dienste und geteiltes Design-System für die anderen Apps
- Dienste (Gitea, Kanban, Cloud …) werden im System-Tab gepflegt und erscheinen als Kacheln auf der Startseite; GET /api/services ist öffentlich, Pflege braucht den settings-Scope - /brand.css liefert die Design-Tokens (Farben live aus dem Brand-Tab, ändern sich damit überall mit) plus Basis-Klassen d4rk-card, d4rk-btn, d4rk-title, d4rk-tag - /brand-nav.js baut die D4RKST3R-Leiste in jede fremde App ein, mit Links zum Hub und zu allen gepflegten Diensten; die Links stecken fertig im Skript, dadurch kein zweiter Request und kein CORS nötig - Beide Dateien mit offenem CORS-Header und 5 Minuten Cache - Doku für Anbindung + Schriften in docs/sso.md ergänzt Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -161,6 +161,10 @@ export default function Settings({ me }) {
|
||||
const [botDesc, setBotDesc] = useState('');
|
||||
const [team, setTeam] = useState([]);
|
||||
const [audit, setAudit] = useState([]);
|
||||
// Portal-Dienste (Kacheln auf der Startseite)
|
||||
const emptyService = { id: null, name: '', url: '', icon: '', description: '', sort: 0 };
|
||||
const [services, setServices] = useState([]);
|
||||
const [serviceDraft, setServiceDraft] = useState(emptyService);
|
||||
// SSO-Apps (Fremd-Dienste mit Discord-Login)
|
||||
const [ssoApps, setSsoApps] = useState([]);
|
||||
const [ssoDraft, setSsoDraft] = useState({ slug: '', name: '', redirect_prefix: '' });
|
||||
@@ -208,6 +212,7 @@ export default function Settings({ me }) {
|
||||
apiGet('/api/webadmins').then((d) => setTeam(d.admins)).catch(() => {});
|
||||
apiGet('/api/auditlog').then((d) => setAudit(d.entries)).catch(() => {});
|
||||
apiGet('/api/ssoapps').then((d) => setSsoApps(d.apps)).catch(() => {});
|
||||
apiGet('/api/services').then((d) => setServices(d.services)).catch(() => {});
|
||||
}, [me.admin, me.scopes?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -241,6 +246,28 @@ export default function Settings({ me }) {
|
||||
if (res) setTeam(res.admins);
|
||||
}
|
||||
|
||||
async function saveService() {
|
||||
if (!serviceDraft.name.trim() || !serviceDraft.url.trim()) {
|
||||
flash('✗ Name und URL nötig');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = serviceDraft.id
|
||||
? await apiPut(`/api/services/${serviceDraft.id}`, serviceDraft)
|
||||
: await apiPost('/api/services', serviceDraft);
|
||||
setServices(res.services);
|
||||
setServiceDraft(emptyService);
|
||||
flash('✓ Dienst gespeichert');
|
||||
} catch {
|
||||
flash('✗ Speichern fehlgeschlagen (URL prüfen)');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeService(id) {
|
||||
const res = await apiDelete(`/api/services/${id}`).catch(() => null);
|
||||
if (res) setServices(res.services);
|
||||
}
|
||||
|
||||
async function createSsoApp() {
|
||||
if (!ssoDraft.slug.trim() || !ssoDraft.name.trim() || !ssoDraft.redirect_prefix.trim()) {
|
||||
flash('✗ Kürzel, Name und Rücksprung-URL nötig');
|
||||
@@ -1246,6 +1273,42 @@ export default function Settings({ me }) {
|
||||
</div>
|
||||
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Dienste (Portal)</h2>
|
||||
<p className="section-intro">
|
||||
Erscheinen als Kacheln auf der Startseite — z. B. Gitea, Kanban oder die Cloud.
|
||||
</p>
|
||||
{services.length > 0 && (
|
||||
<div className="key-list" style={{ marginBottom: '1.2rem' }}>
|
||||
{services.map((s) => (
|
||||
<div className="key-row" key={s.id}>
|
||||
<span className="key-name">{s.icon} {s.name}</span>
|
||||
<span className="key-used" title={s.url}>{s.url}</span>
|
||||
<button className="btn btn-mini" onClick={() => setServiceDraft({
|
||||
id: s.id, name: s.name, url: s.url, icon: s.icon,
|
||||
description: s.description ?? '', sort: s.sort ?? 0,
|
||||
})}>Bearbeiten</button>
|
||||
<button className="card-delete" title="Dienst entfernen" onClick={() => removeService(s.id)}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>{serviceDraft.id ? `Dienst #${serviceDraft.id} bearbeiten` : 'Neuer Dienst'}</label>
|
||||
<div className="field-row">
|
||||
<input type="text" style={{ flex: 'none', width: '4rem' }} placeholder="🗂️" value={serviceDraft.icon} onChange={(e) => setServiceDraft({ ...serviceDraft, icon: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '10rem' }} placeholder="Kanban" value={serviceDraft.name} onChange={(e) => setServiceDraft({ ...serviceDraft, name: e.target.value })} />
|
||||
<input type="text" placeholder="https://kanban.d4rkst3r.de" value={serviceDraft.url} onChange={(e) => setServiceDraft({ ...serviceDraft, url: e.target.value })} />
|
||||
<input type="number" style={{ flex: 'none', width: '5rem' }} placeholder="Pos." value={serviceDraft.sort} onChange={(e) => setServiceDraft({ ...serviceDraft, sort: e.target.value })} />
|
||||
</div>
|
||||
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
||||
<input type="text" placeholder="Kurzbeschreibung für die Kachel" value={serviceDraft.description} onChange={(e) => setServiceDraft({ ...serviceDraft, description: e.target.value })} />
|
||||
<button className="btn btn-save" onClick={saveService}>{serviceDraft.id ? 'Aktualisieren' : 'Hinzufügen'}</button>
|
||||
{serviceDraft.id && <button className="btn btn-ghost" onClick={() => setServiceDraft(emptyService)}>Abbrechen</button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Impressum & Datenschutz</h2>
|
||||
<p className="field-hint" style={{ marginBottom: '.9rem' }}>
|
||||
|
||||
Reference in New Issue
Block a user