Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30a2142085 | ||
|
|
95913a5bd5 | ||
|
|
f7e0ec8bc1 | ||
|
|
08d442e1c1 | ||
|
|
be4f341e42 |
@@ -7,6 +7,14 @@ and this project follows [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.42.1] - 2026-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
- **The production Hub's public key is built in** (`backend/src/hub/publicKey.js`), so sites can
|
||||
check licences without `HUB_PUBLIC_KEY` in their `.env`. `HUB_PUBLIC_KEY` still overrides it (for
|
||||
a local Hub while developing).
|
||||
|
||||
## [1.42.0] - 2026-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "event-management-backend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.10.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"description": "Event Management System Backend",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* it, so a licence can't be faked by editing the database. The matching private key exists
|
||||
* only on the Hub server (events-hub repo, `npm run generate-keys`).
|
||||
*
|
||||
* Set PRODUCTION_PUBLIC_KEY to the key the production Hub prints, before the release that ships
|
||||
* the Hub connection. HUB_PUBLIC_KEY (env) overrides it — for a local Hub while developing.
|
||||
* PRODUCTION_PUBLIC_KEY is the key the production Hub prints (also served at /api/public-key); if
|
||||
* the Hub key is regenerated, update it here. HUB_PUBLIC_KEY (env) overrides it — for a local Hub.
|
||||
*/
|
||||
|
||||
const PRODUCTION_PUBLIC_KEY = null;
|
||||
const PRODUCTION_PUBLIC_KEY = "MCowBQYDK2VwAyEAcKpEo/JdS96wR3r9yh8uFUxBlzPUd2YKbC60fNRUhoU=";
|
||||
|
||||
/**
|
||||
* A public key as proper PEM, however it was pasted: with real line breaks, with "\n", all on
|
||||
@@ -26,7 +26,7 @@ function normalisePem(input) {
|
||||
}
|
||||
|
||||
function hubPublicKey() {
|
||||
return normalisePem(process.env.HUB_PUBLIC_KEY) || PRODUCTION_PUBLIC_KEY;
|
||||
return normalisePem(process.env.HUB_PUBLIC_KEY) || normalisePem(PRODUCTION_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
module.exports = { hubPublicKey, normalisePem, PRODUCTION_PUBLIC_KEY };
|
||||
|
||||
@@ -124,6 +124,18 @@ describe('pasting the Hub public key', () => {
|
||||
expect(normalisePem('')).toBeNull();
|
||||
expect(normalisePem(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
test('without HUB_PUBLIC_KEY the built-in production key is used', () => {
|
||||
const { hubPublicKey, PRODUCTION_PUBLIC_KEY } = require('../src/hub/publicKey');
|
||||
const saved = process.env.HUB_PUBLIC_KEY;
|
||||
delete process.env.HUB_PUBLIC_KEY;
|
||||
try {
|
||||
expect(PRODUCTION_PUBLIC_KEY).toBeTruthy();
|
||||
expect(crypto.createPublicKey(hubPublicKey()).asymmetricKeyType).toBe('ed25519');
|
||||
} finally {
|
||||
if (saved !== undefined) process.env.HUB_PUBLIC_KEY = saved;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('version comparison for "update available"', () => {
|
||||
|
||||
+31
-16
@@ -1,13 +1,22 @@
|
||||
# Handover — where things stand and how to carry on
|
||||
|
||||
Last updated: 2026-10-27, after **1.41.2** (and Hub 1.0.0). The plan itself is in
|
||||
Last updated: 2026-10-27, after **1.42.1** (and Hub 1.1.0). The plan itself is in
|
||||
[ROADMAP-v2.md](./ROADMAP-v2.md) (decisions table at the end); the previous round of
|
||||
features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md).
|
||||
|
||||
The product is called **Cross Events** (renamed from Hope Events in 1.41.3); the Hub is the
|
||||
**Cross Events Hub**. Technical names keep `hope-events` on purpose — the repo, browser storage
|
||||
keys (`hope_events_*`), the settings encryption salt, applied migrations, PM2/nginx names, the
|
||||
install folder and the database name — because renaming them would sign everyone out, make
|
||||
encrypted settings unreadable or break existing installs.
|
||||
|
||||
## Done so far (road to 2.0)
|
||||
|
||||
| Version | What |
|
||||
|---------|------|
|
||||
| 1.42.1 | Production Hub public key built in (`publicKey.js`); `HUB_PUBLIC_KEY` only needed for a local Hub |
|
||||
| 1.42.0 | **Request update** instead of the deploy command: Site settings → Licence sends a signed update request to the Hub (protocol §3a), which opens an alert and emails Joshua; "Update requested on …" until the site runs that version, and the admin update banner hides meanwhile. Audit action `hub_update_requested`. Needs Hub 1.1.0 |
|
||||
| 1.41.3 | Renamed Hope Events → **Cross Events** in all UI text and docs (technical identifiers unchanged, see above) |
|
||||
| 1.41.2 | Background jobs page: last-run times were 2 h ahead (Prisma's pg adapter drops the offset of timestamptz in raw queries — read pg-boss times `AT TIME ZONE 'UTC'`); `APP_TIMEZONE` documented |
|
||||
| 1.41.1 | Licence fixes — no licence (never connected, or the Hub answers `INSTALL_NOT_FOUND`, which now drops the old licence) is read-only like suspended; read-only also blocks editing events/options/variants; heartbeat 30 s after start and after connecting; `LICENCE_CHECK_DISABLED` for e2e |
|
||||
| 1.41.0 | Cross Events Hub connection — licence step in `/setup` and Site settings → Licence, 15-minute heartbeat (health, totals, income bands), licence states checked offline (grace banner for site admins only, read-only when suspended/revoked), `/health`, "update available", `scripts/install.sh`. The Hub itself is `joshua/events-hub` 1.0.0 |
|
||||
@@ -28,13 +37,12 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md).
|
||||
|
||||
## Next up
|
||||
|
||||
1. **Go live with the Hub:** generate the production signing key on the Hub server
|
||||
(`npm run generate-keys` in `events-hub/backend`), put the **public** key in
|
||||
`backend/src/hub/publicKey.js` and release it as **1.41.1** — until then installs can't
|
||||
connect (they work as "not connected"). Deploy the Hub to `events-hub.crosscode.co.za`, create
|
||||
the admin (`npm run create-admin`), generate Hope Family Church's licence and connect it (install #1).
|
||||
Try `scripts/install.sh` on a test server.
|
||||
2. **1.42.0 — Multi-campus** (§12).
|
||||
1. **Roll out the update requests:** deploy **Hub 1.1.0**, then set "Latest Cross Events release"
|
||||
to `1.42.0` in Hub → Settings, then update the sites to 1.42.0 (the last update done without a
|
||||
request). A 1.42.0 site talking to an older Hub shows "The Hub doesn't take update requests yet".
|
||||
2. **Try `scripts/install.sh` on a test server.** The production Hub key is built in since
|
||||
1.42.1, so a fresh install needs no `HUB_PUBLIC_KEY`; existing installs can drop it from `.env`.
|
||||
3. **1.43.0 — Multi-campus** (§12).
|
||||
|
||||
## Working on this repo
|
||||
|
||||
@@ -158,19 +166,26 @@ features is in [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md).
|
||||
(`prepare-db.js` → `retention`); `privacy-notice.spec.ts` publishes a version that needs accepting, then accepts it for all
|
||||
seeded users in `afterAll` so later spec files don't meet the prompt.
|
||||
|
||||
## Where the Hub client lives (1.41.0)
|
||||
## Where the Hub client lives (1.41.0, update requests 1.42.0)
|
||||
|
||||
- Protocol: `docs/hub-protocol.md` — the same file is in the Hub repo (`joshua/events-hub`,
|
||||
developed at `../events-hub`); change both together.
|
||||
- `backend/src/hub/`: `publicKey.js` (the production Hub key — **must be set before releasing**;
|
||||
`HUB_PUBLIC_KEY` env overrides it for a local Hub), `licence.js` (verify tokens, work out
|
||||
active / grace / suspended / revoked / unlicensed, cached 30 s), `client.js` (connect, heartbeat;
|
||||
AppSetting `hub_*` keys, which the generic settings endpoints never touch), `report.js` (health,
|
||||
- `backend/src/hub/`: `publicKey.js` (the production Hub key, built in since 1.42.1 — if the Hub's
|
||||
signing key is ever regenerated, update it here in a release; `HUB_PUBLIC_KEY` env overrides it
|
||||
for a local Hub), `licence.js` (verify tokens, work out
|
||||
active / grace / suspended / revoked / unlicensed, cached 30 s), `client.js` (connect, heartbeat,
|
||||
update request — the last two share `signedPost`; AppSetting `hub_*` keys, which the generic
|
||||
settings endpoints never touch), `report.js` (health,
|
||||
totals, income bands), `readOnly.js` (the routes blocked in read-only mode — add any new
|
||||
"create" or event-edit route there). Read-only = suspended, revoked or unlicensed.
|
||||
- Job `hub-heartbeat` (every 15 min), routes `/api/hub/*`, `/health`.
|
||||
- Frontend: `components/admin/LicenceTab.tsx` (Site settings → Licence; its `ConnectForm` is also on
|
||||
the last `/setup` screen), `components/shared/LicenceBanner.tsx` (site admins only).
|
||||
- Job `hub-heartbeat` (every 15 min), routes `/api/hub/*` (status, connect, heartbeat,
|
||||
update-request), `/health`.
|
||||
- Frontend: `components/admin/LicenceTab.tsx` (Site settings → Licence, with the **Request update**
|
||||
button; its `ConnectForm` is also on the last `/setup` screen), `components/shared/LicenceBanner.tsx`
|
||||
(site admins only).
|
||||
- **Updates:** sites never update themselves and never show the deploy command. "Request update"
|
||||
tells the Hub (an `update_requested` alert + one email to Joshua); the Hub resolves it when the
|
||||
site's heartbeat reports a newer version. Joshua then runs `scripts/deploy.sh` on the server.
|
||||
- Local testing: run the Hub (`../events-hub`, ports 5200/3200), create a licence there, and start
|
||||
Cross Events with `HUB_URL=http://localhost:5200` and `HUB_PUBLIC_KEY` = the key on Hub → Settings.
|
||||
- `scripts/install.sh` — fresh Ubuntu server to a running site (written and syntax-checked; not yet
|
||||
|
||||
+23
-15
@@ -11,7 +11,7 @@ sold — trustworthy (POPIA), flexible (multi-day events, several payment
|
||||
providers), maintainable (tests, CI, typed backend, versioned API), and with
|
||||
**AI helpers** as the headline feature (§15).
|
||||
|
||||
**Status (2026-10-23):** all features in this document are approved. Shipped
|
||||
**Status (2026-10-27):** all features in this document are approved. Shipped
|
||||
so far: 1.32.0 early-bird ribbon on event cards; 1.33.0 multi-day events
|
||||
phase A (sessions, late joining, per-session check-in, attendance register,
|
||||
reminders, certificates); 1.34.0 job queue (pg-boss), Playwright E2E tests, Gitea CI
|
||||
@@ -22,8 +22,11 @@ picker at the door and session choice on "All sessions" tickets; 1.36.0 payment
|
||||
1.37.1 "Pay online" hidden when it can't be used; 1.37.2 payment status checks for missing
|
||||
notifications. SnapScan and Zapper are dropped — Yoco and PayFast are enough (2026-10-23);
|
||||
1.38.0 POPIA toolkit part 1 (privacy page, unsubscribe, WhatsApp STOP, data requests); 1.39.0 part 2 (privacy notice editor and versions, operator register, consent and access logs); 1.40.0 part 3 (retention policies and job, breach register, children’s-data flags, readiness page).
|
||||
1.41.0 the Hub connection (licence, heartbeat, read-only mode, `/health`, install script) with Hub 1.0.0 (`joshua/events-hub`).
|
||||
**Next: 1.42.0 Multi-campus.**
|
||||
1.41.0 the Hub connection (licence, heartbeat, read-only mode, `/health`, install script) with Hub 1.0.0 (`joshua/events-hub`);
|
||||
1.41.1–1.41.2 licence and job-time fixes; 1.41.3 renamed **Hope Events → Cross Events** (Hub 1.0.1:
|
||||
Cross Events Hub); 1.42.0 **Request update** button instead of the deploy command, with Hub 1.1.0
|
||||
(update requests). Later versions moved up by one (2026-10-27).
|
||||
**Next: 1.43.0 Multi-campus.**
|
||||
|
||||
**Repositories:**
|
||||
|
||||
@@ -601,11 +604,11 @@ whether they're healthy, basic usage stats, and their **licence** status.
|
||||
- Same stack (Next.js + Express + Prisma + Postgres) so it's familiar.
|
||||
- **What lives where:**
|
||||
- Hub repo: dashboard, customers, licences, licence **signing** (private
|
||||
key — only ever on the Hub server), `/api/installs/register` and
|
||||
`/api/installs/heartbeat` endpoints, alerts.
|
||||
key — only ever on the Hub server), `/api/installs/register`,
|
||||
`/api/installs/heartbeat` and `/api/installs/update-request` endpoints, alerts.
|
||||
- Cross Events repo: the Hub **client** — licence step in `/setup`,
|
||||
"Connect to Hub" in Site settings, heartbeat job, licence verification
|
||||
with the Hub's **public** key, read-only mode, banners.
|
||||
with the Hub's **public** key, read-only mode, banners, "Request update".
|
||||
- **The contract between them** (register / heartbeat payloads, licence token
|
||||
format) is versioned with a `protocolVersion` field and documented in both
|
||||
repos (`docs/hub-protocol.md`). The Hub accepts the current and previous
|
||||
@@ -661,7 +664,9 @@ All later calls are signed with the install secret (HMAC) and go over HTTPS.
|
||||
- **Alerts** (email/WhatsApp to Joshua): install offline > 1 h, backup
|
||||
failed or older than 48 h, disk low, errors spiking, licence expiring.
|
||||
- **Releases:** latest version and changelog; installs see "Update
|
||||
available" in their admin.
|
||||
available" in their admin with a **Request update** button (1.42.0 / Hub 1.1.0).
|
||||
The request is signed like a heartbeat, shows as an alert (emailed once) and
|
||||
clears when the install reports a newer version. Joshua does the update.
|
||||
- **Customers:** contact details, plan, licence(s), billing notes.
|
||||
|
||||
### Licences
|
||||
@@ -695,7 +700,8 @@ All later calls are signed with the install secret (HMAC) and go over HTTPS.
|
||||
- The Hub should not offer remote commands (run updates, impersonate,
|
||||
database access) in v1: it's the most valuable target an attacker could
|
||||
hit, so keeping it read-only toward installs keeps every church safe if it
|
||||
is ever compromised. Remote "trigger update" can be revisited later.
|
||||
is ever compromised. Updates stay that way: the install *asks* (Request update)
|
||||
and Joshua updates the server; the Hub never runs anything on an install.
|
||||
- The Hub itself needs 2FA, backups and the same audit log approach.
|
||||
|
||||
### Data model (Hub, sketch)
|
||||
@@ -707,7 +713,7 @@ model Install { id, customerId, licenceId, name, url, secretHash, version,
|
||||
lastHeartbeatAt, health Json, registeredAt }
|
||||
model Heartbeat { id, installId, receivedAt, version, health Json, stats Json }
|
||||
// rolled up to daily after 30 days
|
||||
model InstallEvent { id, installId, type, message, createdAt } // alerts, version changes
|
||||
model InstallEvent { id, installId, type, message, createdAt } // alerts, version changes, update requests
|
||||
// 2.0, AI (§15): Licence gets aiMonthlyCapCents; usage per call, no prompt/response text
|
||||
model AiUsage { id, installId, helper, model, inputTokens, outputTokens, costCents, createdAt }
|
||||
```
|
||||
@@ -869,12 +875,12 @@ income justify the hardware:
|
||||
| 3 | 1.35.0 ✅ | Multi-day events — phase B (pick sessions, per-session capacity) | Builds on phase A, now with E2E coverage |
|
||||
| 4 | 1.36.0 ✅ – 1.37.2 | Payment provider interface (Yoco moved behind it) → PayFast, then hiding "Pay online" when it can't be used and payment status checks (SnapScan and Zapper dropped) | Payment code gets split out of the big controller as part of this |
|
||||
| 5 | 1.38.0 ✅ 1.39.0 ✅ 1.40.0 ✅ | **POPIA toolkit** (1.38.0: user privacy centre, unsubscribe, WhatsApp STOP, data requests inbox; 1.39.0: privacy notice editor + versions, operator register, consent log, access log; 1.40.0: retention jobs, breach register, children’s-data flags, readiness page) | Main selling point; retention jobs use the job queue |
|
||||
| 6 | 1.41.0 ✅ + Hub v1 ✅ | Install/update scripts, `/health`, licence step in `/setup`, "Connect to Hub", heartbeat, licence states — plus the **Hub** app (separate repo) | Must exist before the first church is sold; Hope Family Church registers as install #1 |
|
||||
| 7 | 1.42.0 | **Multi-campus** | Kids rooms and volunteer roles are per campus, so campuses come first |
|
||||
| 8 | 1.43.0 – 1.44.0 | Kids check-in, Volunteer rosters | Depend on sessions and campuses |
|
||||
| 9 | 1.45.0 – 1.47.0 | Event series & cloning, Wallet passes, Outbound webhooks | Smaller, independent wins |
|
||||
| 10 | 1.48.0 – 1.50.0 | Admin dashboard/Ctrl+K/bulk actions, Afrikaans, Accessibility pass | Polish before the big release |
|
||||
| 11 | 1.51.0 | TypeScript backend + OpenAPI + `/api/v2` alongside v1 | The breaking-change groundwork |
|
||||
| 6 | 1.41.0 ✅ – 1.42.0 ✅ + Hub 1.0 ✅ – 1.1 ✅ | Install/update scripts, `/health`, licence step in `/setup`, "Connect to Hub", heartbeat, licence states — plus the **Hub** app (separate repo); rename to Cross Events (1.41.3); Request update (1.42.0 / Hub 1.1.0) | Must exist before the first church is sold; Hope Family Church registers as install #1 |
|
||||
| 7 | 1.43.0 | **Multi-campus** | Kids rooms and volunteer roles are per campus, so campuses come first |
|
||||
| 8 | 1.44.0 – 1.45.0 | Kids check-in, Volunteer rosters | Depend on sessions and campuses |
|
||||
| 9 | 1.46.0 – 1.48.0 | Event series & cloning, Wallet passes, Outbound webhooks | Smaller, independent wins |
|
||||
| 10 | 1.49.0 – 1.51.0 | Admin dashboard/Ctrl+K/bulk actions, Afrikaans, Accessibility pass | Polish before the big release |
|
||||
| 11 | 1.52.0 | TypeScript backend + OpenAPI + `/api/v2` alongside v1 | The breaking-change groundwork |
|
||||
| 12 | **2.0.0** | **AI helpers** (§15 — headline feature: draft an event, summarise reports and feedback, draft messages, improve text; through the Hub with a rand cap per licence), v1 API removed, migration guide, marketing site/docs for selling | The release. AI needs the Hub (phase 6) for the key, metering and caps |
|
||||
|
||||
Each phase should add matching help content (`frontend/src/content/help/`),
|
||||
@@ -920,6 +926,8 @@ feature.
|
||||
| — | POPIA: privacy notice | Today's notice becomes the starting template, one editable box per section; org name, Information Officer, retention periods and the operator list fill in automatically. Publishing saves a numbered version (2026-10-23) |
|
||||
| — | POPIA: re-accepting the notice | The admin decides per version ("significant change — ask everyone to accept again"); people are asked once after login (2026-10-23) |
|
||||
| — | POPIA: inactive accounts | Anonymised after 5 years without a login or registration, **on by default**, with a warning email 30 days before; staff/supervisor/admin accounts never (2026-10-23) |
|
||||
| — | Product name | **Cross Events** (was Hope Events), Hub: **Cross Events Hub**. UI text and docs renamed in 1.41.3; technical identifiers (`hope-events` repo, storage keys, encryption salt, migrations, PM2/nginx names) stay, so installs and sign-ins keep working (2026-10-27) |
|
||||
| — | Updates | Sites show **Request update**, never the deploy command: the request goes to the Hub, which tells Joshua; he updates the server. 1.42.0 / Hub 1.1.0; Multi-campus and everything after moves up by one (2026-10-27) |
|
||||
| — | POPIA: WhatsApp STOP | In 1.38.0, via WAWP's incoming `message` webhook with a secret in the URL (WAWP doesn't sign webhooks) (2026-10-23) |
|
||||
|
||||
## Still open
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.64.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.42.0",
|
||||
"version": "1.42.1",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user