Render SSO buttons per configured Kratos OIDC provider (todo §4); flow-view collects oidc nodes → auth-card submit buttons, server-side visibility, drop mockup #sso-toggle CSS
This commit is contained in:
@@ -213,6 +213,7 @@ const loginFlow = (id: string): Flow => ({
|
||||
node({ name: "identifier", required: true, type: "email" }, "E-Mail"),
|
||||
node({ name: "password", required: true, type: "password" }, "Password"),
|
||||
node({ name: "method", type: "submit", value: "password" }, "Sign in"),
|
||||
{ attributes: { name: "provider", type: "submit", value: "google" }, group: "oidc", messages: [], meta: { label: { id: 1, text: "Sign in with Google", type: "info" } }, type: "input" },
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -256,6 +257,9 @@ test("renders a fetched flow as the themed auth page: fields post straight to Kr
|
||||
assert.match(html, /name="password"[^>]*type="password"/);
|
||||
assert.match(html, /<button type="submit"[^>]*name="method" value="password">Sign in<\/button>/);
|
||||
assert.match(html, /<a href="\/registration">Create one<\/a>/); // alt link to register
|
||||
// Configured OIDC provider → an SSO submit button in the same form (posts provider=google).
|
||||
assert.match(html, /<div class="sso"/);
|
||||
assert.match(html, /<button type="submit" class="sso-btn" name="provider" value="google">.*Sign in with Google<\/span><\/button>/s);
|
||||
// The flow-level error renders as an alert.
|
||||
assert.match(html, /class="alert alert-neg"/);
|
||||
assert.match(html, /The provided credentials are invalid\./);
|
||||
|
||||
@@ -14,6 +14,7 @@ test("auth-card renders head, SSO providers (text logo + icon link), body slot a
|
||||
sso: { providers: [
|
||||
{ label: "Continue with Google", logo: "G" },
|
||||
{ label: "Continue with SAML SSO", icon: "i-shield", href: "/sso/saml" },
|
||||
{ label: "Sign in with Microsoft", logo: "M", name: "provider", value: "microsoft" },
|
||||
] },
|
||||
body: '<div id="fields">FORM</div><button class="btn btn-primary btn-block">Sign in</button>',
|
||||
alt: { text: "Don't have an account?", href: "/register", label: "Create one" },
|
||||
@@ -25,6 +26,8 @@ test("auth-card renders head, SSO providers (text logo + icon link), body slot a
|
||||
assert.match(html, /<div class="sso" aria-label="Single sign-on options"><ul class="sso-list">/);
|
||||
assert.match(html, /<li><button type="button" class="sso-btn"><span class="sso-logo" aria-hidden="true">G<\/span><span class="sso-label">Continue with Google<\/span><\/button><\/li>/);
|
||||
assert.match(html, /<li><a class="sso-btn" href="\/sso\/saml"><span class="sso-logo" aria-hidden="true"><svg class="ico ico-sm"><use href="#i-shield"\s*\/?><\/svg><\/span><span class="sso-label">Continue with SAML SSO<\/span><\/a><\/li>/);
|
||||
// A provider with name/value submits to the form (Kratos OIDC) — type="submit", not a decorative button.
|
||||
assert.match(html, /<li><button type="submit" class="sso-btn" name="provider" value="microsoft"><span class="sso-logo" aria-hidden="true">M<\/span><span class="sso-label">Sign in with Microsoft<\/span><\/button><\/li>/);
|
||||
assert.match(html, /<\/ul><div class="auth-divider">or<\/div><\/div>/);
|
||||
|
||||
// Body slot lands inside .auth-form; alt footer renders text + link.
|
||||
|
||||
@@ -43,6 +43,9 @@ test("maps a password login flow: csrf hidden, themed email/password fields, a s
|
||||
// One submit button carrying its method name/value.
|
||||
assert.deepEqual(view.buttons, [{ label: "Sign in", name: "method", value: "password" }]);
|
||||
|
||||
// No OIDC providers configured ⇒ no SSO buttons.
|
||||
assert.deepEqual(view.sso, []);
|
||||
|
||||
// Chrome derived from the flow type.
|
||||
assert.equal(view.title, "Sign in");
|
||||
assert.equal(view.alt?.href, "/registration");
|
||||
@@ -72,15 +75,22 @@ test("maps field errors and flow-level messages by tone", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("skips oidc (SSO) nodes but keeps the default-group csrf — SSO buttons are a later item", () => {
|
||||
test("collects oidc nodes as SSO providers (text logo = initial), keeping csrf and the password submit separate", () => {
|
||||
const view = buildFlowView(
|
||||
flow([
|
||||
node({ name: "csrf_token", type: "hidden", value: "tok" }),
|
||||
node({ name: "provider", type: "submit", value: "google" }, { label: "Sign in with Google", group: "oidc" }),
|
||||
node({ name: "provider", type: "submit", value: "microsoft" }, { label: "Sign in with Microsoft", group: "oidc" }),
|
||||
node({ name: "method", type: "submit", value: "password" }, { label: "Sign in", group: "password" }),
|
||||
]),
|
||||
"login",
|
||||
);
|
||||
// One provider button per oidc node — a submit (name/value) posting to the same Kratos form.
|
||||
assert.deepEqual(view.sso, [
|
||||
{ label: "Sign in with Google", logo: "G", name: "provider", value: "google" },
|
||||
{ label: "Sign in with Microsoft", logo: "M", name: "provider", value: "microsoft" },
|
||||
]);
|
||||
// SSO nodes don't leak into hidden/buttons.
|
||||
assert.deepEqual(view.hidden, [{ name: "csrf_token", value: "tok" }]);
|
||||
assert.deepEqual(view.buttons, [{ label: "Sign in", name: "method", value: "password" }]);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Kratos flow → themed view model (todo §4). Pure: turns a fetched self-service Flow
|
||||
// (src/kratos-public.ts) into the data views/auth.ejs renders — hidden inputs (incl. the
|
||||
// CSRF token), themed fields, submit buttons, and tone-mapped messages. The form posts
|
||||
// straight back to `flow.ui.action`, so Kratos owns its CSRF; we only render and map errors.
|
||||
// SSO/oidc buttons are skipped here — they're derived per provider in the next §4 item.
|
||||
// CSRF token), themed fields, submit buttons, tone-mapped messages, and one SSO button per
|
||||
// configured `oidc` provider. The form posts straight back to `flow.ui.action`, so Kratos
|
||||
// owns its CSRF; we only render and map errors. No providers configured ⇒ no SSO buttons.
|
||||
|
||||
import type { Flow, FlowType, UiNode } from "./kratos-public.ts";
|
||||
|
||||
@@ -24,6 +24,14 @@ export interface FlowButton {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
// An OIDC provider, rendered as a submit button (name/value) posting to the same Kratos form.
|
||||
export interface SsoProvider {
|
||||
label: string; // Kratos' own label, e.g. "Sign in with Google"
|
||||
logo: string; // text logo (provider initial) — lucide ships no brand marks
|
||||
name: string; // submit field (Kratos: "provider")
|
||||
value: string; // provider id (Kratos: "google")
|
||||
}
|
||||
|
||||
export interface FlowMessage {
|
||||
text: string;
|
||||
tone: "info" | "neg" | "pos" | "warn";
|
||||
@@ -43,6 +51,7 @@ export interface FlowView extends FlowChrome {
|
||||
hidden: { name: string; value: string }[];
|
||||
messages: FlowMessage[];
|
||||
method: string;
|
||||
sso: SsoProvider[]; // one per configured oidc provider; empty ⇒ no SSO section
|
||||
}
|
||||
|
||||
// Themed route → Kratos flow type. The routes mirror kratos.yml's flow ui_urls.
|
||||
@@ -79,6 +88,8 @@ function tone(type: string): FlowMessage["tone"] {
|
||||
return "info";
|
||||
}
|
||||
|
||||
const ssoLogo = (value: string): string => (value.charAt(0) || "?").toUpperCase();
|
||||
|
||||
function toField(node: UiNode, name: string, type: string): FlowField {
|
||||
const value = str(node.attributes["value"]);
|
||||
const autocomplete = str(node.attributes["autocomplete"]);
|
||||
@@ -101,12 +112,19 @@ export function buildFlowView(flow: Flow, type: FlowType): FlowView {
|
||||
const hidden: { name: string; value: string }[] = [];
|
||||
const fields: FlowField[] = [];
|
||||
const buttons: FlowButton[] = [];
|
||||
const sso: SsoProvider[] = [];
|
||||
|
||||
for (const node of flow.ui.nodes) {
|
||||
if (node.type !== "input" || node.group === "oidc") continue; // SSO buttons: next §4 item
|
||||
if (node.type !== "input") continue;
|
||||
const name = str(node.attributes["name"]) ?? "";
|
||||
const inputType = str(node.attributes["type"]) ?? "text";
|
||||
if (inputType === "hidden") {
|
||||
if (node.group === "oidc") {
|
||||
// One submit button per configured provider; posts provider=<value> to the same form.
|
||||
if (inputType === "submit" || inputType === "button") {
|
||||
const value = str(node.attributes["value"]) ?? "";
|
||||
sso.push({ label: node.meta.label?.text ?? value, logo: ssoLogo(value), name, value });
|
||||
}
|
||||
} else if (inputType === "hidden") {
|
||||
hidden.push({ name, value: str(node.attributes["value"]) ?? "" });
|
||||
} else if (inputType === "submit" || inputType === "button") {
|
||||
const value = str(node.attributes["value"]);
|
||||
@@ -123,6 +141,7 @@ export function buildFlowView(flow: Flow, type: FlowType): FlowView {
|
||||
hidden,
|
||||
messages: (flow.ui.messages ?? []).map((m) => ({ text: m.text, tone: tone(m.type) })),
|
||||
method: flow.ui.method || "post",
|
||||
sso,
|
||||
...CHROME[type],
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user