// Admin endpoints for managing a bot's authorized-user list: // GET /CollabCentral/:app/admin/users — list // POST /CollabCentral/:app/admin/users — add (by email) // DELETE /CollabCentral/:app/admin/users/:id — remove // // Admin authority lives in authorized.admins (personIds). Every admin // route rejects with 403 for non-admin callers — 403 rather than 401 so // the client can tell an admin-only route apart from a signed-out state // (which would 401). Admin actions are cross-bot in concept but the // routes are still :app-scoped because the resource being edited is // per-bot (authorized users on THAT bot) and it keeps the OAuth session // model unchanged (admin uses the same session cookie as any other page). // // registerAdminRoutes(app, { isAdmin, authorized, webex, saveConfig, // logger }) // requireAdmin returns null on success or an Express-response-sending // function on failure, so each handler stays a straight-line function. function makeRequireAdmin(isAdmin) { return function requireAdmin(req, res) { if (!req.cookies || !req.cookies.id) return res.status(401).send('Unauthorized.'); if (!isAdmin(req.cookies.id)) return res.status(403).send('Admin only.'); return null; }; } // Shape of a single row returned by the admin users endpoints. Kept in // one helper so add / delete / list all produce identical output — the // client can treat every response as an authoritative "here's the // current state". function adminUserRow(entry) { var groups = Array.isArray(entry && entry.groups) ? entry.groups : []; return { id: entry.id, displayName: entry.displayName || '', email: entry.email || '', avatar: entry.avatar || null, groupCount: groups.length }; } export function registerAdminRoutes(app, deps) { var isAdmin = deps.isAdmin; var authorized = deps.authorized; var webex = deps.webex; var saveConfig = deps.saveConfig; var logger = deps.logger || function () {}; var requireAdmin = makeRequireAdmin(isAdmin); function adminUsersList(appName) { var bucket = (authorized.bot && authorized.bot[appName]) || {}; return Object.keys(bucket).map(function (id) { return adminUserRow(bucket[id]); }); } app.get('/CollabCentral/:app/admin/users', function (req, res) { if (requireAdmin(req, res)) return; logger('apiEndpoint(' + req.params.app + ')', 'GET /admin/users'); res.status(200).send(adminUsersList(req.params.app)); }); app.post('/CollabCentral/:app/admin/users', async function (req, res) { if (requireAdmin(req, res)) return; var appName = req.params.app; var email = req.body && req.body.email && String(req.body.email).trim(); if (!email) return res.status(400).send('Missing email.'); var person; try { person = await webex.findPersonByEmail(email); } catch (err) { logger('apiEndpoint(' + appName + ')', 'admin/users lookup failed for "' + email + '": ' + (err && err.message || err)); return res.status(502).send('Failed to reach Webex directory.'); } if (!person) return res.status(404).send('No Webex user found for that email.'); if (!authorized.bot[appName]) authorized.bot[appName] = {}; var bucket = authorized.bot[appName]; if (!bucket[person.id]) { bucket[person.id] = { id: person.id, displayName: person.displayName, email: person.email, avatar: person.avatar, groups: [] }; try { saveConfig(authorized, './config/authorized.json'); } catch (err) { logger('apiEndpoint(' + appName + ')', 'admin/users save failed: ' + (err && err.message || err)); return res.status(500).send('Failed to save.'); } logger('apiEndpoint(' + appName + ')', 'admin/users + "' + person.displayName + '" <' + person.email + '>'); } res.status(200).send({ user: adminUserRow(bucket[person.id]), users: adminUsersList(appName) }); }); app.delete('/CollabCentral/:app/admin/users/:id', function (req, res) { if (requireAdmin(req, res)) return; var appName = req.params.app; var targetId = req.params.id; var bucket = (authorized.bot && authorized.bot[appName]) || {}; if (!bucket[targetId]) { return res.status(200).send({ removed: false, users: adminUsersList(appName) }); } var name = bucket[targetId].displayName || targetId; delete bucket[targetId]; try { saveConfig(authorized, './config/authorized.json'); } catch (err) { logger('apiEndpoint(' + appName + ')', 'admin/users delete save failed: ' + (err && err.message || err)); return res.status(500).send('Failed to save.'); } logger('apiEndpoint(' + appName + ')', 'admin/users - "' + name + '"'); res.status(200).send({ removed: true, users: adminUsersList(appName) }); }); }