Skip to content

Latest commit

 

History

History
88 lines (65 loc) · 5.05 KB

File metadata and controls

88 lines (65 loc) · 5.05 KB

Architecture

Domains

JupiterAPI has three domains:

  1. Read API — proxies ESN Jupiter (upstream SaaS) with normalization, caching, and section-level statistics.
  2. Write proxy (Hercules) — JWT-gated board operations persisted to Firestore (events, meetings, offices, trainings, fidelity, attendance, board approvals, audit logs).
  3. Auth — Cognito-backed login/refresh for the board frontend plus JWKS-based token verification.

Dependency direction

                   ┌────────────┐
                   │ index.js   │  bootstrap: cors, rate limit, api key, mounts
                   └─────┬──────┘
                         │ requires
        ┌────────────────┼──────────────────┐
        ▼                ▼                  ▼
  src/routes/*      src/middleware/*    src/config.js
  (HTTP only)       (cross-cutting)     (sole env reader)
        │                │                  ▲
        ▼                │                  │ all modules may read config
  src/jupiter.js ◄───────┘ (only jupiter calls upstream)
        │
        ▼
  src/normalizers.js ──► src/formatting.js ──► src/nationality.js
        │                    │
        ├────────────────────┴──► src/fieldsOfStudy.js
        └───────────────────────► src/eventLocation.js

  src/routes/hercules.js ──► firebase-admin (Firestore)   [only Firestore writer]
  src/routes/auth.js ──► amazon-cognito-identity-js + middleware/jwtAuth (JWKS)

Rules encoded by the arrows:

  • routes/* never call upstream or Firestore directly except hercules.js (Firestore) and jupiter.js (upstream).
  • Domain modules (normalizers, formatting, nationality, fieldsOfStudy, eventLocation) depend only on each other and Node built-ins — never express, never jupiter.js.
  • config.js depends on nothing but dotenv; everything else reads config from it.

Main flows

Read request

client → apiKeyAuth → rateLimiter → router (src/routes/<domain>.js)
       → jupiter.js (Cognito token cached 55 min, response cached 5 min,
         interceptors absorb "null" strings)
       → normalizers.js (users/events/subscriptions → stable JSON shape)
       → res.json

Comprehensive stats

GET /stats/comprehensive keeps its own 15-minute cache and fans out to event subscriptions in batches of 5 (getEventParticipantUserIds) to compute inactive-member lists. All other stats endpoints are computed per request over cached user lists.

Hercules write

client → jwtAuth (Cognito ID token via JWKS) → requireBoardMember
       (config.boardMemberIds, immutable sub IDs only)
       → validation.js (whitelists, enums, ranges)
       → Firestore write

Fidelity entries run in a transaction that also updates progress/{userId} counters (fidelityPointsTotal, fidelityEntriesCount, fidelityEventsCount); deletes reverse them. Progress updates go through a field whitelist (mass-assignment protection).

Auth

  • POST /auth/login|refresh use the Cognito SDK directly.
  • POST /auth/verify and middleware/jwtAuth verify ID tokens against Cognito JWKS; public keys are cached for 1 hour with force-refresh retry on unknown kid (key rotation).

Boundaries

Upstream quirks are absorbed at the edge so routers see clean data:

  • Jupiter sometimes returns the string "null" or literal nulljupiter.js interceptors convert to [].
  • A user without a subscription makes Jupiter return 400 (not 404) → getUserSubscription maps both to null.
  • City inference (domicile → profile → field-of-study campus map; venue/address/postal-code/fidelity-points parsing for events) lives in eventLocation.js / fieldsOfStudy.js; normalizers.js attaches citySource / cityInferred / cityUnknown so clients can show provenance.

Decisions

  • In-memory caches (jupiter 5 min, comprehensive stats 15 min, JWKS 1 h): assumes a single instance; horizontal scaling would need a shared cache.
  • Academic year runs Sept–Aug and rolls over automatically on September 1 (getCurrentAcademicYear() / isCurrentAcademicYear() in src/formatting.js). No manual update is needed when the season changes: on Sep 1 the API starts reporting the new year (e.g. 2026-2027) and /events begins filtering to the new window. Test fixtures derive their timestamps from the live academic year, so the suite stays green across rollovers.
  • Board authorization by immutable sub: usernames change; subs do not. BOARD_MEMBER_IDS is parsed once in config.js.
  • require.cache test mocking instead of DI: tests inject fakes for jupiter.js, firebase-admin, the Cognito SDK, and JWKS verification via test/helpers/mockModule.js — no mocking framework.
  • Centralized fail-fast config: missing core env vars exit(1) at first require of config.js, before any route can serve traffic.