v0.1.2 — 2FA lockout and fresh-install fixes
Upgrade from v0.1.1 as soon as practical. Two bugs in that release
are severe enough on their own to justify this one:
- Any account with 2FA enabled was permanently locked out of login,
with no way back in — not even via a recovery code. The login
response's 2FA-pending branch never set the CSRF cookietotp/verify
requires, so everytotp/verifycall (including recovery-code
attempts) 403'd with "CSRF token mismatch," unconditionally, for
every account. If you're running v0.1.1 and have 2FA enabled on
any staff account, upgrade before enrolling further users — those
accounts have been unable to log in since enabling it, and existing
locked-out accounts will need a staff admin to disable 2FA for them
(POST /auth/totp/disable, or directly via the database) after
upgrading, since the fix doesn't retroactively unlock an in-progress
login attempt. - A genuinely fresh install failed immediately on the exact command
the docs tell you to run. Everydeploy/native/*.shscript an admin
runs directly was git-tracked without the executable bit;sudo ./install-native.shright aftergit clonedied at "Permission
denied" trying to execsetup-database.sh. Anyone who successfully
installed v0.1.1 did so from a checkout that had picked up a local,
uncommittedchmod +xsomewhere along the way (e.g. copying files
instead of cloning) — a literal fresh clone was never viable.
Fixed
- The 2FA lockout above also had a second, independent bug: the
code-verification schema only accepted 6 digits, rejecting every
recovery code'sXXXXX-XXXXXformat before it reached the
already-correct recovery-code check. Both fixed together; verified
live end-to-end (enroll, login-requires-code, wrong code rejected,
recovery code works once and is rejected on reuse). - Company config had no way to set
companyGstin/gstStateCodeafter
the initial seed, despite the frozen booking service already reading
gstStateCodeto decide CGST+SGST vs IGST. Added, with GSTIN format
validation (checksum digit deferred — seedocs/todo.md) and a
Company Config UI section. - Health endpoint's
versionfield was hardcoded0.1.0in every real
deployment (npm_package_versionis only set bypnpm run, never by
systemd's/Docker's directnode dist/main.js). Now reads
package.jsondirectly. - 18 of 19 generic master types (unit types, inquiry sources, charge
types, etc.) 500'd on any create/update call that included a
description —createMasterSchema's optionaldescriptionfield is
only backed by a real column onPaymentPlanTemplate; the shared
factory blindly spread the whole dto into Prisma'sdata. Fixed once,
at the root, in the factory. DocumentType,InterestRule, andTransferFeeRulecouldn't be
created via the API at all — each has its own required, non-nullable
columns (entityType;rateType/ratePercent/frequency;
feeType) that the generic master schema never had, so every attempt
500'd on a Prisma "Argument missing" error instead of failing
validation cleanly. The shared factory now supports a per-model
extraFieldsschema extension.LetterTemplatehad no working create path at all — zero templates
could ever exist, blocking demand/allotment/reminder letter generation
entirely. It was routed through the generic master factory (whose
schema has nosubject/entityType/body) instead of a dedicated
module; given one, mirroring the existingSmsTemplateModule
precedent, with merge-field validation at save time.- Duplicate master names (any of the 18+ shared-factory types) returned
a raw 500 instead of a clean "already exists" 400 — nothing in this
codebase had ever caught Prisma's P2002 unique-constraint error for
these dynamically-keyed services (unlikeRolesService/UsersService,
which pre-check viafindFirst). Mapped once, in the factory. install-native.sh/upgrade-native.sh's database migration step
failed on any host where the git checkout lives under a directory
tree thepostgresOS user can't traverse (e.g. GitHub Actions
runners:/home/runneris mode0750) — Prisma 6.19+ auto-discovers
aprisma.config.*file in the current working directory before
running any command, and that lookup'slstat()failsEACCES(not
ENOENT) in that case, which Prisma treats as a hard failure rather
than "no config file, proceed."run_as_superuser()now runs from
the already-world-traversable release directory instead of the
checkout.install-native.sh's finalsystemctl reload nginxfailed outright
("nginx.service is not active, cannot reload") on any host where apt
installed nginx without starting it — the script only ever checked
that thenginxbinary was present, never that the service was
running. Nowenables andreload-or-restarts it, correct whether
nginx was already running or not.CustomFieldDefinition.defaultValue— accepted by the create/update
schema since it was written, but no backing column ever existed, so
any real caller sending it 500'd. Added the missing column.POST /usersnever returned thephoneit had just saved — a
selectallowlist copy/paste gap (present inupdate(), missing
fromcreate()) left an admin with no way to confirm the phone
number was stored.
Everything above except the two headline bugs was found by a full
production-readiness pass and a new through-the-wire creation test for
every master type and admin-creatable entity (users, roles, custom
fields) — the existing suite seeded rows directly, which is exactly why
these bugs survived to a tagged release. Also added: real-HTTP creation
coverage for all 22 master types plus users/roles/custom fields, and a
native-install CI job that runs the full native install on a real
ubuntu-latest runner on every push. That CI job is not yet green — a
separate, CI-runner-specific issue (the deployed app crash-looping with
SIGSEGV, isolated to argon2's native module, confirmed not to reproduce
on a real server) is still open and tracked in CLAUDE.md; it does not
affect real installs, only that one job's own coverage.