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)
|
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)
|
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)
|
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)
|
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)
|
docs/ Reference docs (plugin-contract.md — the authoritative plugin API)
|
||||||
e2e/ Playwright visual + functional E2E (Dockerfile.e2e + compose.e2e.yml run it)
|
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
|
retries: 10
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
pgdata:
|
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
|
## 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.
|
- [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 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 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).
|
- [ ] Kratos session settings (cookie name, lifespan, sliding refresh).
|
||||||
|
|||||||
Reference in New Issue
Block a user