Refactor: Admin-Login aus allen Plugins entfernt
Duplizierte Auth-Logik aus Notifications, Game Library und Streaming Plugins komplett entfernt (-251 Zeilen). Alle Plugins nutzen jetzt die zentrale Auth aus core/auth.ts via isAdmin Prop. Admin-Buttons (Settings-Zahnrad) erscheinen nur noch wenn global eingeloggt. Kein separater Login pro Tab mehr noetig. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b3080fb763
commit
f27093b87a
5 changed files with 28 additions and 251 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||||
import { sseBroadcast } from '../../core/sse.js';
|
import { sseBroadcast } from '../../core/sse.js';
|
||||||
|
import { requireAdmin as requireAdminFactory } from '../../core/auth.js';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
@ -58,34 +59,6 @@ const STEAM_API_KEY = process.env.STEAM_API_KEY || '2481D43449821AA4FF66502A9166
|
||||||
|
|
||||||
// ── Admin auth helpers (same system as soundboard) ──
|
// ── Admin auth helpers (same system as soundboard) ──
|
||||||
|
|
||||||
function readCookie(req: express.Request, name: string): string | undefined {
|
|
||||||
const raw = req.headers.cookie || '';
|
|
||||||
const match = raw.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
||||||
return match ? decodeURIComponent(match[1]) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64url(str: string): string {
|
|
||||||
return Buffer.from(str).toString('base64url');
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyAdminToken(adminPwd: string, token: string | undefined): boolean {
|
|
||||||
if (!token || !adminPwd) return false;
|
|
||||||
const [body, sig] = token.split('.');
|
|
||||||
if (!body || !sig) return false;
|
|
||||||
const expected = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
|
||||||
if (expected !== sig) return false;
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(Buffer.from(body, 'base64').toString('utf8')) as { iat: number; exp: number };
|
|
||||||
return typeof payload.exp === 'number' && Date.now() < payload.exp;
|
|
||||||
} catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function signAdminToken(adminPwd: string): string {
|
|
||||||
const payload = { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600_000 };
|
|
||||||
const body = b64url(JSON.stringify(payload));
|
|
||||||
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
|
||||||
return `${body}.${sig}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Data Persistence ──
|
// ── Data Persistence ──
|
||||||
|
|
||||||
|
|
@ -893,37 +866,7 @@ const gameLibraryPlugin: Plugin = {
|
||||||
// Admin endpoints (same auth as soundboard)
|
// Admin endpoints (same auth as soundboard)
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
const requireAdmin = (req: express.Request, res: express.Response, next: () => void) => {
|
const requireAdmin = requireAdminFactory(ctx.adminPwd);
|
||||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
|
||||||
if (!verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin'))) {
|
|
||||||
res.status(401).json({ error: 'Nicht eingeloggt' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── GET /api/game-library/admin/status ──
|
|
||||||
app.get('/api/game-library/admin/status', (req, res) => {
|
|
||||||
if (!ctx.adminPwd) { res.json({ admin: false, configured: false }); return; }
|
|
||||||
const valid = verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin'));
|
|
||||||
res.json({ admin: valid, configured: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /api/game-library/admin/login ──
|
|
||||||
app.post('/api/game-library/admin/login', (req, res) => {
|
|
||||||
const password = String(req.body?.password || '');
|
|
||||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
|
||||||
if (password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
|
|
||||||
const token = signAdminToken(ctx.adminPwd);
|
|
||||||
res.setHeader('Set-Cookie', `admin=${token}; HttpOnly; Path=/; Max-Age=${7 * 24 * 3600}; SameSite=Lax`);
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── POST /api/game-library/admin/logout ──
|
|
||||||
app.post('/api/game-library/admin/logout', (_req, res) => {
|
|
||||||
res.setHeader('Set-Cookie', 'admin=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax');
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── GET /api/game-library/admin/profiles ── Alle Profile mit Details
|
// ── GET /api/game-library/admin/profiles ── Alle Profile mit Details
|
||||||
app.get('/api/game-library/admin/profiles', requireAdmin, (_req, res) => {
|
app.get('/api/game-library/admin/profiles', requireAdmin, (_req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import type express from 'express';
|
import type express from 'express';
|
||||||
import crypto from 'node:crypto';
|
|
||||||
import { Client, EmbedBuilder, TextChannel, ChannelType } from 'discord.js';
|
import { Client, EmbedBuilder, TextChannel, ChannelType } from 'discord.js';
|
||||||
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
import type { Plugin, PluginContext } from '../../core/plugin.js';
|
||||||
import { getState, setState } from '../../core/persistence.js';
|
import { getState, setState } from '../../core/persistence.js';
|
||||||
|
import { requireAdmin as requireAdminFactory } from '../../core/auth.js';
|
||||||
|
|
||||||
const NB = '[Notifications]';
|
const NB = '[Notifications]';
|
||||||
|
|
||||||
|
|
@ -26,40 +26,6 @@ let _client: Client | null = null;
|
||||||
let _ctx: PluginContext | null = null;
|
let _ctx: PluginContext | null = null;
|
||||||
let _publicUrl = '';
|
let _publicUrl = '';
|
||||||
|
|
||||||
// ── Admin Auth (JWT-like with HMAC) ──
|
|
||||||
|
|
||||||
type AdminPayload = { iat: number; exp: number };
|
|
||||||
|
|
||||||
function readCookie(req: express.Request, name: string): string | undefined {
|
|
||||||
const header = req.headers.cookie;
|
|
||||||
if (!header) return undefined;
|
|
||||||
const match = header.split(';').map(s => s.trim()).find(s => s.startsWith(`${name}=`));
|
|
||||||
return match?.split('=').slice(1).join('=');
|
|
||||||
}
|
|
||||||
|
|
||||||
function b64url(str: string): string {
|
|
||||||
return Buffer.from(str).toString('base64url');
|
|
||||||
}
|
|
||||||
|
|
||||||
function verifyAdminToken(adminPwd: string, token: string | undefined): boolean {
|
|
||||||
if (!adminPwd || !token) return false;
|
|
||||||
const parts = token.split('.');
|
|
||||||
if (parts.length !== 2) return false;
|
|
||||||
const [body, sig] = parts;
|
|
||||||
const expected = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
|
||||||
if (expected !== sig) return false;
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(Buffer.from(body, 'base64url').toString('utf8')) as AdminPayload;
|
|
||||||
return typeof payload.exp === 'number' && Date.now() < payload.exp;
|
|
||||||
} catch { return false; }
|
|
||||||
}
|
|
||||||
|
|
||||||
function signAdminToken(adminPwd: string): string {
|
|
||||||
const payload: AdminPayload = { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600_000 };
|
|
||||||
const body = b64url(JSON.stringify(payload));
|
|
||||||
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
|
||||||
return `${body}.${sig}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Exported notification functions (called by other plugins) ──
|
// ── Exported notification functions (called by other plugins) ──
|
||||||
|
|
||||||
|
|
@ -159,33 +125,7 @@ const notificationsPlugin: Plugin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
registerRoutes(app, ctx) {
|
registerRoutes(app, ctx) {
|
||||||
const requireAdmin = (req: express.Request, res: express.Response, next: () => void): void => {
|
const requireAdmin = requireAdminFactory(ctx.adminPwd);
|
||||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
|
||||||
if (!verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin'))) { res.status(401).json({ error: 'Nicht eingeloggt' }); return; }
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Admin status
|
|
||||||
app.get('/api/notifications/admin/status', (req, res) => {
|
|
||||||
if (!ctx.adminPwd) { res.json({ admin: false }); return; }
|
|
||||||
res.json({ admin: verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin')) });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin login
|
|
||||||
app.post('/api/notifications/admin/login', (req, res) => {
|
|
||||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
|
||||||
const { password } = req.body ?? {};
|
|
||||||
if (password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
|
|
||||||
const token = signAdminToken(ctx.adminPwd);
|
|
||||||
res.setHeader('Set-Cookie', `admin=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${7 * 86400}`);
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin logout
|
|
||||||
app.post('/api/notifications/admin/logout', (_req, res) => {
|
|
||||||
res.setHeader('Set-Cookie', 'admin=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0');
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// List available text channels (requires admin)
|
// List available text channels (requires admin)
|
||||||
app.get('/api/notifications/channels', requireAdmin, async (_req, res) => {
|
app.get('/api/notifications/channels', requireAdmin, async (_req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ function formatDate(iso: string): string {
|
||||||
COMPONENT
|
COMPONENT
|
||||||
══════════════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
export default function GameLibraryTab({ data }: { data: any }) {
|
export default function GameLibraryTab({ data, isAdmin: isAdminProp = false }: { data: any; isAdmin?: boolean }) {
|
||||||
// ── State ──
|
// ── State ──
|
||||||
const [profiles, setProfiles] = useState<ProfileSummary[]>([]);
|
const [profiles, setProfiles] = useState<ProfileSummary[]>([]);
|
||||||
const [mode, setMode] = useState<'overview' | 'user' | 'common'>('overview');
|
const [mode, setMode] = useState<'overview' | 'user' | 'common'>('overview');
|
||||||
|
|
@ -111,11 +111,9 @@ export default function GameLibraryTab({ data }: { data: any }) {
|
||||||
|
|
||||||
// ── Admin state ──
|
// ── Admin state ──
|
||||||
const [showAdmin, setShowAdmin] = useState(false);
|
const [showAdmin, setShowAdmin] = useState(false);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const isAdmin = isAdminProp;
|
||||||
const [adminPwd, setAdminPwd] = useState('');
|
|
||||||
const [adminProfiles, setAdminProfiles] = useState<any[]>([]);
|
const [adminProfiles, setAdminProfiles] = useState<any[]>([]);
|
||||||
const [adminLoading, setAdminLoading] = useState(false);
|
const [adminLoading, setAdminLoading] = useState(false);
|
||||||
const [adminError, setAdminError] = useState('');
|
|
||||||
|
|
||||||
// ── SSE data sync ──
|
// ── SSE data sync ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -133,42 +131,6 @@ export default function GameLibraryTab({ data }: { data: any }) {
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── Admin: check login status on mount ──
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/game-library/admin/status', { credentials: 'include' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(d => setIsAdmin(d.admin === true))
|
|
||||||
.catch(() => {});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Admin: login ──
|
|
||||||
const adminLogin = useCallback(async () => {
|
|
||||||
setAdminError('');
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/game-library/admin/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ password: adminPwd }),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
|
||||||
setIsAdmin(true);
|
|
||||||
setAdminPwd('');
|
|
||||||
} else {
|
|
||||||
const d = await resp.json();
|
|
||||||
setAdminError(d.error || 'Fehler');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setAdminError('Verbindung fehlgeschlagen');
|
|
||||||
}
|
|
||||||
}, [adminPwd]);
|
|
||||||
|
|
||||||
// ── Admin: logout ──
|
|
||||||
const adminLogout = useCallback(async () => {
|
|
||||||
await fetch('/api/game-library/admin/logout', { method: 'POST', credentials: 'include' });
|
|
||||||
setIsAdmin(false);
|
|
||||||
setShowAdmin(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// ── Admin: load profiles ──
|
// ── Admin: load profiles ──
|
||||||
const loadAdminProfiles = useCallback(async () => {
|
const loadAdminProfiles = useCallback(async () => {
|
||||||
|
|
@ -552,9 +514,11 @@ export default function GameLibraryTab({ data }: { data: any }) {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="gl-login-bar-spacer" />
|
<div className="gl-login-bar-spacer" />
|
||||||
|
{isAdmin && (
|
||||||
<button className="gl-admin-btn" onClick={openAdmin} title="Admin Panel">
|
<button className="gl-admin-btn" onClick={openAdmin} title="Admin Panel">
|
||||||
⚙️
|
⚙️
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Profile Chips ── */}
|
{/* ── Profile Chips ── */}
|
||||||
|
|
@ -990,29 +954,10 @@ export default function GameLibraryTab({ data }: { data: any }) {
|
||||||
<button className="gl-admin-close" onClick={() => setShowAdmin(false)}>✕</button>
|
<button className="gl-admin-close" onClick={() => setShowAdmin(false)}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isAdmin ? (
|
|
||||||
<div className="gl-admin-login">
|
|
||||||
<p>Admin-Passwort eingeben:</p>
|
|
||||||
<div className="gl-admin-login-row">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="gl-dialog-input"
|
|
||||||
placeholder="Passwort"
|
|
||||||
value={adminPwd}
|
|
||||||
onChange={e => setAdminPwd(e.target.value)}
|
|
||||||
onKeyDown={e => { if (e.key === 'Enter') adminLogin(); }}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button className="gl-admin-login-btn" onClick={adminLogin}>Login</button>
|
|
||||||
</div>
|
|
||||||
{adminError && <p className="gl-dialog-status error">{adminError}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="gl-admin-content">
|
<div className="gl-admin-content">
|
||||||
<div className="gl-admin-toolbar">
|
<div className="gl-admin-toolbar">
|
||||||
<span className="gl-admin-status-text">✅ Eingeloggt als Admin</span>
|
<span className="gl-admin-status-text">✅ Eingeloggt als Admin</span>
|
||||||
<button className="gl-admin-refresh-btn" onClick={loadAdminProfiles}>↻ Aktualisieren</button>
|
<button className="gl-admin-refresh-btn" onClick={loadAdminProfiles}>↻ Aktualisieren</button>
|
||||||
<button className="gl-admin-logout-btn" onClick={adminLogout}>Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{adminLoading ? (
|
{adminLoading ? (
|
||||||
|
|
@ -1044,7 +989,6 @@ export default function GameLibraryTab({ data }: { data: any }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1000,13 +1000,15 @@ export default function SoundboardTab({ data, isAdmin: isAdminProp = false }: So
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
className={`admin-btn-icon ${isAdmin ? 'active' : ''}`}
|
className="admin-btn-icon active"
|
||||||
onClick={() => setShowAdmin(true)}
|
onClick={() => setShowAdmin(true)}
|
||||||
title="Admin"
|
title="Admin"
|
||||||
>
|
>
|
||||||
<span className="material-icons">settings</span>
|
<span className="material-icons">settings</span>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ const QUALITY_PRESETS = [
|
||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
export default function StreamingTab({ data }: { data: any }) {
|
export default function StreamingTab({ data, isAdmin: isAdminProp = false }: { data: any; isAdmin?: boolean }) {
|
||||||
// ── State ──
|
// ── State ──
|
||||||
const [streams, setStreams] = useState<StreamInfo[]>([]);
|
const [streams, setStreams] = useState<StreamInfo[]>([]);
|
||||||
const [userName, setUserName] = useState(() => localStorage.getItem('streaming_name') || '');
|
const [userName, setUserName] = useState(() => localStorage.getItem('streaming_name') || '');
|
||||||
|
|
@ -75,9 +75,7 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
|
|
||||||
// ── Admin / Notification Config ──
|
// ── Admin / Notification Config ──
|
||||||
const [showAdmin, setShowAdmin] = useState(false);
|
const [showAdmin, setShowAdmin] = useState(false);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const isAdmin = isAdminProp;
|
||||||
const [adminPwd, setAdminPwd] = useState('');
|
|
||||||
const [adminError, setAdminError] = useState('');
|
|
||||||
const [availableChannels, setAvailableChannels] = useState<Array<{ channelId: string; channelName: string; guildId: string; guildName: string }>>([]);
|
const [availableChannels, setAvailableChannels] = useState<Array<{ channelId: string; channelName: string; guildId: string; guildName: string }>>([]);
|
||||||
const [notifyConfig, setNotifyConfig] = useState<Array<{ channelId: string; channelName: string; guildId: string; guildName: string; events: string[] }>>([]);
|
const [notifyConfig, setNotifyConfig] = useState<Array<{ channelId: string; channelName: string; guildId: string; guildName: string; events: string[] }>>([]);
|
||||||
const [configLoading, setConfigLoading] = useState(false);
|
const [configLoading, setConfigLoading] = useState(false);
|
||||||
|
|
@ -138,12 +136,8 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
return () => document.removeEventListener('click', handler);
|
return () => document.removeEventListener('click', handler);
|
||||||
}, [openMenu]);
|
}, [openMenu]);
|
||||||
|
|
||||||
// Check admin status on mount
|
// Load notification bot status on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/notifications/admin/status', { credentials: 'include' })
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(d => setIsAdmin(d.admin === true))
|
|
||||||
.catch(() => {});
|
|
||||||
fetch('/api/notifications/status')
|
fetch('/api/notifications/status')
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(d => setNotifyStatus(d))
|
.then(d => setNotifyStatus(d))
|
||||||
|
|
@ -610,34 +604,6 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
setOpenMenu(null);
|
setOpenMenu(null);
|
||||||
}, [buildStreamLink]);
|
}, [buildStreamLink]);
|
||||||
|
|
||||||
// ── Admin functions ──
|
|
||||||
const adminLogin = useCallback(async () => {
|
|
||||||
setAdminError('');
|
|
||||||
try {
|
|
||||||
const resp = await fetch('/api/notifications/admin/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ password: adminPwd }),
|
|
||||||
credentials: 'include',
|
|
||||||
});
|
|
||||||
if (resp.ok) {
|
|
||||||
setIsAdmin(true);
|
|
||||||
setAdminPwd('');
|
|
||||||
loadNotifyConfig();
|
|
||||||
} else {
|
|
||||||
const d = await resp.json();
|
|
||||||
setAdminError(d.error || 'Fehler');
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setAdminError('Verbindung fehlgeschlagen');
|
|
||||||
}
|
|
||||||
}, [adminPwd]);
|
|
||||||
|
|
||||||
const adminLogout = useCallback(async () => {
|
|
||||||
await fetch('/api/notifications/admin/logout', { method: 'POST', credentials: 'include' });
|
|
||||||
setIsAdmin(false);
|
|
||||||
setShowAdmin(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadNotifyConfig = useCallback(async () => {
|
const loadNotifyConfig = useCallback(async () => {
|
||||||
setConfigLoading(true);
|
setConfigLoading(true);
|
||||||
|
|
@ -796,9 +762,11 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
{starting ? 'Starte...' : '\u{1F5A5}\uFE0F Stream starten'}
|
{starting ? 'Starte...' : '\u{1F5A5}\uFE0F Stream starten'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{isAdmin && (
|
||||||
<button className="stream-admin-btn" onClick={openAdmin} title="Notification Einstellungen">
|
<button className="stream-admin-btn" onClick={openAdmin} title="Notification Einstellungen">
|
||||||
{'\u2699\uFE0F'}
|
{'\u2699\uFE0F'}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{streams.length === 0 && !isBroadcasting ? (
|
{streams.length === 0 && !isBroadcasting ? (
|
||||||
|
|
@ -912,24 +880,6 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
<button className="stream-admin-close" onClick={() => setShowAdmin(false)}>{'\u2715'}</button>
|
<button className="stream-admin-close" onClick={() => setShowAdmin(false)}>{'\u2715'}</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isAdmin ? (
|
|
||||||
<div className="stream-admin-login">
|
|
||||||
<p>Admin-Passwort eingeben:</p>
|
|
||||||
<div className="stream-admin-login-row">
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
className="stream-input"
|
|
||||||
placeholder="Passwort"
|
|
||||||
value={adminPwd}
|
|
||||||
onChange={e => setAdminPwd(e.target.value)}
|
|
||||||
onKeyDown={e => { if (e.key === 'Enter') adminLogin(); }}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
<button className="stream-btn" onClick={adminLogin}>Login</button>
|
|
||||||
</div>
|
|
||||||
{adminError && <p className="stream-admin-error">{adminError}</p>}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="stream-admin-content">
|
<div className="stream-admin-content">
|
||||||
<div className="stream-admin-toolbar">
|
<div className="stream-admin-toolbar">
|
||||||
<span className="stream-admin-status">
|
<span className="stream-admin-status">
|
||||||
|
|
@ -937,7 +887,6 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
? <>{'\u2705'} Bot online: <b>{notifyStatus.botTag}</b></>
|
? <>{'\u2705'} Bot online: <b>{notifyStatus.botTag}</b></>
|
||||||
: <>{'\u26A0\uFE0F'} Bot offline — <code>DISCORD_TOKEN_NOTIFICATIONS</code> setzen</>}
|
: <>{'\u26A0\uFE0F'} Bot offline — <code>DISCORD_TOKEN_NOTIFICATIONS</code> setzen</>}
|
||||||
</span>
|
</span>
|
||||||
<button className="stream-admin-logout" onClick={adminLogout}>Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{configLoading ? (
|
{configLoading ? (
|
||||||
|
|
@ -993,7 +942,6 @@ export default function StreamingTab({ data }: { data: any }) {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue