Link the admin's recovery-code notice to the flow the code was minted on
CI / full-gate (push) Failing after 1m1s
CI / full-gate (push) Failing after 1m1s
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 %>">
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user