@ofidj/generator-fidj 1.3.1 → 1.4.1
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,28 @@ function startModule() {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
function render() {
|
|
277
|
+
// Once the mounted app owns the document the shell cannot draw over it, and
|
|
278
|
+
// its own router will try to match addresses that were never its business.
|
|
279
|
+
// So any address it does not own means starting the document again — checked
|
|
280
|
+
// first, because every later branch writes into an element that is gone.
|
|
281
|
+
if (moduleStarted && !moduleRoute()) {
|
|
282
|
+
window.location.reload();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// The provider handed this person here to answer a question. Nothing else
|
|
286
|
+
// this app might want to show belongs on the screen until they have.
|
|
287
|
+
if (interactionId) {
|
|
288
|
+
if (interactionFailed) {
|
|
289
|
+
root.innerHTML = `<section class="card"><p role="alert" class="error">${escape(message)}</p><p><a href="#/signin">Back to sign in</a></p></section>`;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!interaction) {
|
|
293
|
+
root.innerHTML = '<p role="status">Loading…</p>';
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
interactionScreen();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
277
299
|
if (!initialized) {
|
|
278
300
|
root.innerHTML = '<p role="status">Loading your session…</p>';
|
|
279
301
|
return;
|
|
@@ -285,11 +307,6 @@ function render() {
|
|
|
285
307
|
startModule();
|
|
286
308
|
return;
|
|
287
309
|
}
|
|
288
|
-
// The mounted app has taken the document; the shell cannot draw over it.
|
|
289
|
-
if (moduleStarted) {
|
|
290
|
-
window.location.reload();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
310
|
let route = currentRoute();
|
|
294
311
|
if (
|
|
295
312
|
!signedIn &&
|
|
@@ -519,6 +536,144 @@ function accountForm(route: string) {
|
|
|
519
536
|
: `<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
537
|
}
|
|
521
538
|
|
|
539
|
+
|
|
540
|
+
// ------------------------------------------------ signing in, for the provider
|
|
541
|
+
//
|
|
542
|
+
// When Fidj's provider needs a person to identify themselves it hands them to a
|
|
543
|
+
// Fidj front end — this one — rather than serving a page of its own. So this
|
|
544
|
+
// screen belongs to the provider's conversation, not to this app's: it says who
|
|
545
|
+
// is asking, collects what is being asked for, and posts it straight back.
|
|
546
|
+
//
|
|
547
|
+
// The post is a real form navigation, not a fetch: the provider answers with a
|
|
548
|
+
// redirect that carries the person onward through the authorization, and only a
|
|
549
|
+
// navigation can follow it. The single-use token comes from the context call,
|
|
550
|
+
// which is the only thing that reads the interaction cookie.
|
|
551
|
+
type Interaction = {
|
|
552
|
+
prompt: string;
|
|
553
|
+
csrf: string;
|
|
554
|
+
app: { id: string; title: string; description: string };
|
|
555
|
+
scopes: string[];
|
|
556
|
+
termsUri: string;
|
|
557
|
+
privacyUri: string;
|
|
558
|
+
action: string;
|
|
559
|
+
};
|
|
560
|
+
let interactionId = "";
|
|
561
|
+
let interactionError = "";
|
|
562
|
+
let interaction: Interaction | null = null;
|
|
563
|
+
let interactionFailed = false;
|
|
564
|
+
|
|
565
|
+
const scopeMeaning: Record<string, string> = {
|
|
566
|
+
openid: "An identity specific to this app",
|
|
567
|
+
profile: "Your display name",
|
|
568
|
+
email: "Your email and verification status",
|
|
569
|
+
offline_access: "Stay signed in",
|
|
570
|
+
"fidj:api": "Use Fidj account and privacy services for this app",
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const refusals: Record<string, string> = {
|
|
574
|
+
credentials: "We could not sign you in. Check your email and password.",
|
|
575
|
+
signup:
|
|
576
|
+
"Could not create an account. Use a valid email and a password of at least 12 characters, or sign in to your existing account.",
|
|
577
|
+
agreement: "Accept the app's service agreement to continue.",
|
|
578
|
+
refused: "That could not be completed. Please try again.",
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
function readInteraction() {
|
|
582
|
+
const query = window.location.hash.slice(2).split("?")[1] || "";
|
|
583
|
+
const parameters = new URLSearchParams(query);
|
|
584
|
+
const uid = parameters.get("interaction") || "";
|
|
585
|
+
if (!uid) return false;
|
|
586
|
+
interactionId = uid;
|
|
587
|
+
interactionError = parameters.get("error") || "";
|
|
588
|
+
// The id stays in the address while the screen is up. Taking it out looked
|
|
589
|
+
// tidier and made the screen a trap: the address became "#/signin", so going
|
|
590
|
+
// back to "#/signin" changed nothing, the document never reloaded, and the
|
|
591
|
+
// person stayed on a screen they had asked to leave. It is single-use, it is
|
|
592
|
+
// where the provider put it, and the form navigates away from it.
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async function loadInteraction() {
|
|
597
|
+
const endpoint = new URL(
|
|
598
|
+
`/oidc/interaction/${encodeURIComponent(interactionId)}/context`,
|
|
599
|
+
config.apiEndpoint,
|
|
600
|
+
);
|
|
601
|
+
const response = await fetch(endpoint.href, {
|
|
602
|
+
credentials: "include",
|
|
603
|
+
headers: {Accept: "application/json"},
|
|
604
|
+
signal: AbortSignal.timeout(10000),
|
|
605
|
+
});
|
|
606
|
+
if (!response.ok) throw new Error("This sign-in has expired. Start again from the app.");
|
|
607
|
+
interaction = (await response.json()) as Interaction;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function interactionScreen() {
|
|
611
|
+
const details = interaction!;
|
|
612
|
+
const asking = escape(details.app.title);
|
|
613
|
+
const notice = interactionError
|
|
614
|
+
? `<p role="alert" class="error">${escape(refusals[interactionError] || refusals.refused)}</p>`
|
|
615
|
+
: "";
|
|
616
|
+
const action = new URL(details.action, config.apiEndpoint).href;
|
|
617
|
+
// A refusal comes back as a redirect, so the typed address would be lost —
|
|
618
|
+
// and retyping an address is the part a person gets wrong twice. It is kept
|
|
619
|
+
// in this browser, never in the address: nothing about them travels in a URL.
|
|
620
|
+
let typed = "";
|
|
621
|
+
try {
|
|
622
|
+
typed = sessionStorage.getItem("fidj.interaction.email") || "";
|
|
623
|
+
} catch {}
|
|
624
|
+
const body =
|
|
625
|
+
details.prompt === "login"
|
|
626
|
+
? `<h2>Sign in to continue to ${asking}</h2>
|
|
627
|
+
<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>
|
|
628
|
+
${notice}
|
|
629
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
630
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
631
|
+
<label for="email">Email</label><input id="email" name="email" type="email" value="${escape(typed)}" autocomplete="username" required>
|
|
632
|
+
<div class="field-head"><label for="password">Password</label><a href="${escape(config.dashboardUrl)}/#/forgot">Forgot?</a></div>
|
|
633
|
+
<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>
|
|
634
|
+
<button class="primary" type="submit" name="action" value="continue">Sign in</button>
|
|
635
|
+
<button class="secondary" type="submit" name="action" value="signup">Create a Fidj account</button>
|
|
636
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
637
|
+
</form>`
|
|
638
|
+
: `<h2>Continue to ${asking}</h2>
|
|
639
|
+
<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>
|
|
640
|
+
${notice}
|
|
641
|
+
<ul class="scope-list">${details.scopes
|
|
642
|
+
.filter((scope) => scopeMeaning[scope])
|
|
643
|
+
.map((scope) => `<li>${escape(scopeMeaning[scope])}</li>`)
|
|
644
|
+
.join("")}</ul>
|
|
645
|
+
<form method="post" action="${escape(action)}" id="interaction">
|
|
646
|
+
<input type="hidden" name="csrf" value="${escape(details.csrf)}">
|
|
647
|
+
<label class="agreement-choice"><input type="checkbox" name="terms" value="true" required><span>I accept ${asking}'s service agreement.</span></label>
|
|
648
|
+
${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>` : ""}
|
|
649
|
+
<button class="primary" type="submit" name="action" value="continue">Allow and continue</button>
|
|
650
|
+
<button class="quiet" type="submit" name="action" value="cancel" formnovalidate>Cancel and go back</button>
|
|
651
|
+
</form>`;
|
|
652
|
+
|
|
653
|
+
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>
|
|
654
|
+
<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>
|
|
655
|
+
${highlights()}</div>
|
|
656
|
+
<div class="signin-form"><div>${body}</div>
|
|
657
|
+
<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>
|
|
658
|
+
${badges()}</section>`;
|
|
659
|
+
|
|
660
|
+
element("reveal")?.addEventListener("click", () => {
|
|
661
|
+
const field = element<HTMLInputElement>("password");
|
|
662
|
+
const button = element("reveal");
|
|
663
|
+
if (!field || !button) return;
|
|
664
|
+
const hidden = field.type === "password";
|
|
665
|
+
field.type = hidden ? "text" : "password";
|
|
666
|
+
button.textContent = hidden ? "Hide" : "Show";
|
|
667
|
+
});
|
|
668
|
+
element("interaction")?.addEventListener("submit", () => {
|
|
669
|
+
const address = element<HTMLInputElement>("email")?.value || "";
|
|
670
|
+
try {
|
|
671
|
+
if (address) sessionStorage.setItem("fidj.interaction.email", address);
|
|
672
|
+
else sessionStorage.removeItem("fidj.interaction.email");
|
|
673
|
+
} catch {}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
522
677
|
function renderAccount(route: string) {
|
|
523
678
|
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
679
|
<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,11 +741,28 @@ function wireAccount(route: string) {
|
|
|
586
741
|
}
|
|
587
742
|
window.addEventListener("hashchange", render);
|
|
588
743
|
render();
|
|
744
|
+
if (readInteraction()) {
|
|
745
|
+
render();
|
|
746
|
+
void loadInteraction()
|
|
747
|
+
.catch((error) => {
|
|
748
|
+
interactionFailed = true;
|
|
749
|
+
failed = true;
|
|
750
|
+
message =
|
|
751
|
+
error instanceof Error
|
|
752
|
+
? error.message
|
|
753
|
+
: "This sign-in could not be loaded. Start again from the app.";
|
|
754
|
+
})
|
|
755
|
+
.finally(render);
|
|
756
|
+
}
|
|
589
757
|
void action(async () => {
|
|
758
|
+
if (interactionId) return;
|
|
590
759
|
if (oidc && new URL(window.location.href).searchParams.has("state")) {
|
|
591
760
|
const callback = new URL(window.location.href);
|
|
592
761
|
window.history.replaceState(null, "", window.location.pathname + "#/content");
|
|
593
762
|
await oidc.completeLogin(callback);
|
|
763
|
+
try {
|
|
764
|
+
sessionStorage.removeItem("fidj.interaction.email");
|
|
765
|
+
} catch {}
|
|
594
766
|
}
|
|
595
767
|
await sdk.init(config.appId, {
|
|
596
768
|
apiEndpoint: config.apiEndpoint,
|