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>
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>
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>
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>
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>
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>
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>
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>
- .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>
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>