studio-os 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -3
- package/.gitattributes +8 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +49 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +30 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/.github/dependabot.yml +33 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/guards.yml +45 -0
- package/CHANGELOG.md +159 -86
- package/README.md +270 -253
- package/SECURITY.md +27 -0
- package/SPEC.md +4 -4
- package/package.json +54 -54
- package/runner-lock.json +15 -0
- package/src/app.js +163 -141
- package/src/db/003-stripe-invoices.sql +6 -0
- package/src/db/index.js +83 -82
- package/src/routes/me.js +1 -1
- package/src/routes/public.js +13 -3
- package/src/services/auth.js +88 -81
- package/src/services/booking.js +19 -13
- package/src/services/stripe.js +44 -1
- package/test/booking-link.test.js +47 -0
- package/test/deps.test.js +108 -0
- package/test/error-paths.test.js +49 -0
- package/test/membership-cycle.test.js +78 -0
- package/test/proxy-auth.test.js +114 -0
- package/test/stripe-halfconfig.test.js +98 -0
- package/test/stripe.test.js +84 -1
- package/test/web.test.js +10 -1
- package/views/admin/dashboard.ejs +1 -1
- package/views/admin/settings.ejs +7 -2
- package/views/public/book_result.ejs +5 -5
- package/PROGRESS.md +0 -60
package/.env.example
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
# Public base URL used in emails/magic links (default http://localhost:PORT)
|
|
15
15
|
#BASE_URL=https://booking.example.com
|
|
16
16
|
|
|
17
|
-
# Set when running behind a reverse proxy (Caddy/nginx/Traefik)
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# Set when running behind a reverse proxy (Caddy/nginx/Traefik). Rate limiting
|
|
18
|
+
# then keys on the first X-Forwarded-For hop, and X-Forwarded-Proto is trusted
|
|
19
|
+
# so emailed magic links and Stripe redirects use https and the session cookie
|
|
20
|
+
# is marked Secure. Leave unset otherwise — both headers are client-spoofable
|
|
21
|
+
# without a trusted proxy in front.
|
|
20
22
|
#TRUST_PROXY=1
|
|
21
23
|
|
|
22
24
|
# --- SMTP (optional). When unset, emails are written to data/outbox/*.eml
|
|
@@ -29,6 +31,8 @@
|
|
|
29
31
|
|
|
30
32
|
# --- Stripe (optional, bring-your-own account). When unset, purchase flows
|
|
31
33
|
# fall back to "pay at studio" and manual activation.
|
|
34
|
+
# Online payment needs BOTH of the first two. With only the secret key the app
|
|
35
|
+
# keeps using pay-at-studio, because the webhook is what creates the pass.
|
|
32
36
|
#STRIPE_SECRET_KEY=sk_test_...
|
|
33
37
|
#STRIPE_WEBHOOK_SECRET=whsec_...
|
|
34
38
|
#STRIPE_PUBLISHABLE_KEY=pk_test_...
|
package/.gitattributes
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Normalise to LF in the repository and in the working tree on every platform.
|
|
2
|
+
# A Windows clone with core.autocrlf=true otherwise rewrites shebangs to CRLF,
|
|
3
|
+
# and a publish from that clone ships a binary Linux rejects as "bad interpreter".
|
|
4
|
+
* text=auto eol=lf
|
|
5
|
+
|
|
6
|
+
# Collapsed in pull requests and excluded from the language bar. Dependabot
|
|
7
|
+
# opens these weekly and the churn is machine-written, not reviewable by eye.
|
|
8
|
+
package-lock.json linguist-generated=true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github: Booyaka101
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something didn't work the way it should
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for the report. Please **never paste Stripe keys, session cookies or customer data** — screenshots of a live studio leak more than people expect. Redact before posting.
|
|
9
|
+
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: what
|
|
12
|
+
attributes:
|
|
13
|
+
label: What happened
|
|
14
|
+
description: What you ran, and what came back.
|
|
15
|
+
placeholder: |
|
|
16
|
+
$ npm start
|
|
17
|
+
Error: ...
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: expected
|
|
23
|
+
attributes:
|
|
24
|
+
label: What you expected instead
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
27
|
+
|
|
28
|
+
- type: input
|
|
29
|
+
id: version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Studio OS version
|
|
32
|
+
description: From `package.json`, or the tag you deployed.
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
|
|
36
|
+
- type: input
|
|
37
|
+
id: runtime
|
|
38
|
+
attributes:
|
|
39
|
+
label: How you are running it
|
|
40
|
+
description: Node version, or the Docker image tag, plus the OS and the database you pointed it at.
|
|
41
|
+
placeholder: Docker, node:22-alpine, Postgres 16 on Debian 12
|
|
42
|
+
validations:
|
|
43
|
+
required: true
|
|
44
|
+
|
|
45
|
+
- type: textarea
|
|
46
|
+
id: impact
|
|
47
|
+
attributes:
|
|
48
|
+
label: Did this affect real bookings or payments?
|
|
49
|
+
description: Say so plainly if it did. Anything touching money or a customer's booking gets looked at first.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Stripe API documentation
|
|
4
|
+
url: https://docs.stripe.com/api
|
|
5
|
+
about: Payment intents, webhooks and payout timing are Stripe's behaviour, not this app's.
|
|
6
|
+
- name: Security issue
|
|
7
|
+
url: https://github.com/Booyaka101/studio-os/security/advisories/new
|
|
8
|
+
about: Please report vulnerabilities privately, not as a public issue.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest something Studio OS should do
|
|
3
|
+
labels: [enhancement]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Self-hosted. It holds your customers' booking data and talks to your own payment account. Nothing is sent to us.
|
|
9
|
+
|
|
10
|
+
- type: textarea
|
|
11
|
+
id: problem
|
|
12
|
+
attributes:
|
|
13
|
+
label: What are you trying to do?
|
|
14
|
+
description: The situation, not the solution. What got in your way?
|
|
15
|
+
validations:
|
|
16
|
+
required: true
|
|
17
|
+
|
|
18
|
+
- type: textarea
|
|
19
|
+
id: proposal
|
|
20
|
+
attributes:
|
|
21
|
+
label: What would you like it to do?
|
|
22
|
+
validations:
|
|
23
|
+
required: false
|
|
24
|
+
|
|
25
|
+
- type: textarea
|
|
26
|
+
id: alternatives
|
|
27
|
+
attributes:
|
|
28
|
+
label: What are you doing instead today?
|
|
29
|
+
validations:
|
|
30
|
+
required: false
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
## What this changes
|
|
2
|
+
|
|
3
|
+
<!-- And why. Link an issue if there is one. -->
|
|
4
|
+
|
|
5
|
+
## How you verified it
|
|
6
|
+
|
|
7
|
+
<!--
|
|
8
|
+
Be specific, and keep the claims separate — they carry different weight:
|
|
9
|
+
- `npm test` passes
|
|
10
|
+
- ran it against a real project
|
|
11
|
+
- added a test case covering this
|
|
12
|
+
-->
|
|
13
|
+
|
|
14
|
+
## Checklist
|
|
15
|
+
|
|
16
|
+
- [ ] `npm test` passes
|
|
17
|
+
- [ ] Added or updated a test for this change
|
|
18
|
+
- [ ] Updated `README.md` if behaviour changed
|
|
19
|
+
- [ ] Added a `CHANGELOG.md` entry under "Unreleased"
|
|
20
|
+
- [ ] No Stripe key, session cookie or customer data in the diff or in any screenshot
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
# Staying current is a standing requirement, so it is automated rather than
|
|
4
|
+
# remembered.
|
|
5
|
+
updates:
|
|
6
|
+
- package-ecosystem: npm
|
|
7
|
+
directory: /
|
|
8
|
+
schedule:
|
|
9
|
+
interval: weekly
|
|
10
|
+
open-pull-requests-limit: 10
|
|
11
|
+
commit-message:
|
|
12
|
+
prefix: 'chore(deps)'
|
|
13
|
+
groups:
|
|
14
|
+
# Minor and patch only. Majors deliberately arrive as their own PR so one
|
|
15
|
+
# breaking upgrade cannot redden, and block, a batch of safe ones.
|
|
16
|
+
all-minor-patch:
|
|
17
|
+
patterns: ['*']
|
|
18
|
+
update-types: ['minor', 'patch']
|
|
19
|
+
|
|
20
|
+
- package-ecosystem: github-actions
|
|
21
|
+
directory: /
|
|
22
|
+
schedule:
|
|
23
|
+
interval: weekly
|
|
24
|
+
open-pull-requests-limit: 10
|
|
25
|
+
commit-message:
|
|
26
|
+
prefix: 'chore(ci)'
|
|
27
|
+
groups:
|
|
28
|
+
# Minor and patch only. Majors deliberately arrive as their own PR so one
|
|
29
|
+
# breaking upgrade cannot redden, and block, a batch of safe ones.
|
|
30
|
+
actions-minor-patch:
|
|
31
|
+
patterns: ['*']
|
|
32
|
+
update-types: ['minor', 'patch']
|
|
33
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
|
|
18
|
+
# Our own tool. Locks the runner image's tool versions and diffs them when
|
|
19
|
+
# GitHub bumps the image — including when a floating label like
|
|
20
|
+
# ubuntu-latest silently re-points. Report-only; no fail-on yet.
|
|
21
|
+
- name: Runner image drift
|
|
22
|
+
uses: Booyaka101/runner-drift@v1
|
|
23
|
+
with:
|
|
24
|
+
fail-on-retirement: 60
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-node@v7
|
|
27
|
+
with:
|
|
28
|
+
node-version: '22'
|
|
29
|
+
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npm test
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Guards
|
|
2
|
+
|
|
3
|
+
# Our own shipped tools, run against this repo. If they are worth shipping they
|
|
4
|
+
# are worth depending on ourselves — a guard that never runs on our own code is
|
|
5
|
+
# an untested guard.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
pull_request:
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: '0 6 * * 1'
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
guards:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-node@v7
|
|
24
|
+
with:
|
|
25
|
+
node-version: '22'
|
|
26
|
+
|
|
27
|
+
- name: Install-script audit
|
|
28
|
+
run: npx --yes npm-script-lens@latest audit --path .
|
|
29
|
+
|
|
30
|
+
# Version cooldown: npm worms are typically identified within hours, so
|
|
31
|
+
# refuse to be among the first to install a brand-new version.
|
|
32
|
+
- name: Dependency cooldown
|
|
33
|
+
run: npx --yes npm-script-lens@latest audit --path . --cooldown
|
|
34
|
+
|
|
35
|
+
# Drift gate: fails if a dependency gains an install script nobody approved.
|
|
36
|
+
- name: Allowlist drift
|
|
37
|
+
run: npx --yes npm-script-lens@1.4.0 sync --path . --check
|
|
38
|
+
|
|
39
|
+
# Shell/Dockerfile/Terraform/PHP — the file types GitHub gated behind paid
|
|
40
|
+
# Advanced Security. This repo ships a Dockerfile. Reports without gating.
|
|
41
|
+
- name: Free GHAS-equivalent scan
|
|
42
|
+
continue-on-error: true
|
|
43
|
+
uses: Booyaka101/ghas-free-pack@v1
|
|
44
|
+
with:
|
|
45
|
+
github-token: ${{ github.token }}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,86 +1,159 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
on
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- **
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- **
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
-
|
|
79
|
-
(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 — 2026-09-27
|
|
4
|
+
|
|
5
|
+
**Upgrading with Stripe:** add `checkout.session.async_payment_succeeded` and
|
|
6
|
+
`invoice.paid` to your webhook endpoint's events. Without them, delayed
|
|
7
|
+
payments never turn into passes and renewals still go unrecorded. The database
|
|
8
|
+
migrates itself on start (schema v3, one new column on `payments`).
|
|
9
|
+
|
|
10
|
+
- **Security: booking in someone else's name handed over their magic link.**
|
|
11
|
+
Booking takes only a name and an email, and the confirmation page always
|
|
12
|
+
showed the booked client's `/me` link, a 7-day token to their bookings,
|
|
13
|
+
passes and cancel buttons. With SMTP configured the link now only goes out
|
|
14
|
+
by email. Without SMTP it's still shown on screen, as before, and the
|
|
15
|
+
README says what that means.
|
|
16
|
+
- **Security: a 100kb POST to any public form leaked a stack trace.** The
|
|
17
|
+
body-size 413 reached the error handler before the layout locals were set,
|
|
18
|
+
`error.ejs` threw, and Express's fallback answered 500 with the
|
|
19
|
+
ReferenceError and server paths (hidden only when `NODE_ENV=production`,
|
|
20
|
+
which the Docker image sets). The error page now renders on its own and 4xx
|
|
21
|
+
statuses stay 4xx.
|
|
22
|
+
- **Stripe renewals were never recorded.** Only a membership's first month
|
|
23
|
+
showed up as a payment, so the revenue report undercounted every
|
|
24
|
+
subscription after month one. `invoice.paid` now records each renewal,
|
|
25
|
+
keyed on the invoice id so webhook retries can't count a month twice.
|
|
26
|
+
- **Delayed Stripe payments got a pass before the money cleared.** Bank
|
|
27
|
+
debits and other delayed methods complete Checkout with `payment_status:
|
|
28
|
+
unpaid`, and fulfilment ignored that. Unpaid sessions now wait for
|
|
29
|
+
`checkout.session.async_payment_succeeded`.
|
|
30
|
+
- **Mindbody imports past a few hundred clients got a 413.** Admin → Import
|
|
31
|
+
now accepts up to 20mb.
|
|
32
|
+
- **The waitlist page said "You're # in line"** with no number. It now shows
|
|
33
|
+
the actual position.
|
|
34
|
+
- `GET /me` without a token answered 500 instead of the "link expired" page.
|
|
35
|
+
- **Dependencies**: dotenv 17 → 18, nodemailer → 10.0.10.
|
|
36
|
+
- **Tests**: 127 → 135.
|
|
37
|
+
|
|
38
|
+
## 0.3.0 — 2026-09-17
|
|
39
|
+
|
|
40
|
+
- **Security: magic links were emailed as `http://` behind a reverse proxy.**
|
|
41
|
+
`TRUST_PROXY` was read by the rate limiter but Express's own `trust proxy`
|
|
42
|
+
setting was never applied, so with TLS terminating at the proxy
|
|
43
|
+
`req.protocol` read `http` and every emailed link carried its 7-day auth
|
|
44
|
+
token over plaintext. Stripe's success/cancel redirects had the same scheme.
|
|
45
|
+
Setting `TRUST_PROXY=1` now also makes the app honour `X-Forwarded-Proto`,
|
|
46
|
+
and the session cookie picks up its `Secure` flag over https (it stays off
|
|
47
|
+
on plain http, so local dev is unchanged). If you run behind Caddy/nginx/
|
|
48
|
+
Traefik, set `TRUST_PROXY=1` or pin `BASE_URL` to your https origin.
|
|
49
|
+
- **Security: the staff login could enumerate accounts.** `authenticate()`
|
|
50
|
+
returned early for an unknown email and skipped bcrypt entirely, answering
|
|
51
|
+
~3000x faster than for a real one. Both paths now do one bcrypt comparison.
|
|
52
|
+
`app_secret` is also no longer reachable from templates.
|
|
53
|
+
- **A half-configured Stripe took money and delivered nothing.** Online
|
|
54
|
+
checkout was offered whenever `STRIPE_SECRET_KEY` was set, but fulfilment
|
|
55
|
+
needs `STRIPE_WEBHOOK_SECRET` too. Without it the client paid at Stripe,
|
|
56
|
+
`/webhooks/stripe` rejected the callback with 501, and no pass and no payment
|
|
57
|
+
row was created. Checkout is now offered only when both are set, otherwise
|
|
58
|
+
purchases use the existing pay-at-studio flow. The admin dashboard and
|
|
59
|
+
settings page say which key is missing instead of reporting "connected".
|
|
60
|
+
- **Membership credit cycles drifted for end-of-month joins.** Cycles were
|
|
61
|
+
stepped from the previous cycle, so February's day clamp compounded: a
|
|
62
|
+
membership joined on the 31st became the 28th and stayed there. Cycles now
|
|
63
|
+
anchor on the join date (31 Jan, 28 Feb, 31 Mar). Correcting an
|
|
64
|
+
already-drifted membership does not refill its credits mid-month.
|
|
65
|
+
- **Node 22 is now the minimum** (was 20), and better-sqlite3 moves 11 → 13.
|
|
66
|
+
The Docker image already ships Node 22, so this only matters if you run the
|
|
67
|
+
app straight off your own Node. README and SPEC still said 20+; they now
|
|
68
|
+
agree with `engines`.
|
|
69
|
+
- **Dependencies**: stripe 16 → 22, nodemailer 9 → 10 (clears
|
|
70
|
+
GHSA-8m3c-c648-2xjj), marked → 18.0.13. `npm audit` is clean.
|
|
71
|
+
- **Tests**: 110 → 127. The Stripe and mailer suites only ever ran against a
|
|
72
|
+
mock client and the offline outbox, so neither package was imported under
|
|
73
|
+
test and a major bump could not have failed CI. New tests exercise the real
|
|
74
|
+
packages, including webhook signature verification.
|
|
75
|
+
|
|
76
|
+
## 0.2.0 — 2026-07-28
|
|
77
|
+
|
|
78
|
+
- **Instructor logins.** New `instructor` user role alongside the existing
|
|
79
|
+
owner/staff (admin) roles — existing accounts are untouched by the
|
|
80
|
+
migration (v2 rebuilds the `users` CHECK; SQLite cannot alter one in
|
|
81
|
+
place). Instructors sign in on the same staff login form (`/login` now
|
|
82
|
+
aliases `/admin/login`) and land on their own portal:
|
|
83
|
+
- `GET /instructor/schedule` — the classes they are assigned to, from
|
|
84
|
+
today onward, sorted by date
|
|
85
|
+
- `GET /instructor/classes/:id/roster` — attendee list with status
|
|
86
|
+
(booked / checked-in / no-show / cancelled), class time and capacity;
|
|
87
|
+
client emails are *not* exposed to instructors
|
|
88
|
+
- `POST /instructor/classes/:id/checkin/:booking_id` — check an attendee
|
|
89
|
+
in (same `markAttendance` business logic as the admin roster)
|
|
90
|
+
- Route guards: every `/admin/*` route returns **403** for instructor
|
|
91
|
+
accounts; rosters and check-in return **403** for classes the
|
|
92
|
+
instructor is not assigned to.
|
|
93
|
+
- **Admin: instructor account management.** The Instructors page gains an
|
|
94
|
+
"Instructor logins" section; `/admin/instructors/new` creates an account
|
|
95
|
+
(name, email, password ≥ 8 chars, role `instructor`);
|
|
96
|
+
`/admin/instructors/:id/classes` assigns/unassigns upcoming classes via
|
|
97
|
+
checkboxes (many-to-many `instructor_class_assignments` table, cascade
|
|
98
|
+
on account/instance deletion; past assignments are left untouched).
|
|
99
|
+
- 12 new tests (guards, scoping, check-in, admin CRUD, migration; the four
|
|
100
|
+
new views covered by the integrity suite) — 110 total, no network.
|
|
101
|
+
Upgrade path verified against a real v1 database file.
|
|
102
|
+
|
|
103
|
+
## 0.1.0-hardening — 2026-07-28
|
|
104
|
+
|
|
105
|
+
- **Fix: CSRF inputs injected inside attribute values.** The hardening pass had
|
|
106
|
+
inserted the hidden `_csrf` input at the wrong offset in 22 forms whose
|
|
107
|
+
`action` contained an EJS expression, corrupting the submit URL and leaking
|
|
108
|
+
markup as visible text. All POST forms now carry the token immediately after
|
|
109
|
+
the form open tag, enforced by a new static views-integrity test suite
|
|
110
|
+
(`test/views.test.js`), and verified with a real in-browser form submission.
|
|
111
|
+
- **README screenshots**: 8 captured views of the seeded demo studio (public
|
|
112
|
+
schedule/booking/buy, admin dashboard/schedule/roster/client/reports).
|
|
113
|
+
- **Docker verified end-to-end**: `docker compose build && up -d` → setup
|
|
114
|
+
wizard, admin class creation, public schedule and a guest booking all
|
|
115
|
+
exercised against the running container, then `down -v`. Fixed
|
|
116
|
+
`docker-compose.yml`: an `environment:` key with only commented entries is
|
|
117
|
+
invalid YAML for compose ("must be a mapping") — the example block is now
|
|
118
|
+
fully commented out.
|
|
119
|
+
- **Rate limiting**: dependency-free in-memory fixed-window limiter
|
|
120
|
+
(`src/lib/ratelimit.js`), keyed per client IP + route: magic-link requests
|
|
121
|
+
5/15 min, admin login 10/15 min, public booking/buy POSTs 30/15 min;
|
|
122
|
+
friendly 429 with `Retry-After`. `X-Forwarded-For` is honored only when
|
|
123
|
+
`TRUST_PROXY` is set. Single-process counters (reset on restart) — see the
|
|
124
|
+
new README Security section.
|
|
125
|
+
- **CSRF protection**: session-bound random token (stored in the signed
|
|
126
|
+
cookie-session, no deprecated `csurf` dependency), hidden `_csrf` input on
|
|
127
|
+
every state-changing form (public booking/buy/magic-link, client cancel,
|
|
128
|
+
setup wizard, admin login and all admin forms); POST/PUT/DELETE/PATCH
|
|
129
|
+
without a valid token → 403. `/webhooks/stripe` exempt (Stripe-signature
|
|
130
|
+
verified raw body instead). `x-csrf-token` header accepted as an
|
|
131
|
+
alternative to the form field.
|
|
132
|
+
|
|
133
|
+
## 0.1.0 — 2026-07-27
|
|
134
|
+
|
|
135
|
+
First release. Single-studio, self-hosted, bring-your-own Stripe.
|
|
136
|
+
|
|
137
|
+
- Public schedule (14-day view, filters), class pages, guest booking with
|
|
138
|
+
waiver capture, returning-client booking by email
|
|
139
|
+
- Booking engine: membership → soonest-expiring pack → drop-in payment
|
|
140
|
+
resolution; atomic capacity; FIFO waitlist with auto-promotion;
|
|
141
|
+
cancellation-window refunds; late-cancel forfeit/refund policy; attendance
|
|
142
|
+
- Client self-service via HMAC magic links (no client passwords): view
|
|
143
|
+
upcoming bookings, cancel within policy
|
|
144
|
+
- Rolling 8-week schedule materialization from weekly rules (boot + daily +
|
|
145
|
+
on change), one-off classes, class cancellation with notify + refund
|
|
146
|
+
- Admin: dashboard (today's rosters, week revenue, expiring passes), class
|
|
147
|
+
types / instructors / weekly rules CRUD, roster check-in/no-show/walk-in,
|
|
148
|
+
client profiles (manual passes, memberships, payments, waiver, magic link),
|
|
149
|
+
products (packs + membership plans), revenue & attendance reports + CSV,
|
|
150
|
+
settings, SQLite backup via `VACUUM INTO`
|
|
151
|
+
- Buy page: Stripe Checkout for packs (one-time) and memberships
|
|
152
|
+
(subscription) with signature-verified, idempotent webhook fulfillment;
|
|
153
|
+
full manual "pay at studio" fallback when Stripe is unconfigured
|
|
154
|
+
- Email via SMTP, or `data/outbox/*.eml` + on-screen links when unset
|
|
155
|
+
- Mindbody CSV importer (clients + pricing options): CLI + admin page,
|
|
156
|
+
JSON column mapping with defaults, dry-run, idempotent by email
|
|
157
|
+
- Seed script (`npm run seed`), Dockerfile + docker-compose, PWA manifest +
|
|
158
|
+
service worker, vendored htmx/Pico.css (no CDN), 67 tests (no network)
|
|
159
|
+
|