Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b28c8a8876 | ||
|
|
31aa787b17 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+2
-2
@@ -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,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": {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Generated
+2
-2
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "hope-events-frontend",
|
||||
"version": "1.52.2",
|
||||
"version": "1.52.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Generated
+2
-2
@@ -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
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user