Commit Graph

8 Commits (0adce5844be107b1ca13fad1e6709e7be6646dc0)

Author SHA1 Message Date
Thomas Joise 809976fd8b feat(d36): login history (IP + device) + first-login change nudge
Captures every /auth/login attempt in a login_event table (SQLite schema + PG migration
013): staff_id (or null for an unknown user), username tried, outcome (success/failed/
locked), IP, and user-agent — best-effort, never breaks login. New owner 'Login history'
page (Admin): who signed in / failed, when, from which IP + parsed device, outcome filter,
paginated. login() now returns mustChangePassword; an invited user on their default
password is sent to Profile to change it on sign-in. typecheck + auth/employee tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 days ago
Thomas Joise 8092c78264 feat(d25): login by username; email demoted to a plain user field
- staff_user gains a unique username (both engines: migrate + pg 009); older
  rows and the seed owner backfill username=email so every existing login keeps
  working. login() matches username OR email (legacy fallback).
- createStaff/createEmployee take an optional username (defaults to email);
  Employee/API expose it; POST /employees accepts username. Email stays a
  required contact field on the table (making it optional is a later table
  rebuild). Full backend suite 346 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 54cde7961b fix(sec/d24): gate destructive/sensitive ops to managerial
Red-team authorization findings:
- New requireManagerial middleware. Applied to POST /documents/:id/cancel,
  /credit-note (financially destructive — reverse money / consume a number)
  and /documents/:id/share (mints an UNAUTHENTICATED public PDF link — the
  IDOR exfiltration high). Any signed-in staff could previously do all three.
- PATCH /interactions/:id now 403s unless the viewer is the interaction's
  author or managerial — staff can no longer overwrite another employee's
  logged call/visit.
New authz-gates test locks both. Note (deliberate): general client read/edit
stays open to staff — a shared support-team book is the intended model; the
genuinely sensitive credential reveals are already managerial + rate-limited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 10022965d1 fix(sec/d24): auth hardening — login lockout, rate limits, constant-time, trust proxy
Red-team highs/mediums (auth surface):
- staff_user gains failed_count/locked_until; login() locks an account for 15min
  after 8 consecutive failures, clears the counter on success (both engines).
- login() is now constant-time: a missing email still spends one scrypt verify
  against a fixed dummy hash — no timing/enumeration oracle. Returns a typed
  {ok:false,reason} instead of null.
- POST /auth/login gets per-IP (20/min) AND per-email (8/15min) fixed-window
  rate limiting layered over the lockout; 429 on trip.
- Credential reveal routes (db-password, module password, module secret) get a
  per-user reveal limiter (20/min) so one insider cannot bulk-decrypt everything;
  added missing getClientModule 404 guards.
- app.set(trust proxy,1) so req.ip is the real client behind nginx/Caddy and the
  per-IP limiters actually bucket per client.
- makeRateLimiter extracted to rate-limit.ts (shared, no server<->api cycle).
Tests: lockout, counter-reset, unknown-email, and existing auth all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise 5f9f961d20 feat(db): D19 P1-P3 — async DB interface, dual engines, Postgres adapter
P1: every repo, route, scheduler path and test now speaks one async DB
interface (all/get/run/exec/transaction). better-sqlite3 stays underneath
for dev/tests via SqliteDb (statement cache, manual BEGIN/SAVEPOINT
nesting); openDb stays sync so fixtures remain one-liners. Zero behavior
change: 346/346 tests, typecheck clean.

P2: migrations-pg.ts — numbered Postgres migration set (001-init: bigint
paise, integer flags, TEXT ISO dates, TEXT JSON; reminder CHECK born in
final widened form) + schema_migrations runner.

P3: db-pg.ts — pg Pool behind the same interface: ?->$n rewrite outside
string literals, transactions pinned to one pooled client via
AsyncLocalStorage with savepoint nesting, int8/numeric parsed to JS
numbers so paise math is identical on both engines.

Also: reminder upsert INSERT OR IGNORE -> ON CONFLICT DO NOTHING
(portable, same changes-count semantics).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
4 days ago
Thomas Joise efdb34e710 feat(clients): account-owner plumbing — client.owner_id, audited owner route, role gate
Phase 3 of the quote-to-close funnel spec: client.owner_id added additively
(SCHEMA + PRAGMA-guarded migrate) so a bare lead is routable to an employee;
setClientOwner repo fn (validates active employee, audits set_owner in-txn);
PATCH /clients/:id/owner gated inline to owner/manager; shared ownerScope role
gate (staff forced to self, widening ignored) for later phases; ClientDetail
gains an owner/manager-only Account owner dropdown fed by GET /employees.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise 1af7b87aab feat(hq): employee foundation — manager role, employee repo, session hardening (phase 1)
- rebuildTable helper: guarded, txn-wrapped CHECK-constraint rebuild (SQLite-only path)
- staff_user.role CHECK widened to owner|manager|staff (SCHEMA + migrate for old DBs)
- repos-employees.ts: list/get/create/update/setPassword/deactivate/reactivate,
  audited in-txn, password columns never selected, last-active-owner + self guards
- verifySession requires active=1; deactivation purges sessions in the same txn
- 9 tests (apps/hq/test/employees.test.ts); suite now 186 green

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5 days ago
Thomas Joise ac7aa891db feat(hq): staff auth with sessions and owner gate (HQ-1 task 3)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 weeks ago