'use strict'; const express = require('express'); const bcrypt = require('bcryptjs'); const { getAllUsers, getUserById, createUser, updateUser } = require('./db'); const { requireRole } = require('./middleware'); const { getUsage: getSyncroUsage, getHistory: getSyncroHistory, getLimitHits: getSyncroLimitHits } = require('../syncroStats'); const router = express.Router(); const VALID_ROLES = ['superduperadmin', 'admin', 'tech', 'client']; const ELEVATED_ROLES = ['superduperadmin', 'admin']; // GET /admin/users router.get('/users', (req, res) => { res.json({ users: getAllUsers() }); }); // POST /admin/users — create user router.post('/users', async (req, res) => { const username = String(req.body?.username ?? '').trim().toLowerCase(); const name = String(req.body?.name ?? '').trim(); const company = String(req.body?.company ?? '').trim(); const syncroCustomerId = req.body?.syncro_customer_id != null ? parseInt(req.body.syncro_customer_id, 10) : null; const role = String(req.body?.role ?? ''); const password = String(req.body?.password ?? ''); if (!username || !name || !role || !password) { return res.status(400).json({ error: 'All fields are required.' }); } if (!VALID_ROLES.includes(role)) { return res.status(400).json({ error: 'Invalid role.' }); } if (ELEVATED_ROLES.includes(role) && req.session.user.role !== 'superduperadmin') { return res.status(403).json({ error: 'Only superduperadmin can assign admin-level roles.' }); } if (password.length < 12) { return res.status(400).json({ error: 'Password must be at least 12 characters.' }); } const hash = await bcrypt.hash(password, 12); try { createUser(username, hash, name, role, company, syncroCustomerId); res.json({ ok: true }); } catch (err) { if (err.message?.includes('UNIQUE constraint')) { return res.status(409).json({ error: 'Username already exists.' }); } throw err; } }); // PATCH /admin/users/:id — update user fields router.patch('/users/:id', async (req, res) => { const id = parseInt(req.params.id, 10); if (isNaN(id)) return res.status(400).json({ error: 'Invalid user ID.' }); const current = getUserById(id); if (!current) return res.status(404).json({ error: 'User not found.' }); const isSelf = id === req.session.user.id; // Nobody can change their own active status or role via the API if (isSelf && 'active' in req.body) { return res.status(403).json({ error: 'Cannot change your own active status.' }); } const fields = {}; const { name, company, syncro_customer_id, role, active, password } = req.body ?? {}; if (name !== undefined) { fields.name = String(name).trim(); if (!fields.name) return res.status(400).json({ error: 'Name cannot be empty.' }); } if (company !== undefined) { fields.company = String(company).trim(); } if (syncro_customer_id !== undefined) { fields.syncro_customer_id = syncro_customer_id != null ? parseInt(syncro_customer_id, 10) : null; } if (role !== undefined && !isSelf) { if (!VALID_ROLES.includes(role)) return res.status(400).json({ error: 'Invalid role.' }); if (ELEVATED_ROLES.includes(role) && req.session.user.role !== 'superduperadmin') { return res.status(403).json({ error: 'Only superduperadmin can assign admin-level roles.' }); } fields.role = role; } if (active !== undefined) { fields.active = active ? 1 : 0; } if (password !== undefined && password !== '') { if (String(password).length < 12) { return res.status(400).json({ error: 'Password must be at least 12 characters.' }); } fields.password_hash = await bcrypt.hash(String(password), 12); } updateUser(id, fields); res.json({ ok: true }); }); // GET /admin/syncro-usage — rolling 60s Syncro API request count + 7-day limit-hit count router.get('/syncro-usage', (req, res) => { const limit = 180; res.json({ requests: getSyncroUsage(), limit, windowMs: 60_000, limitHits7d: getSyncroLimitHits(limit) }); }); // GET /admin/syncro-history?window= — per-minute buckets for the given window (max 7 days) router.get('/syncro-history', (req, res) => { const raw = parseInt(req.query.window ?? '3600000', 10); const windowMs = isNaN(raw) ? 3_600_000 : Math.min(Math.max(raw, 60_000), 7 * 24 * 60 * 60 * 1000); res.json({ buckets: getSyncroHistory(windowMs), windowMs }); }); // POST /admin/restart — exit cleanly; PM2 will restart the process router.post('/restart', requireRole('superduperadmin'), (req, res) => { res.json({ ok: true }); setTimeout(() => process.exit(0), 150); }); module.exports = router;