Built-in Users admin screen (todo §5); /admin/users list (filter/sort/paginate) + create/edit/deactivate/delete + trigger-recovery, writing only to Kratos via the admin client — gated admin-only (anon→/login, non-admin→403) and CSRF-guarded like logout. New kratosAdmin.createRecoveryCode; reserved the "admin" plugin id; views:[viewsDir] so subfolder views reuse partials/. Reviewer §5 opener: extracted shell-context.ts (buildShellContext/shellUser) shared by dashboard+admin, threading the real signed-in user (drops the hardcoded demo profile). 217→228 units + 8 visual E2E green; boot-verified full CRUD+recovery live on the Ory stack
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
Form field: label (+ inline link / "Optional") · icon input · hint · error.
|
||||
Mirrors html-css-foundation auth markup. Config:
|
||||
id, name, label
|
||||
type? (default "text"), value?, placeholder?, autocomplete?, required?, minlength?
|
||||
type? (default "text"), value?, placeholder?, autocomplete?, required?, readonly?, minlength?
|
||||
icon? input-ico id (e.g. "i-mail") → left-padded input
|
||||
optional? show an "Optional" tag in the label row
|
||||
link? { href, label } inline beside the label (e.g. Forgot password?)
|
||||
@@ -24,7 +24,7 @@
|
||||
<% } else { -%>
|
||||
<label for="<%= id %>"><%= locals.label %></label>
|
||||
<% } -%>
|
||||
<div class="input-wrap"><% if (icon) { %><svg class="ico ico-sm input-ico" aria-hidden="true"><use href="#<%= icon %>"/></svg><% } %><input class="input<% if (icon) { %> has-ico<% } %>" id="<%= id %>" name="<%= locals.name %>" type="<%= type %>"<% if (locals.autocomplete) { %> autocomplete="<%= locals.autocomplete %>"<% } %><% if (locals.placeholder) { %> placeholder="<%= locals.placeholder %>"<% } %><% if (locals.value != null) { %> value="<%= locals.value %>"<% } %><% if (locals.minlength != null) { %> minlength="<%= locals.minlength %>"<% } %><% if (error) { %> aria-invalid="true" aria-describedby="<%= errId %>"<% } %><% if (locals.required) { %> required<% } %>></div>
|
||||
<div class="input-wrap"><% if (icon) { %><svg class="ico ico-sm input-ico" aria-hidden="true"><use href="#<%= icon %>"/></svg><% } %><input class="input<% if (icon) { %> has-ico<% } %>" id="<%= id %>" name="<%= locals.name %>" type="<%= type %>"<% if (locals.autocomplete) { %> autocomplete="<%= locals.autocomplete %>"<% } %><% if (locals.placeholder) { %> placeholder="<%= locals.placeholder %>"<% } %><% if (locals.value != null) { %> value="<%= locals.value %>"<% } %><% if (locals.minlength != null) { %> minlength="<%= locals.minlength %>"<% } %><% if (error) { %> aria-invalid="true" aria-describedby="<%= errId %>"<% } %><% if (locals.required) { %> required<% } %><% if (locals.readonly) { %> readonly<% } %>></div>
|
||||
<% if (hint) { -%>
|
||||
<span class="field-hint"><%= hint %></span>
|
||||
<% } -%>
|
||||
|
||||
36
views/partials/user-form-body.ejs
Normal file
36
views/partials/user-form-body.ejs
Normal file
@@ -0,0 +1,36 @@
|
||||
<%#
|
||||
Admin user create/edit form body (todo §5), 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?, link? } shown after a recovery link is generated
|
||||
error? string shown when a write was rejected
|
||||
%><%
|
||||
const form = locals.form;
|
||||
const edit = locals.edit;
|
||||
const recovery = locals.recovery;
|
||||
-%>
|
||||
<div class="form-page">
|
||||
<% if (locals.error) { -%>
|
||||
<%- include("alert", { text: locals.error, tone: "neg" }) %>
|
||||
<% } -%>
|
||||
<% 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>Recovery link generated</strong><% if (recovery.link) { %><span class="recovery-link"><a href="<%= recovery.link %>"><%= recovery.link %></a></span><% } %><% if (recovery.code) { %><span>Code: <code><%= recovery.code %></code></span><% } %></div></div>
|
||||
<% } -%>
|
||||
<form class="form-card" method="post" action="<%= form.action %>">
|
||||
<input type="hidden" name="_csrf" value="<%= form.csrfToken %>">
|
||||
<% form.fields.forEach((field) => { -%>
|
||||
<%- include("field", field) %>
|
||||
<% }) -%>
|
||||
<div class="form-actions">
|
||||
<a class="btn" href="<%= form.cancelHref %>">Cancel</a>
|
||||
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
|
||||
</div>
|
||||
</form>
|
||||
<% if (edit) { -%>
|
||||
<section class="form-card admin-actions" aria-label="Account actions">
|
||||
<form method="post" action="<%= edit.recoveryAction %>"><input type="hidden" name="_csrf" value="<%= form.csrfToken %>"><button class="btn" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-mail"/></svg>Send recovery link</button></form>
|
||||
<form method="post" action="<%= edit.stateAction %>"><input type="hidden" name="_csrf" value="<%= form.csrfToken %>"><button class="btn" type="submit"><%= edit.nextLabel %></button></form>
|
||||
<form method="post" action="<%= edit.deleteAction %>"><input type="hidden" name="_csrf" value="<%= form.csrfToken %>"><button class="btn btn-danger" type="submit"><svg class="ico ico-sm" aria-hidden="true"><use href="#i-trash"/></svg>Delete user</button></form>
|
||||
</section>
|
||||
<% } -%>
|
||||
</div>
|
||||
Reference in New Issue
Block a user