Auth: assignable-users endpoint + maintenance app seed row

GET /api/auth/users?app=<slug> — non-admin user list for task allocation
(requester must have access to the app; returns active users with access,
minimal fields). Seed row for the maintenance app (LXC 121).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-03 21:29:08 +00:00
parent 608ed4ce19
commit a03b065c43
2 changed files with 55 additions and 1 deletions

View file

@ -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), ('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), ('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), ('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 ON CONFLICT (slug) DO UPDATE SET
icon = EXCLUDED.icon, icon = EXCLUDED.icon,
category = EXCLUDED.category, category = EXCLUDED.category,

View file

@ -227,4 +227,57 @@ export async function authRoutes(app) {
caps, 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
})
} }