Every app.get/post/delete handler used to live directly in index.js,
which turned the file into the routing layer, the state store, and
the wiring root all at once. This peels the handlers out into five
per-concern modules and leaves index.js as a small composition root.
routes/info.js
- GET /status
- GET /CollabCentral/:app/info
routes/auth.js
- GET /CollabCentral/:app/authUrl
- GET /CollabCentral/:app/oauth (owns SESSION_COOKIE_OPTIONS now)
routes/user.js
- GET /CollabCentral/:app/user/:scope/:action (groups list / find)
- POST /CollabCentral/:app/user/groups/{add,remove}
routes/admin.js
- GET/POST /CollabCentral/:app/admin/users
- DELETE /CollabCentral/:app/admin/users/:id
- Owns requireAdmin + adminUserRow + adminUsersList (private to
the module now that no other caller needs them).
routes/jobs.js
- POST /CollabCentral/:app/jobs/:action (edit / runNow / schedule /
cancel)
- GET /CollabCentral/:app/jobs/list/:scope
- GET /CollabCentral/:app/jobs/detail/:jobId
- Owns buildJob (moved from index.js) since nothing outside the
jobs routes ever called it.
Wiring pattern: each module exports registerXxxRoutes(app, deps)
and receives its dependencies through a dep-bag (isAuthorized,
isAdmin, webex, translator, jobsPipeline, saveConfig, helpers,
authorized, jobs, upload, sharp, uuid, fs, logger, ...). No route
module reaches into module-level state — that stays owned by
index.js.
index.js: 969 -> 457 lines (72% smaller than the original 1,642).
Now contains only imports, state loading (config, jobs, authorized,
tokens, botProfiles, groupsCache), the lib factory instantiations,
Express startup + cron schedules, the four register* calls, and a
handful of small helpers (saveConfig, cleanCompletedJobs,
isAuthorized, isAdmin, logger).
Tests still 53/53 green. Smoke-tested every route (auth matrix +
unknown bot + jobs detail 404 + signed-out 401 vs non-admin 403)
against a live process; every case matches pre-R3 behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
5.2 KiB
JavaScript
124 lines
5.2 KiB
JavaScript
// 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) });
|
|
});
|
|
}
|