Built-in Groups admin screen (todo §5); /admin/groups list (search/sort/paginate) + create/delete + membership (add/remove users & nested groups), writing only to Keto — gated admin-only + CSRF-guarded like Users (Kratos read only to label pickers). A group = Keto subject set Group:<name>#members, exists while it has ≥1 member: create writes the first-member tuple, delete removes all by partial-filter. Extracted shared admin-nav.ts (Dashboard·Users·Groups); new generic rowHeader <th scope=row> data-table cell. Stability-reviewer run as a local PR: symmetric subject UUID-validation, duplicate-name rejection, malformed-%→404. 228→237 units + typecheck green; core Keto interactions boot-verified live

This commit is contained in:
2026-06-18 17:40:36 +02:00
parent 79cfa2ee7f
commit 32e5e2f7eb
16 changed files with 798 additions and 30 deletions

View File

@@ -0,0 +1,26 @@
<%#
Admin group create form body (todo §5), captured into the shell content slot. Config:
form { action, csrfToken, submitLabel, cancelHref, nameField: field.ejs config,
memberOptions: {label,value}[], selectedMember }
error? string shown when a write was rejected
%><%
const form = locals.form;
-%>
<div class="form-page">
<% if (locals.error) { -%>
<%- include("alert", { text: locals.error, tone: "neg" }) %>
<% } -%>
<form class="form-card" method="post" action="<%= form.action %>">
<input type="hidden" name="_csrf" value="<%= form.csrfToken %>">
<%- include("field", form.nameField) %>
<div class="field">
<label for="member">First member</label>
<span class="select"><select id="member" name="member" required><option value="" disabled<% if (!form.selectedMember) { %> selected<% } %>>Choose a member…</option><% form.memberOptions.forEach((o) => { %><option value="<%= o.value %>"<% if (form.selectedMember === o.value) { %> selected<% } %>><%= o.label %></option><% }) %></select></span>
<span class="field-hint">A group exists once it has a member; add more after creating it.</span>
</div>
<div class="form-actions">
<a class="btn" href="<%= form.cancelHref %>">Cancel</a>
<button class="btn btn-primary" type="submit"><%= form.submitLabel %></button>
</div>
</form>
</div>