Skip to content

Latest commit

 

History

History
362 lines (338 loc) · 18.9 KB

File metadata and controls

362 lines (338 loc) · 18.9 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Fixed

  • PORT=0 is honored instead of being silently coerced to 3000 (#124). server.js resolved its listen port via parseInt(process.env.PORT, 10) || 3000, which short-circuited on the legitimate "kernel pick a free port" case because 0 || 3000 evaluates to 3000. tests/api/server-boots.test.js relied on this exact behavior (port 0) to avoid colliding with whatever's already on :3000 on a dev box, so the smoke-test was failing on master for any contributor with a busy 3000. Replaced the falsy-fallback with an explicit `Number.isFinite(parsed) && parsed

    = 0check so onlyNaN/ negativePORT` values fall back to the default.

Changed

  • Production hard-fails on empty DB_PASSWORD (#119). Previously a missing DB_PASSWORD in NODE_ENV=production would warn and start anyway; /healthz reported degraded, but a load balancer keyed purely off HTTP 200 from /healthz could still flip traffic to a pod that couldn't reach its database. Now the process logs to stderr and process.exit(1) immediately so systemd / k8s catch the misconfiguration before traffic ever lands. Development and test paths still warn-and-continue so the suite runs without a real DB.
  • DB_PASSWORD is documented as REQUIRED in production (#120). README + .env.example callouts updated to match the new hard-fail behavior above; operators get the warning at config-edit time, not just at first deploy.
  • pg bumped 8.20.0 → 8.21.0 (#122). Patch-level dep refresh in the minor-and-patch Dependabot group.

Added

  • OpenAPI: Idempotency-Replay response-header declaration on every single-create POST 201 (#245 sweep — landed across 16 PRs from #246 through #288). Every /v1/* POST that flows through the Idempotency-Key middleware now advertises the Idempotency-Replay header on its 201 response in the spec — SDK generators (openapi-typescript, etc.) carry the replay flag into client-facing types instead of having callers infer it from prose in the request header's description. The bulk endpoints picked this up in #168; this sweep extends it to single-create symmetry.
  • Real-PG integration coverage for the cascade auth helpers (#121, follow-up to #117). Six new test cases against postgres:16-alpine for the four getCompanyIdBy* helpers in app/middleware/auth.js (Customer, Job, PurchaseOrderVendor, PurchaseOrderHeader cascades). Includes a regression-pin that archiving an intermediate Customer drops a downstream Job's auth scope to -1 — the correct security outcome since the parent's scope no longer applies. Auto-skips locally without a DB; runs on every CI build.
  • npm run dev (#116). Uses Node's built-in --watch flag (stable since Node 22; project pins >=20) to restart on changes to app/ and server.js. No new dev dependency. CONTRIBUTING.md quick-start updated.

Security

  • Fixed an IPv6 rate-limit bypass. The custom keyByAuthKeyOrIp rate-limit key generator was reading req.ip directly and concatenating it into the key. express-rate-limit v8+ surfaced this as ERR_ERL_KEY_GEN_IPV6: IPv6 clients could rotate through their /64 allocation, each appearing as a distinct IP, to bypass the per-IP rate-limit budget on the anonymous (brute-force) path. Fix routes the IP through the package's ipKeyGenerator() helper, which normalizes IPv6 to a /64 prefix. Authenticated keying (sha256 of the authKey header) is unchanged. Operators on previous releases should upgrade.

Removed

  • Unused production dependencies: express-asyncify and express-promise-router. Both were listed in package.json since the initial port but never imported anywhere. Surfaces as a smaller npm ci footprint; no behavior change.

Changed

  • CI gains a live Postgres service. Both GitHub Actions and Woodpecker spin up postgres:16-alpine alongside the Node matrix, run setup/TimeTracker.sql for the dbo schema bootstrap, then npm run migrate to apply every migration. The integration suite at tests/integration/db-roundtrip.test.js no longer self-skips in CI — it gates every PR against schema / migration drift. The unit + api suites still pass with or without a DB, so local npm test works unchanged.

Added

  • OpenAPI completeness pass. All 12 previously-undocumented bulk-create endpoints now appear in the spec via a shared bulkPath(bodyKey, schemaName) helper (kept the entries from drifting into 13 hand-maintained near-duplicates). The Idempotency-Key header is documented as an optional parameter on every bulk POST. /metrics gets its own path entry with the Prometheus text-format response and the METRICS_BEARER_TOKEN 401-gate documented. Three new OpenAPI tests pin the additions.
  • Bulk-create endpoints for 7 indirect-scoped entities (P3-H2). New POST /v1/<entity>/bulk on Job, Invoice, CustomerPayment, InvoiceJob, ProductEntry, PurchaseOrderHeader, PurchaseOrderLine. Same 500-entry cap and transactional all-or-nothing semantics as the direct-compId family from P3-H, but per-entry auth scope is resolved through the parent FK (Customer / Job / Vendor / Header) via the existing helpers in app/middleware/auth.js. A new makeBulkCreateIndirect factory in app/controllers/_bulk-helpers.js parameterizes over the parent FK column + the auth-helper that resolves it; the 7 controllers gain ~10 LOC each instead of ~120. The bulk surface now covers all 13 soft-deletable entities.

Changed

  • app/middleware/auth.js is now testable end-to-end (P5-M). Two changes:
    1. Every DB hit now goes through the Sequelize model layer (ApiMaster.findOne, Customer.findByPk, etc.) instead of raw sequelize.query calls — fewer hand-rolled SQL strings, and the archive-filter relies on the P2-E defaultScope rather than a repeated <arch>: false WHERE clause.
    2. A test-only _setDbForTesting(stub) seam lets unit tests substitute model fixtures directly. vitest's vi.mock does not intercept this codebase's CJS require() reliably; the explicit setter is the smallest practical injection point. Production code MUST NOT call it. Auth helpers gain 19 new unit-level cases including the previously un-mockable "row found → returns the value" success paths.

Added

  • ESLint flat config + CI gate (P5-L). New eslint.config.js with rules tuned for high-signal bug catching rather than style preferences: no-unused-vars (with ^_ opt-out), eqeqeq, no-console (allowing only error/warn outside tests), prefer-const, no-var. Tests get a relaxed variant (no-unused-vars as a warning, console allowed). Migrations ignore unused args to honor sequelize-cli's (queryInterface, Sequelize) contract. Wired into GitHub Actions and Woodpecker so every PR now runs npm run lint ahead of the vitest suite. npm run lint:fix available for autofixable rules.
  • createdAt / updatedAt on every domain entity (P4-K). New migration adds two TIMESTAMPTZ NOT NULL DEFAULT now() columns to 18 tables (everything except IdempotencyKey, which already tracks its own time fields). All 18 models flip timestamps: falsetrue so Sequelize auto-populates the columns on every .create() / .update(). Existing rows are backfilled to now() at apply time; operators with the original SQL Server timestamps from the Atbash legacy can patch real values post-migration via a one-off UPDATE.
  • Prometheus /metrics endpoint (P4-J). Exposes prom-client's default Node.js metrics (event-loop lag, heap, GC, etc.) plus per-request http_requests_total{method,route,status} and http_request_duration_seconds{method,route,status} series. Route labels use the Express pattern (/v1/customer/:id) not the rendered path, so cardinality stays bounded. Authentication is OPTIONAL: unset METRICS_BEARER_TOKEN leaves the endpoint open (the usual private-network deployment); setting it requires Authorization: Bearer <token> on the scrape. Token comparison is constant-time.
  • migration field on GET /healthz (P4-I). Body now reports the last applied migration name from SequelizeMeta (e.g. "20260519000000-idempotency-keys"). Lets a rolling-deploy caller verify each pod is at the expected schema version. Null when SequelizeMeta is missing (fresh DB pre-migration) or unreadable — the probe never flips to status: degraded over a migration-read failure, since the DB itself is still up.
  • Bulk-create endpoints for 5 direct-compId entities (P3-H). New POST /v1/<entity>/bulk on Worker, BillingType, InventoryItem, InventoryTransaction, and PurchaseOrderVendor. Same shape as the existing POST /v1/customer/bulk: 500-entry cap, zod-strict whitelist, transactional all-or-nothing insert, master vs. non-master scoping enforced per entry. Shared app/controllers/_bulk-helpers.js#makeBulkCreate factory removes ~150 lines of would-be duplication; Customer's pre-existing handler keeps its bespoke logic until a follow-up unifies them.
  • Idempotency-Key support on POST routes (P3-G). Clients may send an Idempotency-Key: <printable-ASCII, 1-255> header on any POST under /v1/*. The first response (status + JSON body) is cached in the new dbo.IdempotencyKey table for 24h, keyed by sha256(authKey:method:path) + the raw key value. Retries with the same body replay the cached response and carry an Idempotency-Replay: true response header; retries with a DIFFERENT body return 409 { code: "idempotency_key_reused" }. No-op for POSTs without the header — legacy clients are unaffected.
  • Sequelize associations across the full entity graph (PR #54). Every FK now has a hasMany/belongsTo pair in db.config.js, enabling include-based eager loading and the auto-generated getter/setter methods. Verified via a 24-test unit suite that walks Model.associations and asserts each expected edge.
  • Integration test harness against a real Postgres (PR #55). tests/integration/ runs only when DB_PASSWORD is set + the Sequelize authenticate() call succeeds; skips gracefully otherwise so npm test keeps working unchanged.
  • Pre-built Postman collection at setup/TimeTrackerAPI.postman_collection.json (PR #59). Generated from app/config/openapi.js via openapi-to-postmanv2; covers all 47 endpoints across 16 entity folders. Regenerate after API changes with the one-liner in the README.
  • TLS reverse-proxy compose layer (PR #60). Opt-in docker-compose.tls.yml puts Caddy in front of the api: automatic Let's Encrypt cert provisioning + renewal, HTTP → HTTPS redirect, HTTP/2 + HTTP/3 on :443, HSTS, X-Forwarded-* headers, gzip. TLS_DOMAIN=localhost opts into Caddy's built-in CA for local self-signed dev.
  • Committed docker-compose.override.yml (PR #56) — exposes Postgres on 127.0.0.1:5432 for host-side integration test runs.

Fixed

  • setup/TimeTracker.sql is now idempotent against a populated database (PR #57). Re-running docker compose up setup against an existing schema is a no-op exit-0 rather than the previous exit-3 "schema dbo already exists" failure. Removes the docker compose down -v workaround from the dev flow.

Docs

  • Full integration-test bring-up flow documented in tests/integration/README.md (PRs #56 + #58). Covers the override file, env vars, fresh vs re-run behavior, and the conventions for cleanup sentinels.
  • README gets sections for Testing, Behind TLS (production), and a pointer to the Postman collection.

Added (earlier in this [Unreleased] window)

  • PurchaseOrder + Inventory API surface (#49, PRs #50, #51, #52): Full CRUD endpoints for the four tables added by the 20260517000000 migration —
    • PurchaseOrderVendor — direct compId scoping
    • PurchaseOrderHeader — vendor-scoped via new auth.getCompanyIdByPovId() helper
    • PurchaseOrderLine — header-scoped via new auth.getCompanyIdByPohId() helper (two-hop FK walk through header → vendor)
    • InventoryTransaction — direct compId scoping; invtDirection constrained to 0 (inbound) or 1 (outbound) at the zod boundary
  • JSON_BODY_LIMIT env override for express.json() body cap (#45, PRs #46 and #47). Default 100kb matches the express built-in; operators can raise it (JSON_BODY_LIMIT=512kb) for endpoints that legitimately accept larger payloads.

Changed

  • npm audit fix cleared 10 transitive-dep vulnerabilities (dottie, moment, moment-timezone, path-to-regexp, qs, underscore, validator). Direct deps bumped to latest patch within current majors: express 4.21.1 → 4.22.2, pg 8.6.0 → 8.20.0, express-promise-router 4.0.1 → 4.1.1, sequelize 6.6.5 → 6.37.8. (PR #48; closes Snyk-backlog tracker #30; supersedes / closes 11 stale Snyk PRs.)

Added (earlier in this [Unreleased] window)

  • API surface expansion (#38, PR #39): full CRUD for ten entities that were in setup/TimeTracker.sql but lacked endpoints — Worker, Company, BillingType, InventoryItem, Job, Invoice, CustomerPayment, InvoiceJob, ProductEntry, VersionInfo. Path count went from 7 to 35.
  • Three centralized auth-scoping patterns in middleware/auth.js:
    • Direct compId scoping (Worker, BillingType, InventoryItem) — same as Customer.
    • Customer-scoped via new getCompanyIdByCustomerId() helper (Job, Invoice, CustomerPayment) — auth walks parent FK.
    • Job-scoped via new getCompanyIdByJobId() helper (InvoiceJob, ProductEntry) — auth walks two-hop FK.
  • Specials: Company has compId IS the company id (master-only POST/DELETE/list; GET/PATCH scoped to own row). VersionInfo is global, no archive column, reads open to any authKey, mutations master-only, DELETE is a hard destroy.
  • Migration 20260517000000-purchase-orders-and-archive-columns: creates PurchaseOrderHeaders, PurchaseOrderLines, PurchaseOrderVendors, and InventoryTransactions tables (omitted from the initial PG port of the BACPAC), and retrofits the missing invitArch and injbArch columns the new soft-delete logic depends on.
  • docker compose now applies Sequelize migrations between the SQL bootstrap and the api start (new migrate one-shot service). Without this, migrations landed after the baseline never applied to containerized deploys. (#40, PR #41)
  • tini runs as PID 1 in the container image for clean signal forwarding to the Node process (server.js already had the graceful-shutdown handler; tini just makes sure it gets the signal). (#40, PR #41)
  • OCI org.opencontainers.image.* labels on the runtime image (source URL, license, vendor). (#40, PR #41)

Changed

  • sequelize-cli moved from devDependencies to dependencies so the production npm ci --omit=dev build can run migrations.
  • HEALTHCHECK in the Dockerfile uses Node's built-in http module instead of wget; drops the wget apt-install layer. (#40)
  • .dockerignore excludes tests/, vitest.config.js, README.md, CHANGELOG.md, and docs/ from the runtime image; explicitly keeps LICENSE (Apache-2.0 §4(c) requires it accompany derivative works, including container images).

Added (still earlier in this [Unreleased] window)

  • Codeberg mirror at https://codeberg.org/CryptoJones/TimeTrackerAPI; README now carries badges for both forges.
  • GET /healthz liveness + DB-readiness probe. No auth. Returns {status, db, uptime_s, version, elapsed_ms} as 200 ok / 503 degraded.
  • helmet security headers (X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security, Referrer-Policy, etc.). CSP off by default for the JSON API; enable via HELMET_CSP=1.
  • express-rate-limit on /v1 (default 100 requests / 15-minute window per IP, env-tunable via RATE_LIMIT_MAX / RATE_LIMIT_WINDOW_MS). /healthz is exempt. TRUST_PROXY env for real-IP keying behind nginx / Caddy / Cloudflare.
  • pino structured logger with authKey redaction; pino-http for per-request access logging. LOG_LEVEL / LOG_PRETTY / TRIAGE_NO_LOG-style env knobs.
  • POST /v1/customer — first write endpoint. Body whitelist (mass-assignment defense); master keys must specify custCompId, non-master keys default to their own.
  • POST | GET | PATCH | DELETE /v1/timeentry/[:id] and GET /v1/timeentry/bycompany/:id — five new time-entry endpoints, the headline feature for a project named "TimeTracker". Includes the TimeEntry Sequelize model, setup/TimeEntry.sql migration, two hot-path indexes, teMinutes computed on close, soft-delete via teArch.
  • Pagination on GET /v1/customer/bycompany/:id (?limit, ?offset, default 100 / max 500). Response now includes count for clients to paginate without an extra round-trip.
  • OpenAPI 3.0 spec at /openapi.json + Swagger UI at /docs (both unauthenticated by design).
  • Zod-backed request validation at the middleware boundary — validate.body / .query / .params reject malformed inputs with 400
    • structured issues array BEFORE they reach Sequelize.
  • Dockerfile + docker-compose. git clone + docker compose up brings postgres + the schema bootstrap + the API up on port 3000.
  • CI: GitHub Actions + Codeberg Woodpecker pipelines running vitest on Node 20 + 22.

Changed

  • getCustomerById was rewritten to fix a double-response fall-through bug where the master-key branch sent a response inside a .then() but didn't exit the surrounding async function, causing Cannot set headers after they are sent on real master-key traffic.
  • IsMaster / GetCompanyId / GetCustomerCompanyId no longer index result[0].x without checking the array is non-empty (eliminates a noisy log entry every time an unknown authKey hits the API).
  • console.log / console.error everywhere → log.error({err}, '...') with structured fields. Tests set LOG_LEVEL=silent so error-path cases don't flood stdout.
  • GET /v1/customer/bycompany/:id now filters out soft-deleted customers (custArch = true). Clients were already manually filtering; this aligns server behavior with the contract.
  • IsMaster and GetCompanyId extracted from both controllers into app/middleware/auth.js — single source of truth.

Removed

  • body-parser dependency. Express has had it built-in as express.json() since 4.16 (Oct 2017); we're on ^4.21.

Security

  • Server runs as non-root inside the Docker image.
  • authKey is redacted from all pino log lines via pino.redact paths covering req.headers.authkey, req.headers.authKey, and req.headers.authorization.
  • All mass-assignment vectors (writing custId, custArch, teId, teMinutes, teArch via request body) blocked at the validation middleware boundary.

Proudly Made in Nebraska. Go Big Red! 🌽 https://xkcd.com/2347/