feat(mam-api): add users admin CRUD routes
This commit is contained in:
parent
f93feb6e40
commit
5ed604136c
1 changed files with 117 additions and 0 deletions
117
services/mam-api/src/routes/users.js
Normal file
117
services/mam-api/src/routes/users.js
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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;
|
||||
Loading…
Reference in a new issue