// User CRUD. Mounted at /api/v1/auth/users by index.js (behind the auth gate). // Flat access: any logged-in user can manage other users (spec). import express from 'express'; import pool from '../db/pool.js'; import { hashPassword } from '../auth/passwords.js'; import { DEV_USER_ID } from '../middleware/auth.js'; const router = express.Router(); const MIN_PASSWORD_LEN = 12; function bad(res, msg) { return res.status(400).json({ error: msg }); } // GET / — list users (real ones; dev seed hidden) router.get('/', async (_req, res, next) => { try { const { rows } = await pool.query( `SELECT id, username, display_name, role, last_login_at, created_at FROM users WHERE id <> $1 ORDER BY username`, [DEV_USER_ID]); res.json(rows); } catch (err) { next(err); } }); // POST / — create user router.post('/', async (req, res, next) => { try { const { username, password, display_name, role } = req.body || {}; if (!username || typeof username !== 'string') return bad(res, 'username required'); if (!password || password.length < MIN_PASSWORD_LEN) return bad(res, 'password must be at least ' + MIN_PASSWORD_LEN + ' chars'); const hash = await hashPassword(password); const { rows } = await pool.query( `INSERT INTO users (username, password_hash, display_name, role) VALUES ($1, $2, $3, $4) RETURNING id, username, display_name, role, created_at`, [username.trim(), hash, display_name || username.trim(), role || 'admin'] ); res.status(201).json(rows[0]); } catch (err) { if (err.code === '23505') return res.status(409).json({ error: 'username already exists' }); next(err); } }); // POST /:id/password — admin reset another user's password router.post('/:id/password', async (req, res, next) => { try { const { new_password } = req.body || {}; if (!new_password || new_password.length < MIN_PASSWORD_LEN) { return bad(res, 'new password must be at least ' + MIN_PASSWORD_LEN + ' chars'); } const hash = await hashPassword(new_password); const { rowCount } = await pool.query( `UPDATE users SET password_hash = $1, password_updated_at = NOW() WHERE id = $2 AND id <> $3`, [hash, req.params.id, DEV_USER_ID]); if (rowCount === 0) return res.status(404).json({ error: 'user not found' }); res.status(204).end(); } catch (err) { next(err); } }); // DELETE /:id — delete a user, except the last real user router.delete('/:id', async (req, res, next) => { try { if (req.params.id === DEV_USER_ID) return res.status(400).json({ error: 'cannot delete dev user' }); const { rows } = await pool.query( `SELECT COUNT(*)::int AS n FROM users WHERE id <> $1`, [DEV_USER_ID]); if (rows[0].n <= 1) return res.status(409).json({ error: 'cannot delete last user' }); const { rowCount } = await pool.query(`DELETE FROM users WHERE id = $1`, [req.params.id]); if (rowCount === 0) return res.status(404).json({ error: 'user not found' }); res.status(204).end(); } catch (err) { next(err); } }); export default router;