| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-07-31 | 6.2 kB | |
| v0.1.2 -- 2FA lockout and fresh-install fixes source code.tar.gz | 2026-07-31 | 734.9 kB | |
| v0.1.2 -- 2FA lockout and fresh-install fixes source code.zip | 2026-07-31 | 1.0 MB | |
| Totals: 3 Items | 1.8 MB | 0 | |
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 cookie
totp/verifyrequires, 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. Every
deploy/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's
XXXXX-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 readinggstStateCodeto 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 readspackage.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-modelextraFieldsschema 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 existingSmsTemplateModuleprecedent, 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 (unlike
RolesService/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(notENOENT) 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 — aselectallowlist 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.