collabcentral/README.md
Joseph B. McQueen 44568044e8 Phase R1: split per-user data into gitignored authorized.json
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>
2026-07-01 21:06:44 -04:00

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.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:

    {
        "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:
    "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:
    "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):
    {
        "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.
  5. Icons — drop alerts.png and alerts.ico into html/ (they're served by the per-bot static mount).
  6. 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.
  7. 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.