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).
Viewers can no longer see New Check, New Deposit, Import .mdb, or
Import QBO buttons. Deposit row Edit/Delete buttons are also hidden
for viewers, matching the existing behavior on check rows.
- 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
Red "Delete Account" button at top of Account Settings. Clicking opens
a second modal with an irreversibility warning and the account name,
requiring a second explicit confirmation before deletion. Deletes the
account row plus all associated checks and deposits. Redirects to the
setup wizard if no accounts remain.
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.
- Halve all check line thicknesses (line_thick * 0.5)
- Add second_signature column to account table with runtime migration
- Account settings toggle: enables a second signature line drawn 0.25"
above the primary signature line, matching its width and thickness
- Move check slot 0 up 0.25" to match physical stock alignment;
slots 1 and 2 are unaffected
- Remove toLocaleString() from next check number header display
so no comma appears in the thousands place
Pencil button next to "Next check: X" in the header opens a modal
with a warning about sequence gaps/duplicates. Saves via new
PUT /api/account/:id/check-no endpoint (sets current_check_no = next - 1).
- Remove the outer border rect around the main content area
- Change DEPOSIT TOTAL font from Helvetica-Bold to Courier to match
the monospaced style used for other amount fields
- Update strip element Y positions for better spacing
- Add select-all checkbox to checks table header; checks/unchecks all
visible (filtered) rows; supports indeterminate state
- Add summary bar below checks toolbar showing count and total amount
of filtered checks
- Deposit slip: output to full 8.5x11 letter page for trimming
- Deposit slip: remove beige left strip fill (white background)
- Deposit slip: remove vertical separator between depositor/bank info
- Deposit slip: stack bank info below account info instead of side-by-side
- Deposit slip: lower date underline position
- Deposit slip left strip: flip text orientation to read tilt-left
(rotate 90 instead of -90; reposition all strip element anchors)
- Deposit slip left strip: center MICR and labels in strip width
- Deposit slip total: include decimal point in rotated digit amount
- 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
- Add gear button in header to open account settings modal
- Modal covers Organization, Bank, Account, Logo upload, and Printer Offset fields
- PUT /api/account/:id backend endpoint with full field validation
- Logo file reader with inline preview; only updates logo if a new file is chosen
- CSS for btn-header-icon, settings-section-label, logo-preview, form-row-4
- Add GnuMICR.otf (tracked alongside existing GnuMICR.ttf)
- 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
Font files were excluded by fonts/*.ttf and fonts/*.otf in .gitignore,
so GitHub Actions builds never had them and the container fell back to Courier.
Commit the TTF (more compatible with PDFKit than the PS-converted OTF).
- Use GnuMICR.ttf instead of .otf (OTF converted from PS Type 1 may not embed in PDFKit)
- Add renderLines() helper that splits on \n and places each line at explicit Y offset,
so lineBreak:false is honored per-line and PDFKit cursor doesn't drift
- Include bank_name as first line of Bank Information field
- 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
docker-compose pulls :latest but the workflow only pushed branch-name
tags (e.g. :main) and SHA tags -- never :latest. Portainer stacks
reported "manifest unknown" when trying to pull the image.
Add type=raw,value=latest scoped to the default branch so pushes to
main now also tag the image as :latest.
- Add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true to docker-build workflow
- Switch login step to vars.DOCKERHUB_USERNAME (non-secret)
- Rename docker-compose service, container, volume, and DB file to check-printing