feat(auth): admin role management with logbook entries
This commit is contained in:
12
src/App.tsx
12
src/App.tsx
@ -361,6 +361,17 @@ export default function App() {
|
||||
} catch (err: any) { throw err; }
|
||||
};
|
||||
|
||||
const handleSetUserRole = async (id: string, role: string) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${id}/role`, { method: 'PATCH', body: JSON.stringify({ role }) });
|
||||
if (res.ok) {
|
||||
const updated: User = await res.json();
|
||||
setUsers(prev => prev.map(u => u.id === id ? updated : u));
|
||||
if (updated.id === currentUser?.id) setCurrentUser(updated);
|
||||
} else { const d = await res.json(); throw new Error(d.error); }
|
||||
} catch (err: any) { throw err; }
|
||||
};
|
||||
|
||||
// Quick-link handlers (shared link dashboard)
|
||||
const handleAddLink = async (newLink: Omit<QuickLink, 'id' | 'createdAt' | 'createdBy'>) => {
|
||||
try {
|
||||
@ -620,6 +631,7 @@ export default function App() {
|
||||
bookings={bookings}
|
||||
onDeleteUser={handleDeleteUser}
|
||||
onUpdateUser={handleUpdateUser}
|
||||
onSetRole={handleSetUserRole}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'logs' && (
|
||||
|
||||
@ -102,7 +102,7 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
|
||||
placeholder="user@airit.rocks"
|
||||
placeholder=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -119,7 +119,7 @@ export default function LoginPage({ onLogin, onNavigateToRegister, authError }:
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2.5 pr-10 text-sm text-white placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
|
||||
placeholder="••••••••"
|
||||
placeholder=""
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { User, Booking } from '../types';
|
||||
import { Users, Search, Mail, Calendar, Activity, Trash2, Pencil, X, Save, AlertCircle } from 'lucide-react';
|
||||
import { Users, Search, Mail, Calendar, Activity, Trash2, Pencil, X, Save, AlertCircle, ShieldCheck, Shield } from 'lucide-react';
|
||||
|
||||
interface UserDirectoryProps {
|
||||
users: User[];
|
||||
@ -8,6 +8,7 @@ interface UserDirectoryProps {
|
||||
bookings: Booking[];
|
||||
onDeleteUser: (id: string) => Promise<void>;
|
||||
onUpdateUser: (id: string, name: string, email: string) => Promise<void>;
|
||||
onSetRole: (id: string, role: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
@ -117,10 +118,13 @@ function EditModal({ user, onClose, onSave }: EditModalProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserDirectory({ users, currentUser, bookings, onDeleteUser, onUpdateUser }: UserDirectoryProps) {
|
||||
export default function UserDirectory({ users, currentUser, bookings, onDeleteUser, onUpdateUser, onSetRole }: UserDirectoryProps) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [togglingRoleId, setTogglingRoleId] = useState<string | null>(null);
|
||||
const [roleError, setRoleError] = useState<string | null>(null);
|
||||
const isCurrentUserAdmin = currentUser.role.toLowerCase() === 'admin';
|
||||
|
||||
const bookingCount = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
@ -152,6 +156,18 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
|
||||
try { await onDeleteUser(id); } finally { setDeletingId(null); }
|
||||
}
|
||||
|
||||
async function handleToggleRole(user: User) {
|
||||
setTogglingRoleId(user.id);
|
||||
setRoleError(null);
|
||||
try {
|
||||
await onSetRole(user.id, user.role.toLowerCase() === 'admin' ? 'User' : 'admin');
|
||||
} catch (err: any) {
|
||||
setRoleError(err.message || 'Failed to change role.');
|
||||
} finally {
|
||||
setTogglingRoleId(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveEdit(name: string, email: string) {
|
||||
if (!editingUser) return;
|
||||
await onUpdateUser(editingUser.id, name, email);
|
||||
@ -190,6 +206,14 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{roleError && (
|
||||
<div className="flex items-center gap-2 bg-red-950/50 border border-red-900/50 rounded-lg px-3 py-2 text-xs text-red-300">
|
||||
<AlertCircle className="w-3.5 h-3.5 shrink-0" />
|
||||
{roleError}
|
||||
<button onClick={() => setRoleError(null)} className="ml-auto text-red-400 hover:text-white"><X className="w-3 h-3" /></button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-2.5 text-slate-500"><Search className="w-4 h-4" /></span>
|
||||
@ -240,7 +264,10 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-slate-800 flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] font-mono text-slate-500 uppercase tracking-wider">Operator</span>
|
||||
{user.role.toLowerCase() === 'admin'
|
||||
? <span className="flex items-center gap-1 text-[10px] font-mono text-amber-400 uppercase tracking-wider"><ShieldCheck className="w-3 h-3" />Admin</span>
|
||||
: <span className="text-[10px] font-mono text-slate-500 uppercase tracking-wider">User</span>
|
||||
}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-3 text-[10px] font-mono text-slate-400">
|
||||
@ -256,6 +283,20 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-1 ml-1">
|
||||
{isCurrentUserAdmin && !isMe && (
|
||||
<button
|
||||
onClick={() => handleToggleRole(user)}
|
||||
disabled={togglingRoleId === user.id}
|
||||
className={`p-1.5 rounded-lg transition-all disabled:opacity-40 ${user.role.toLowerCase() === 'admin' ? 'text-amber-400 hover:text-slate-400 hover:bg-slate-900' : 'text-slate-500 hover:text-amber-400 hover:bg-slate-900'}`}
|
||||
title={user.role.toLowerCase() === 'admin' ? 'Remove admin' : 'Make admin'}
|
||||
>
|
||||
{togglingRoleId === user.id
|
||||
? <span className="w-3.5 h-3.5 border-2 border-slate-600 border-t-amber-400 rounded-full animate-spin inline-block" />
|
||||
: user.role.toLowerCase() === 'admin'
|
||||
? <ShieldCheck className="w-3.5 h-3.5" />
|
||||
: <Shield className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setEditingUser(user)}
|
||||
className="p-1.5 rounded-lg text-slate-500 hover:text-cyan-400 hover:bg-slate-900 transition-all"
|
||||
|
||||
Reference in New Issue
Block a user