import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { Users, Shield, Search, Trash2, ChevronDown, Loader2, MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User, Upload, X as XIcon, Image, Type, Palette, Send, Copy, Clock, Check, ShieldCheck, Globe, Link as LinkIcon, LogIn, DoorOpen, Eye, ExternalLink, } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import { useBranding } from '../contexts/BrandingContext'; import { themes } from '../themes'; import api from '../services/api'; import toast from 'react-hot-toast'; export default function Admin() { const { user } = useAuth(); const { t, language } = useLanguage(); const { appName, hasLogo, logoUrl, defaultTheme, registrationMode, imprintUrl, privacyUrl, hideAppName, refreshBranding } = useBranding(); const navigate = useNavigate(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [openMenu, setOpenMenu] = useState(null); const [resetPwModal, setResetPwModal] = useState(null); const [newPassword, setNewPassword] = useState(''); const [showCreateUser, setShowCreateUser] = useState(false); const [creatingUser, setCreatingUser] = useState(false); const [newUser, setNewUser] = useState({ name: '', display_name: '', email: '', password: '', role: 'user' }); const menuBtnRefs = useRef({}); const [menuPos, setMenuPos] = useState(null); // Invite state const [invites, setInvites] = useState([]); const [inviteEmail, setInviteEmail] = useState(''); const [sendingInvite, setSendingInvite] = useState(false); const [savingRegMode, setSavingRegMode] = useState(false); // Branding state const [editAppName, setEditAppName] = useState(''); const [savingName, setSavingName] = useState(false); const [uploadingLogo, setUploadingLogo] = useState(false); const logoInputRef = useRef(null); const [editDefaultTheme, setEditDefaultTheme] = useState(''); const [savingDefaultTheme, setSavingDefaultTheme] = useState(false); const [editImprintUrl, setEditImprintUrl] = useState(''); const [savingImprintUrl, setSavingImprintUrl] = useState(false); const [editPrivacyUrl, setEditPrivacyUrl] = useState(''); const [savingPrivacyUrl, setSavingPrivacyUrl] = useState(false); const [savingHideAppName, setSavingHideAppName] = useState(false); // OAuth state const [oauthConfig, setOauthConfig] = useState(null); const [oauthLoading, setOauthLoading] = useState(true); const [oauthForm, setOauthForm] = useState({ issuer: '', clientId: '', clientSecret: '', displayName: 'SSO', autoRegister: true }); const [savingOauth, setSavingOauth] = useState(false); // Rooms state const [adminRooms, setAdminRooms] = useState([]); const [adminRoomsLoading, setAdminRoomsLoading] = useState(true); const [roomSearch, setRoomSearch] = useState(''); useEffect(() => { if (user?.role !== 'admin') { navigate('/dashboard'); return; } fetchUsers(); fetchInvites(); fetchOauthConfig(); fetchAdminRooms(); }, [user]); useEffect(() => { setEditAppName(appName || 'Redlight'); }, [appName]); useEffect(() => { setEditDefaultTheme(defaultTheme || 'dark'); }, [defaultTheme]); useEffect(() => { setEditImprintUrl(imprintUrl || ''); }, [imprintUrl]); useEffect(() => { setEditPrivacyUrl(privacyUrl || ''); }, [privacyUrl]); const fetchUsers = async () => { try { const res = await api.get('/admin/users'); setUsers(res.data.users); } catch { toast.error(t('admin.roleUpdateFailed')); } finally { setLoading(false); } }; const fetchInvites = async () => { try { const res = await api.get('/admin/invites'); setInvites(res.data.invites); } catch { // silently fail } }; const fetchAdminRooms = async () => { setAdminRoomsLoading(true); try { const res = await api.get('/admin/rooms'); setAdminRooms(res.data.rooms); } catch { // silently fail } finally { setAdminRoomsLoading(false); } }; const handleAdminDeleteRoom = async (uid, name) => { if (!confirm(t('admin.deleteRoomConfirm', { name }))) return; try { await api.delete(`/rooms/${uid}`); toast.success(t('admin.roomDeleted')); fetchAdminRooms(); } catch (err) { toast.error(err.response?.data?.error || t('admin.roomDeleteFailed')); } }; const handleRoleChange = async (userId, newRole) => { try { await api.put(`/admin/users/${userId}/role`, { role: newRole }); toast.success(t('admin.roleUpdated')); fetchUsers(); } catch (err) { toast.error(err.response?.data?.error || t('admin.roleUpdateFailed')); } setOpenMenu(null); setMenuPos(null); }; const handleDelete = async (userId, userName) => { if (!confirm(t('admin.deleteUserConfirm', { name: userName }))) return; try { await api.delete(`/admin/users/${userId}`); toast.success(t('admin.userDeleted')); fetchUsers(); } catch (err) { toast.error(err.response?.data?.error || t('admin.userDeleteFailed')); } setOpenMenu(null); setMenuPos(null); }; const handleResetPassword = async (e) => { e.preventDefault(); try { await api.put(`/admin/users/${resetPwModal}/password`, { newPassword }); toast.success(t('admin.passwordReset')); setResetPwModal(null); setNewPassword(''); } catch (err) { toast.error(err.response?.data?.error || t('admin.passwordResetFailed')); } }; // ── Branding handlers ────────────────────────────────────────────────── const handleLogoUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; setUploadingLogo(true); try { const formData = new FormData(); formData.append('logo', file); await api.post('/branding/logo', formData, { headers: { 'Content-Type': undefined }, }); toast.success(t('admin.logoUploaded')); refreshBranding(); } catch (err) { toast.error(err.response?.data?.error || t('admin.logoUploadFailed')); } finally { setUploadingLogo(false); if (logoInputRef.current) logoInputRef.current.value = ''; } }; const handleLogoRemove = async () => { try { await api.delete('/branding/logo'); toast.success(t('admin.logoRemoved')); refreshBranding(); } catch { toast.error(t('admin.logoRemoveFailed')); } }; const handleHideAppNameToggle = async (value) => { setSavingHideAppName(true); try { await api.put('/branding/hide-app-name', { hideAppName: value }); refreshBranding(); } catch { toast.error(t('admin.hideAppNameFailed')); } finally { setSavingHideAppName(false); } }; const handleAppNameSave = async () => { if (!editAppName.trim()) return; setSavingName(true); try { await api.put('/branding/name', { appName: editAppName.trim() }); toast.success(t('admin.appNameUpdated')); refreshBranding(); } catch { toast.error(t('admin.appNameUpdateFailed')); } finally { setSavingName(false); } }; const handleDefaultThemeSave = async () => { if (!editDefaultTheme) return; setSavingDefaultTheme(true); try { await api.put('/branding/default-theme', { defaultTheme: editDefaultTheme }); toast.success(t('admin.defaultThemeSaved')); refreshBranding(); } catch { toast.error(t('admin.defaultThemeUpdateFailed')); } finally { setSavingDefaultTheme(false); } }; const handleCreateUser = async (e) => { e.preventDefault(); setCreatingUser(true); try { await api.post('/admin/users', newUser); toast.success(t('admin.userCreated')); setShowCreateUser(false); setNewUser({ name: '', display_name: '', email: '', password: '', role: 'user' }); fetchUsers(); } catch (err) { toast.error(err.response?.data?.error || t('admin.userCreateFailed')); } finally { setCreatingUser(false); } }; const handleSendInvite = async (e) => { e.preventDefault(); setSendingInvite(true); try { const res = await api.post('/admin/invites', { email: inviteEmail }); toast.success(t('admin.inviteSent')); setInviteEmail(''); fetchInvites(); } catch (err) { toast.error(err.response?.data?.error || t('admin.inviteFailed')); } finally { setSendingInvite(false); } }; const handleDeleteInvite = async (id) => { try { await api.delete(`/admin/invites/${id}`); toast.success(t('admin.inviteDeleted')); fetchInvites(); } catch { toast.error(t('admin.inviteDeleteFailed')); } }; const handleCopyInviteLink = (token) => { const baseUrl = window.location.origin; navigator.clipboard.writeText(`${baseUrl}/register?invite=${token}`); toast.success(t('admin.inviteLinkCopied')); }; const handleRegModeChange = async (mode) => { setSavingRegMode(true); try { await api.put('/branding/registration-mode', { registrationMode: mode }); toast.success(t('admin.regModeSaved')); refreshBranding(); } catch { toast.error(t('admin.regModeFailed')); } finally { setSavingRegMode(false); } }; const handleImprintUrlSave = async () => { setSavingImprintUrl(true); try { await api.put('/branding/imprint-url', { imprintUrl: editImprintUrl.trim() }); toast.success(t('admin.imprintUrlSaved')); refreshBranding(); } catch { toast.error(t('admin.imprintUrlFailed')); } finally { setSavingImprintUrl(false); } }; const handlePrivacyUrlSave = async () => { setSavingPrivacyUrl(true); try { await api.put('/branding/privacy-url', { privacyUrl: editPrivacyUrl.trim() }); toast.success(t('admin.privacyUrlSaved')); refreshBranding(); } catch { toast.error(t('admin.privacyUrlFailed')); } finally { setSavingPrivacyUrl(false); } }; // ── OAuth handlers ────────────────────────────────────────────────────── const fetchOauthConfig = async () => { setOauthLoading(true); try { const res = await api.get('/admin/oauth'); if (res.data.configured) { setOauthConfig(res.data.config); setOauthForm({ issuer: res.data.config.issuer || '', clientId: res.data.config.clientId || '', clientSecret: '', displayName: res.data.config.displayName || 'SSO', autoRegister: res.data.config.autoRegister ?? true, }); } else { setOauthConfig(null); } } catch { // silently fail } finally { setOauthLoading(false); } }; const handleOauthSave = async (e) => { e.preventDefault(); setSavingOauth(true); try { await api.put('/admin/oauth', oauthForm); toast.success(t('admin.oauthSaved')); fetchOauthConfig(); refreshBranding(); } catch (err) { toast.error(err.response?.data?.error || t('admin.oauthSaveFailed')); } finally { setSavingOauth(false); } }; const handleOauthRemove = async () => { if (!confirm(t('admin.oauthRemoveConfirm'))) return; try { await api.delete('/admin/oauth'); toast.success(t('admin.oauthRemoved')); setOauthConfig(null); setOauthForm({ issuer: '', clientId: '', clientSecret: '', displayName: 'SSO', autoRegister: true }); refreshBranding(); } catch { toast.error(t('admin.oauthRemoveFailed')); } }; const filteredUsers = users.filter(u => (u.display_name || u.name).toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()) ); if (loading) { return (
); } return (

{t('admin.title')}

{t('admin.userCount', { count: users.length })}

{/* Branding */}

{t('admin.brandingTitle')}

{t('admin.brandingDescription')}

{/* Logo upload */}
{hasLogo && logoUrl ? (
Logo
) : (
)}

{t('admin.logoHint')}

{/* App name */}
setEditAppName(e.target.value)} className="input-field pl-9 text-sm" placeholder="Redlight" maxLength={30} />
{hasLogo && (

{t('admin.hideAppNameLabel')}

{t('admin.hideAppNameHint')}

)}
{/* Default theme */}

{t('admin.defaultThemeDesc')}

{/* Legal links */}

{t('admin.legalLinksDesc')}

{/* Imprint */}
setEditImprintUrl(e.target.value)} className="input-field text-sm flex-1" placeholder="https://example.com/imprint" maxLength={500} />
{/* Privacy Policy */}
setEditPrivacyUrl(e.target.value)} className="input-field text-sm flex-1" placeholder="https://example.com/privacy" maxLength={500} />
{/* Registration Mode */}

{t('admin.regModeTitle')}

{t('admin.regModeDescription')}

{/* User Invites */}

{t('admin.inviteTitle')}

{t('admin.inviteDescription')}

{/* Send invite form */}
setInviteEmail(e.target.value)} className="input-field pl-9 text-sm" placeholder={t('auth.emailPlaceholder')} required />
{/* Invite list */} {invites.length > 0 && (
{invites.map(inv => { const isExpired = new Date(inv.expires_at) < new Date(); const isUsed = !!inv.used_at; return (
{isUsed ? : isExpired ? : }

{inv.email}

{isUsed ? `${t('admin.inviteUsedBy')} ${inv.used_by_name}` : isExpired ? t('admin.inviteExpired') : `${t('admin.inviteExpiresAt')} ${new Date(inv.expires_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}` }

{!isUsed && !isExpired && ( )}
); })}
)} {invites.length === 0 && (

{t('admin.noInvites')}

)}
{/* OAuth / SSO Configuration */}

{t('admin.oauthTitle')}

{t('admin.oauthDescription')}

{oauthLoading ? (
) : (
setOauthForm(f => ({ ...f, issuer: e.target.value }))} className="input-field text-sm" placeholder="https://auth.example.com/realms/main" required />

{t('admin.oauthIssuerHint')}

setOauthForm(f => ({ ...f, clientId: e.target.value }))} className="input-field text-sm" placeholder="redlight" required />
setOauthForm(f => ({ ...f, clientSecret: e.target.value }))} className="input-field text-sm" placeholder={oauthConfig?.hasClientSecret ? '••••••••' : ''} /> {oauthConfig?.hasClientSecret && (

{t('admin.oauthClientSecretHint')}

)}
setOauthForm(f => ({ ...f, displayName: e.target.value }))} className="input-field text-sm" placeholder="Company SSO" maxLength={50} />

{t('admin.oauthDisplayNameHint')}

{/* Room Management */}

{t('admin.roomsTitle')}

{t('admin.roomsDescription')}

{adminRoomsLoading ? (
) : ( <> {/* Room search */}
setRoomSearch(e.target.value)} className="input-field pl-9 text-sm" placeholder={t('admin.searchRooms')} />
{adminRooms .filter(r => r.name.toLowerCase().includes(roomSearch.toLowerCase()) || r.owner_name.toLowerCase().includes(roomSearch.toLowerCase()) || r.uid.toLowerCase().includes(roomSearch.toLowerCase()) ) .map(r => ( ))}
{t('admin.roomName')} {t('admin.roomOwner')} {t('admin.roomShares')} {t('admin.roomCreated')} {t('admin.actions')}

{r.name}

{r.uid}

{r.owner_name}

{r.owner_email}

{r.share_count} {new Date(r.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
{adminRooms.length === 0 && (

{t('admin.noRoomsFound')}

)} )}
{/* Search */}
setSearch(e.target.value)} className="input-field pl-11" placeholder={t('admin.searchUsers')} />
{/* Users table */}
{filteredUsers.map(u => ( ))}
{t('admin.user')} {t('admin.role')} {t('admin.rooms')} {t('admin.registered')} {t('admin.actions')}
{u.avatar_image ? ( ) : ( (u.display_name || u.name)[0]?.toUpperCase() )}

{u.display_name || u.name}

@{u.name} · {u.email}

{u.role === 'admin' ? : } {u.role === 'admin' ? t('admin.admin') : t('admin.user')} {u.room_count} {new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
{filteredUsers.length === 0 && (

{t('admin.noUsersFound')}

)}
{/* Context menu portal */} {openMenu && menuPos && openMenu !== user.id && (() => { const u = users.find(u => u.id === openMenu); if (!u) return null; return ( <>
{ setOpenMenu(null); setMenuPos(null); }} />
); })()} {/* Reset password modal */} {resetPwModal && (
setResetPwModal(null)} />

{t('admin.resetPasswordTitle')}

setNewPassword(e.target.value)} className="input-field" placeholder={t('auth.minPassword')} required minLength={6} />
)} {/* Create user modal */} {showCreateUser && (
setShowCreateUser(false)} />

{t('admin.createUserTitle')}

setNewUser({ ...newUser, name: e.target.value })} className="input-field pl-11" placeholder={t('auth.usernamePlaceholder')} required />

{t('auth.usernameHint')}

setNewUser({ ...newUser, display_name: e.target.value })} className="input-field pl-11" placeholder={t('auth.displayNamePlaceholder')} />
setNewUser({ ...newUser, email: e.target.value })} className="input-field pl-11" placeholder={t('auth.emailPlaceholder')} required />
setNewUser({ ...newUser, password: e.target.value })} className="input-field pl-11" placeholder={t('auth.minPassword')} required minLength={6} />
)}
); }