diff --git a/server.ts b/server.ts index ed74b44..8f625a6 100644 --- a/server.ts +++ b/server.ts @@ -405,6 +405,29 @@ async function startServer() { db.prepare('UPDATE users SET name = COALESCE(?, name), email = COALESCE(?, email) WHERE id = ?') .run(name ?? null, email ?? null, id); const updated = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User; + const changes: string[] = []; + if (name && name !== existing.name) changes.push(`name "${existing.name}" → "${name}"`); + if (email && email !== existing.email) changes.push(`email "${existing.email}" → "${email}"`); + if (changes.length > 0) { + addLog('system', `User ${updated.email} updated: ${changes.join(', ')}.`, { userId: req.user!.userId }); + } + res.json(updated); + } catch (err: any) { + res.status(500).json({ error: err.message }); + } + }); + + app.patch('/api/users/:id/role', requireAuth, requireAdmin, (req, res) => { + try { + const id = req.params.id; + const { role } = req.body as { role: string }; + const safeRole = role?.toLowerCase() === 'admin' ? 'admin' : 'User'; + const existing = db.prepare('SELECT id, name, email, role FROM users WHERE id = ?').get(id) as User | undefined; + if (!existing) return res.status(404).json({ error: 'User not found.' }); + if (existing.role === safeRole) return res.json(existing); + db.prepare('UPDATE users SET role = ? WHERE id = ?').run(safeRole, id); + const updated = db.prepare('SELECT id, name, role, email FROM users WHERE id = ?').get(id) as User; + addLog('system', `User ${updated.email} role changed to ${safeRole}.`, { userId: req.user!.userId }); res.json(updated); } catch (err: any) { res.status(500).json({ error: err.message }); diff --git a/src/App.tsx b/src/App.tsx index b788d7d..69e9803 100644 --- a/src/App.tsx +++ b/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) => { try { @@ -620,6 +631,7 @@ export default function App() { bookings={bookings} onDeleteUser={handleDeleteUser} onUpdateUser={handleUpdateUser} + onSetRole={handleSetUserRole} /> )} {activeTab === 'logs' && ( diff --git a/src/components/LoginPage.tsx b/src/components/LoginPage.tsx index 9d50d8a..31be28e 100644 --- a/src/components/LoginPage.tsx +++ b/src/components/LoginPage.tsx @@ -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="" /> @@ -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="" /> + + )} + {/* Search */}
@@ -240,7 +264,10 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs
- Operator + {user.role.toLowerCase() === 'admin' + ? Admin + : User + }
@@ -256,6 +283,20 @@ export default function UserDirectory({ users, currentUser, bookings, onDeleteUs {/* Action buttons */}
+ {isCurrentUserAdmin && !isMe && ( + + )}