Compare commits

...
Author SHA1 Message Date
joshua 61ce138447 Merge branch 'fix/read-only-v2' 2026-11-02 15:14:50 +02:00
joshuaandClaude Opus 5.5 b28c8a8876 Fix: licence read-only mode blocks the v2 routes too (1.52.3)
The read-only guard only listed v1 paths, and every screen has used
/api/v2 since 1.51.15, so a suspended, revoked or unlicensed site could
still create events, take registrations and start payments. The v2
successors of each blocked v1 route are blocked now; a test checks each
blocked v2 path exists in the OpenAPI document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:14:50 +02:00
joshua 31aa787b17 Merge branch 'fix/deploy-local-changes' 2026-11-02 14:04:27 +02:00
10 changed files with 96 additions and 9 deletions
+11
View File
@@ -7,6 +7,17 @@ and this project follows [Semantic Versioning](https://semver.org/).
## [Unreleased]
## [1.52.3] - 2026-11-02
### Fixed
- **Licence read-only mode works again.** The read-only guard (`src/hub/readOnly.js`) only listed
v1 paths, and every screen has used `/api/v2` since 1.51.15 — so a suspended, revoked or
unlicensed site could still create and change events, take registrations and start payments.
The v2 successors of every blocked v1 route are now blocked too (answered as `READ_ONLY` with
reason `LICENCE_READ_ONLY`). A test checks each blocked v2 path is a real route in the OpenAPI
document, so a renamed route can't slip out of the list.
## [1.52.2] - 2026-11-02
### Changed
+6
View File
@@ -12,6 +12,12 @@ Write for church admins, not developers: no environment variables, endpoints or
developer log is CHANGELOG.md. A tagged version without a section here shows "No details given".
-->
## 1.52.3
- **A licence check fix.** When a site's licence has run out or isn't connected, the site is
meant to go read-only (tickets still scan and all data can still be viewed and exported). Since
1.51.15 that wasn't being applied; it is again. Sites with an active licence see no change.
## 1.52.2
- **Safer updates.** If files on your server were changed by hand, an update now stops straight
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "event-management-backend",
"version": "1.52.2",
"version": "1.52.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "event-management-backend",
"version": "1.52.2",
"version": "1.52.3",
"hasInstallScript": true,
"dependencies": {
"@cj-tech-master/excelts": "^10.2.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "event-management-backend",
"version": "1.52.2",
"version": "1.52.3",
"description": "Event Management System Backend",
"main": "src/index.js",
"scripts": {
+28
View File
@@ -10,6 +10,8 @@
const { licenceStatus } = require('./licence');
// v1 paths (until 2.0 removes them) and their /api/v2 successors (1.52.3: the screens have used v2
// since 1.51.15, so without these a suspended or unlicensed site wasn't read-only any more).
const BLOCKED = [
['POST', /^\/api\/events\/?$/],
['PUT', /^\/api\/events\/[^/]+\/?$/],
@@ -38,6 +40,32 @@ const BLOCKED = [
['POST', /^\/api\/volunteers\/roles\/[^/]+\/sign-up\/?$/],
['POST', /^\/api\/volunteers\/manage\/events\/[^/]+\/roles\/?$/],
['POST', /^\/api\/volunteers\/manage\/roles\/[^/]+\/assign\/?$/],
// /api/v2 — the same actions.
['POST', /^\/api\/v2\/events\/?$/],
['PATCH', /^\/api\/v2\/events\/[^/]+\/?$/],
['POST', /^\/api\/v2\/events\/[^/]+\/copy-from\/?$/],
['POST', /^\/api\/v2\/series\/?$/],
['PATCH', /^\/api\/v2\/series\/[^/]+\/?$/],
['POST', /^\/api\/v2\/season-passes\/?$/],
['PATCH', /^\/api\/v2\/season-passes\/[^/]+\/?$/],
['POST', /^\/api\/v2\/season-passes\/[^/]+\/buy\/?$/],
['POST', /^\/api\/v2\/season-pass-purchases\/[^/]+\/checkout\/?$/],
['POST', /^\/api\/v2\/events\/[^/]+\/ticket-options\/?$/],
['PATCH', /^\/api\/v2\/ticket-options\/[^/]+\/?$/],
['POST', /^\/api\/v2\/ticket-options\/[^/]+\/variants\/?$/],
['PATCH', /^\/api\/v2\/variants\/[^/]+\/?$/],
['POST', /^\/api\/v2\/registrations\/?$/],
['POST', /^\/api\/v2\/registrations\/manual\/?$/],
['PUT', /^\/api\/v2\/registrations\/[^/]+\/items\/?$/],
['POST', /^\/api\/v2\/payments\/?$/],
['POST', /^\/api\/v2\/registrations\/[^/]+\/(checkout|payment-link(\/send)?)\/?$/],
['POST', /^\/api\/v2\/payment-links\/[^/]+\/?$/],
['POST', /^\/api\/v2\/waitlist\/?$/],
['POST', /^\/api\/v2\/coupons\/?$/],
['POST', /^\/api\/v2\/events\/[^/]+\/bundles\/?$/],
['POST', /^\/api\/v2\/volunteer-roles\/[^/]+\/(sign-up|assign)\/?$/],
['POST', /^\/api\/v2\/events\/[^/]+\/volunteer-roles\/?$/],
];
function isBlocked(method, path) {
+42
View File
@@ -105,6 +105,48 @@ describe('read-only mode', () => {
['POST', '/api/users/login'],
['POST', '/api/reports/pdf'],
])('%s %s keeps working', (m, p) => expect(isBlocked(m, p)).toBe(false));
// 1.52.3: the screens use /api/v2, so its routes must be blocked too.
test.each([
['POST', '/api/v2/events'],
['PATCH', '/api/v2/events/ev_1'],
['POST', '/api/v2/events/ev_1/copy-from'],
['POST', '/api/v2/events/ev_1/ticket-options'],
['PATCH', '/api/v2/ticket-options/opt_1'],
['PATCH', '/api/v2/variants/var_1'],
['POST', '/api/v2/registrations'],
['POST', '/api/v2/registrations/manual'],
['PUT', '/api/v2/registrations/abc/items'],
['POST', '/api/v2/registrations/abc/checkout'],
['POST', '/api/v2/registrations/abc/payment-link/send'],
['POST', '/api/v2/payments'],
['POST', '/api/v2/payment-links/chk_1'],
['POST', '/api/v2/waitlist'],
['POST', '/api/v2/season-passes/p1/buy'],
['POST', '/api/v2/volunteer-roles/r1/sign-up'],
])('v2 %s %s is blocked', (m, p) => expect(isBlocked(m, p)).toBe(true));
test.each([
['POST', '/api/v2/checkin/QR123'],
['GET', '/api/v2/events'],
['PUT', '/api/v2/events/ev_1/notify-recipients'],
['POST', '/api/v2/registrations/abc/cancel'],
['POST', '/api/v2/payments/refunds'],
['POST', '/api/v2/me/privacy/requests'],
['POST', '/api/v2/auth/login'],
])('v2 %s %s keeps working', (m, p) => expect(isBlocked(m, p)).toBe(false));
test('every blocked v2 path is a real v2 route', () => {
const doc = require('../../frontend/src/lib/api-v2/openapi.json');
const routes = Object.entries(doc.paths).flatMap(([p, ops]) =>
Object.keys(ops).map(m => [m.toUpperCase(), `/api/v2${p.replace(/\{[^}]+\}/g, 'x1')}`]));
const { BLOCKED } = require('../src/hub/readOnly');
const unmatched = BLOCKED
.filter(([, re]) => re.source.startsWith('^\\/api\\/v2'))
.filter(([m, re]) => !routes.some(([rm, rp]) => rm === m && re.test(rp)))
.map(([m, re]) => `${m} ${re}`);
expect(unmatched).toEqual([]);
});
});
describe('pasting the Hub public key', () => {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "hope-events-frontend",
"version": "1.52.2",
"version": "1.52.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hope-events-frontend",
"version": "1.52.2",
"version": "1.52.3",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@hookform/resolvers": "^5.2.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events-frontend",
"version": "1.52.2",
"version": "1.52.3",
"private": true,
"scripts": {
"dev": "next dev",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "hope-events",
"version": "1.52.2",
"version": "1.52.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "hope-events",
"version": "1.52.2",
"version": "1.52.3",
"license": "ISC",
"devDependencies": {
"@axe-core/playwright": "^4.14.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hope-events",
"version": "1.52.2",
"version": "1.52.3",
"main": "index.js",
"scripts": {
"dev:backend": "cd backend && npm run dev",