117 lines
4.7 KiB
JavaScript
117 lines
4.7 KiB
JavaScript
/**
|
|
* User management routes (admin-only when AUTH_ENABLED=true)
|
|
*
|
|
* GET /api/v1/users — list all users
|
|
* POST /api/v1/users — create user
|
|
* GET /api/v1/users/:id — get user
|
|
* PATCH /api/v1/users/:id — update user (display_name, role, password)
|
|
* DELETE /api/v1/users/:id — delete user
|
|
*/
|
|
import express from 'express';
|
|
import bcrypt from 'bcrypt';
|
|
import pool from '../db/pool.js';
|
|
import { requireAuth, requireAdmin } from '../middleware/auth.js';
|
|
|
|
const router = express.Router();
|
|
router.use(requireAuth, requireAdmin);
|
|
|
|
const VALID_ROLES = ['admin', 'editor', 'viewer'];
|
|
|
|
// ── List ──────────────────────────────────────────────────────
|
|
router.get('/', async (_req, res, next) => {
|
|
try {
|
|
const { rows } = await pool.query(
|
|
`SELECT u.id, u.username, u.display_name, u.role, u.created_at,
|
|
COUNT(ug.group_id)::int AS group_count
|
|
FROM users u
|
|
LEFT JOIN user_groups ug ON ug.user_id = u.id
|
|
GROUP BY u.id
|
|
ORDER BY u.created_at`
|
|
);
|
|
res.json(rows);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// ── Create ────────────────────────────────────────────────────
|
|
router.post('/', async (req, res, next) => {
|
|
try {
|
|
const { username, password, display_name, role = 'editor' } = req.body;
|
|
if (!username || !password)
|
|
return res.status(400).json({ error: 'username and password required' });
|
|
if (password.length < 8)
|
|
return res.status(400).json({ error: 'Password must be ≥ 8 characters' });
|
|
if (!VALID_ROLES.includes(role))
|
|
return res.status(400).json({ error: `role must be one of: ${VALID_ROLES.join(', ')}` });
|
|
|
|
const hash = await bcrypt.hash(password, 12);
|
|
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().toLowerCase(), hash, display_name || username, role]
|
|
);
|
|
res.status(201).json(rows[0]);
|
|
} catch (err) {
|
|
if (err.code === '23505') return res.status(409).json({ error: 'Username already exists' });
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
// ── Get ───────────────────────────────────────────────────────
|
|
router.get('/:id', async (req, res, next) => {
|
|
try {
|
|
const { rows } = await pool.query(
|
|
`SELECT id, username, display_name, role, created_at FROM users WHERE id = $1`,
|
|
[req.params.id]
|
|
);
|
|
if (!rows.length) return res.status(404).json({ error: 'User not found' });
|
|
res.json(rows[0]);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// ── Update ────────────────────────────────────────────────────
|
|
router.patch('/:id', async (req, res, next) => {
|
|
try {
|
|
const { display_name, role, password } = req.body;
|
|
const sets = []; const vals = [];
|
|
|
|
if (display_name !== undefined) {
|
|
sets.push(`display_name = $${sets.length + 1}`);
|
|
vals.push(display_name);
|
|
}
|
|
if (role !== undefined) {
|
|
if (!VALID_ROLES.includes(role))
|
|
return res.status(400).json({ error: `role must be one of: ${VALID_ROLES.join(', ')}` });
|
|
sets.push(`role = $${sets.length + 1}`);
|
|
vals.push(role);
|
|
}
|
|
if (password) {
|
|
if (password.length < 8)
|
|
return res.status(400).json({ error: 'Password must be ≥ 8 characters' });
|
|
const hashed = await bcrypt.hash(password, 12);
|
|
sets.push(`password_hash = $${sets.length + 1}`);
|
|
vals.push(hashed);
|
|
}
|
|
if (!sets.length) return res.status(400).json({ error: 'Nothing to update' });
|
|
|
|
vals.push(req.params.id);
|
|
const { rows } = await pool.query(
|
|
`UPDATE users SET ${sets.join(', ')} WHERE id = $${vals.length}
|
|
RETURNING id, username, display_name, role, created_at`,
|
|
vals
|
|
);
|
|
if (!rows.length) return res.status(404).json({ error: 'User not found' });
|
|
res.json(rows[0]);
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
// ── Delete ────────────────────────────────────────────────────
|
|
router.delete('/:id', async (req, res, next) => {
|
|
try {
|
|
const { rowCount } = await pool.query('DELETE FROM users WHERE id = $1', [req.params.id]);
|
|
if (!rowCount) return res.status(404).json({ error: 'User not found' });
|
|
res.json({ message: 'User deleted' });
|
|
} catch (err) { next(err); }
|
|
});
|
|
|
|
export default router;
|