// Admin only: users, studies, sign-up rules and invites, server health and snapshots. import { adminDeleteProject, adminDeleteUser, adminGetAllProjects, adminGetAllUsers, adminGetProject, createInvite, dataDir, deleteInvite, destroyAllSessionsForUser, getSetting, getUserById, listInvites, setSetting, setUserPassword } from '../db.js'; import { generateTemporaryPassword, hashPassword, requireAdmin, requireAuth } from '../auth.js'; import { SIGNUP_MODES, generateInviteCode, isSignupMode, normalizeInviteCode } from '../signup.js'; import { restoreSnapshot, runBackup, snapshotPath } from '../backups.js'; import { backupStatus, diskUse, logEvent, recentEvents, uptimeSeconds } from '../health.js'; export function registerAdminRoutes(app) { // --------------------------------------------------------------------------- // Admin — restricted to the single designated admin account (see ADMIN_EMAIL // in server/auth.js). Full visibility/control over every user and project. // --------------------------------------------------------------------------- app.get('/api/admin/users', requireAuth, requireAdmin, (req, res) => { try { res.json(adminGetAllUsers()); } catch (err) { console.error('GET /api/admin/users error:', err); res.status(500).json({ error: 'Failed to list users.' }); } }); app.delete('/api/admin/users/:id', requireAuth, requireAdmin, (req, res) => { try { if (req.params.id === req.session.userId) { return res.status(400).json({ error: "Can't delete your own admin account." }); } adminDeleteUser(req.params.id); destroyAllSessionsForUser(req.params.id); res.json({ ok: true }); } catch (err) { console.error('DELETE /api/admin/users/:id error:', err); res.status(500).json({ error: 'Failed to delete user.' }); } }); // POST /api/admin/users/:id/reset-password — sets a random temporary password and // signs the user out everywhere, since there's no self-service "forgot password" flow yet. app.post('/api/admin/users/:id/reset-password', requireAuth, requireAdmin, async (req, res) => { try { const temporaryPassword = generateTemporaryPassword(); const passwordHash = await hashPassword(temporaryPassword); setUserPassword(req.params.id, passwordHash); destroyAllSessionsForUser(req.params.id); res.json({ temporaryPassword }); } catch (err) { console.error('POST /api/admin/users/:id/reset-password error:', err); res.status(500).json({ error: 'Failed to reset password.' }); } }); // Who may sign up, and the invite codes for it // Server health: disk use, snapshots, and a short log of failures (in memory; resets on restart). app.get('/api/admin/health', requireAuth, requireAdmin, (_req, res) => { try { const emailOf = (id) => (id ? getUserById(id)?.email ?? null : null); const withUser = (e) => ({ ...e, email: emailOf(e.userId) }); res.json({ uptimeSeconds: uptimeSeconds(), node: process.version, dataDir: dataDir(), disk: diskUse(), backups: backupStatus(), iqConfigured: !!process.env.IQBIBLE_API_KEY, signinFailures: recentEvents('signin', 30).map(withUser), notifyFailures: recentEvents('notify', 30).map(withUser), backupFailures: recentEvents('backup', 10), }); } catch (err) { console.error('GET /api/admin/health error:', err); res.status(500).json({ error: 'Failed to read server health.' }); } }); app.post('/api/admin/backups', requireAuth, requireAdmin, async (_req, res) => { try { await runBackup(); res.json(backupStatus()); } catch (err) { console.error('POST /api/admin/backups error:', err); logEvent('backup', err.message); res.status(500).json({ error: 'The snapshot could not be made.' }); } }); app.get('/api/admin/backups/:name', requireAuth, requireAdmin, (req, res) => { const file = snapshotPath(req.params.name); if (!file) return res.status(404).json({ error: 'No such snapshot.' }); res.download(file, req.params.name); }); // Restoring replaces every account's data, so the snapshot's name has to be typed back as confirmation. app.post('/api/admin/backups/:name/restore', requireAuth, requireAdmin, async (req, res) => { if (!snapshotPath(req.params.name)) return res.status(404).json({ error: 'No such snapshot.' }); if (req.body?.confirm !== req.params.name) return res.status(400).json({ error: 'Type the snapshot’s name to confirm.' }); try { const result = await restoreSnapshot(req.params.name); res.json({ restored: result.restored, safetyCopy: result.safetyCopy.split('/').pop() }); } catch (err) { console.error('POST /api/admin/backups/:name/restore error:', err); res.status(500).json({ error: 'The snapshot could not be restored.' }); } }); app.get('/api/admin/signup', requireAuth, requireAdmin, (_req, res) => { res.json({ mode: getSetting('signup', 'open'), invites: listInvites() }); }); app.put('/api/admin/signup', requireAuth, requireAdmin, (req, res) => { if (!isSignupMode(req.body?.mode)) return res.status(400).json({ error: `Mode must be one of: ${SIGNUP_MODES.join(', ')}.` }); setSetting('signup', req.body.mode); res.json({ mode: req.body.mode }); }); app.post('/api/admin/invites', requireAuth, requireAdmin, (req, res) => { const days = Math.min(90, Math.max(1, Number(req.body?.days) || 7)); const code = generateInviteCode(); createInvite(code, String(req.body?.note ?? '').trim().slice(0, 80), Date.now() + days * 86_400_000); res.json({ code, expiresInDays: days }); }); app.delete('/api/admin/invites/:code', requireAuth, requireAdmin, (req, res) => { deleteInvite(normalizeInviteCode(req.params.code)); res.json({ ok: true }); }); app.get('/api/admin/projects', requireAuth, requireAdmin, (req, res) => { try { res.json(adminGetAllProjects()); } catch (err) { console.error('GET /api/admin/projects error:', err); res.status(500).json({ error: 'Failed to list projects.' }); } }); app.get('/api/admin/projects/:id', requireAuth, requireAdmin, (req, res) => { try { const project = adminGetProject(req.params.id); if (!project) return res.status(404).json({ error: 'Project not found.' }); res.json(project); } catch (err) { console.error('GET /api/admin/projects/:id error:', err); res.status(500).json({ error: 'Failed to load project.' }); } }); app.delete('/api/admin/projects/:id', requireAuth, requireAdmin, (req, res) => { try { adminDeleteProject(req.params.id); res.json({ ok: true }); } catch (err) { console.error('DELETE /api/admin/projects/:id error:', err); res.status(500).json({ error: 'Failed to delete project.' }); } }); }