From d23ca9be73c062e6e37ae63749970cf843da3a82 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Mon, 18 May 2026 12:50:49 -0400 Subject: [PATCH] feat(mam-api): add groups admin CRUD routes with member management --- services/mam-api/src/routes/groups.js | 114 ++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 services/mam-api/src/routes/groups.js diff --git a/services/mam-api/src/routes/groups.js b/services/mam-api/src/routes/groups.js new file mode 100644 index 0000000..cf475d3 --- /dev/null +++ b/services/mam-api/src/routes/groups.js @@ -0,0 +1,114 @@ +/** + * Group management routes (admin-only when AUTH_ENABLED=true) + * + * GET /api/v1/groups — list all groups + * POST /api/v1/groups — create group + * PATCH /api/v1/groups/:id — update group + * DELETE /api/v1/groups/:id — delete group + * GET /api/v1/groups/:id/members — list members + * POST /api/v1/groups/:id/members — add member { user_id } + * DELETE /api/v1/groups/:id/members/:uid — remove member + */ +import express from 'express'; +import pool from '../db/pool.js'; +import { requireAuth, requireAdmin } from '../middleware/auth.js'; + +const router = express.Router(); +router.use(requireAuth, requireAdmin); + +// ── List ────────────────────────────────────────────────────── +router.get('/', async (_req, res, next) => { + try { + const { rows } = await pool.query( + `SELECT g.id, g.name, g.description, g.created_at, + COUNT(ug.user_id)::int AS member_count + FROM groups g + LEFT JOIN user_groups ug ON ug.group_id = g.id + GROUP BY g.id + ORDER BY g.name` + ); + res.json(rows); + } catch (err) { next(err); } +}); + +// ── Create ──────────────────────────────────────────────────── +router.post('/', async (req, res, next) => { + try { + const { name, description } = req.body; + if (!name) return res.status(400).json({ error: 'name required' }); + const { rows } = await pool.query( + `INSERT INTO groups (name, description) VALUES ($1, $2) RETURNING *`, + [name.trim(), description || null] + ); + res.status(201).json(rows[0]); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Group name already exists' }); + next(err); + } +}); + +// ── Update ──────────────────────────────────────────────────── +router.patch('/:id', async (req, res, next) => { + try { + const { name, description } = req.body; + const sets = []; const vals = []; + if (name !== undefined) { sets.push(`name = $${sets.length + 1}`); vals.push(name); } + if (description !== undefined) { sets.push(`description = $${sets.length + 1}`); vals.push(description); } + if (!sets.length) return res.status(400).json({ error: 'Nothing to update' }); + vals.push(req.params.id); + const { rows } = await pool.query( + `UPDATE groups SET ${sets.join(', ')} WHERE id = $${vals.length} RETURNING *`, + vals + ); + if (!rows.length) return res.status(404).json({ error: 'Group 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 groups WHERE id = $1', [req.params.id]); + if (!rowCount) return res.status(404).json({ error: 'Group not found' }); + res.json({ message: 'Group deleted' }); + } catch (err) { next(err); } +}); + +// ── Members ─────────────────────────────────────────────────── +router.get('/:id/members', async (req, res, next) => { + try { + const { rows } = await pool.query( + `SELECT u.id, u.username, u.display_name, u.role + FROM user_groups ug + JOIN users u ON u.id = ug.user_id + WHERE ug.group_id = $1 + ORDER BY u.username`, + [req.params.id] + ); + res.json(rows); + } catch (err) { next(err); } +}); + +router.post('/:id/members', async (req, res, next) => { + try { + const { user_id } = req.body; + if (!user_id) return res.status(400).json({ error: 'user_id required' }); + await pool.query( + `INSERT INTO user_groups (user_id, group_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, + [user_id, req.params.id] + ); + res.status(201).json({ message: 'Member added' }); + } catch (err) { next(err); } +}); + +router.delete('/:id/members/:uid', async (req, res, next) => { + try { + await pool.query( + `DELETE FROM user_groups WHERE group_id = $1 AND user_id = $2`, + [req.params.id, req.params.uid] + ); + res.json({ message: 'Member removed' }); + } catch (err) { next(err); } +}); + +export default router;