Refactor: Zentralisiertes Admin-Login im Top-Menü

- Admin-Login aus 3 Plugins (Soundboard, Streaming, Game Library) entfernt
- Zentraler 🔒/🔓 Button im Header mit Login-Modal
- isAdmin wird als Prop an alle Plugins weitergegeben
- Settings-Buttons (Gear-Icons) nur sichtbar wenn eingeloggt
- Alle Plugins nutzen weiterhin den shared admin-Cookie für Operationen
- Login/Logout-Formulare und Buttons aus Plugin-Panels entfernt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-09 21:59:12 +01:00
parent bccfee3de2
commit e9931d82af
10 changed files with 5077 additions and 5063 deletions

View file

@ -186,25 +186,6 @@ async function apiGetVolume(guildId: string): Promise<number> {
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> {
const res = await fetch(`${API_BASE}/admin/sounds/delete`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include',
@ -324,13 +305,14 @@ interface VoiceStats {
interface SoundboardTabProps {
data: any;
isAdmin?: boolean;
}
/*
COMPONENT
*/
export default function SoundboardTab({ data }: SoundboardTabProps) {
export default function SoundboardTab({ data, isAdmin: isAdminProp }: SoundboardTabProps) {
/* ── Data ── */
const [sounds, setSounds] = useState<Sound[]>([]);
const [total, setTotal] = useState(0);
@ -378,9 +360,8 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
const volDebounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
/* ── Admin ── */
const [isAdmin, setIsAdmin] = useState(false);
const isAdmin = isAdminProp ?? false;
const [showAdmin, setShowAdmin] = useState(false);
const [adminPwd, setAdminPwd] = useState('');
const [adminSounds, setAdminSounds] = useState<Sound[]>([]);
const [adminLoading, setAdminLoading] = useState(false);
const [adminQuery, setAdminQuery] = useState('');
@ -521,7 +502,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
setSelected(match ? `${g}:${serverCid}` : `${ch[0].guildId}:${ch[0].channelId}`);
}
} catch (e: any) { notify(e?.message || 'Channel-Fehler', 'error'); }
try { setIsAdmin(await apiAdminStatus()); } catch { }
try { const c = await fetchCategories(); setCategories(c.categories || []); } catch { }
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -879,28 +859,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 ── */
const displaySounds = useMemo(() => {
if (activeTab === 'favorites') return sounds.filter(s => favs[s.relativePath ?? s.fileName]);
@ -1040,13 +998,15 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
)}
</div>
)}
<button
className={`admin-btn-icon ${isAdmin ? 'active' : ''}`}
onClick={() => setShowAdmin(true)}
title="Admin"
>
<span className="material-icons">settings</span>
</button>
{isAdmin && (
<button
className="admin-btn-icon active"
onClick={() => setShowAdmin(true)}
title="Admin"
>
<span className="material-icons">settings</span>
</button>
)}
</div>
</header>
@ -1447,21 +1407,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
<span className="material-icons" style={{ fontSize: 18 }}>close</span>
</button>
</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-header-row">
<p className="admin-status">Eingeloggt als Admin</p>
@ -1473,7 +1418,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
>
Aktualisieren
</button>
<button className="admin-btn-action outline" onClick={handleAdminLogout}>Logout</button>
</div>
</div>
@ -1585,7 +1529,6 @@ export default function SoundboardTab({ data }: SoundboardTabProps) {
)}
</div>
</div>
)}
</div>
</div>
)}