11 Commits
Author SHA1 Message Date
mars3142 028c835800 Use Postgres 18's native uuidv7() instead of app-level generation
CI / test (push) Successful in 54s
CI / build-and-push (push) Successful in 33s
CI / deploy (push) Successful in 5s
Postgres 18 (already the pinned image) ships uuidv7() as a built-in
function, so the app-level generator from the previous commit was
unnecessary: same v7 ids, one line per column instead of a dependency
plus a lib file, and it now works no matter what actually performs the
insert (a migration, psql, another service) rather than only inserts
that go through Drizzle's $defaultFn.

Verified: schema-driven Drizzle insert and a plain SQL insert via psql
both come back with a version-7-shaped id.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-06 21:45:26 +02:00
mars3142 4c03f45f24 Generate primary key UUIDs as v7 at app level
CI / test (push) Successful in 2m12s
CI / build-and-push (push) Successful in 59s
CI / deploy (push) Successful in 5s
Every table used Postgres's gen_random_uuid() (v4, fully random) as
the id default. Switched to UUIDv7 generated in application code
instead: its leading timestamp keeps new rows appending near the end
of each primary-key index rather than landing at random positions
across it, and rows sort by creation order for free. Generating it in
JS (src/lib/ids.ts) rather than DB-side keeps the id available before
the INSERT runs, same as the defaultRandom() pattern it replaces.

Migration drops the DB-side default on all 13 id columns; Drizzle's
$defaultFn supplies the value from here on.

Verified against a real Postgres insert (id came back version-7
shaped) in addition to the format/uniqueness/ordering unit tests.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-06 18:05:14 +02:00
mars3142 1339157182 Add global rate limiting
CI / test (push) Successful in 1m2s
CI / build-and-push (push) Successful in 43s
CI / deploy (push) Successful in 5s
Redis-backed when REDIS_URL is set, in-memory otherwise - the counter
store needs to be shared once findr-api runs as more than one replica
behind Traefik, or each instance would count its own share and the
real allowed rate would silently multiply by the replica count. One
findr-api instance (the default today) just uses the in-memory store.

/healthz is exempt so the container healthcheck never trips it. The
allowlist matcher is pulled into its own pure function (lib/rate-limit-
allowlist.ts) and unit tested - @fastify/rate-limit's allowList option
has two shapes (string[] matches request.ip, a function matches
whatever you check) and passing paths as the array form silently
allows nothing, which is exactly what a manual smoke test caught
before this got committed.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 23:21:43 +02:00
mars3142 f5a8c63bf6 Add personal access tokens for machine callers
For scripts and tools that call findr-api's HTTP endpoints directly
without an interactive OIDC login - a KiCad plugin, a backup job, a
CLI import script. Not used by the ESP32 controller, which only ever
speaks MQTT to Mosquitto and has nothing to do with this.

- api_tokens table: subject (OIDC sub) + name + a hashed fdr_... token
  + scopes + optional expiry. Plaintext is generated once at creation
  and never stored.
- plugins/auth.ts gains a second verification path: a fdr_-prefixed
  bearer is looked up by hash instead of JWT-verified, then mapped to
  the same request.user shape the existing scope checks already use.
- A token's scopes must be a subset of whatever the creating credential
  itself currently holds (services/api-tokens.service.ts) - no
  self-escalation, enforced server-side regardless of what a client UI
  shows.
- /v1/tokens (list/create/revoke), gated by plain authentication rather
  than a SCOPES.* requireScope - this is about identity, not a findr
  domain permission.

Verified end-to-end against a real Postgres: unauthenticated 401,
valid-token 200/201, scope escalation 403, revoke 204, revoked-token
reuse 401.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 23:21:30 +02:00
mars3142 8f9e62d93f Rename KEYCLOAK_* env vars to OIDC_*
CI / test (push) Successful in 58s
CI / build-and-push (push) Successful in 17s
CI / deploy (push) Successful in 5s
Nothing about the JWT validation itself was Keycloak-specific (issuer +
audience + JWKS via discovery already work against any OIDC IdP) - only
the names were. Renaming lets findr-web and findr-api share one
OIDC_ISSUER value in the stack's env file instead of two names for the
same URL.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 22:29:16 +02:00
mars3142 b308ed86ae Add distributors, parameters, projects and pick lists to schema
CI / test (push) Successful in 2m15s
CI / build-and-push (push) Successful in 21s
CI / deploy (push) Successful in 6s
Extends the parts domain with what the mockups already show but the
schema didn't cover yet:

- part_parameters replaces the parameters JSON blob (comment said
  'promote to a table later') - text and min/typical/max+unit values,
  so numeric specs stay filterable/sortable later without another
  migration.
- distributors + part_distributors: one row per (part, distributor)
  price, matching the LCSC/Mouser/Reichelt tab on the part detail page.
- projects + project_parts: BOM ('used in'), with a single status
  (planned/building/built) driving the 'N planned' vs 'N built' wording
  instead of two separate counters.
- pick_lists + pick_list_items: checklist for one build batch,
  independent of projects since a pick list can be ad-hoc.

The below-minimum reorder list needs no new table - it's already the
existing belowMinimum filter on GET /v1/parts.

Verified by running both migrations against a real Postgres 18 (ICU
de-DE collation) and the existing test suite.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 17:06:54 +02:00
mars3142 fe8f87602b Add HEALTHCHECK to Dockerfile
CI / test (push) Successful in 58s
CI / build-and-push (push) Successful in 18s
CI / deploy (push) Successful in 5s
Matches findr-web's pattern: the image declares its own healthcheck
against /healthz, so Compose doesn't need to restate it.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 15:39:46 +02:00
mars3142 ec7a6d57cb Deploy per Portainer-Webhook statt Polling
CI / test (push) Successful in 4m7s
CI / build-and-push (push) Successful in 5m40s
CI / deploy (push) Successful in 6s
Nach erfolgreichem Push in findr/infrastructure den Stack-Webhook
aufrufen (PORTAINER_WEBHOOK-Secret), damit der Redeploy sofort
statt auf einen Timer passiert.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-04 10:50:06 +02:00
mars3142 3fc0c82012 CI: Container bauen und Hash in infra deployen
CI / test (push) Successful in 1m59s
CI / build-and-push (push) Successful in 1m26s
CI / deploy (push) Successful in 5s
- .gitea/workflows/ci.yml: Lint/Typecheck/Test, dann Image in die
  Registry pushen, dann Deploy-Job pinnt den Commit-Hash in
  findr/infrastructure (Rebase-Retry gegen findr-web)
- Dockerfile: npm 11 (npm 10.9 bricht npm ci mit EBADPLATFORM auf
  esbuilds Plattform-Paketen ab), NODE_ENV erst zur Laufzeit
- package-lock.json neu erzeugt, war nicht in sync

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-03 21:41:45 +02:00
mars3142 fb518bc689 renovate.json hinzugefügt 2026-09-02 23:13:12 +00:00
mars3142 cadc280761 Scaffold Fastify + Drizzle API with pick-by-light
Layered: routes -> services -> repositories -> db, over PostgreSQL 18 with
split roles (findr_migrator for DDL, findr_app for DML). Keycloak JWT auth
with per-endpoint scope guards. Drizzle migrations run with the DDL role at
container start.

Pick-by-light: one ESP32 controller drives one WS2812 chain through several
boxes; each box owns a contiguous LED slice. MQTT contract in
src/lib/mqtt-topics.ts / docs/mqtt.md. /v1/pick controls light/idle/off and
/v1/boxes + /v1/controllers manage the strip mapping.

Reference resource /v1/parts wired end to end. Vitest, Biome, multi-stage
Dockerfile, local compose.yml with Postgres.

Signed-off-by: Peter Siegmund <mars3142@noreply.mars3142.dev>
2026-09-03 01:07:35 +02:00