- Fix session store expiry: cookie.maxAge is already in milliseconds, so
stored sessions outlived the cookie by 1000x
- Regenerate the session ID on login, first-run setup, and OIDC login to
prevent session fixation
- Mark session cookies Secure on TLS connections (secure: 'auto') and add
TRUST_PROXY support for reverse-proxy deployments
- Build password reset links from APP_BASE_URL instead of the Host header
to prevent reset-link poisoning
- Rate-limit forgot-password requests (5 per IP per 15 minutes)
- Strip OIDC debug logging that leaked authorization codes, subject IDs,
and emails to logs
OIDC configuration now comes from environment variables instead of
the database settings table. This is more natural for Docker/compose
deployments where secrets live in .env files.
Env vars: OIDC_ENABLED, OIDC_DISCOVERY_URL, OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_BUTTON_LABEL.
Also adds detailed [oidc] console logging throughout the authorize,
callback, and link flows to aid debugging connection issues.
Removes the OIDC settings UI section from the admin modal and the
GET/PUT /api/settings/oidc endpoints.
Add OpenID Connect as an alternative login method. Users can sign in
via an external identity provider (e.g., Authentik, Keycloak, Google).
- OIDC settings configured in admin UI (discovery URL, client ID/secret,
redirect URI, button label, enable/disable toggle)
- PKCE-based authorization code flow with state and nonce validation
- Admin can manually link any user's OIDC identity (sub/issuer fields)
- Self-service linking: logged-in users can link/unlink their own account
- SSO button conditionally shown on login page when OIDC is enabled
- Username in header now clickable to open profile for all users
- Callback errors/success communicated via URL hash fragments
Password reset: users with a registered email can request a reset link
from the login screen. A one-hour signed token is emailed via SMTP;
clicking the link opens a set-new-password form. Tokens are hashed
(SHA-256) before storage and invalidated after use.
SMTP settings: admin-only panel in the Users modal lets admins
configure host, port, encryption, credentials, and from address.
Settings persisted in a new key-value settings table. The SMTP
password is never returned to the client.
Users: email field added to the create/edit form and stored in a new
users.email column. Email is used for password reset lookup.
Add Account: admins now have a + button in the header that opens the
existing setup wizard to add additional checking accounts.
Schema: adds password_reset_tokens and settings tables with automatic
runtime migrations for existing databases.
IDOR (critical): GET /api/checks/:id and GET /api/deposits/:id now
verify the requesting user has access to the record's account before
returning data. Previously any authenticated user could fetch any
record by ID across accounts.
Printed check guard (critical): PUT and DELETE on checks now return
409 if the check has already been printed, enforcing the business rule
that printed checks are immutable. Previously the printed flag was
only enforced in the frontend.
PDF DoS (medium): checkIds array capped at 300 (100 pages × 3 per page).
QBO import DoS (medium): records array capped at 1000 per confirm call.
PDF error detail (medium): internal err.message no longer returned to
the client on PDF generation failure.
SESSION_SECRET (low): removed NODE_ENV=production condition — the
server now exits immediately on startup if SESSION_SECRET is unset
regardless of environment. Dev script updated to load .env via
node --env-file=.env so developers set it once in a local .env file.
Password hints (low): updated all three UI labels from "min 8 chars"
to "min 10 chars, include a digit or symbol" to match the actual
server-side validation.
Content-Security-Policy: add header with default-src 'self',
unsafe-inline for styles (needed for JS-generated inline style attrs),
and data: for embedded logo/signature images.
JSON body limit: reduce from 10mb to 2mb (logo cap is 512KB base64).
Session maxAge: now configurable via SESSION_MAX_AGE_HOURS env var
(default 168h / 7 days). Documented in .env.example.
Password strength: centralize validation in auth.js and raise the bar
to 10+ characters with at least one letter and one non-letter. Applied
consistently to all four password-setting paths (initial setup,
login change-password, admin create user, admin edit user).
CSRF: upgrade session cookie sameSite from 'lax' to 'strict'.
Rate limiting: login endpoint now blocks an IP after 10 failed attempts
in a 15-minute window; resets on success. In-memory, no new dependency.
SESSION_SECRET: server exits at startup when NODE_ENV=production and
SESSION_SECRET is unset. docker-compose.yml updated to pass it via env;
.env.example added with generation instructions.
Security headers: add X-Content-Type-Options, X-Frame-Options, and
Referrer-Policy to all responses.
Sensitive data: routing_number and account_number are now omitted from
GET /api/account/:id responses for non-admin users.
Image size: logo upload capped at 512 KB in the account PUT handler.
Amount validation: checks (POST/PUT) and deposit items (POST/PUT) now
reject non-finite and non-positive amounts.
QBO import: uploaded file is rejected if its MIME type is not text or
a known CSV variant.
mark-printed: was only checking the first check's account — now fetches
all check IDs upfront, verifies they all exist and share the same
account, then checks editor access once on that account.
PDF generation: was authorizing against the client-supplied account_id
but fetching checks by ID without confirming they belong to that account
— now rejects any check ID whose account_id doesn't match.
Role/account-assignment changes: active sessions for the affected user
are now deleted immediately via json_extract on the sessions table, so
demotions take effect at once rather than at session expiry (up to 7d).
- Fix account settings modal overflow: add max-height to .modal, make
.modal-body flex/scrollable, widen #acct-settings-modal to 620px
- Add role column to user_accounts (editor|viewer) with migration;
existing assignments promoted to editor
- New isEditorForAccount() in auth middleware for per-account write checks
- Replace global requireEditor with per-account checks in checks.js,
deposits.js, pdf.js, deposit-pdf.js, qbo-import.js
- GET /api/accounts now returns user_role per account
- users.js returns {account_id, role} per assignment; POST/PUT accept
accounts as [{id, role}]
- Frontend: state.accountRole tracks effective role for active account;
applyRoleUI and renderRow use it; user management shows role dropdown
per account assignment
Three-tier user model: admin (all accounts, all actions), editor
(assigned accounts, read/write), viewer (assigned accounts, read-only).
Backend:
- express-session with custom SQLite session store (no extra packages)
- bcryptjs for password hashing
- src/middleware/auth.js: requireAuth, requireAdmin, requireEditor,
canAccessAccount helpers
- src/routes/auth.js: login, logout, /me, setup-needed, change-password
- src/routes/users.js: full CRUD + account assignments (admin only)
- All API routes protected; /api/accounts filtered by user access;
write routes gated by requireEditor; admin-only routes locked down
Frontend:
- Login overlay (full-page) with first-run admin-setup flow
- Role-based UI: admin-only elements hidden for non-admins; edit/delete
and PDF buttons hidden for viewers; account switcher shows only
accessible accounts for non-admins
- Users modal (admin only): user list with role badges, create/edit/delete
users, set account access via checkboxes
- Change-password section available to all logged-in users
- apiFetch redirects to login on 401
Two-tab modal: "Checks to Print" parses QBO Transaction List / Check
Detail CSV exports; "Deposits" parses QBO Deposit Detail CSV exports
and groups by date. Both tabs show a preview before confirming import.
- New Deposits tab with ledger: date, checks total, cash, deposit total, item count, status
- Slide-in deposit panel: date, currency, coin, cash back, dynamic check entry rows, live totals
- Save deposit, then generate Deposit Slip or Deposit Report PDF
- Deposit slip: 3.375" x 8.5" portrait with Style A background drawn server-side,
digit-column amounts, GnuMICR routing/account line rotated 90 deg, rotated
deposit total and check count in left margin
- Deposit report: plain Courier ledger with depositor/bank info, check grid, totals
- deposits and deposit_items tables in schema; ON DELETE CASCADE for items
- Routes: GET/POST/PUT/DELETE /api/deposits, POST /api/deposit-pdf
- Generating a slip marks deposit as printed; date range and status filters
- README updated to describe deposit slip feature
- Schema: account_id FK on checks and layout_fields; UNIQUE per-account on check_no and field_name
- DB: runtime migration recreates both tables to add account_id (assigns existing rows to account 1)
- Routes: GET /api/accounts lists all; GET /api/account/:id replaces hardcoded id=1; POST /api/account/setup always creates a new account and returns accountId
- checks.js: all queries scoped by account_id; POST requires account_id in body
- pdf.js: resolves account from check's account_id instead of id=1; layout fields fetched per-account
- import-mdb.js: always INSERTs a new account (never deletes existing); all records tagged with new accountId
- Frontend: account switcher in header; activeAccountId persisted to localStorage; all API calls pass account_id; switching accounts reloads checks; wizard and import auto-switch to newly created account
- pdfService: batch checks into groups of 3, add doc.addPage() between groups
- pdf route: remove 1-3 limit, accept any number of check IDs
- Frontend: remove 3-check selection cap; any number of checks can be selected
- All checks shown in one table by default (load all, no server-side printed filter)
- Add payee search, date-from/to, and status filter controls to toolbar
- Filtering is client-side; no extra API calls on filter changes
- All checks get Edit/Delete buttons regardless of printed status
- All checks get checkboxes for PDF selection
- Remove separate Reprint button and reprintCheck function
- Remove printed guard from PUT and DELETE endpoints
- Hardcode GnuMICR.otf path in pdfService.js; remove MICR_FONT_PATH env var
- Fix normalizeDate to handle MM/DD/YY (2-digit year) and return null on no match
- Fix generatePdf button DOM bug: update span directly instead of overwriting textContent
- Remove .env.example and NTFY_URL from docker-compose (app has no required config)
- Remove redundant fonts volume mount from docker-compose (fonts bundled in image)
- Mark MVP TODO items complete; add // TODO comments in source for post-MVP features
- Update README: correct slot height, remove stale env var docs