feat: add room management functionality for admins with listing and deletion options
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m12s
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m12s
This commit is contained in:
@@ -362,4 +362,28 @@ router.delete('/oauth', authenticateToken, requireAdmin, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Room Management (admin only) ────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /api/admin/rooms - List all rooms with owner info
|
||||||
|
router.get('/rooms', authenticateToken, requireAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const db = getDb();
|
||||||
|
const rooms = await db.all(`
|
||||||
|
SELECT r.id, r.uid, r.name, r.user_id, r.max_participants, r.access_code,
|
||||||
|
r.mute_on_join, r.record_meeting, r.guest_access, r.presentation_file,
|
||||||
|
r.created_at, r.updated_at,
|
||||||
|
COALESCE(NULLIF(u.display_name,''), u.name) as owner_name,
|
||||||
|
u.email as owner_email,
|
||||||
|
(SELECT COUNT(*) FROM room_shares rs WHERE rs.room_id = r.id) as share_count
|
||||||
|
FROM rooms r
|
||||||
|
JOIN users u ON r.user_id = u.id
|
||||||
|
ORDER BY r.created_at DESC
|
||||||
|
`);
|
||||||
|
res.json({ rooms });
|
||||||
|
} catch (err) {
|
||||||
|
log.admin.error(`List rooms error: ${err.message}`);
|
||||||
|
res.status(500).json({ error: 'Rooms could not be loaded' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -484,7 +484,20 @@
|
|||||||
"oauthRemoveConfirm": "OAuth-Konfiguration wirklich entfernen? Benutzer können sich dann nicht mehr per SSO anmelden.",
|
"oauthRemoveConfirm": "OAuth-Konfiguration wirklich entfernen? Benutzer können sich dann nicht mehr per SSO anmelden.",
|
||||||
"oauthNotConfigured": "OAuth ist noch nicht konfiguriert.",
|
"oauthNotConfigured": "OAuth ist noch nicht konfiguriert.",
|
||||||
"oauthSave": "OAuth speichern",
|
"oauthSave": "OAuth speichern",
|
||||||
"oauthRemove": "OAuth entfernen"
|
"oauthRemove": "OAuth entfernen",
|
||||||
|
"roomsTitle": "Raumverwaltung",
|
||||||
|
"roomsDescription": "Alle Räume der Instanz einsehen, verwalten und bei Bedarf löschen.",
|
||||||
|
"searchRooms": "Räume suchen...",
|
||||||
|
"roomName": "Name",
|
||||||
|
"roomOwner": "Besitzer",
|
||||||
|
"roomShares": "Geteilt",
|
||||||
|
"roomCreated": "Erstellt",
|
||||||
|
"roomView": "Raum öffnen",
|
||||||
|
"deleteRoom": "Raum löschen",
|
||||||
|
"deleteRoomConfirm": "Raum \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||||
|
"roomDeleted": "Raum gelöscht",
|
||||||
|
"roomDeleteFailed": "Raum konnte nicht gelöscht werden",
|
||||||
|
"noRoomsFound": "Keine Räume vorhanden"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"bell": "Benachrichtigungen",
|
"bell": "Benachrichtigungen",
|
||||||
|
|||||||
@@ -484,7 +484,20 @@
|
|||||||
"oauthRemoveConfirm": "Really remove OAuth configuration? Users will no longer be able to sign in with SSO.",
|
"oauthRemoveConfirm": "Really remove OAuth configuration? Users will no longer be able to sign in with SSO.",
|
||||||
"oauthNotConfigured": "OAuth is not configured yet.",
|
"oauthNotConfigured": "OAuth is not configured yet.",
|
||||||
"oauthSave": "Save OAuth",
|
"oauthSave": "Save OAuth",
|
||||||
"oauthRemove": "Remove OAuth"
|
"oauthRemove": "Remove OAuth",
|
||||||
|
"roomsTitle": "Room Management",
|
||||||
|
"roomsDescription": "View, manage, and delete all rooms on this instance.",
|
||||||
|
"searchRooms": "Search rooms...",
|
||||||
|
"roomName": "Name",
|
||||||
|
"roomOwner": "Owner",
|
||||||
|
"roomShares": "Shared",
|
||||||
|
"roomCreated": "Created",
|
||||||
|
"roomView": "View room",
|
||||||
|
"deleteRoom": "Delete room",
|
||||||
|
"deleteRoomConfirm": "Really delete room \"{name}\"? This cannot be undone.",
|
||||||
|
"roomDeleted": "Room deleted",
|
||||||
|
"roomDeleteFailed": "Room could not be deleted",
|
||||||
|
"noRoomsFound": "No rooms found"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"bell": "Notifications",
|
"bell": "Notifications",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
Users, Shield, Search, Trash2, ChevronDown, Loader2,
|
Users, Shield, Search, Trash2, ChevronDown, Loader2,
|
||||||
MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User,
|
MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User,
|
||||||
Upload, X as XIcon, Image, Type, Palette, Send, Copy, Clock, Check,
|
Upload, X as XIcon, Image, Type, Palette, Send, Copy, Clock, Check,
|
||||||
ShieldCheck, Globe, Link as LinkIcon, LogIn,
|
ShieldCheck, Globe, Link as LinkIcon, LogIn, DoorOpen, Eye, ExternalLink,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
@@ -55,6 +55,11 @@ export default function Admin() {
|
|||||||
const [oauthForm, setOauthForm] = useState({ issuer: '', clientId: '', clientSecret: '', displayName: 'SSO', autoRegister: true });
|
const [oauthForm, setOauthForm] = useState({ issuer: '', clientId: '', clientSecret: '', displayName: 'SSO', autoRegister: true });
|
||||||
const [savingOauth, setSavingOauth] = useState(false);
|
const [savingOauth, setSavingOauth] = useState(false);
|
||||||
|
|
||||||
|
// Rooms state
|
||||||
|
const [adminRooms, setAdminRooms] = useState([]);
|
||||||
|
const [adminRoomsLoading, setAdminRoomsLoading] = useState(true);
|
||||||
|
const [roomSearch, setRoomSearch] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.role !== 'admin') {
|
if (user?.role !== 'admin') {
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
@@ -63,6 +68,7 @@ export default function Admin() {
|
|||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchInvites();
|
fetchInvites();
|
||||||
fetchOauthConfig();
|
fetchOauthConfig();
|
||||||
|
fetchAdminRooms();
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -101,6 +107,29 @@ export default function Admin() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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) => {
|
const handleRoleChange = async (userId, newRole) => {
|
||||||
try {
|
try {
|
||||||
await api.put(`/admin/users/${userId}/role`, { role: newRole });
|
await api.put(`/admin/users/${userId}/role`, { role: newRole });
|
||||||
@@ -790,6 +819,114 @@ export default function Admin() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Room Management */}
|
||||||
|
<div className="card p-6 mb-8">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<DoorOpen size={20} className="text-th-accent" />
|
||||||
|
<h2 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-th-text-s mb-5">{t('admin.roomsDescription')}</p>
|
||||||
|
|
||||||
|
{adminRoomsLoading ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Loader2 size={20} className="animate-spin text-th-accent" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Room search */}
|
||||||
|
<div className="relative mb-4">
|
||||||
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={roomSearch}
|
||||||
|
onChange={e => setRoomSearch(e.target.value)}
|
||||||
|
className="input-field pl-9 text-sm"
|
||||||
|
placeholder={t('admin.searchRooms')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-th-border">
|
||||||
|
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5">
|
||||||
|
{t('admin.roomName')}
|
||||||
|
</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden sm:table-cell">
|
||||||
|
{t('admin.roomOwner')}
|
||||||
|
</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden md:table-cell">
|
||||||
|
{t('admin.roomShares')}
|
||||||
|
</th>
|
||||||
|
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden lg:table-cell">
|
||||||
|
{t('admin.roomCreated')}
|
||||||
|
</th>
|
||||||
|
<th className="text-right text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5">
|
||||||
|
{t('admin.actions')}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{adminRooms
|
||||||
|
.filter(r =>
|
||||||
|
r.name.toLowerCase().includes(roomSearch.toLowerCase()) ||
|
||||||
|
r.owner_name.toLowerCase().includes(roomSearch.toLowerCase()) ||
|
||||||
|
r.uid.toLowerCase().includes(roomSearch.toLowerCase())
|
||||||
|
)
|
||||||
|
.map(r => (
|
||||||
|
<tr key={r.id} className="border-b border-th-border last:border-0 hover:bg-th-hover transition-colors">
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-th-text">{r.name}</p>
|
||||||
|
<p className="text-xs text-th-text-s font-mono">{r.uid}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 hidden sm:table-cell">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-th-text">{r.owner_name}</p>
|
||||||
|
<p className="text-xs text-th-text-s">{r.owner_email}</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-th-text hidden md:table-cell">
|
||||||
|
{r.share_count}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-sm text-th-text-s hidden lg:table-cell">
|
||||||
|
{new Date(r.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center justify-end gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/rooms/${r.uid}`)}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||||
|
title={t('admin.roomView')}
|
||||||
|
>
|
||||||
|
<Eye size={15} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleAdminDeleteRoom(r.uid, r.name)}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-th-hover text-th-error transition-colors"
|
||||||
|
title={t('admin.deleteRoom')}
|
||||||
|
>
|
||||||
|
<Trash2 size={15} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{adminRooms.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<DoorOpen size={36} className="mx-auto text-th-text-s/40 mb-2" />
|
||||||
|
<p className="text-th-text-s text-sm">{t('admin.noRoomsFound')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="card p-4 mb-6">
|
<div className="card p-4 mb-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
Reference in New Issue
Block a user