/** * 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;