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 (
{t('admin.userCount', { count: users.length })}
{t('admin.brandingDescription')}
{t('admin.logoHint')}
{t('admin.hideAppNameLabel')}
{t('admin.hideAppNameHint')}
{t('admin.defaultThemeDesc')}
{t('admin.legalLinksDesc')}
{t('admin.regModeDescription')}
{t('admin.inviteDescription')}
{/* Send invite form */} {/* Invite list */} {invites.length > 0 && ({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')}` }
{t('admin.noInvites')}
)}{t('admin.oauthDescription')}
{oauthLoading ? ({t('admin.roomsDescription')}
{adminRoomsLoading ? (| {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')} |
|
{t('admin.noRoomsFound')}
| {t('admin.user')} | {t('admin.role')} | {t('admin.rooms')} | {t('admin.registered')} | {t('admin.actions')} |
|---|---|---|---|---|
|
{u.avatar_image ? (
{u.display_name || u.name} @{u.name} · {u.email} |
{u.role === 'admin' ? |
{u.room_count} | {new Date(u.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')} |
|
{t('admin.noUsersFound')}