Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30a2142085 | ||
|
|
95913a5bd5 | ||
|
|
f7e0ec8bc1 | ||
|
|
08d442e1c1 | ||
|
|
be4f341e42 | ||
|
|
1caa0f0ea7 | ||
|
|
91f6481fb7 | ||
|
|
bf887720a1 | ||
|
|
6de0b5525b | ||
|
|
e261705214 | ||
|
|
4ef9cea634 |
@@ -7,6 +7,35 @@ 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
|
||||
|
||||
- **"Request update" instead of the deploy command.** When a new version is out, Site settings →
|
||||
Licence shows a **Request update** button instead of `bash scripts/deploy.sh …`. It sends a
|
||||
signed request to the Hub (`docs/hub-protocol.md` §3a), which tells the provider; the site never
|
||||
updates itself. Afterwards it shows "Update requested on …" until the site runs that version, and
|
||||
the "update available" banner for admins goes away. Logged in the audit log
|
||||
(`hub_update_requested`). Needs Cross Events Hub 1.1.0; an older Hub answers "The Hub doesn't take
|
||||
update requests yet".
|
||||
|
||||
## [1.41.3] - 2026-10-27
|
||||
|
||||
### Changed
|
||||
|
||||
- **The platform is now called Cross Events** (and the Hub, Cross Events Hub): Site settings →
|
||||
Licence, the "update available" banner, the setup wizard, the audit log, help and the docs.
|
||||
Technical names stay as they are (`hope-events` repo, PM2 and nginx names, install folder, browser
|
||||
storage keys), so existing installs and sign-ins keep working.
|
||||
|
||||
## [1.41.2] - 2026-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
# Deploying Cross Events behind frp
|
||||
|
||||
This guide is for a server that is **not reachable from the internet directly**: it runs
|
||||
`frpc`, which opens a tunnel to `frps` on a public server (the "VPS"). Visitors reach the VPS,
|
||||
and frp carries the traffic through to this server. The frp client config lives at
|
||||
**`/etc/frp/frpc.ini`**.
|
||||
|
||||
For a server with a public IP, `scripts/install.sh` does everything (see the README). Behind frp
|
||||
it needs a few changes, because:
|
||||
|
||||
- **HTTPS can't be set up by the installer.** DNS points at the VPS, not at this server, so
|
||||
`certbot --nginx` can't work the usual way. HTTPS ends either on the VPS or in `frpc`.
|
||||
- **There are more proxies in the way.** The app must be told which of them to trust, or it sees
|
||||
every visitor as `127.0.0.1` (rate limiting then treats everyone as one person, and PayFast
|
||||
payment notifications fail their IP check).
|
||||
- **The app must know the site is HTTPS** even though nginx on this server only speaks HTTP;
|
||||
otherwise it builds `http://` links (event images, payment return URLs).
|
||||
|
||||
## 1. How the traffic flows
|
||||
|
||||
Pick the setup that matches how your other frp sites get HTTPS.
|
||||
|
||||
**A. HTTPS ends on the VPS** (nginx/Caddy on the VPS holds the certificate) — recommended if the
|
||||
VPS already does this for other sites:
|
||||
|
||||
```
|
||||
visitor ──https──▶ VPS nginx/Caddy ──http──▶ frps (vhost_http_port) ══tunnel══▶ frpc ──▶ nginx :80 ──▶ app
|
||||
```
|
||||
|
||||
**B. HTTPS ends in frpc** (the VPS only runs frps; the certificate lives on this server and
|
||||
frpc's `https2http` plugin uses it):
|
||||
|
||||
```
|
||||
visitor ──https──▶ frps (vhost_https_port) ══tunnel══▶ frpc https2http ──▶ nginx 127.0.0.1:8080 ──▶ app
|
||||
visitor ──http───▶ frps (vhost_http_port) ══tunnel══▶ frpc ──▶ nginx :80 (certificate check + redirect to https)
|
||||
```
|
||||
|
||||
On this server, nginx sends `/api`, `/uploads`, `/health` and `/docs` to the backend (port 5000)
|
||||
and everything else to the Next.js site (port 3000), the same as a normal install.
|
||||
|
||||
## 2. Before you start
|
||||
|
||||
- Ubuntu 22.04 or 24.04 on this server, with `frpc` already installed and connected (it runs as
|
||||
a systemd service, usually `frpc.service`).
|
||||
- The site's DNS record (e.g. `events.yourchurch.org`) points at the **VPS**.
|
||||
- On the VPS, `frps.ini` has `vhost_http_port` set (and `vhost_https_port` for setup B). Note
|
||||
the values; you need them below.
|
||||
- The licence key for this site from the Cross Events Hub.
|
||||
|
||||
## 3. Install the app
|
||||
|
||||
On this server, as root, run the installer **without** its certificate step:
|
||||
|
||||
```bash
|
||||
curl -fsSLO https://c-git.crosscode.co.za/joshua/hope-events/raw/branch/main/scripts/install.sh
|
||||
sudo bash install.sh --domain events.yourchurch.org --no-ssl --version v1.41.2
|
||||
```
|
||||
|
||||
It installs Node 20, PostgreSQL, PM2 and nginx, creates the database, writes the config, builds
|
||||
and starts the app (in `/opt/hope-events`, run by the `hopeevents` user). Use `--name`, `--dir`,
|
||||
`--web-port` and `--api-port` if other apps already use those names or ports; adjust the ports
|
||||
below to match.
|
||||
|
||||
### Switch the URLs to https
|
||||
|
||||
With `--no-ssl` the installer writes `http://` addresses. Change them to `https://`:
|
||||
|
||||
```bash
|
||||
cd /opt/hope-events
|
||||
sudo sed -i 's#=http://#=https://#' backend/.env frontend/.env.production
|
||||
grep -E 'URL=' backend/.env frontend/.env.production # all should now start with https://
|
||||
```
|
||||
|
||||
While `backend/.env` is open, set which proxies to trust (all of them run on loopback in both
|
||||
setups; see [step 7](#7-check-that-visitor-addresses-come-through) to confirm):
|
||||
|
||||
```bash
|
||||
sudo sed -i 's#^TRUST_PROXY=.*#TRUST_PROXY=loopback#' backend/.env
|
||||
```
|
||||
|
||||
`NEXT_PUBLIC_API_URL` is baked in when the site is built, so rebuild the frontend and restart:
|
||||
|
||||
```bash
|
||||
sudo -u hopeevents -H bash -lc 'cd /opt/hope-events/frontend && npm run build'
|
||||
sudo -u hopeevents -H bash -lc 'cd /opt/hope-events && pm2 reload ecosystem.config.js --update-env'
|
||||
```
|
||||
|
||||
Keep `JWT_SECRET` in `backend/.env` as it is: it also encrypts the secrets stored in the
|
||||
database (SMTP, payment keys).
|
||||
|
||||
## 4. nginx on this server
|
||||
|
||||
The installer wrote `/etc/nginx/sites-available/hope-events`. Replace it with the version for
|
||||
your setup, changing the domain (and the ports if you changed them).
|
||||
|
||||
### Setup A (HTTPS on the VPS)
|
||||
|
||||
```nginx
|
||||
# Shows the forwarded-for chain in the access log (see step 7). Safe to keep.
|
||||
log_format hope_proxy '$remote_addr xff="$http_x_forwarded_for" "$request" $status';
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name events.yourchurch.org;
|
||||
client_max_body_size 25m;
|
||||
access_log /var/log/nginx/hope-events.access.log hope_proxy;
|
||||
|
||||
# Visitors only reach this through the VPS over HTTPS, so tell the app so.
|
||||
location ~ ^/(api|uploads|health|docs)(/|$) {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The VPS nginx/Caddy must redirect plain HTTP to HTTPS and pass the `Host` header through to frps
|
||||
unchanged (frps picks the site by its host name). For nginx on the VPS, roughly:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name events.yourchurch.org;
|
||||
# ssl_certificate / ssl_certificate_key from certbot on the VPS
|
||||
client_max_body_size 25m;
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080; # frps vhost_http_port
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Setup B (HTTPS in frpc)
|
||||
|
||||
Port 80 only answers certificate checks and redirects everything else; the real site is on
|
||||
`127.0.0.1:8080`, which only frpc's `https2http` plugin reaches.
|
||||
|
||||
```nginx
|
||||
log_format hope_proxy '$remote_addr xff="$http_x_forwarded_for" "$request" $status';
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name events.yourchurch.org;
|
||||
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||
location / { return 301 https://$host$request_uri; }
|
||||
}
|
||||
|
||||
server {
|
||||
listen 127.0.0.1:8080;
|
||||
server_name events.yourchurch.org;
|
||||
client_max_body_size 25m;
|
||||
access_log /var/log/nginx/hope-events.access.log hope_proxy;
|
||||
|
||||
location ~ ^/(api|uploads|health|docs)(/|$) {
|
||||
proxy_pass http://127.0.0.1:5000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then, for either setup:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/certbot # setup B only
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 5. frpc: `/etc/frp/frpc.ini`
|
||||
|
||||
Back it up first, then **add** a section for the site below the existing ones. Leave `[common]`
|
||||
(server address, port, token) and the other sections as they are. Section names must be unique
|
||||
across everything connected to your frps.
|
||||
|
||||
```bash
|
||||
sudo cp /etc/frp/frpc.ini /etc/frp/frpc.ini.bak-$(date +%F)
|
||||
sudo nano /etc/frp/frpc.ini
|
||||
```
|
||||
|
||||
### Setup A
|
||||
|
||||
```ini
|
||||
[hope-events-http]
|
||||
type = http
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 80
|
||||
custom_domains = events.yourchurch.org
|
||||
```
|
||||
|
||||
### Setup B
|
||||
|
||||
```ini
|
||||
# Plain HTTP: certificate checks and the redirect to https
|
||||
[hope-events-http]
|
||||
type = http
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 80
|
||||
custom_domains = events.yourchurch.org
|
||||
|
||||
# HTTPS: frpc holds the certificate and passes plain HTTP to nginx on 127.0.0.1:8080
|
||||
[hope-events-https]
|
||||
type = https
|
||||
custom_domains = events.yourchurch.org
|
||||
plugin = https2http
|
||||
plugin_local_addr = 127.0.0.1:8080
|
||||
plugin_crt_path = /etc/letsencrypt/live/events.yourchurch.org/fullchain.pem
|
||||
plugin_key_path = /etc/letsencrypt/live/events.yourchurch.org/privkey.pem
|
||||
plugin_header_X-Forwarded-Proto = https
|
||||
```
|
||||
|
||||
Restart frpc and check it registered the new sections without errors:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart frpc
|
||||
sudo journalctl -u frpc -n 30 --no-pager # look for "start proxy success" for each section
|
||||
```
|
||||
|
||||
> Newer frp releases (0.52+) prefer `frpc.toml`, but still read `.ini`. Don't mix the two: if
|
||||
> your service already uses `frpc.ini`, keep adding to it.
|
||||
|
||||
### Certificate (setup B only)
|
||||
|
||||
Request it through the tunnel with the webroot method (not `--nginx`, which would rewrite the
|
||||
nginx config), then have renewals restart frpc so it loads the new certificate:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y certbot
|
||||
sudo certbot certonly --webroot -w /var/www/certbot -d events.yourchurch.org \
|
||||
-m you@yourchurch.org --agree-tos --non-interactive
|
||||
echo 'systemctl restart frpc' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/restart-frpc.sh
|
||||
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/restart-frpc.sh
|
||||
sudo systemctl restart frpc
|
||||
```
|
||||
|
||||
frpc needs to be able to read the certificate files (it does when it runs as root, the usual
|
||||
setup). If the `[hope-events-https]` section fails to start, check `journalctl -u frpc`.
|
||||
|
||||
## 6. Finish in the browser
|
||||
|
||||
1. Open `https://events.yourchurch.org/health` — it should answer with the version.
|
||||
2. Open `https://events.yourchurch.org/setup`: create the admin account.
|
||||
3. **Site settings → Licence**: enter the licence key to connect to the Hub. The site checks in
|
||||
every 15 minutes from then on (outgoing HTTPS to `events-hub.crosscode.co.za`, which frp
|
||||
doesn't affect).
|
||||
4. Configure SMTP, payments (Yoco/PayFast webhooks use the `https://` `BACKEND_URL`), branding,
|
||||
etc. in Site settings.
|
||||
|
||||
## 7. Check that visitor addresses come through
|
||||
|
||||
Open the site from your phone (on mobile data, so it isn't your own network), then on the
|
||||
server:
|
||||
|
||||
```bash
|
||||
sudo tail -n 5 /var/log/nginx/hope-events.access.log
|
||||
```
|
||||
|
||||
The `xff="…"` part is what the app sees, with nginx's own entry added at the end:
|
||||
|
||||
- `xff="<your phone's IP>, 127.0.0.1"` (only `127.0.0.1` after the real address): good,
|
||||
`TRUST_PROXY=loopback` is right.
|
||||
- `xff="<your phone's IP>, 203.0.113.10"` (a non-local address after it, e.g. the VPS's): add
|
||||
that address: `TRUST_PROXY=loopback,203.0.113.10`, then
|
||||
`pm2 reload ecosystem.config.js --update-env`.
|
||||
- Your phone's IP doesn't appear at all: frp isn't passing the visitor's address on (can happen
|
||||
with setup B on some frp versions). Rate limiting and the PayFast IP check will then misbehave.
|
||||
Fix it by moving TLS to the VPS (setup A), or with frp's `proxy_protocol_version = v2` on the
|
||||
frpc section plus nginx's `listen … proxy_protocol` and `set_real_ip_from 127.0.0.1;
|
||||
real_ip_header proxy_protocol;`.
|
||||
|
||||
The backend logs the setting it uses at startup: `pm2 logs hope-events-backend | grep trust`.
|
||||
|
||||
## 8. Updating
|
||||
|
||||
Nothing about frp changes for updates. On the server:
|
||||
|
||||
```bash
|
||||
cd /opt/hope-events
|
||||
sudo -u hopeevents -H bash scripts/deploy.sh v1.41.2 # or no tag for the latest main
|
||||
```
|
||||
|
||||
It backs up the database, checks out the release, installs, migrates, builds and reloads PM2,
|
||||
and goes back to the previous version if anything fails first. `backend/.env`,
|
||||
`frontend/.env.production`, `ecosystem.config.js` and the nginx/frpc configs are not in git, so
|
||||
updates keep your changes from this guide.
|
||||
|
||||
### Deploying from CI over SSH
|
||||
|
||||
The Gitea workflow deploys on a `v*` tag by SSHing to `DEPLOY_HOST`. Behind frp, that needs an
|
||||
SSH tunnel too, e.g. in `frpc.ini`:
|
||||
|
||||
```ini
|
||||
[hope-events-ssh]
|
||||
type = tcp
|
||||
local_ip = 127.0.0.1
|
||||
local_port = 22
|
||||
remote_port = 6022
|
||||
```
|
||||
|
||||
Then use the VPS as `DEPLOY_HOST`. The workflow's `ssh` call (`.gitea/workflows/ci.yml`) doesn't
|
||||
take a port yet, so add `-p 6022` there (or a `DEPLOY_PORT` secret) before relying on it. Only
|
||||
open this if you need it: it exposes this server's SSH on the VPS. Use key-only login. Deploying
|
||||
by hand (above) needs none of this.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause |
|
||||
| --- | --- |
|
||||
| frps shows "404 Not Found" page | The `custom_domains` value doesn't match the address typed, or frpc didn't start that section (`journalctl -u frpc`). |
|
||||
| 502 Bad Gateway | nginx is up but the app isn't: `sudo -u hopeevents pm2 ls` and `pm2 logs`. |
|
||||
| Site loads but login/API calls fail (CORS) | `FRONTEND_URL` in `backend/.env` is still `http://`, or the frontend wasn't rebuilt after changing `NEXT_PUBLIC_API_URL`. |
|
||||
| Images or links use `http://` (browser "mixed content" warnings) | nginx isn't sending `X-Forwarded-Proto https`, or `TRUST_PROXY` isn't set. |
|
||||
| "Too many requests" for everyone at once | All visitors look like one address: see step 7. |
|
||||
| PayFast payments stay pending | Same as above: the PayFast notification's IP check sees the proxy, not PayFast. |
|
||||
| Uploads over ~1 MB fail | `client_max_body_size` missing on this server's nginx or the VPS's. |
|
||||
| Redirect loop (setup B) | The site's `server` block is on port 80 instead of `127.0.0.1:8080`, or `plugin_local_addr` points at port 80. |
|
||||
+5
-4
@@ -1,4 +1,4 @@
|
||||
# Hope Events — Feature Overview
|
||||
# Cross Events — Feature Overview
|
||||
|
||||
A complete map of what the platform does, grouped by who uses it. For setup and
|
||||
deployment see [README.md](./README.md); for the API see
|
||||
@@ -642,7 +642,7 @@ can't start, the same jobs run on in-process timers.
|
||||
| `consent` — parental consent reminders, and deleting medical details after events | 09:00 daily (`CONSENT_JOBS_ENABLED=false`) |
|
||||
| `privacy` — remind admins of privacy requests due within a week or overdue | 08:00 daily (`PRIVACY_JOBS_ENABLED=false`) |
|
||||
| `retention` — delete/anonymise personal information past its retention period (Admin → Privacy → Retention) | 03:30 daily (`RETENTION_JOBS_ENABLED=false`) |
|
||||
| `hub-heartbeat` — check in with the Hope Events Hub and pick up licence changes (only once connected) | Every 15 minutes (`HUB_HEARTBEAT_ENABLED=false`) |
|
||||
| `hub-heartbeat` — check in with the Cross Events Hub and pick up licence changes (only once connected) | Every 15 minutes (`HUB_HEARTBEAT_ENABLED=false`) |
|
||||
| `analytics-rollup` — roll raw visit rows older than 90 days into daily totals | 04:15 daily (`ANALYTICS_ROLLUP_ENABLED=false`) |
|
||||
| `backup` — database backup (`pg_dump`, gzipped, 14-day retention) | 02:00 daily (`BACKUP_ENABLED=false`) |
|
||||
| `temp-cleanup` — temp files older than 7 days in `/temp` | 03:00 daily |
|
||||
@@ -653,13 +653,14 @@ can't start, the same jobs run on in-process timers.
|
||||
|
||||
- **Install script** (`scripts/install.sh`) — a fresh Ubuntu server to a running site: Node 20,
|
||||
PostgreSQL, PM2, nginx, HTTPS, configuration, migrations and build; then `/setup`.
|
||||
- **Hope Events Hub** — every install connects with a licence key (`/setup`, or Site settings →
|
||||
- **Cross Events Hub** — every install connects with a licence key (`/setup`, or Site settings →
|
||||
Licence) and checks in every 15 minutes with its version, health and totals (no personal
|
||||
information; income as a range). Licences are signed tokens checked on the site, so a Hub outage
|
||||
never switches it off: past the end date there's a grace period (banner for site admins only),
|
||||
then — or when suspended, revoked or not licensed — the site is read-only (events can't be
|
||||
created or edited; no new registrations or payments; tickets still scan, data can be exported).
|
||||
The site checks in 30 seconds after starting and after a licence key is connected. Admins see when an update is available.
|
||||
The site checks in 30 seconds after starting and after a licence key is connected. Admins see when an update is available and press **Request update**,
|
||||
which tells the provider through the Hub (the site never updates itself).
|
||||
See `docs/hub-protocol.md`.
|
||||
- **`/health`** — status, version, database, background jobs and last backup.
|
||||
- **PM2** (`ecosystem.config.js`) runs backend and frontend, restarting either
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hope Events Platform
|
||||
# Cross Events Platform
|
||||
|
||||
Hope Events is a full-stack event management platform that allows users to browse events, register for them, make payments, and manage their tickets.
|
||||
Cross Events is a full-stack event management platform that allows users to browse events, register for them, make payments, and manage their tickets.
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -111,9 +111,11 @@ It installs Node 20, PostgreSQL (with its own database and a random password), P
|
||||
API, uploads, `/health` and `/docs` to the backend, everything else to the site) and an HTTPS
|
||||
certificate, writes `backend/.env` and `frontend/.env.production`, migrates, builds and starts the
|
||||
app. Then open `https://<domain>/setup`: create the admin account and enter the **licence key**
|
||||
to connect the site to the Hope Events Hub. Several installs can share a server with different
|
||||
to connect the site to the Cross Events Hub. Several installs can share a server with different
|
||||
`--name`, `--dir`, `--web-port` and `--api-port`. Run `--help` for all options.
|
||||
|
||||
Behind an frp tunnel (no public IP on the server)? See [DEPLOYMENT.md](DEPLOYMENT.md).
|
||||
|
||||
### Updating
|
||||
|
||||
To update a running install, on the server in the repo folder:
|
||||
|
||||
+1
-1
@@ -85,7 +85,7 @@ Managed in the Payments tab (through `/api/payments/providers/settings` and `/ap
|
||||
| `consent_medical_retention_days` | `30` | Youth events: medical and emergency details from parental consent forms are deleted this many days after the event ends (daily 09:00 job). The consent record itself — who agreed, when, to which text — is kept. Set under Site Settings → Legal. |
|
||||
| `org_email` (as used on legal pages specifically) | `"privacy@example.com"` | Different fallback than `org_email`'s general default (blank) — also rendered as a `mailto:` link, so it needs to stay a valid-looking address. |
|
||||
|
||||
## Hope Events Hub (licence)
|
||||
## Cross Events Hub (licence)
|
||||
|
||||
| Setting | Default when unset | Notes |
|
||||
|---|---|---|
|
||||
|
||||
@@ -81,7 +81,7 @@ PRIVACY_JOBS_ENABLED=true
|
||||
# accounts are anonymised after 5 years by default (with a warning 30 days before).
|
||||
RETENTION_JOBS_ENABLED=true
|
||||
|
||||
# ─── Hope Events Hub (licence and health) ─────────────────────────────────────
|
||||
# ─── Cross Events Hub (licence and health) ─────────────────────────────────────
|
||||
# Where the Hub runs. The site checks in every 15 minutes once connected (Site settings → Licence).
|
||||
# HUB_URL=https://events-hub.crosscode.co.za
|
||||
# For a local or test Hub only: its public key (the release has the production key built in).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hope Events — API Documentation
|
||||
# Cross Events — API Documentation
|
||||
|
||||
Full interactive docs (with request/response examples) are available at `GET /docs?token=<admin-jwt>` on the running backend.
|
||||
|
||||
@@ -839,7 +839,7 @@ Manages the WAWP WhatsApp API instance. Credentials are stored in `AppSetting` (
|
||||
|
||||
## Hub and licence
|
||||
|
||||
The connection to the Hope Events Hub (`src/hub/`, protocol in `docs/hub-protocol.md`).
|
||||
The connection to the Cross Events Hub (`src/hub/`, protocol in `docs/hub-protocol.md`).
|
||||
|
||||
| Method | Path | Auth | Description |
|
||||
|--------|------|------|-------------|
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Hope Events — Backend API
|
||||
# Cross Events — Backend API
|
||||
|
||||
Node.js + Express REST API for the Hope Events platform used by Hope Family Church (`events.hopehenley.co.za`).
|
||||
Node.js + Express REST API for the Cross Events platform used by Hope Family Church (`events.hopehenley.co.za`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "event-management-backend",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.10.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "event-management-backend",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"description": "Event Management System Backend",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -13,6 +13,12 @@ function compareVersions(a, b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** An update request still waiting: until this site runs the version that was asked for. */
|
||||
function pendingUpdateRequest(version, requestedAt, requestedVersion) {
|
||||
if (!requestedAt || !requestedVersion || compareVersions(version, requestedVersion) >= 0) return null;
|
||||
return { requestedAt, version: requestedVersion };
|
||||
}
|
||||
|
||||
/** What the admin pages show: licence state, Hub connection, version and update. */
|
||||
async function statusPayload({ fresh = false } = {}) {
|
||||
const s = await licenceStatus({ fresh });
|
||||
@@ -40,6 +46,7 @@ async function statusPayload({ fresh = false } = {}) {
|
||||
version: APP_VERSION,
|
||||
latestVersion: s.latestVersion,
|
||||
updateAvailable: !!(s.latestVersion && compareVersions(APP_VERSION, s.latestVersion) < 0),
|
||||
updateRequest: pendingUpdateRequest(APP_VERSION, s.updateRequestedAt, s.updateRequestedVersion),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,4 +91,20 @@ const heartbeatNow = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getStatus, connect, heartbeatNow, statusPayload, compareVersions };
|
||||
// @desc Ask the Hub for this site to be updated (the provider is told; nothing runs here)
|
||||
// @route POST /api/hub/update-request
|
||||
// @access Private/Admin
|
||||
const requestUpdate = async (req, res) => {
|
||||
try {
|
||||
const result = await hub.requestUpdate();
|
||||
if (!result.alreadyRequested) {
|
||||
logAdminAction({ actorId: req.user.id, actorRole: req.user.role, action: 'hub_update_requested', targetType: 'hub', metadata: { from: APP_VERSION, to: result.version }, ip: getClientIp(req) });
|
||||
}
|
||||
res.json(await statusPayload({ fresh: true }));
|
||||
} catch (e) {
|
||||
const status = e.statusCode && e.statusCode < 500 && e.code !== 'INSTALL_NOT_FOUND' ? e.statusCode : 502;
|
||||
res.status(status).json({ message: e.message || safeErrorMessage(e), code: e.code });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { getStatus, connect, heartbeatNow, requestUpdate, statusPayload, compareVersions, pendingUpdateRequest };
|
||||
|
||||
+40
-15
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* Talking to the Hope Events Hub (docs/hub-protocol.md): register with a licence key, then a
|
||||
* signed heartbeat every 15 minutes. Install → Hub only. Stored in AppSetting:
|
||||
* Talking to the Cross Events Hub (docs/hub-protocol.md): register with a licence key, then a
|
||||
* signed heartbeat every 15 minutes, and a signed update request when an admin asks for one.
|
||||
* Install → Hub only. Stored in AppSetting:
|
||||
* hub_install_id, hub_install_secret (encrypted), hub_licence (signed token),
|
||||
* hub_last_heartbeat_at, hub_last_error, hub_latest_version.
|
||||
* hub_last_heartbeat_at, hub_last_error, hub_latest_version,
|
||||
* hub_update_requested_at, hub_update_requested_version.
|
||||
* These keys are kept out of the generic settings endpoints (settingsController).
|
||||
*/
|
||||
|
||||
@@ -14,7 +16,7 @@ const report = require('./report');
|
||||
|
||||
const PROTOCOL_VERSION = 1;
|
||||
const TIMEOUT_MS = 15000;
|
||||
const HUB_KEYS = ['hub_install_id', 'hub_install_secret', 'hub_licence', 'hub_last_heartbeat_at', 'hub_last_error', 'hub_latest_version'];
|
||||
const HUB_KEYS = ['hub_install_id', 'hub_install_secret', 'hub_licence', 'hub_last_heartbeat_at', 'hub_last_error', 'hub_latest_version', 'hub_update_requested_at', 'hub_update_requested_version'];
|
||||
|
||||
function hubUrl() {
|
||||
return String(process.env.HUB_URL || 'https://events-hub.crosscode.co.za').replace(/\/$/, '');
|
||||
@@ -59,10 +61,25 @@ async function post(path, body, headers = {}) {
|
||||
return data;
|
||||
}
|
||||
|
||||
/** POST signed with the install secret (§3). Throws NOT_CONNECTED when there is no connection. */
|
||||
async function signedPost(path, body) {
|
||||
const s = await read(['hub_install_id', 'hub_install_secret']);
|
||||
if (!s.hub_install_id || !s.hub_install_secret) throw new HubError('This site isn\'t connected to the Hub yet.', 'NOT_CONNECTED', 400);
|
||||
const secret = decrypt(s.hub_install_secret);
|
||||
return post(path, body, raw => {
|
||||
const timestamp = new Date().toISOString();
|
||||
return {
|
||||
'X-Hub-Install': s.hub_install_id,
|
||||
'X-Hub-Timestamp': timestamp,
|
||||
'X-Hub-Signature': crypto.createHmac('sha256', secret).update(`${timestamp}.${raw}`).digest('hex'),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Register this install with a licence key. Returns the licence claims. */
|
||||
async function connect(licenceKey, adminContact) {
|
||||
if (!require('./publicKey').hubPublicKey()) {
|
||||
throw new HubError('This version of Hope Events has no Hub key built in, so it can\'t check licences yet.', 'NO_PUBLIC_KEY', 500);
|
||||
throw new HubError('This version of Cross Events has no Hub key built in, so it can\'t check licences yet.', 'NO_PUBLIC_KEY', 500);
|
||||
}
|
||||
const install = await report.installInfo();
|
||||
if (!/^https?:\/\//.test(install.url)) {
|
||||
@@ -92,17 +109,9 @@ async function connect(licenceKey, adminContact) {
|
||||
async function heartbeat() {
|
||||
const s = await read(['hub_install_id', 'hub_install_secret']);
|
||||
if (!s.hub_install_id || !s.hub_install_secret) return { skipped: 'not connected' };
|
||||
const secret = decrypt(s.hub_install_secret);
|
||||
const body = { protocolVersion: PROTOCOL_VERSION, version: report.APP_VERSION, health: await report.health(), stats: await report.stats() };
|
||||
try {
|
||||
const data = await post('/api/installs/heartbeat', body, raw => {
|
||||
const timestamp = new Date().toISOString();
|
||||
return {
|
||||
'X-Hub-Install': s.hub_install_id,
|
||||
'X-Hub-Timestamp': timestamp,
|
||||
'X-Hub-Signature': crypto.createHmac('sha256', secret).update(`${timestamp}.${raw}`).digest('hex'),
|
||||
};
|
||||
});
|
||||
const data = await signedPost('/api/installs/heartbeat', body);
|
||||
const claims = verifyToken(data.licence);
|
||||
await save({
|
||||
// Keep the old token if the new one doesn't check out (never accept an unverified one).
|
||||
@@ -124,6 +133,22 @@ async function heartbeat() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the Hub for this site to be updated (§3a). Joshua is told; the site never updates itself.
|
||||
* Returns { requestedAt, version, alreadyRequested }.
|
||||
*/
|
||||
async function requestUpdate() {
|
||||
let data;
|
||||
try {
|
||||
data = await signedPost('/api/installs/update-request', { protocolVersion: PROTOCOL_VERSION, version: report.APP_VERSION });
|
||||
} catch (e) {
|
||||
if (e.code === 'NOT_FOUND') throw new HubError('The Hub doesn\'t take update requests yet. Please contact your provider.', 'NOT_SUPPORTED', 502);
|
||||
throw e;
|
||||
}
|
||||
await save({ hub_update_requested_at: data.requestedAt, hub_update_requested_version: data.version || null });
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* A heartbeat soon (on startup, and after connecting), so a licence change on the Hub shows
|
||||
* without waiting for the next 15-minute run. Never throws; skipped when the job is switched off.
|
||||
@@ -139,4 +164,4 @@ function heartbeatSoon(delayMs = 30 * 1000) {
|
||||
soonTimer.unref?.();
|
||||
}
|
||||
|
||||
module.exports = { PROTOCOL_VERSION, HUB_KEYS, hubUrl, HubError, connect, heartbeat, heartbeatSoon };
|
||||
module.exports = { PROTOCOL_VERSION, HUB_KEYS, hubUrl, HubError, connect, heartbeat, heartbeatSoon, requestUpdate };
|
||||
|
||||
@@ -80,7 +80,7 @@ let cache = null;
|
||||
/** This install's licence: claims, state and Hub connection details. Cached for 30 s. */
|
||||
async function licenceStatus({ fresh = false } = {}) {
|
||||
if (!fresh && cache && Date.now() - cache.at < CACHE_MS) return cache.value;
|
||||
const rows = await prisma.appSetting.findMany({ where: { key: { in: ['hub_install_id', 'hub_licence', 'hub_last_heartbeat_at', 'hub_last_error', 'hub_latest_version'] } } });
|
||||
const rows = await prisma.appSetting.findMany({ where: { key: { in: ['hub_install_id', 'hub_licence', 'hub_last_heartbeat_at', 'hub_last_error', 'hub_latest_version', 'hub_update_requested_at', 'hub_update_requested_version'] } } });
|
||||
const s = Object.fromEntries(rows.map(r => [r.key, r.value]));
|
||||
const claims = verifyToken(s.hub_licence);
|
||||
const value = {
|
||||
@@ -93,6 +93,8 @@ async function licenceStatus({ fresh = false } = {}) {
|
||||
lastHeartbeatAt: s.hub_last_heartbeat_at || null,
|
||||
lastError: s.hub_last_error || null,
|
||||
latestVersion: s.hub_latest_version || null,
|
||||
updateRequestedAt: s.hub_update_requested_at || null,
|
||||
updateRequestedVersion: s.hub_update_requested_version || null,
|
||||
};
|
||||
cache = { at: Date.now(), value };
|
||||
return value;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* The Hope Events Hub's public licence key (Ed25519). Installs check every licence token with
|
||||
* The Cross Events Hub's public licence key (Ed25519). Installs check every licence token with
|
||||
* 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 };
|
||||
|
||||
@@ -136,7 +136,7 @@ async function installInfo() {
|
||||
require('../payments').availableProviders().then(list => list.map(p => p.id)).catch(() => []),
|
||||
]);
|
||||
return {
|
||||
name: name || 'Hope Events',
|
||||
name: name || 'Cross Events',
|
||||
url: String(url || process.env.FRONTEND_URL || '').split(',')[0].trim().replace(/\/$/, ''),
|
||||
version: APP_VERSION,
|
||||
timezone: process.env.APP_TIMEZONE || 'Africa/Johannesburg',
|
||||
|
||||
@@ -916,11 +916,13 @@ app.get('/docs', async (req, res) => {
|
||||
responses:[{ status:200, desc:'Healthy', body:{ status:'ok', version:'1.41.0', db:true, jobs:{ mode:'queue', failedLast24h:0 }, lastBackupAt:'2026-10-26T00:00:12.000Z' }}]},
|
||||
{ method:'GET', path:'/api/hub/status', auth:'admin', desc:'Licence state (active, grace, suspended, revoked, unlicensed), Hub connection, version and update',
|
||||
responses:[{ status:200, desc:'Success', body:{ state:'grace', readOnly:false, daysLeft:9, connected:true, version:'1.41.0', latestVersion:'1.42.0', updateAvailable:true }}]},
|
||||
{ method:'POST', path:'/api/hub/connect', auth:'admin', desc:'Connect to the Hope Events Hub with a licence key (also the /setup licence step)',
|
||||
{ method:'POST', path:'/api/hub/connect', auth:'admin', desc:'Connect to the Cross Events Hub with a licence key (also the /setup licence step)',
|
||||
request:{ body:{ licenceKey:'CEH-7K2M-9QXA-P4RT-WD8C' } },
|
||||
responses:[{ status:200, desc:'Connected (status as above)' }, { status:404, desc:'Unknown key', body:{ code:'LICENCE_NOT_FOUND', message:'That licence key was not found.' }}]},
|
||||
{ method:'POST', path:'/api/hub/heartbeat', auth:'admin', desc:'Check in with the Hub now (picks up licence changes)',
|
||||
responses:[{ status:200, desc:'Status' }, { status:502, desc:'Hub unreachable' }]},
|
||||
{ method:'POST', path:'/api/hub/update-request', auth:'admin', desc:'Ask the Hub for this site to be updated to the latest version (the provider is told; the site never updates itself)',
|
||||
responses:[{ status:200, desc:'Status, with updateRequest', body:{ updateAvailable:true, updateRequest:{ requestedAt:'2026-10-27T09:00:00.000Z', version:'1.42.0' } }}, { status:409, desc:'Already on the latest version', body:{ code:'UP_TO_DATE', message:'This site already runs the latest version.' }}, { status:502, desc:'Hub unreachable' }]},
|
||||
]},
|
||||
|
||||
{ title: 'Privacy (POPIA)', base: '/api/privacy', endpoints: [
|
||||
|
||||
@@ -57,7 +57,7 @@ const JOBS = [
|
||||
run: () => require('../utils/privacy').remindDueRequests(new Date()),
|
||||
},
|
||||
{
|
||||
name: 'hub-heartbeat', label: 'Hope Events Hub: heartbeat and licence refresh', cron: '*/15 * * * *', env: 'HUB_HEARTBEAT_ENABLED',
|
||||
name: 'hub-heartbeat', label: 'Cross Events Hub: heartbeat and licence refresh', cron: '*/15 * * * *', env: 'HUB_HEARTBEAT_ENABLED',
|
||||
run: () => require('../hub/client').heartbeat(),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getStatus, connect, heartbeatNow } = require('../controllers/hubController');
|
||||
const { getStatus, connect, heartbeatNow, requestUpdate } = require('../controllers/hubController');
|
||||
const { protect, admin } = require('../middleware/authMiddleware');
|
||||
|
||||
// Licence and the Hope Events Hub (docs/hub-protocol.md). Admins only; the setup wizard's
|
||||
// Licence and the Cross Events Hub (docs/hub-protocol.md). Admins only; the setup wizard's
|
||||
// admin token counts, so the licence step can run during /setup.
|
||||
router.get('/status', protect, admin, getStatus);
|
||||
router.post('/connect', protect, admin, connect);
|
||||
router.post('/heartbeat', protect, admin, heartbeatNow);
|
||||
router.post('/update-request', protect, admin, requestUpdate);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -4,7 +4,7 @@ const crypto = require('crypto');
|
||||
const { verifyToken, stateFor } = require('../src/hub/licence');
|
||||
const { incomeBand } = require('../src/hub/report');
|
||||
const { isBlocked } = require('../src/hub/readOnly');
|
||||
const { compareVersions } = require('../src/controllers/hubController');
|
||||
const { compareVersions, pendingUpdateRequest } = require('../src/controllers/hubController');
|
||||
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
|
||||
function sign(payload, key = privateKey) {
|
||||
@@ -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"', () => {
|
||||
@@ -131,3 +143,11 @@ test('version comparison for "update available"', () => {
|
||||
expect(compareVersions('1.41.0', '1.41.0')).toBe(0);
|
||||
expect(compareVersions('2.0.0', '1.50.3')).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('an update request shows until the site runs the version asked for', () => {
|
||||
const at = '2026-10-27T09:00:00.000Z';
|
||||
expect(pendingUpdateRequest('1.41.2', at, '1.42.0')).toEqual({ requestedAt: at, version: '1.42.0' });
|
||||
expect(pendingUpdateRequest('1.42.0', at, '1.42.0')).toBeNull();
|
||||
expect(pendingUpdateRequest('1.43.0', at, '1.42.0')).toBeNull();
|
||||
expect(pendingUpdateRequest('1.41.2', null, null)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Hope Events — New Features
|
||||
# Cross Events — New Features
|
||||
|
||||
Planned features for the next round of development. Each section covers what
|
||||
the feature does, who uses it, how it fits the existing platform, and the edge
|
||||
|
||||
+33
-18
@@ -1,16 +1,25 @@
|
||||
# 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 | Hope 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 |
|
||||
| 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 |
|
||||
| 1.40.0 | POPIA toolkit part 3 — readiness checklist with score, retention policies + nightly `retention` job (inactive accounts anonymised after 5 years with a 30-day warning, on by default), breach register with notification drafts, children's/medical flag in the access log, `{retention}` in the notice |
|
||||
| 1.39.0 | POPIA toolkit part 2 — Admin → Privacy: privacy notice editor (built-in text as the start, placeholders, drafts, numbered versions, re-acceptance after login), operator register, consent log, access log; Information Officer on the contact page |
|
||||
| 1.38.0 | POPIA toolkit part 1 — Privacy page (news opt-in + history, download my data, correction and deletion requests), Event update vs News & announcements on every bulk send, unsubscribe links + one-click headers, WhatsApp STOP, one-time news question, Admin → Data requests, thorough anonymising |
|
||||
@@ -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,21 +166,28 @@ 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
|
||||
Hope Events with `HUB_URL=http://localhost:5200` and `HUB_PUBLIC_KEY` = the key on Hub → Settings.
|
||||
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
|
||||
run on a real server).
|
||||
|
||||
|
||||
+32
-24
@@ -1,6 +1,6 @@
|
||||
# Hope Events — Road to 2.0.0
|
||||
# Cross Events — Road to 2.0.0
|
||||
|
||||
Planned features and platform work to take Hope Events from 1.32.x to 2.0.0.
|
||||
Planned features and platform work to take Cross Events from 1.32.x to 2.0.0.
|
||||
Each section covers what the feature does, who uses it, how it fits the
|
||||
existing platform, and the edge cases to handle. See [FEATURES.md](../FEATURES.md)
|
||||
for what already exists, and [FEATURES-ROUND-1.md](./FEATURES-ROUND-1.md) for the
|
||||
@@ -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:**
|
||||
|
||||
@@ -48,7 +51,7 @@ notifications. SnapScan and Zapper are dropped — Yoco and PayFast are enough (
|
||||
10. [Languages (Afrikaans first)](#10-languages-afrikaans-first)
|
||||
11. [Accessibility pass (WCAG 2.2 AA)](#11-accessibility-pass-wcag-22-aa)
|
||||
12. [Multi-campus (one install per church)](#12-multi-campus-one-install-per-church)
|
||||
13. [Central management platform (Hope Events Hub)](#13-central-management-platform-hope-events-hub)
|
||||
13. [Central management platform (Cross Events Hub)](#13-central-management-platform-cross-events-hub)
|
||||
14. [Foundations (platform work)](#14-foundations-platform-work)
|
||||
15. [AI helpers (2.0 headline)](#15-ai-helpers-20-headline)
|
||||
16. [AI: later stages](#16-ai-later-stages)
|
||||
@@ -464,7 +467,7 @@ Plan who serves where, for an event or each session.
|
||||
|
||||
## 8. Outbound webhooks
|
||||
|
||||
Let other systems react to what happens in Hope Events.
|
||||
Let other systems react to what happens in Cross Events.
|
||||
|
||||
- Admin adds endpoints and picks events: `registration.created`,
|
||||
`registration.paid`, `registration.cancelled`, `payment.refunded`,
|
||||
@@ -580,13 +583,13 @@ Because every church is a separate install, a few tools make that manageable:
|
||||
migrations, build, restart; roll back on failure.
|
||||
- **Version and health:** current version shown in admin and on a `/health`
|
||||
endpoint (version, DB, jobs, last backup), reported to the
|
||||
[Hub](#13-central-management-platform-hope-events-hub).
|
||||
[Hub](#13-central-management-platform-cross-events-hub).
|
||||
|
||||
---
|
||||
|
||||
## 13. Central management platform (Hope Events Hub)
|
||||
## 13. Central management platform (Cross Events Hub)
|
||||
|
||||
A separate app, run by Joshua, to see and manage **every Hope Events
|
||||
A separate app, run by Joshua, to see and manage **every Cross Events
|
||||
install** from one place: who the customers are, which version each runs,
|
||||
whether they're healthy, basic usage stats, and their **licence** status.
|
||||
|
||||
@@ -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.
|
||||
- Hope Events repo: the Hub **client** — licence step in `/setup`,
|
||||
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/`),
|
||||
@@ -896,7 +902,7 @@ feature.
|
||||
| 6 | Selling model | **One install per church** — data separation, any server. No multi-tenant. **Multi-campus** inside an install is in scope |
|
||||
| — | Pro-rata rounding | Nearest R10 |
|
||||
| — | Multi-campus | In scope (§12), inside one install |
|
||||
| — | Central management | **Hope Events Hub** in its **own git repo** (`joshua/events-hub`): installs register on setup, send heartbeats (health, version, aggregate stats), licences switched on/off centrally |
|
||||
| — | Central management | **Cross Events Hub** in its **own git repo** (`joshua/events-hub`): installs register on setup, send heartbeats (health, version, aggregate stats), licences switched on/off centrally |
|
||||
| 7 | Payment provider order | Yoco · PayFast. Joshua can supply documentation links |
|
||||
| — | SnapScan, Zapper | Dropped (2026-10-23): Yoco and PayFast are enough. Later versions moved up by two |
|
||||
| — | Provider fees | Not deducted or estimated automatically; handled by people during cashup. No "pass fee to payer" |
|
||||
@@ -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
|
||||
|
||||
+23
-2
@@ -1,6 +1,6 @@
|
||||
# Hub protocol (version 1)
|
||||
|
||||
The contract between a **Hope Events install** (the church's site) and the **Hope Events
|
||||
The contract between a **Cross Events install** (the church's site) and the **Cross Events
|
||||
Hub** (`events-hub.crosscode.co.za`). A copy of this file lives in both repositories
|
||||
(`joshua/hope-events` → `docs/hub-protocol.md`, `joshua/events-hub` → `docs/hub-protocol.md`);
|
||||
change both together.
|
||||
@@ -110,6 +110,27 @@ X-Hub-Signature: <hex HMAC-SHA256(installSecret, timestamp + "." + rawBody)>
|
||||
`R1 001–R5 000`, `R5 001–R20 000`, `R20 001–R50 000`, `R50 001–R100 000`, `over R100 000`.
|
||||
- The Hub keeps every heartbeat for 30 days, then one per install per day.
|
||||
|
||||
## 3a. Update request
|
||||
|
||||
Sent when a site admin presses **Request update** (Site settings → Licence). Installs never update
|
||||
themselves and never show the deploy command; the request tells Joshua, who updates the server.
|
||||
Signed exactly like the heartbeat.
|
||||
|
||||
```
|
||||
POST /api/installs/update-request
|
||||
X-Hub-Install / X-Hub-Timestamp / X-Hub-Signature (as in §3)
|
||||
{ "protocolVersion": 1, "version": "1.41.2" }
|
||||
→ 201 { "requestedAt": "2026-10-27T09:00:00.000Z", "version": "1.42.0", "alreadyRequested": false }
|
||||
→ 200 { …, "alreadyRequested": true } // a request is already open: not emailed again
|
||||
```
|
||||
|
||||
- The Hub opens an `update_requested` alert on the install (shown under Alerts) and emails the
|
||||
alert address once. It is resolved when a heartbeat reports a newer version than before.
|
||||
- `409 UP_TO_DATE` — the install already runs the Hub's latest version (or none is set).
|
||||
- Errors as in §3 (`INSTALL_NOT_FOUND`, `STALE_REQUEST`, `BAD_SIGNATURE`). Rate-limited (10 an
|
||||
hour per IP). A Hub older than this section answers `404 NOT_FOUND`.
|
||||
- No personal information: not who pressed the button (the Hub already has the admin contact).
|
||||
|
||||
## 4. Licence token
|
||||
|
||||
`hel1.<payload>.<signature>` — `payload` is base64url JSON, `signature` is the base64url
|
||||
@@ -156,5 +177,5 @@ grace starts). Blocking the Hub can't keep a suspended licence active for more t
|
||||
|
||||
## 6. Later (not in v1)
|
||||
|
||||
- **AI proxy** (Hope Events 2.0): `POST /api/ai/complete`, signed like the heartbeat; the Hub holds
|
||||
- **AI proxy** (Cross Events 2.0): `POST /api/ai/complete`, signed like the heartbeat; the Hub holds
|
||||
the Claude API key, meters each call in rand and enforces `limits.aiMonthlyCapCents`.
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# Hope Events — Frontend
|
||||
# Cross Events — Frontend
|
||||
|
||||
Next.js 15 (App Router) frontend for the Hope Events platform used by Hope Family Church (`events.hopehenley.co.za`).
|
||||
Next.js 15 (App Router) frontend for the Cross Events platform used by Hope Family Church (`events.hopehenley.co.za`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.41.2",
|
||||
"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.41.2",
|
||||
"version": "1.42.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
|
||||
@@ -95,7 +95,8 @@ const ACTION_LABELS: Record<string, string> = {
|
||||
retention_run: "Retention job deleted/anonymised data",
|
||||
breach_recorded: "Security compromise recorded",
|
||||
breach_updated: "Security compromise updated",
|
||||
hub_connected: "Connected to the Hope Events Hub",
|
||||
hub_connected: "Connected to the Cross Events Hub",
|
||||
hub_update_requested: "Asked the Hub for an update",
|
||||
};
|
||||
|
||||
interface AuditLogEntry {
|
||||
|
||||
@@ -594,14 +594,14 @@ export default function SetupPage() {
|
||||
Email delivery is not configured yet. Set it up under Admin → Site Settings after logging in.
|
||||
</p>
|
||||
)}
|
||||
{/* Licence: connect to the Hope Events Hub now, or later in Site settings → Licence. */}
|
||||
{/* Licence: connect to the Cross Events Hub now, or later in Site settings → Licence. */}
|
||||
<div className="text-left border rounded-xl p-4 space-y-2">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Licence</h3>
|
||||
{licenceConnected ? (
|
||||
<p className="text-sm text-green-700">✓ Connected — licensed to {licenceConnected}.</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-xs text-gray-500">Enter the licence key you received to connect this site to the Hope Events Hub. Until a licence is connected, events can't be created or edited. You can also do this later under Admin → Site Settings → Licence.</p>
|
||||
<p className="text-xs text-gray-500">Enter the licence key you received to connect this site to the Cross Events Hub. Until a licence is connected, events can't be created or edited. You can also do this later under Admin → Site Settings → Licence.</p>
|
||||
<ConnectForm token={setupToken} compact onConnected={s => setLicenceConnected(s.licence?.customer || "your organisation")} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type HubStatus = {
|
||||
version: string;
|
||||
latestVersion: string | null;
|
||||
updateAvailable: boolean;
|
||||
updateRequest: { requestedAt: string; version: string } | null;
|
||||
};
|
||||
|
||||
const STATE: Record<HubStatus["state"], { label: string; tone: string; text: string }> = {
|
||||
@@ -93,6 +94,19 @@ export function LicenceTab({ active }: { active: boolean }) {
|
||||
}
|
||||
};
|
||||
|
||||
const requestUpdate = async () => {
|
||||
setBusy(true);
|
||||
setMsg(null);
|
||||
try {
|
||||
setStatus(await apiFetch<HubStatus>("/api/hub/update-request", { method: "POST", authToken: token }));
|
||||
setMsg({ type: "ok", text: "Update requested. Your provider has been told and will update the site." });
|
||||
} catch (e) {
|
||||
setMsg({ type: "err", text: errorMessage(e) || "The Hub could not be reached." });
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!status) return msg ? <p className="text-sm text-red-700">{msg.text}</p> : <p className="text-sm text-gray-400">Loading…</p>;
|
||||
const st = STATE[status.state];
|
||||
|
||||
@@ -100,7 +114,7 @@ export function LicenceTab({ active }: { active: boolean }) {
|
||||
<div className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Licence</h2>
|
||||
<p className="text-sm text-gray-500">This site checks in with the Hope Events Hub 30 seconds after starting and every 15 minutes after that: version, health and totals only — never anyone's personal information.</p>
|
||||
<p className="text-sm text-gray-500">This site checks in with the Cross Events Hub 30 seconds after starting and every 15 minutes after that: version, health and totals only — never anyone's personal information.</p>
|
||||
</div>
|
||||
|
||||
<div className={`border rounded-xl p-4 ${st.tone}`}>
|
||||
@@ -114,7 +128,7 @@ export function LicenceTab({ active }: { active: boolean }) {
|
||||
|
||||
{!status.keyConfigured && (
|
||||
<p className="text-sm text-amber-800 bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
This version has no Hub key built in yet, so it can't check licences. Update Hope Events, or set HUB_PUBLIC_KEY on the server for testing.
|
||||
This version has no Hub key built in yet, so it can't check licences. Update Cross Events, or set HUB_PUBLIC_KEY on the server for testing.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -147,12 +161,21 @@ export function LicenceTab({ active }: { active: boolean }) {
|
||||
|
||||
<div className="border rounded-xl p-4 text-sm">
|
||||
<div className="flex items-center gap-2 font-medium text-gray-900 mb-1"><ArrowUpCircle className="w-4 h-4" /> Version</div>
|
||||
<p>This site runs Hope Events <strong>{status.version}</strong>.{status.latestVersion && !status.updateAvailable && " It's up to date."}</p>
|
||||
{status.updateAvailable && (
|
||||
<p className="mt-1 text-brand-700">
|
||||
Version {status.latestVersion} is available. On the server: <code className="bg-gray-100 px-1 rounded">bash scripts/deploy.sh v{status.latestVersion}</code>
|
||||
<p>This site runs Cross Events <strong>{status.version}</strong>.{status.latestVersion && !status.updateAvailable && " It's up to date."}</p>
|
||||
{status.updateAvailable && (status.updateRequest ? (
|
||||
<p className="mt-1 text-gray-600">
|
||||
Version {status.latestVersion} is available. Update requested on {fmt(status.updateRequest.requestedAt)} — your provider will install it.
|
||||
</p>
|
||||
)}
|
||||
) : (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3">
|
||||
<p className="text-brand-700">Version {status.latestVersion} is available.</p>
|
||||
{status.connected && (
|
||||
<button onClick={requestUpdate} disabled={busy} className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-brand-600 text-white text-sm font-medium hover:bg-brand-700 disabled:opacity-50">
|
||||
<ArrowUpCircle className="w-4 h-4" /> {busy ? "Requesting…" : "Request update"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{msg && <div className={`p-3 rounded-lg text-sm ${msg.type === "ok" ? "bg-green-50 text-green-800 border border-green-200" : "bg-red-50 text-red-800 border border-red-200"}`}>{msg.text}</div>}
|
||||
|
||||
@@ -50,11 +50,12 @@ export function LicenceBanner() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (status.updateAvailable) {
|
||||
// Once an update has been requested there's nothing more for the admin to do.
|
||||
if (status.updateAvailable && !status.updateRequest) {
|
||||
return (
|
||||
<div role="status" className="bg-brand-50 border-b border-brand-100 text-brand-800 text-sm px-4 py-2 flex items-center gap-2">
|
||||
<ArrowUpCircle className="w-4 h-4 shrink-0" />
|
||||
<span>Hope Events {status.latestVersion} is available (this site runs {status.version}). {link}</span>
|
||||
<span>Cross Events {status.latestVersion} is available (this site runs {status.version}). Request the update in {link}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Building2, Palette, Bell, Mail, Scale, MessageCircle, LayoutDashboard, Timer, Play, CreditCard, ArrowUpDown, KeyRound } from "lucide-react";
|
||||
import { Building2, Palette, Bell, Mail, Scale, MessageCircle, LayoutDashboard, Timer, Play, CreditCard, ArrowUpDown, KeyRound, ArrowUpCircle } from "lucide-react";
|
||||
import { GuideItem } from "@/components/shared/GuideItem";
|
||||
import type { HelpContent } from "./types";
|
||||
|
||||
@@ -145,7 +145,7 @@ export const siteSettingsHelpContent: HelpContent = {
|
||||
icon: KeyRound,
|
||||
content: (
|
||||
<div className="space-y-4">
|
||||
<p>The site's licence and its connection to the Hope Events Hub. The site checks in every 15 minutes with its version, health and totals — never anyone's personal information.</p>
|
||||
<p>The site's licence and its connection to the Cross Events Hub. The site checks in every 15 minutes with its version, health and totals — never anyone's personal information.</p>
|
||||
<div className="space-y-4">
|
||||
<GuideItem icon={KeyRound} title="Connecting" tone="brand">
|
||||
Enter the licence key you received (CEH-XXXX-XXXX-XXXX-XXXX). If the site moves to a new address, ask your provider to release the licence, then connect again.
|
||||
@@ -156,6 +156,9 @@ export const siteSettingsHelpContent: HelpContent = {
|
||||
<GuideItem icon={Scale} title="Read-only" tone="gray">
|
||||
When a licence is suspended, revoked or past its grace period, nothing new can be added — no events, registrations or payments. Tickets for events people already paid for still scan, and all data can still be viewed and exported.
|
||||
</GuideItem>
|
||||
<GuideItem icon={ArrowUpCircle} title="Updates" tone="brand">
|
||||
When a new version is out, press Request update. Your provider is told and updates the site for you — there's nothing to run yourself.
|
||||
</GuideItem>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Hope Events — Event Management Platform for Churches
|
||||
# Cross Events — Event Management Platform for Churches
|
||||
|
||||
## What is it?
|
||||
|
||||
Hope Events is a complete event management platform built specifically for churches and faith-based organisations. It handles everything from advertising upcoming events and taking online registrations to collecting payments and donations, sending QR-coded tickets, checking people in at the door, and balancing the books afterwards.
|
||||
Cross Events is a complete event management platform built specifically for churches and faith-based organisations. It handles everything from advertising upcoming events and taking online registrations to collecting payments and donations, sending QR-coded tickets, checking people in at the door, and balancing the books afterwards.
|
||||
|
||||
It runs in any web browser — on a phone, tablet, or computer — with nothing to install.
|
||||
|
||||
@@ -76,7 +76,7 @@ It runs in any web browser — on a phone, tablet, or computer — with nothing
|
||||
|
||||
## Pricing
|
||||
|
||||
Hope Events is offered as a **hosted service**. You don't need to worry about servers, databases, or software updates — we handle everything.
|
||||
Cross Events is offered as a **hosted service**. You don't need to worry about servers, databases, or software updates — we handle everything.
|
||||
|
||||
| Plan | Details |
|
||||
|------|---------|
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "hope-events",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.64.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events",
|
||||
"version": "1.41.2",
|
||||
"version": "1.42.1",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs Hope Events on a fresh Ubuntu server (22.04 or 24.04): Node 20, PostgreSQL, PM2,
|
||||
# Installs Cross Events on a fresh Ubuntu server (22.04 or 24.04): Node 20, PostgreSQL, PM2,
|
||||
# nginx and an HTTPS certificate, then the app itself. Finish in the browser at /setup.
|
||||
#
|
||||
# Usage (as root, or with sudo):
|
||||
|
||||
Reference in New Issue
Block a user