Store Terms version alongside acceptance timestamp

Reviewer's follow-up: it's not enough to record when a user accepted
the Terms; we also need to record which version of the text they saw.

Changes:
- journal.users gains a nullable `terms_version` text column (nullable
  so the three pre-existing users without a version are kept as-is).
- New apps/journal/app/lib/legal.ts exports TERMS_VERSION as a single
  source of truth, reused by the legal pages' "Last updated" header
  and by the registration flow as the value to send/store.
- Registration form posts `termsVersion` alongside `termsAccepted` on
  all three relevant steps (start, finish, register-magic-link).
- API route validates that `termsVersion` is a non-empty string on
  any step that requires terms, and forwards it to the auth server.
- auth.server finishRegistration and registerWithMagicLink now take
  `termsVersion` and persist it on the users row.
- journal-auth spec gets a new scenario for version storage and a
  rejection scenario for missing version.

PRIVACY_LAST_UPDATED is also exported from the same module and used
by the Privacy page header, keeping both pages on a single legal.ts
source of truth for "last updated" labels. Privacy is not per-user
stored — it's informational, not contract.

Existing users have NULL terms_version; if we ever prompt them to
re-accept updated Terms, we can backfill with the version they
re-accept at that point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-19 07:46:34 +02:00
parent c532a1024b
commit 9c4c3d6444
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
8 changed files with 69 additions and 10 deletions

View file

@ -0,0 +1,19 @@
/**
* Version identifier for the currently-published Terms of Service.
*
* Stored on `users.terms_version` when a user accepts the Terms at
* registration. Bump this string whenever the Terms text changes in a way
* that warrants a re-acceptance typically on each legal-review update.
*
* Kept as a plain date string (the "Last updated" date shown on the Terms
* page itself) so spec, storage, and UI stay in lockstep without a separate
* versioning scheme.
*/
export const TERMS_VERSION = "2026-04-19";
/**
* "Last updated" date shown on the Privacy Policy. Privacy changes don't
* require re-acceptance (the policy is informational, not contract), so this
* is display-only not persisted.
*/
export const PRIVACY_LAST_UPDATED = "2026-04-19";