diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fc38df..287ee49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0. ## Unreleased +### The admin's recovery-code notice links to the flow the code was minted on + +After an admin generates a recovery code for a user, the notice's link opens the recovery flow the +code belongs to, where the user enters it. Before, it opened a new recovery flow at its email step, +which doesn't accept the code. + ### A verification or recovery code pasted with whitespace around it is accepted The code field on the verification and recovery pages takes a code with spaces or tabs around it. diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 69e6c51..46aa50b 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -257,3 +257,20 @@ Read most (opens and searches): - `README.md` 2 - `AGENTS.md` 1 - `src/ui/theme-switch.test.ts` 1 + +## Reading cost, chunk 4a258a237c53, finished 2026-10-31T12:04:07Z + +154777 new input tokens before the first commit. + +Read most (opens and searches): + +- `e2e-tests/full-flow.spec.ts` 9 +- `todo.md` 6 +- `README.md` 5 +- `src/auth/flow-view.ts` 5 +- `src/auth/flow-view.test.ts` 4 +- `src/auth/routes.ts` 4 +- `src/auth/kratos-public.test.ts` 3 +- `src/i18n/locales/en-US.ts` 3 +- `e2e-tests/compose.full.yml` 2 +- `src/auth/kratos-admin.ts` 2 diff --git a/examples/plugins/admin/views/partials/user-form-body.ejs b/examples/plugins/admin/views/partials/user-form-body.ejs index a9c3d78..9fe4183 100644 --- a/examples/plugins/admin/views/partials/user-form-body.ejs +++ b/examples/plugins/admin/views/partials/user-form-body.ejs @@ -2,7 +2,7 @@ Admin user create/edit form body, captured into the shell content slot. Config: form { action, csrfToken, submitLabel, cancelHref, fields: field.ejs config[] } edit? { nextLabel, stateAction, recoveryAction, deleteAction } (edit mode only) - recovery? { code? } shown after a recovery code is generated (recovery is code-based) + recovery? { code, link } shown after a recovery code is generated; link is the flow the code was minted on, the only one that accepts it error? string shown when a write was rejected %><% const form = locals.form; @@ -14,7 +14,7 @@ <%- include("partials/alert", { text: locals.error, tone: "neg" }) %> <% } -%> <% if (recovery) { -%> -
<%= t("admin.users.recovery.title") %><%= t("admin.users.recovery.body") %> "><%= t("admin.users.recovery.link") %><% if (recovery.code) { %><%= recovery.code %><% } %>
+
<%= t("admin.users.recovery.title") %><%= t("admin.users.recovery.body") %> <%= t("admin.users.recovery.link") %><% if (recovery.code) { %><%= recovery.code %><% } %>
<% } -%>
diff --git a/src/http/app-admin.test.ts b/src/http/app-admin.test.ts index dcf1c4b..9b84c1a 100644 --- a/src/http/app-admin.test.ts +++ b/src/http/app-admin.test.ts @@ -63,7 +63,7 @@ test("admin Users screen: gate, list/filter, create, edit, deactivate, delete, r let lastCreate: { traits?: unknown } | undefined; const kratosAdmin = stubAdmin({ createIdentity: async (payload) => { lastCreate = payload as { traits?: unknown }; const created = mk("grace@example.com"); store.push(created); return created; }, - createRecoveryCode: async (id) => ({ code: "123456", link: `http://kratos/self-service/recovery?code=123456&id=${id}` }), + createRecoveryCode: async (id) => ({ code: "123456", link: `http://kratos/self-service/recovery?flow=flow-${id}` }), deleteIdentity: async (id) => { const i = store.findIndex((x) => x.id === id); if (i >= 0) store.splice(i, 1); }, getIdentity: async (id) => store.find((x) => x.id === id) ?? null, listIdentities: async () => ({ identities: store, nextPageToken: null }), @@ -133,13 +133,13 @@ test("admin Users screen: gate, list/filter, create, edit, deactivate, delete, r assert.equal(target.state, "inactive"); assert.equal(denylist.isRevoked(target.id, 0), true); - // Recovery: renders the edit page (200) showing the generated code (code-based; no admin-host link). + // Recovery: renders the edit page (200) showing the generated code, linked to the flow it was minted on. const rec = await post(`/admin/users/${target.id}/recovery`, `_csrf=${token}`); assert.equal(rec.status, 200); const recHtml = await rec.text(); assert.match(recHtml, /Recovery code generated/); assert.match(recHtml, /123456<\/code>/); - assert.doesNotMatch(recHtml, /self-service\/recovery\?code=/); // the unreachable admin-API link is gone + assert.match(recHtml, new RegExp(``)); // the code only works in the flow that minted it // Delete needs a deliberate confirm step (zero-JS): GET renders the interstitial, POST performs it. const confirm = await (await get(`/admin/users/${target.id}/delete`)).text(); diff --git a/todo.md b/todo.md index 9c1dcb5..5d88bde 100644 --- a/todo.md +++ b/todo.md @@ -12,7 +12,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| -| 45 | 0.4.21 | | | **Link the admin's recovery-code notice to the flow the code was minted on, so the user can enter it.** | 1 | 2 | 2 | 3 | 2 | 2 by 5 | 7.0 | | 19 | 0.5.0 | | question | **Run the documented Docker commands on macOS and fix whatever misbehaves.** | 2 | 2 | 1 | 3 | 3 | 1 by 3 | 3.3 | | 16 | 0.5.0 | | question | **Decide whether the single Keto `Resource` namespace splits into per-domain namespaces.** | 3 | 2 | 3 | 6 | 6 | 7 by 2 | 0.5 | | 6 | 0.5.0 | | | **Warn at discovery when a route or nav node gates on a permission no plugin declares.** | 2 | 1 | 1 | 2 | 2 | 7 by 3, 9 by 3 | 8.0 | @@ -26,13 +25,6 @@ ## Details -### 45. Link the admin's recovery-code notice to the flow the code was minted on, so the user can enter it. - -The notice (`examples/plugins/admin/views/partials/user-form-body.ejs`) links a bare `/recovery`, -which starts a fresh flow at its email step. Kratos binds an admin-minted code to the flow it was minted on, -whose URL is the `recovery_link` that `createRecoveryCode` returns as `RecoveryCode.link`. Render that -link instead. - ### 19. Run the documented Docker commands on macOS and fix whatever misbehaves. Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is