@ofidj/generator-fidj 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -274,6 +274,20 @@ function startModule() {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
function render() {
|
|
277
|
+
// The provider handed this person here to answer a question. Nothing else
|
|
278
|
+
// this app might want to show belongs on the screen until they have.
|
|
279
|
+
if (interactionId) {
|
|
280
|
+
if (interactionFailed) {
|
|
281
|
+
root.innerHTML = `<section class="card"><p role="alert" class="error">${escape(message)}</p><p><a href="#/signin">Back to sign in</a></p></section>`;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (!interaction) {
|
|
285
|
+
root.innerHTML = '<p role="status">Loading…</p>';
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
interactionScreen();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
277
291
|
if (!initialized) {
|
|
278
292
|
root.innerHTML = '<p role="status">Loading your session…</p>';
|
|
279
293
|
return;
|
|
@@ -519,6 +533,128 @@ function accountForm(route: string) {
|
|
|
519
533
|
: `<h2>My Fidj account</h2><p>Your identity is shared across your apps. Privacy choices remain separate for each app.</p><p id="verification-status">${emailVerified ? "Your email address is verified." : "Your email is not verified yet."}</p><button id="check-verification">Refresh verification status</button>${emailVerified ? "" : '<button id="resend-verification">Send verification email</button>'}<p><a href="#/forgot">Reset my password</a></p><button id="continue-app" class="primary">Continue to ${escape(config.title)}</button>`;
|
|
520
534
|
}
|
|
521
535
|
|
|
536
|
+
|
|
537
|
+
// ------------------------------------------------ signing in, for the provider
|
|
538
|
+
//
|
|
539
|
+
// When Fidj's provider needs a person to identify themselves it hands them to a
|
|
540
|
+
// Fidj front end — this one — rather than serving a page of its own. So this
|
|
541
|
+
// screen belongs to the provider's conversation, not to this app's: it says who
|
|
542
|
+
// is asking, collects what is being asked for, and posts it straight back.
|
|
543
|
+
//
|
|
544
|
+
// The post is a real form navigation, not a fetch: the provider answers with a
|
|
545
|
+
// redirect that carries the person onward through the authorization, and only a
|
|
546
|
+
// navigation can follow it. The single-use token comes from the context call,
|
|
547
|
+
// which is the only thing that reads the interaction cookie.
|
|
548
|
+
type Interaction = {
|
|
549
|
+
prompt: string;
|
|
550
|
+
csrf: string;
|
|
551
|
+
app: { id: string; title: string; description: string };
|
|
552
|
+
scopes: string[];
|
|
553
|
+
termsUri: string;
|
|
554
|
+
privacyUri: string;
|
|
555
|
+
action: string;
|
|
556
|
+
};
|
|
557
|
+
let interactionId = "";
|
|
558
|
+
let interactionError = "";
|
|
559
|
+
let interaction: Interaction | null = null;
|
|
560
|
+
let interactionFailed = false;
|
|
561
|
+
|
|
562
|
+
const scopeMeaning: Record<string, string> = {
|
|
563
|
+
openid: "An identity specific to this app",
|
|
564
|
+
profile: "Your display name",
|
|
565
|
+
email: "Your email and verification status",
|
|
566
|
+
offline_access: "Stay signed in",
|
|
567
|
+
"fidj:api": "Use Fidj account and privacy services for this app",
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const refusals: Record<string, string> = {
|
|
571
|
+
credentials: "We could not sign you in. Check your email and password.",
|
|
572
|
+
signup:
|
|
573
|
+
"Could not create an account. Use a valid email and a password of at least 12 characters, or sign in to your existing account.",
|
|
574
|
+
agreement: "Accept the app's service agreement to continue.",
|
|
575
|
+
refused: "That could not be completed. Please try again.",
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
function readInteraction() {
|
|
579
|
+
const query = window.location.hash.slice(2).split("?")[1] || "";
|
|
580
|
+
const parameters = new URLSearchParams(query);
|
|
581
|
+
const uid = parameters.get("interaction") || "";
|
|
582
|
+
if (!uid) return false;
|
|
583
|
+
interactionId = uid;
|
|
584
|
+
interactionError = parameters.get("error") || "";
|
|
585
|
+
// The address is cleaned immediately: the interaction id is single-use and has
|
|
586
|
+
// no business staying in history or in a shared link.
|
|
587
|
+
window.history.replaceState(null, "", "#/signin");
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function loadInteraction() {
|
|
592
|
+
const endpoint = new URL(
|
|
593
|
+
`/oidc/interaction/${encodeURIComponent(interactionId)}/context`,
|
|
594
|
+
config.apiEndpoint,
|
|
595
|
+
);
|
|
596
|
+
const response = await fetch(endpoint.href, {
|
|
597
|
+
credentials: "include",
|
|
598
|
+
headers: {Accept: "application/json"},
|
|
599
|
+
signal: AbortSignal.timeout(10000),
|
|
600
|
+
});
|
|
601
|
+
if (!response.ok) throw new Error("This sign-in has expired. Start again from the app.");
|
|
602
|
+
interaction = (await response.json()) as Interaction;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function interactionScreen() {
|
|
606
|
+
const details = interaction!;
|
|
607
|
+
const asking = escape(details.app.title);
|
|
608
|
+
const notice = interactionError
|
|
609
|
+
? `<p role="alert" class="error">${escape(refusals[interactionError] || refusals.refused)}</p>`
|
|
610
|
+
: "";
|
|
611
|
+
const action = new URL(details.action, config.apiEndpoint).href;
|
|
612
|
+
const body =
|
|
613
|
+
details.prompt === "login"
|
|
614
|
+
? `<h2>Sign in to continue to ${asking}</h2>
|
|
615
|
+
<p class="signin-lead">This is Fidj, the account behind ${asking}. One account, and separate choices for every app that uses it — ${asking} never sees your password.</p>
|
|
616
|
+
${notice}
|
|
617
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
618
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
619
|
+
<label for="email">Email</label><input id="email" name="email" type="email" autocomplete="username" required>
|
|
620
|
+
<div class="field-head"><label for="password">Password</label><a href="${escape(config.dashboardUrl)}/#/forgot">Forgot?</a></div>
|
|
621
|
+
<div class="password-field"><input id="password" name="password" type="password" autocomplete="current-password" required><button type="button" id="reveal" aria-controls="password">Show</button></div>
|
|
622
|
+
<button class="primary" type="submit" name="action" value="continue">Sign in</button>
|
|
623
|
+
<button class="secondary" type="submit" name="action" value="signup">Create a Fidj account</button>
|
|
624
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
625
|
+
</form>`
|
|
626
|
+
: `<h2>Continue to ${asking}</h2>
|
|
627
|
+
<p class="signin-lead">${asking} is asking for the information below. Optional privacy choices stay separate, and you can change them in Fidj at any time.</p>
|
|
628
|
+
${notice}
|
|
629
|
+
<ul class="scope-list">${details.scopes
|
|
630
|
+
.filter((scope) => scopeMeaning[scope])
|
|
631
|
+
.map((scope) => `<li>${escape(scopeMeaning[scope])}</li>`)
|
|
632
|
+
.join("")}</ul>
|
|
633
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
634
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
635
|
+
<label class="agreement-choice"><input type="checkbox" name="terms" value="true" required><span>I accept ${asking}'s service agreement.</span></label>
|
|
636
|
+
${details.termsUri ? `<p class="fineprint"><a href="${escape(details.termsUri)}" target="_blank" rel="noopener noreferrer">Service agreement</a>${details.privacyUri ? ` · <a href="${escape(details.privacyUri)}" target="_blank" rel="noopener noreferrer">Privacy notice</a>` : ""}</p>` : ""}
|
|
637
|
+
<button class="primary" type="submit" name="action" value="continue">Allow and continue</button>
|
|
638
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
639
|
+
</form>`;
|
|
640
|
+
|
|
641
|
+
root.innerHTML = `<section class="signin-shell"><div class="signin-intro is-plain"><header class="signin-masthead"><img class="app-mark" src="${escape(config.logo)}" alt=""><strong>${escape(config.title)}</strong></header>
|
|
642
|
+
<div class="signin-identity"><h1>Your identity.<br>Your choices.</h1><p class="signin-description">One account across every app that uses Fidj, and a separate set of choices for each one.</p></div>
|
|
643
|
+
${highlights()}</div>
|
|
644
|
+
<div class="signin-form"><div>${body}</div>
|
|
645
|
+
<div class="signin-trust"><p class="signin-trust-head"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><strong>What Fidj is</strong></p><p>Fidj holds your account so each app does not have to. You can see every app you use, what it holds, and take it back — at any time.</p></div></div>
|
|
646
|
+
${badges()}</section>`;
|
|
647
|
+
|
|
648
|
+
element("reveal")?.addEventListener("click", () => {
|
|
649
|
+
const field = element<HTMLInputElement>("password");
|
|
650
|
+
const button = element("reveal");
|
|
651
|
+
if (!field || !button) return;
|
|
652
|
+
const hidden = field.type === "password";
|
|
653
|
+
field.type = hidden ? "text" : "password";
|
|
654
|
+
button.textContent = hidden ? "Hide" : "Show";
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
522
658
|
function renderAccount(route: string) {
|
|
523
659
|
root.innerHTML = `<section class="signin-shell"><div class="signin-intro is-plain"><header class="signin-masthead"><img class="app-mark" src="${escape(config.logo)}" alt=""><strong>${escape(config.title)}</strong></header>
|
|
524
660
|
<div class="signin-identity"><h1>Your account.<br>Your control.</h1><p class="signin-description">Secure access to the apps you use, with one Fidj identity.</p></div>
|
|
@@ -586,7 +722,21 @@ function wireAccount(route: string) {
|
|
|
586
722
|
}
|
|
587
723
|
window.addEventListener("hashchange", render);
|
|
588
724
|
render();
|
|
725
|
+
if (readInteraction()) {
|
|
726
|
+
render();
|
|
727
|
+
void loadInteraction()
|
|
728
|
+
.catch((error) => {
|
|
729
|
+
interactionFailed = true;
|
|
730
|
+
failed = true;
|
|
731
|
+
message =
|
|
732
|
+
error instanceof Error
|
|
733
|
+
? error.message
|
|
734
|
+
: "This sign-in could not be loaded. Start again from the app.";
|
|
735
|
+
})
|
|
736
|
+
.finally(render);
|
|
737
|
+
}
|
|
589
738
|
void action(async () => {
|
|
739
|
+
if (interactionId) return;
|
|
590
740
|
if (oidc && new URL(window.location.href).searchParams.has("state")) {
|
|
591
741
|
const callback = new URL(window.location.href);
|
|
592
742
|
window.history.replaceState(null, "", window.location.pathname + "#/content");
|