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>
182 lines
6.8 KiB
Markdown
182 lines
6.8 KiB
Markdown
# 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.json` and rotated automatically).
|
|
- One Webex bot per app you want to host.
|
|
|
|
## Quick start (local)
|
|
|
|
1. Copy `.env.example` to `.env` and 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=...
|
|
```
|
|
|
|
2. Copy `config/botTokens.example.json` to `config/botTokens.json` and put
|
|
in the bot tokens for each app:
|
|
```json
|
|
{
|
|
"novi": { "token": "…", "enabled": true },
|
|
"techupdates": { "token": "…", "enabled": true }
|
|
}
|
|
```
|
|
|
|
3. Create `config/token.json` with the initial service-account OAuth tokens
|
|
(access + refresh). After the first refresh, the cron job rewrites this
|
|
file automatically.
|
|
|
|
4. 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`.
|
|
|
|
1. **Webex bot** — create the bot at developer.webex.com. Copy its access token.
|
|
2. **Bot token** — add an entry to `config/botTokens.json`:
|
|
```json
|
|
"alerts": { "token": "<bot access token>", "enabled": true }
|
|
```
|
|
Setting `enabled: false` will keep the bot listed but return 404 for all
|
|
`/CollabCentral/alerts/*` routes.
|
|
3. **Bot metadata** — edit `config/config.json` and add a structural entry
|
|
under `webex.bot`:
|
|
```json
|
|
"alerts": { "label": "Ops Alerts" }
|
|
```
|
|
4. **Authorized users** — add per-user data to `config/authorized.json`
|
|
(gitignored; created automatically at first admin action if it doesn't
|
|
exist):
|
|
```json
|
|
{
|
|
"admins": ["<webexPersonId>"],
|
|
"bot": {
|
|
"alerts": {
|
|
"<webexPersonId>": {
|
|
"id": "<webexPersonId>",
|
|
"displayName": "…",
|
|
"email": "…",
|
|
"avatar": "…",
|
|
"groups": [ { "name": "…", "alias": "…", "id": "<webexGroupId>" } ]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
Only person IDs listed under `bot.<appName>` can sign into that bot's UI.
|
|
Anyone listed under `admins` (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.
|
|
4. **Icons** — drop `alerts.png` and `alerts.ico` into `html/` (they're
|
|
served by the per-bot static mount).
|
|
5. **OAuth redirect URI** — in the Webex integration used for OAuth, register
|
|
`https://<your-host>/CollabCentral/alerts/oauth` as an allowed redirect
|
|
URI. Without this step, users get an OAuth error on login.
|
|
6. Restart the server (or the container). The bot's avatar is fetched once
|
|
at startup via Webex `/people/me` using 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 for `botTokens.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:10` in
|
|
`CRON_TIMEZONE`) drops entries from `jobs.completed` older than 30 days.
|
|
Change `COMPLETED_RETENTION_DAYS` in `index.js` to adjust.
|
|
- **Send concurrency**: a shared `p-queue` limits outbound Webex sends to
|
|
10 in-flight requests.
|
|
- **Rate limiting**: `fetchWithRateLimit` transparently retries on Webex 429
|
|
responses honoring `Retry-After`.
|
|
- **Per-bot job isolation**: draft jobs are keyed by `cookieId + appName`,
|
|
and job list / detail endpoints filter by `appName`, so authorized users
|
|
of one bot never see another bot's jobs.
|