Every add/remove of a favorite group or authorized user was rewriting
config.json — the same file that carries structural bot metadata and
was committed to git. This split ends the git-noise and lets ops
deploy fresh installs without a pre-populated user list.
Split
- config.json (committed) stays structural: server, per-bot labels,
integration + service-account ids, languages.
- config/authorized.json (gitignored) is the new mutable source of
truth: { admins: [personId...], bot: { <appName>: { <personId>:
{ id, displayName, email, avatar, groups: [...] } } } }.
- Seeded authorized.json with the current admins list and all
authorized users (3 on novi, 4 on techupdates) so this commit is
a pure move — no data lost, no downtime.
Helpers (lib/helpers.js)
- New getAuthorizedEntry(authorized, app, id) as the single lookup
point every consumer goes through, so nullability is uniform.
- isAuthorized() gains an authorized-doc arg (pure signature stays
testable): fails closed when the doc is missing / partially
loaded, so a broken deploy grants no access.
- isAdmin() now reads authorized.admins instead of config.admins.
Runtime (index.js)
- loadAuthorized() with an ENOENT fallback to { admins: [], bot: {} }
so a fresh deploy can bootstrap via the admin page instead of
requiring a hand-crafted authorized.json.
- All 8 previous config.webex.bot[app].authorized sites (favorites
read/add/remove, admin list/add/delete, isAuthorized) now go
through the authorized doc.
- Every mutation writes to config/authorized.json instead of
config/config.json.
Latent-bug fixup (uncovered while smoke-testing this refactor)
- The /user/:scope/:action fallthroughs used res.status(4xx)
without .send(...), so unknown scopes / unauthorized callers got
a hung request instead of a response. Added ".send(...)" bodies
so the response actually completes.
Docs + tests
- README updated: new "Authorized users" step in "Adding a new bot",
updated file-layout section, docker mount list adds
authorized.json.
- Test suite expanded from 48 → 53 with a new getAuthorizedEntry
group and the existing isAuthorized/isAdmin cases reshaped for
the new signatures.
Smoke tested the auth matrix end-to-end (admin + non-admin + signed-
out across /info, /admin/users, /user/groups/list): every path
returns the expected code and body.
Co-authored-by: Cursor <cursoragent@cursor.com>
6.8 KiB
CollabCentral
A multi-bot mass-messaging service for Webex. Each bot has its own token, avatar, label, and per-user access list, and users can only see the bots and jobs they're authorized on.
Under one server you can host any number of bots (Novi, TechUpdates, …) — each one exposes the same UI at:
/CollabCentral/<botName>/sendMessage.html— compose and send./CollabCentral/<botName>/monitorJobs.html— track running / scheduled / completed jobs./CollabCentral/<botName>/jobDetail.html?jobId=…— detailed per-job view.
Requirements
- Node.js 20+.
- A single Webex integration used for OAuth on all bots.
- A Webex service account (used server-side for group/people lookups; its
refresh token is stored in
config/token.jsonand rotated automatically). - One Webex bot per app you want to host.
Quick start (local)
-
Copy
.env.exampleto.envand fill in the values:SERVER_PORT=1450 CRON_TIMEZONE=America/New_York OAUTH_CALLBACK_URL_TEMPLATE=https://bot.example.com/CollabCentral/:app/oauth WEBEX_INTEGRATION_CLIENT_ID=... WEBEX_INTEGRATION_CLIENT_SECRET=... WEBEX_SERVICE_ACCOUNT_CLIENT_ID=... WEBEX_SERVICE_ACCOUNT_CLIENT_SECRET=... GOOGLE_TRANSLATE_API_KEY=... -
Copy
config/botTokens.example.jsontoconfig/botTokens.jsonand put in the bot tokens for each app:{ "novi": { "token": "…", "enabled": true }, "techupdates": { "token": "…", "enabled": true } } -
Create
config/token.jsonwith the initial service-account OAuth tokens (access + refresh). After the first refresh, the cron job rewrites this file automatically. -
Install dependencies and start:
npm install npm start
npm start uses node --env-file=.env, so no external process manager is
required to load env vars during local development.
Adding a new bot
Say you want to add a bot called alerts.
- Webex bot — create the bot at developer.webex.com. Copy its access token.
- Bot token — add an entry to
config/botTokens.json:
Setting"alerts": { "token": "<bot access token>", "enabled": true }enabled: falsewill keep the bot listed but return 404 for all/CollabCentral/alerts/*routes. - Bot metadata — edit
config/config.jsonand add a structural entry underwebex.bot:"alerts": { "label": "Ops Alerts" } - Authorized users — add per-user data to
config/authorized.json(gitignored; created automatically at first admin action if it doesn't exist):
Only person IDs listed under{ "admins": ["<webexPersonId>"], "bot": { "alerts": { "<webexPersonId>": { "id": "<webexPersonId>", "displayName": "…", "email": "…", "avatar": "…", "groups": [ { "name": "…", "alias": "…", "id": "<webexGroupId>" } ] } } } }bot.<appName>can sign into that bot's UI. Anyone listed underadmins(any personId) can manage authorized users across every bot via the in-app Admin page — much easier than hand- editing this file, but the file is the source of truth. - Icons — drop
alerts.pngandalerts.icointohtml/(they're served by the per-bot static mount). - OAuth redirect URI — in the Webex integration used for OAuth, register
https://<your-host>/CollabCentral/alerts/oauthas an allowed redirect URI. Without this step, users get an OAuth error on login. - Restart the server (or the container). The bot's avatar is fetched once
at startup via Webex
/people/meusing the token you supplied in step 2.
Docker
Dockerfile builds a stateless image. Config, tokens, uploads, and jobs
data are expected to be mounted at runtime.
docker build -t collabcentral .
docker run -d \
--name collabcentral \
--env-file /path/to/.env \
-v /path/to/config:/usr/src/app/config \
-v /path/to/uploads:/usr/src/app/uploads \
-p 1450:1450 \
collabcentral
The config/ mount should contain at minimum config.json, botTokens.json,
token.json, languages.json, authorized.json, and (if you want to
preserve state) jobs.json and userPrefs.json.
File layout
Committed to the repo:
index.js— Express server, OAuth, cron jobs, Webex API integration, and the send queue.html/— frontend (sendMessage / monitorJobs / jobDetail pages, per-bot icons, shared JS libraries).config/config.json— structural: server settings, per-bot labels, integration ids. No per-user data lives here anymore.config/languages.json— supported translation languages.config/botTokens.example.json— template forbotTokens.json..env.example— template for.env.Dockerfile,.dockerignore,.gitignore,package.json,package-lock.json.
Runtime state (gitignored, not committed):
.env— secrets and deployment-specific URLs.config/botTokens.json— per-bot access tokens.config/token.json— service-account OAuth tokens (rotated by the refresh cron).config/authorized.json— admins list + per-bot authorized users (with their favorite groups). Edited at runtime by the admin page and the favorites picker on the compose page.config/jobs.json— building / running / scheduled / completed jobs.config/userPrefs.json— per-user preferences (language, etc.).uploads/— CSVs of recipient IDs and any attached images.
Testing
Unit tests for the pure helpers extracted into lib/helpers.js run under
Node's built-in test runner — no additional dev dependencies required.
npm test
The suite covers bot enable/disable gating (getBotToken, isBotEnabled,
getBotConfig), per-bot draft isolation (buildingKey, jobsForApp), the
authorization check (isAuthorized), OAuth URL construction (buildAuthUrl,
getOAuthRedirectUri), the retention filter (cleanCompletedJobs), and the
duration formatter (msToTime). Adding a new helper? Add it to lib/helpers.js
and cover it in test/helpers.test.js — keeping the state-carrying wrappers
in index.js thin means each helper can be tested without booting the server.
Behavior notes
- Completed-job retention: the daily cleanup cron (default
01:10inCRON_TIMEZONE) drops entries fromjobs.completedolder than 30 days. ChangeCOMPLETED_RETENTION_DAYSinindex.jsto adjust. - Send concurrency: a shared
p-queuelimits outbound Webex sends to 10 in-flight requests. - Rate limiting:
fetchWithRateLimittransparently retries on Webex 429 responses honoringRetry-After. - Per-bot job isolation: draft jobs are keyed by
cookieId + appName, and job list / detail endpoints filter byappName, so authorized users of one bot never see another bot's jobs.