Link the admin's recovery-code notice to the flow the code was minted on
CI / full-gate (push) Failing after 1m1s

This commit is contained in:
2026-10-31 13:05:06 +01:00
parent ccb4d32d39
commit 4cb21d8909
5 changed files with 28 additions and 13 deletions
+6
View File
@@ -6,6 +6,12 @@ at boot. Entries start at 0.3.0.
## Unreleased ## 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 ### 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. The code field on the verification and recovery pages takes a code with spaces or tabs around it.
+17
View File
@@ -257,3 +257,20 @@ Read most (opens and searches):
- `README.md` 2 - `README.md` 2
- `AGENTS.md` 1 - `AGENTS.md` 1
- `src/ui/theme-switch.test.ts` 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
@@ -2,7 +2,7 @@
Admin user create/edit form body, captured into the shell content slot. Config: Admin user create/edit form body, captured into the shell content slot. Config:
form { action, csrfToken, submitLabel, cancelHref, fields: field.ejs config[] } form { action, csrfToken, submitLabel, cancelHref, fields: field.ejs config[] }
edit? { nextLabel, stateAction, recoveryAction, deleteAction } (edit mode only) 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 error? string shown when a write was rejected
%><% %><%
const form = locals.form; const form = locals.form;
@@ -14,7 +14,7 @@
<%- include("partials/alert", { text: locals.error, tone: "neg" }) %> <%- include("partials/alert", { text: locals.error, tone: "neg" }) %>
<% } -%> <% } -%>
<% if (recovery) { -%> <% if (recovery) { -%>
<div class="alert alert-pos" role="status"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-check-circle"/></svg><div class="alert-body"><strong><%= t("admin.users.recovery.title") %></strong><span><%= t("admin.users.recovery.body") %> <a href="<%= localeHref("/recovery") %>"><%= t("admin.users.recovery.link") %></a></span><% if (recovery.code) { %><span class="recovery-code"><code><%= recovery.code %></code></span><% } %></div></div> <div class="alert alert-pos" role="status"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-check-circle"/></svg><div class="alert-body"><strong><%= t("admin.users.recovery.title") %></strong><span><%= t("admin.users.recovery.body") %> <a href="<%= recovery.link %>"><%= t("admin.users.recovery.link") %></a></span><% if (recovery.code) { %><span class="recovery-code"><code><%= recovery.code %></code></span><% } %></div></div>
<% } -%> <% } -%>
<form class="form-card" method="post" action="<%= localeHref(form.action) %>"> <form class="form-card" method="post" action="<%= localeHref(form.action) %>">
<input type="hidden" name="_csrf" value="<%= form.csrfToken %>"> <input type="hidden" name="_csrf" value="<%= form.csrfToken %>">
+3 -3
View File
@@ -63,7 +63,7 @@ test("admin Users screen: gate, list/filter, create, edit, deactivate, delete, r
let lastCreate: { traits?: unknown } | undefined; let lastCreate: { traits?: unknown } | undefined;
const kratosAdmin = stubAdmin({ const kratosAdmin = stubAdmin({
createIdentity: async (payload) => { lastCreate = payload as { traits?: unknown }; const created = mk("grace@example.com"); store.push(created); return created; }, 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); }, 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, getIdentity: async (id) => store.find((x) => x.id === id) ?? null,
listIdentities: async () => ({ identities: store, nextPageToken: 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(target.state, "inactive");
assert.equal(denylist.isRevoked(target.id, 0), true); 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}`); const rec = await post(`/admin/users/${target.id}/recovery`, `_csrf=${token}`);
assert.equal(rec.status, 200); assert.equal(rec.status, 200);
const recHtml = await rec.text(); const recHtml = await rec.text();
assert.match(recHtml, /Recovery code generated/); assert.match(recHtml, /Recovery code generated/);
assert.match(recHtml, /<code>123456<\/code>/); assert.match(recHtml, /<code>123456<\/code>/);
assert.doesNotMatch(recHtml, /self-service\/recovery\?code=/); // the unreachable admin-API link is gone assert.match(recHtml, new RegExp(`<a href="http://kratos/self-service/recovery\\?flow=flow-${target.id}">`)); // 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. // 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(); const confirm = await (await get(`/admin/users/${target.id}/delete`)).text();
-8
View File
@@ -12,7 +12,6 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | | 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 | | 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 | | 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 | | 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 ## 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. ### 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 Needs a Mac. Two suspects, both from `--user "$(id -u):$(id -g)"`: a macOS `id -g` is `20`, which is