Add kratos service + migrate (todo §3); pin oryd/kratos:v26.2.0, identity schema (email, name), bootable password config
This commit is contained in:
@@ -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)
|
||||
|
||||
26
compose.yml
26
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:
|
||||
|
||||
34
ory/kratos/identity.schema.json
Normal file
34
ory/kratos/identity.schema.json
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
ory/kratos/kratos.yml
Normal file
48
ory/kratos/kratos.yml
Normal file
@@ -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
|
||||
47
src/kratos.test.ts
Normal file
47
src/kratos.test.ts
Normal file
@@ -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/);
|
||||
});
|
||||
2
todo.md
2
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).
|
||||
|
||||
Reference in New Issue
Block a user