diff --git a/src/db.js b/src/db.js index bb2ab70..19eb837 100644 --- a/src/db.js +++ b/src/db.js @@ -125,7 +125,8 @@ export async function initDb() { ('hk-planner', 'HK Planner', 'Housekeeping workload and hours planning', '/hk-planner', 'CalendarClock', '#2d6a4f', 'Housekeeping', '10.10.10.118', 3080), ('twin-optimiser', 'Twin Optimiser', 'Identify twin room opportunities from booking grid', '/twin-optimiser', 'LayoutGrid', '#c9841a', 'Housekeeping', '10.10.10.119', 3080), ('forecasting', 'Forecasting', 'Revenue forecasting and reporting', '/forecast', 'TrendingUp', '#0077b6', 'Finance', '10.10.10.113', 3080), - ('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00', 'Finance', '10.10.10.115', 3080) + ('rates', 'Rate Scraper', 'Competitor rate monitoring', '/rates', 'Tag', '#7b4f00', 'Finance', '10.10.10.115', 3080), + ('maintenance', 'Maintenance', 'Maintenance log book — faults, recurring tasks, assets and contractors', '/maintenance', 'Wrench', '#b45309', 'Operations', '10.10.10.121', 3080) ON CONFLICT (slug) DO UPDATE SET icon = EXCLUDED.icon, category = EXCLUDED.category, diff --git a/src/routes/auth.js b/src/routes/auth.js index f8bff25..b42874b 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -227,4 +227,57 @@ export async function authRoutes(app) { caps, } }) + + // GET /api/auth/users?app=slug + // Assignable-user list for apps (e.g. maintenance task allocation). Any user + // with access to the requested app may call it; returns the active users who + // can access that app (minimal fields only — this is not the admin list). + app.get('/users', async (request, reply) => { + const token = request.cookies?.hnf_session + if (!token) return reply.status(401).send({ error: 'Not authenticated' }) + + let payload + try { payload = await verifyToken(token) } + catch { return reply.status(401).send({ error: 'Invalid session' }) } + + const { app: appSlug } = request.query + if (!appSlug) return reply.status(400).send({ error: 'app query parameter required' }) + + // Requester must themselves have access to the app + const { rows: [requester] } = await pool.query( + `SELECT u.id FROM users u + WHERE u.id = $1 AND u.active = true + AND ( + u.is_admin = true + OR EXISTS (SELECT 1 FROM user_app_perms p + JOIN apps a ON a.id = p.app_id + WHERE p.user_id = u.id AND a.slug = $2 AND a.active = true) + OR EXISTS (SELECT 1 FROM user_roles ur + JOIN role_app_perms rap ON rap.role_id = ur.role_id + JOIN apps a ON a.id = rap.app_id + WHERE ur.user_id = u.id AND a.slug = $2 AND a.active = true) + )`, + [payload.user_id, appSlug] + ) + if (!requester) return reply.status(403).send({ error: 'Access denied' }) + + const { rows: users } = await pool.query( + `SELECT DISTINCT u.id, u.email, u.name + FROM users u + WHERE u.active = true + AND ( + u.is_admin = true + OR EXISTS (SELECT 1 FROM user_app_perms p + JOIN apps a ON a.id = p.app_id + WHERE p.user_id = u.id AND a.slug = $1 AND a.active = true) + OR EXISTS (SELECT 1 FROM user_roles ur + JOIN role_app_perms rap ON rap.role_id = ur.role_id + JOIN apps a ON a.id = rap.app_id + WHERE ur.user_id = u.id AND a.slug = $1 AND a.active = true) + ) + ORDER BY u.name`, + [appSlug] + ) + return users + }) }