From 120e1a0929a21e8d367c31564757b6509a2dccfc Mon Sep 17 00:00:00 2001 From: lilleman Date: Tue, 16 Jun 2026 23:24:32 +0200 Subject: [PATCH] =?UTF-8?q?Add=20kratos=20service=20+=20migrate=20(todo=20?= =?UTF-8?q?=C2=A73);=20pin=20oryd/kratos:v26.2.0,=20identity=20schema=20(e?= =?UTF-8?q?mail,=20name),=20bootable=20password=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- compose.yml | 26 ++++++++++++++++++ ory/kratos/identity.schema.json | 34 +++++++++++++++++++++++ ory/kratos/kratos.yml | 48 +++++++++++++++++++++++++++++++++ src/kratos.test.ts | 47 ++++++++++++++++++++++++++++++++ todo.md | 2 +- 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 ory/kratos/identity.schema.json create mode 100644 ory/kratos/kratos.yml create mode 100644 src/kratos.test.ts diff --git a/README.md b/README.md index 3022d17..27a6677 100644 --- a/README.md +++ b/README.md @@ -443,7 +443,7 @@ src/menu-config.ts loadMenuConfig()/defineMenu(): read config/menu.ts (central views/ Core EJS templates (index = the app-shell People dashboard, 403/404/500, partials/ incl. app shell, nav tree, filter bar, data table, pagination, form field, auth card, menu/popover, theme switch, icon sprite) public/ Static assets under /public/ (css/styles.css + auth.css, favicon, robots.txt) config/menu.ts Central menu override + branding (optional; defaults apply if absent) -ory/ Ory service config + storage init (postgres/init/init.sql: one DB per Kratos/Keto/Hydra) +ory/ Ory service config (kratos/: identity schema + kratos.yml) + storage init (postgres/init/init.sql: one DB per service) plugins/ Drop-in plugin folders (scanned at /app/plugins; bind-mount or bake in) (planned) docs/ Reference docs (plugin-contract.md — the authoritative plugin API) e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it) diff --git a/compose.yml b/compose.yml index 294bb68..1c114e4 100644 --- a/compose.yml +++ b/compose.yml @@ -31,5 +31,31 @@ services: retries: 10 restart: unless-stopped + # Ory Kratos — identity & self-service auth. Config + identity schema in ory/kratos/. + # DSN is the per-service `kratos` DB (init.sql); supply POSTGRES_* via env in prod. + kratos-migrate: + image: oryd/kratos:v26.2.0 + depends_on: + postgres: + condition: service_healthy + environment: + DSN: postgres://${POSTGRES_USER:-ory}:${POSTGRES_PASSWORD:-ory}@postgres:5432/kratos?sslmode=disable + volumes: + - ./ory/kratos:/etc/config/kratos:ro + command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes + restart: on-failure + + kratos: + image: oryd/kratos:v26.2.0 + depends_on: + kratos-migrate: + condition: service_completed_successfully + environment: + DSN: postgres://${POSTGRES_USER:-ory}:${POSTGRES_PASSWORD:-ory}@postgres:5432/kratos?sslmode=disable + volumes: + - ./ory/kratos:/etc/config/kratos:ro + command: serve -c /etc/config/kratos/kratos.yml + restart: unless-stopped + volumes: pgdata: diff --git a/ory/kratos/identity.schema.json b/ory/kratos/identity.schema.json new file mode 100644 index 0000000..46f65f9 --- /dev/null +++ b/ory/kratos/identity.schema.json @@ -0,0 +1,34 @@ +{ + "$id": "https://plainpages/kratos/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "minLength": 3, + "maxLength": 320, + "ory.sh/kratos": { + "credentials": { "password": { "identifier": true } }, + "verification": { "via": "email" }, + "recovery": { "via": "email" } + } + }, + "name": { + "type": "object", + "properties": { + "first": { "type": "string", "title": "First name", "maxLength": 256 }, + "last": { "type": "string", "title": "Last name", "maxLength": 256 } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/ory/kratos/kratos.yml b/ory/kratos/kratos.yml new file mode 100644 index 0000000..5514bcf --- /dev/null +++ b/ory/kratos/kratos.yml @@ -0,0 +1,48 @@ +# Ory Kratos — identity & self-service auth. Bootable baseline (§3): identity +# schema (email, name) + password login. DSN comes from the env (compose), so it +# is absent here. Self-service UIs point at the web app's routes; theming those +# pages, SSO, session tuning, and the JWT tokenizer land in later §3/§4 items. +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: false + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:3000/ + allowed_return_urls: + - http://127.0.0.1:3000 + methods: + password: + enabled: true + flows: + error: + ui_url: http://127.0.0.1:3000/error + login: + ui_url: http://127.0.0.1:3000/login + registration: + ui_url: http://127.0.0.1:3000/registration + settings: + ui_url: http://127.0.0.1:3000/settings + logout: + after: + default_browser_return_url: http://127.0.0.1:3000/login + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +# Dev throwaways — production supplies real secrets via env (§3). cipher = 32 chars. +secrets: + cookie: + - PLEASE-CHANGE-ME-dev-kratos-cookie-secret + cipher: + - 0123456789abcdef0123456789abcdef + +log: + level: info + format: text diff --git a/src/kratos.test.ts b/src/kratos.test.ts new file mode 100644 index 0000000..1874f36 --- /dev/null +++ b/src/kratos.test.ts @@ -0,0 +1,47 @@ +// Guards the Ory Kratos config (§3): image pinned to an exact version (AGENTS.md), +// migrations run before the server (kratos-migrate → kratos), the DSN targets the +// kratos database, and the identity schema carries email (password identifier) + +// name traits. Real boot is verified by running the stack; this catches edits. +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; + +const read = (p: string) => readFileSync(new URL(`../${p}`, import.meta.url), "utf8"); +const compose = read("compose.yml"); +const kratosYml = read("ory/kratos/kratos.yml"); +const schema = JSON.parse(read("ory/kratos/identity.schema.json")); + +test("compose pins both kratos services to one exact version", () => { + const tags = [...compose.matchAll(/image:\s*oryd\/kratos:(\S+)/g)].map((m) => m[1]); + assert.equal(tags.length, 2, "kratos + kratos-migrate both present"); + assert.equal(tags[0], tags[1], "both pinned to the same version"); + const tag = tags[0]!; + assert.match(tag, /^v\d+\.\d+\.\d+$/, `${tag} is an exact vMAJOR.MINOR.PATCH`); + assert.doesNotMatch(tag, /latest|[\^~*]/, `${tag} is exact, not floating`); +}); + +test("migrations run once before the server starts", () => { + assert.match(compose, /migrate sql -e --yes/, "kratos-migrate runs SQL migrations"); + assert.match(compose, /condition:\s*service_completed_successfully/, + "kratos waits for kratos-migrate to finish"); +}); + +test("kratos DSN targets the per-service kratos database", () => { + const dsns = [...compose.matchAll(/DSN:\s*(\S+)/g)].map((m) => m[1]); + assert.ok(dsns.length >= 2, "both kratos services set DSN"); + for (const dsn of dsns) assert.match(dsn!, /@postgres:5432\/kratos\b/, `${dsn} hits the kratos DB`); +}); + +test("identity schema requires email (password identifier) + name traits", () => { + const t = schema.properties.traits.properties; + assert.equal(t.email.format, "email"); + assert.equal(t.email["ory.sh/kratos"].credentials.password.identifier, true, + "email is the password login identifier"); + assert.deepEqual(Object.keys(t.name.properties).sort(), ["first", "last"]); + assert.ok(schema.properties.traits.required.includes("email"), "email is required"); +}); + +test("kratos config wires the identity schema", () => { + assert.match(kratosYml, /default_schema_id:\s*default/); + assert.match(kratosYml, /identity\.schema\.json/); +}); diff --git a/todo.md b/todo.md index 06adfb3..b38275a 100644 --- a/todo.md +++ b/todo.md @@ -58,7 +58,7 @@ everything via Docker. ## 3. Ory stack — compose + config - [x] `postgres` service (pinned tag); separate DB/schema per Kratos/Keto/Hydra. → `compose.yml` `postgres` service pinned to `postgres:18.4-alpine3.23` (verified latest stable PG + newest Alpine the official image ships); `ory/postgres/init/init.sql` (mounted at `docker-entrypoint-initdb.d`) creates one DB per service (`kratos`/`keto`/`hydra`) so each owns its schema + migrations. Dev defaults (`ory`/`ory`, env-overridable for prod), named `pgdata` volume mounted at `/var/lib/postgresql` (PG18+ version-subdir layout — not `/data`), `pg_isready` healthcheck. Web app never connects. Verified live: boots healthy, three DBs present, then torn down. `postgres.test.ts` guards the pin + DB-per-service. typecheck + 112 units green. -- [ ] `kratos` service (pinned) + `migrate`; identity schema (traits: email, name). +- [x] `kratos` service (pinned) + `migrate`; identity schema (traits: email, name). → `compose.yml` adds `kratos`/`kratos-migrate` pinned to `oryd/kratos:v26.2.0` (verified latest stable); `kratos-migrate` runs `migrate sql -e --yes` against the per-service `kratos` DB after postgres is healthy, `kratos` waits for it (`service_completed_successfully`). `ory/kratos/identity.schema.json` = email (password identifier, verification/recovery via email) + `name {first,last}`, email required. `ory/kratos/kratos.yml` = bootable baseline: password login, self-service UIs pointing at the web routes (themed in §4), serve URLs, dev-throwaway secrets (prod via env, §3), identity schema wired; DSN via env. Themed flows/SSO/session/tokenizer/JWKS are the next §3/§4 items. Tests-first (`kratos.test.ts`: version pin + migrate-before-serve + DSN→kratos DB + schema traits + schema wiring). Boot-verified: migrate exits 0, kratos serves `/health/ready` 200, serves the identity schema, inits a password login flow; torn down. typecheck + 117 units green. - [ ] Kratos self-service flows (login, registration, recovery, verification, settings) → return URLs at our themed pages. - [ ] Kratos OIDC/SSO providers (Google/Microsoft/SAML) config (secrets via env). **None enabled by default** — a clean clone runs password-only; a provider activates purely by supplying its env creds. - [ ] Kratos session settings (cookie name, lifespan, sliding refresh).