Refactor: Zentralisiertes Admin-Login für alle Tabs
Admin-Auth aus Soundboard-Plugin in core/auth.ts extrahiert. Ein Login-Button im Header gilt jetzt für die gesamte Webseite. Cookie-basiert (HMAC-SHA256, 7 Tage) — überlebt Page-Reload. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8abe0775a5
commit
b3080fb763
6 changed files with 101 additions and 133 deletions
61
server/src/core/auth.ts
Normal file
61
server/src/core/auth.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
const COOKIE_NAME = 'admin_token';
|
||||||
|
const TOKEN_TTL_MS = 7 * 24 * 3600 * 1000; // 7 days
|
||||||
|
|
||||||
|
type AdminPayload = { iat: number; exp: number };
|
||||||
|
|
||||||
|
function b64url(input: Buffer | string): string {
|
||||||
|
return Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function signAdminToken(adminPwd: string): string {
|
||||||
|
const payload: AdminPayload = { iat: Date.now(), exp: Date.now() + TOKEN_TTL_MS };
|
||||||
|
const body = b64url(JSON.stringify(payload));
|
||||||
|
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
||||||
|
return `${body}.${sig}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 AdminPayload;
|
||||||
|
return typeof payload.exp === 'number' && Date.now() < payload.exp;
|
||||||
|
} catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readCookie(req: Request, key: string): string | undefined {
|
||||||
|
const c = req.headers.cookie;
|
||||||
|
if (!c) return undefined;
|
||||||
|
for (const part of c.split(';')) {
|
||||||
|
const [k, v] = part.trim().split('=');
|
||||||
|
if (k === key) return decodeURIComponent(v || '');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAdminCookie(res: Response, token: string): void {
|
||||||
|
res.setHeader('Set-Cookie', `${COOKIE_NAME}=${encodeURIComponent(token)}; HttpOnly; Path=/; Max-Age=${7 * 24 * 3600}; SameSite=Lax`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAdminCookie(res: Response): void {
|
||||||
|
res.setHeader('Set-Cookie', `${COOKIE_NAME}=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requireAdmin(adminPwd: string) {
|
||||||
|
return (req: Request, res: Response, next: NextFunction): void => {
|
||||||
|
if (!adminPwd) { res.status(503).json({ error: 'ADMIN_PWD not configured' }); return; }
|
||||||
|
if (!verifyAdminToken(adminPwd, readCookie(req, COOKIE_NAME))) {
|
||||||
|
res.status(401).json({ error: 'Nicht eingeloggt' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { COOKIE_NAME };
|
||||||
|
|
@ -1,24 +1,8 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import type { PluginContext } from './plugin.js';
|
import type { PluginContext } from './plugin.js';
|
||||||
|
|
||||||
/**
|
// Re-export centralised admin auth
|
||||||
* Admin authentication middleware.
|
export { requireAdmin } from './auth.js';
|
||||||
* Checks `x-admin-password` header against ADMIN_PWD env var.
|
|
||||||
*/
|
|
||||||
export function adminAuth(ctx: PluginContext) {
|
|
||||||
return (req: Request, res: Response, next: NextFunction): void => {
|
|
||||||
if (!ctx.adminPwd) {
|
|
||||||
res.status(503).json({ error: 'ADMIN_PWD not configured' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pwd = req.headers['x-admin-password'] as string | undefined;
|
|
||||||
if (pwd !== ctx.adminPwd) {
|
|
||||||
res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guild filter middleware.
|
* Guild filter middleware.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { createClient } from './core/discord.js';
|
||||||
import { addSSEClient, removeSSEClient, sseBroadcast, getSSEClientCount } from './core/sse.js';
|
import { addSSEClient, removeSSEClient, sseBroadcast, getSSEClientCount } from './core/sse.js';
|
||||||
import { loadState, getFullState, getStateDiag } from './core/persistence.js';
|
import { loadState, getFullState, getStateDiag } from './core/persistence.js';
|
||||||
import { getPlugins, registerPlugin, getPluginCtx, type PluginContext } from './core/plugin.js';
|
import { getPlugins, registerPlugin, getPluginCtx, type PluginContext } from './core/plugin.js';
|
||||||
|
import { signAdminToken, verifyAdminToken, readCookie, setAdminCookie, clearAdminCookie, COOKIE_NAME } from './core/auth.js';
|
||||||
import radioPlugin from './plugins/radio/index.js';
|
import radioPlugin from './plugins/radio/index.js';
|
||||||
import soundboardPlugin from './plugins/soundboard/index.js';
|
import soundboardPlugin from './plugins/soundboard/index.js';
|
||||||
import lolstatsPlugin from './plugins/lolstats/index.js';
|
import lolstatsPlugin from './plugins/lolstats/index.js';
|
||||||
|
|
@ -93,16 +94,25 @@ app.get('/api/health', (_req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Admin Login ──
|
// ── Admin Auth (centralised) ──
|
||||||
app.post('/api/admin/login', (req, res) => {
|
app.post('/api/admin/login', (req, res) => {
|
||||||
if (!ADMIN_PWD) { res.status(503).json({ error: 'ADMIN_PWD not configured' }); return; }
|
if (!ADMIN_PWD) { res.status(503).json({ error: 'ADMIN_PWD not configured' }); return; }
|
||||||
const { password } = req.body ?? {};
|
const { password } = req.body ?? {};
|
||||||
if (password === ADMIN_PWD) {
|
if (password === ADMIN_PWD) {
|
||||||
|
const token = signAdminToken(ADMIN_PWD);
|
||||||
|
setAdminCookie(res, token);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ error: 'Invalid password' });
|
res.status(401).json({ error: 'Invalid password' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
app.post('/api/admin/logout', (_req, res) => {
|
||||||
|
clearAdminCookie(res);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
app.get('/api/admin/status', (req, res) => {
|
||||||
|
res.json({ authenticated: verifyAdminToken(ADMIN_PWD, readCookie(req, COOKIE_NAME)) });
|
||||||
|
});
|
||||||
|
|
||||||
// ── API: List plugins ──
|
// ── API: List plugins ──
|
||||||
app.get('/api/plugins', (_req, res) => {
|
app.get('/api/plugins', (_req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import nacl from 'tweetnacl';
|
||||||
import { ChannelType, Events, type VoiceBasedChannel, type VoiceState, type Message } from 'discord.js';
|
import { ChannelType, Events, type VoiceBasedChannel, type VoiceState, type Message } from 'discord.js';
|
||||||
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';
|
||||||
|
|
||||||
// ── Config (env) ──
|
// ── Config (env) ──
|
||||||
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
const SOUNDS_DIR = process.env.SOUNDS_DIR ?? '/data/sounds';
|
||||||
|
|
@ -583,33 +584,6 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
|
||||||
if (relativeKey) incrementPlaysFor(relativeKey);
|
if (relativeKey) incrementPlaysFor(relativeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Admin Auth (JWT-like with HMAC) ──
|
|
||||||
type AdminPayload = { iat: number; exp: number };
|
|
||||||
function b64url(input: Buffer | string): string {
|
|
||||||
return Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
||||||
}
|
|
||||||
function signAdminToken(adminPwd: string, payload: AdminPayload): string {
|
|
||||||
const body = b64url(JSON.stringify(payload));
|
|
||||||
const sig = crypto.createHmac('sha256', adminPwd).update(body).digest('base64url');
|
|
||||||
return `${body}.${sig}`;
|
|
||||||
}
|
|
||||||
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 AdminPayload;
|
|
||||||
return typeof payload.exp === 'number' && Date.now() < payload.exp;
|
|
||||||
} catch { return false; }
|
|
||||||
}
|
|
||||||
function readCookie(req: express.Request, key: string): string | undefined {
|
|
||||||
const c = req.headers.cookie;
|
|
||||||
if (!c) return undefined;
|
|
||||||
for (const part of c.split(';')) { const [k, v] = part.trim().split('='); if (k === key) return decodeURIComponent(v || ''); }
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Party Mode ──
|
// ── Party Mode ──
|
||||||
function schedulePartyPlayback(guildId: string, channelId: string) {
|
function schedulePartyPlayback(guildId: string, channelId: string) {
|
||||||
|
|
@ -775,28 +749,7 @@ const soundboardPlugin: Plugin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
registerRoutes(app: express.Application, ctx: PluginContext) {
|
registerRoutes(app: express.Application, ctx: PluginContext) {
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Admin Auth ──
|
|
||||||
app.post('/api/soundboard/admin/login', (req, res) => {
|
|
||||||
if (!ctx.adminPwd) { res.status(503).json({ error: 'Admin nicht konfiguriert' }); return; }
|
|
||||||
const { password } = req.body ?? {};
|
|
||||||
if (!password || password !== ctx.adminPwd) { res.status(401).json({ error: 'Falsches Passwort' }); return; }
|
|
||||||
const token = signAdminToken(ctx.adminPwd, { iat: Date.now(), exp: Date.now() + 7 * 24 * 3600 * 1000 });
|
|
||||||
res.setHeader('Set-Cookie', `admin=${encodeURIComponent(token)}; HttpOnly; Path=/; Max-Age=${7 * 24 * 3600}; SameSite=Lax`);
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
app.post('/api/soundboard/admin/logout', (_req, res) => {
|
|
||||||
res.setHeader('Set-Cookie', 'admin=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax');
|
|
||||||
res.json({ ok: true });
|
|
||||||
});
|
|
||||||
app.get('/api/soundboard/admin/status', (req, res) => {
|
|
||||||
res.json({ authenticated: verifyAdminToken(ctx.adminPwd, readCookie(req, 'admin')) });
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Sounds ──
|
// ── Sounds ──
|
||||||
app.get('/api/soundboard/sounds', (req, res) => {
|
app.get('/api/soundboard/sounds', (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ interface PluginInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plugin tab components
|
// Plugin tab components
|
||||||
const tabComponents: Record<string, React.FC<{ data: any }>> = {
|
const tabComponents: Record<string, React.FC<{ data: any; isAdmin?: boolean }>> = {
|
||||||
radio: RadioTab,
|
radio: RadioTab,
|
||||||
soundboard: SoundboardTab,
|
soundboard: SoundboardTab,
|
||||||
lolstats: LolstatsTab,
|
lolstats: LolstatsTab,
|
||||||
|
|
@ -22,7 +22,7 @@ const tabComponents: Record<string, React.FC<{ data: any }>> = {
|
||||||
'game-library': GameLibraryTab,
|
'game-library': GameLibraryTab,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function registerTab(pluginName: string, component: React.FC<{ data: any }>) {
|
export function registerTab(pluginName: string, component: React.FC<{ data: any; isAdmin?: boolean }>) {
|
||||||
tabComponents[pluginName] = component;
|
tabComponents[pluginName] = component;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,12 +149,21 @@ export default function App() {
|
||||||
return () => window.removeEventListener('keydown', handler);
|
return () => window.removeEventListener('keydown', handler);
|
||||||
}, [showVersionModal, showAdminModal]);
|
}, [showVersionModal, showAdminModal]);
|
||||||
|
|
||||||
|
// Check admin status on mount (cookie-based, survives reload)
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/admin/status', { credentials: 'include' })
|
||||||
|
.then(r => r.ok ? r.json() : null)
|
||||||
|
.then(d => { if (d?.authenticated) setAdminLoggedIn(true); })
|
||||||
|
.catch(() => {});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Admin login handler
|
// Admin login handler
|
||||||
const handleAdminLogin = () => {
|
const handleAdminLogin = () => {
|
||||||
if (!adminPassword) return;
|
if (!adminPassword) return;
|
||||||
fetch('/api/admin/login', {
|
fetch('/api/admin/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
body: JSON.stringify({ password: adminPassword }),
|
body: JSON.stringify({ password: adminPassword }),
|
||||||
})
|
})
|
||||||
.then(r => {
|
.then(r => {
|
||||||
|
|
@ -162,6 +171,7 @@ export default function App() {
|
||||||
setAdminLoggedIn(true);
|
setAdminLoggedIn(true);
|
||||||
setAdminPassword('');
|
setAdminPassword('');
|
||||||
setAdminError('');
|
setAdminError('');
|
||||||
|
setShowAdminModal(false);
|
||||||
} else {
|
} else {
|
||||||
setAdminError('Falsches Passwort');
|
setAdminError('Falsches Passwort');
|
||||||
}
|
}
|
||||||
|
|
@ -170,8 +180,15 @@ export default function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdminLogout = () => {
|
const handleAdminLogout = () => {
|
||||||
|
fetch('/api/admin/logout', { method: 'POST', credentials: 'include' })
|
||||||
|
.then(() => {
|
||||||
setAdminLoggedIn(false);
|
setAdminLoggedIn(false);
|
||||||
setShowAdminModal(false);
|
setShowAdminModal(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setAdminLoggedIn(false);
|
||||||
|
setShowAdminModal(false);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -414,7 +431,7 @@ export default function App() {
|
||||||
: { display: 'none' }
|
: { display: 'none' }
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Comp data={pluginData[p.name] || {}} />
|
<Comp data={pluginData[p.name] || {}} isAdmin={adminLoggedIn} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -186,24 +186,6 @@ async function apiGetVolume(guildId: string): Promise<number> {
|
||||||
return typeof data?.volume === 'number' ? data.volume : 1;
|
return typeof data?.volume === 'number' ? data.volume : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiAdminStatus(): Promise<boolean> {
|
|
||||||
const res = await fetch(`${API_BASE}/admin/status`, { credentials: 'include' });
|
|
||||||
if (!res.ok) return false;
|
|
||||||
const data = await res.json();
|
|
||||||
return !!data?.authenticated;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiAdminLogin(password: string): Promise<boolean> {
|
|
||||||
const res = await fetch(`${API_BASE}/admin/login`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
|
|
||||||
body: JSON.stringify({ password })
|
|
||||||
});
|
|
||||||
return res.ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiAdminLogout(): Promise<void> {
|
|
||||||
await fetch(`${API_BASE}/admin/logout`, { method: 'POST', credentials: 'include' });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function apiAdminDelete(paths: string[]): Promise<void> {
|
async function apiAdminDelete(paths: string[]): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE}/admin/sounds/delete`, {
|
const res = await fetch(`${API_BASE}/admin/sounds/delete`, {
|
||||||
|
|
@ -324,13 +306,14 @@ interface VoiceStats {
|
||||||
|
|
||||||
interface SoundboardTabProps {
|
interface SoundboardTabProps {
|
||||||
data: any;
|
data: any;
|
||||||
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ══════════════════════════════════════════════════════════════════
|
/* ══════════════════════════════════════════════════════════════════
|
||||||
COMPONENT
|
COMPONENT
|
||||||
══════════════════════════════════════════════════════════════════ */
|
══════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
export default function SoundboardTab({ data }: SoundboardTabProps) {
|
export default function SoundboardTab({ data, isAdmin: isAdminProp = false }: SoundboardTabProps) {
|
||||||
/* ── Data ── */
|
/* ── Data ── */
|
||||||
const [sounds, setSounds] = useState<Sound[]>([]);
|
const [sounds, setSounds] = useState<Sound[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
@ -378,9 +361,8 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
const volDebounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const volDebounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
|
|
||||||
/* ── Admin ── */
|
/* ── Admin ── */
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const isAdmin = isAdminProp;
|
||||||
const [showAdmin, setShowAdmin] = useState(false);
|
const [showAdmin, setShowAdmin] = useState(false);
|
||||||
const [adminPwd, setAdminPwd] = useState('');
|
|
||||||
const [adminSounds, setAdminSounds] = useState<Sound[]>([]);
|
const [adminSounds, setAdminSounds] = useState<Sound[]>([]);
|
||||||
const [adminLoading, setAdminLoading] = useState(false);
|
const [adminLoading, setAdminLoading] = useState(false);
|
||||||
const [adminQuery, setAdminQuery] = useState('');
|
const [adminQuery, setAdminQuery] = useState('');
|
||||||
|
|
@ -521,7 +503,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
setSelected(match ? `${g}:${serverCid}` : `${ch[0].guildId}:${ch[0].channelId}`);
|
setSelected(match ? `${g}:${serverCid}` : `${ch[0].guildId}:${ch[0].channelId}`);
|
||||||
}
|
}
|
||||||
} catch (e: any) { notify(e?.message || 'Channel-Fehler', 'error'); }
|
} catch (e: any) { notify(e?.message || 'Channel-Fehler', 'error'); }
|
||||||
try { setIsAdmin(await apiAdminStatus()); } catch { }
|
|
||||||
try { const c = await fetchCategories(); setCategories(c.categories || []); } catch { }
|
try { const c = await fetchCategories(); setCategories(c.categories || []); } catch { }
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
|
@ -879,27 +860,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAdminLogin() {
|
|
||||||
try {
|
|
||||||
const ok = await apiAdminLogin(adminPwd);
|
|
||||||
if (ok) {
|
|
||||||
setIsAdmin(true);
|
|
||||||
setAdminPwd('');
|
|
||||||
notify('Admin eingeloggt');
|
|
||||||
}
|
|
||||||
else notify('Falsches Passwort', 'error');
|
|
||||||
} catch { notify('Login fehlgeschlagen', 'error'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleAdminLogout() {
|
|
||||||
try {
|
|
||||||
await apiAdminLogout();
|
|
||||||
setIsAdmin(false);
|
|
||||||
setAdminSelection({});
|
|
||||||
cancelRename();
|
|
||||||
notify('Ausgeloggt');
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Computed ── */
|
/* ── Computed ── */
|
||||||
const displaySounds = useMemo(() => {
|
const displaySounds = useMemo(() => {
|
||||||
|
|
@ -1447,21 +1407,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
<span className="material-icons" style={{ fontSize: 18 }}>close</span>
|
<span className="material-icons" style={{ fontSize: 18 }}>close</span>
|
||||||
</button>
|
</button>
|
||||||
</h3>
|
</h3>
|
||||||
{!isAdmin ? (
|
|
||||||
<div>
|
|
||||||
<div className="admin-field">
|
|
||||||
<label>Passwort</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={adminPwd}
|
|
||||||
onChange={e => setAdminPwd(e.target.value)}
|
|
||||||
onKeyDown={e => e.key === 'Enter' && handleAdminLogin()}
|
|
||||||
placeholder="Admin-Passwort..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button className="admin-btn-action primary" onClick={handleAdminLogin}>Login</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="admin-shell">
|
<div className="admin-shell">
|
||||||
<div className="admin-header-row">
|
<div className="admin-header-row">
|
||||||
<p className="admin-status">Eingeloggt als Admin</p>
|
<p className="admin-status">Eingeloggt als Admin</p>
|
||||||
|
|
@ -1473,7 +1418,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
>
|
>
|
||||||
Aktualisieren
|
Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
<button className="admin-btn-action outline" onClick={handleAdminLogout}>Logout</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1585,7 +1529,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue