@ofidj/generator-fidj 1.0.3 → 1.0.5
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.
- package/generators/app/templates/typescript/README.md +2 -1
- package/generators/app/templates/typescript/package.json +1 -1
- package/generators/app/templates/typescript/server/index.ts +1 -1
- package/generators/app/templates/typescript/src/content.ts +22 -6
- package/generators/app/templates/typescript/src/main.ts +20 -6
- package/generators/app/templates/typescript/src/service-agreement.ts +14 -3
- package/package.json +1 -1
|
@@ -76,7 +76,8 @@ Run `npm run privacy:rehearse` for an isolated temporary-store HTTP scenario cov
|
|
|
76
76
|
|
|
77
77
|
Sign-in and account creation require the unchecked service-agreement checkbox.
|
|
78
78
|
Read the app's current text/version from the dialog before accepting. If the
|
|
79
|
-
agreement
|
|
79
|
+
agreement is not accepted, submitting explains the requirement while preserving
|
|
80
|
+
the form; if the agreement cannot load, the form stays blocked. The API records the accepted
|
|
80
81
|
version per app; optional privacy choices are separate. The app owner must replace
|
|
81
82
|
the default demo agreement using the Fidj app's `configurationAsJSON.serviceAgreement`
|
|
82
83
|
(`version` and `text`) before release. No generated-source edit is needed.
|
|
@@ -285,7 +285,7 @@ export function createApp(
|
|
|
285
285
|
"Content-Type": file[1],
|
|
286
286
|
"X-Content-Type-Options": "nosniff",
|
|
287
287
|
"Referrer-Policy": "no-referrer",
|
|
288
|
-
"Content-Security-Policy": `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https
|
|
288
|
+
"Content-Security-Policy": `default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https: ${apiOrigin}; connect-src 'self' ${apiOrigin}; object-src 'none'; base-uri 'none'; frame-ancestors 'none'`,
|
|
289
289
|
});
|
|
290
290
|
res.end(req.method === "HEAD" ? undefined : content);
|
|
291
291
|
} catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {agreementMarkup, bindAgreement, acceptedAgreement} from "./service-agreement";
|
|
1
|
+
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
|
|
2
2
|
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
3
3
|
import config from "../app.config.json";
|
|
4
4
|
import "./style.css";
|
|
@@ -24,6 +24,9 @@ let message =
|
|
|
24
24
|
let failed = false;
|
|
25
25
|
let busy = false;
|
|
26
26
|
let leaving = false;
|
|
27
|
+
let signInEmail = "";
|
|
28
|
+
let signInPassword = "";
|
|
29
|
+
let signInAgreementAccepted = false;
|
|
27
30
|
const accountRoutes = ["forgot", "reset", "verify", "account"];
|
|
28
31
|
let linkToken = "";
|
|
29
32
|
let verificationConfirmed = false;
|
|
@@ -255,7 +258,7 @@ function render() {
|
|
|
255
258
|
? `<div class="signin-intro${config.highlights?.length ? "" : " is-plain"}"><header class="signin-masthead"><img class="app-mark" src="${escape(config.logo)}" alt=""><strong>${escape(config.title)}</strong></header>
|
|
256
259
|
<div class="signin-identity"><h1>${escape(config.welcome)}</h1><p class="signin-description">${escape(config.description)}</p></div>
|
|
257
260
|
${highlights()}</div>
|
|
258
|
-
<div class="signin-form"><div>${banner()}<h2>Sign in to ${escape(config.title)}</h2><form id="signin"><label for="email">Email</label><input id="email" type="email" placeholder="you@company.com" autocomplete="username" required><div class="field-head"><label for="password">Password</label><a href="#/forgot">Forgot?</a></div><div class="password-field"><input id="password" type="password" placeholder="••••••••••" autocomplete="current-password" required><button type="button" id="reveal" aria-controls="password">Show</button></div>${agreementMarkup()}<button class="primary" type="submit"
|
|
261
|
+
<div class="signin-form"><div>${banner()}<h2>Sign in to ${escape(config.title)}</h2><form id="signin"><label for="email">Email</label><input id="email" type="email" value="${escape(signInEmail)}" placeholder="you@company.com" autocomplete="username" required><div class="field-head"><label for="password">Password</label><a href="#/forgot">Forgot?</a></div><div class="password-field"><input id="password" type="password" value="${escape(signInPassword)}" placeholder="••••••••••" autocomplete="current-password" required><button type="button" id="reveal" aria-controls="password">Show</button></div>${agreementMarkup()}<button class="primary" type="submit">Continue</button><button class="secondary" type="submit" name="signup" value="true">Create an account</button></form>${config.allowAnonymous ? `<div class="signin-divider"><span>or explore first</span></div><button class="anonymous-entry" id="anonymous">Enter anonymously <span aria-hidden="true">→</span></button><p class="signin-footnote">No account needed to view the content.</p>` : ""}
|
|
259
262
|
<div class="signin-trust"><p class="signin-trust-head"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><strong>Your account, with Fidj</strong></p><p>Signing in creates one Fidj account you keep across every app that uses Fidj.</p><p>You choose what this app may store — and can export or erase it at any moment.</p></div></div>
|
|
260
263
|
${badges()}</div>`
|
|
261
264
|
: `
|
|
@@ -295,17 +298,30 @@ function render() {
|
|
|
295
298
|
navigate("content");
|
|
296
299
|
});
|
|
297
300
|
if (oidc && element("signin")) element("signin")!.innerHTML = agreementMarkup() + '<p>Continue securely with your Fidj account. Your password stays with Fidj.</p><button class="primary" type="submit">Continue with Fidj</button>';
|
|
298
|
-
void bindAgreement(element<HTMLFormElement>("signin"), config.title, config.apiEndpoint, config.appId);
|
|
301
|
+
void bindAgreement(element<HTMLFormElement>("signin"), config.title, config.apiEndpoint, config.appId, signInAgreementAccepted);
|
|
299
302
|
element<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
300
303
|
event.preventDefault();
|
|
301
|
-
const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
|
|
302
|
-
if (!acceptance) return;
|
|
303
304
|
const email = element<HTMLInputElement>("email")?.value || "";
|
|
304
305
|
const password = element<HTMLInputElement>("password")?.value || "";
|
|
306
|
+
const agreement = element<HTMLInputElement>("service-agreement");
|
|
307
|
+
signInEmail = email;
|
|
308
|
+
signInPassword = password;
|
|
309
|
+
signInAgreementAccepted = agreement?.checked === true;
|
|
310
|
+
const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
|
|
311
|
+
if (!acceptance) {
|
|
312
|
+
failed = true;
|
|
313
|
+
message = "Please accept the service agreement before continuing.";
|
|
314
|
+
render();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
305
317
|
const signup = (event.submitter as HTMLButtonElement)?.name === "signup";
|
|
306
318
|
void action(async () => {
|
|
307
319
|
if (oidc) {window.location.assign(await oidc.beginLogin()); return;}
|
|
308
|
-
|
|
320
|
+
try {
|
|
321
|
+
await sdk.login(email, password, { autoSignup: signup, ...acceptance });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new Error(signInErrorMessage(error));
|
|
324
|
+
}
|
|
309
325
|
await refresh();
|
|
310
326
|
anonymous = false;
|
|
311
327
|
// A new account belongs where a returning one lands: inside the app.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {agreementMarkup, bindAgreement, acceptedAgreement} from "./service-agreement";
|
|
1
|
+
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
|
|
2
2
|
import { FidjNodeService } from "@ofidj/node";
|
|
3
3
|
import "./style.css";
|
|
4
4
|
|
|
@@ -25,6 +25,9 @@ let notice = "";
|
|
|
25
25
|
let error = "";
|
|
26
26
|
let busy = false;
|
|
27
27
|
let leaving = false;
|
|
28
|
+
let signInEmail = "";
|
|
29
|
+
let signInPassword = "";
|
|
30
|
+
let signInAgreementAccepted = false;
|
|
28
31
|
const escape = (value: unknown) =>
|
|
29
32
|
String(value ?? "").replace(
|
|
30
33
|
/[&<>"']/g,
|
|
@@ -85,7 +88,7 @@ function render() {
|
|
|
85
88
|
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
86
89
|
${
|
|
87
90
|
!session
|
|
88
|
-
? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2><p>Sign in with your Fidj account.</p><label for="email">Email</label><input id="email" type="email" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" required>${agreementMarkup()}<button class="primary" type="submit"
|
|
91
|
+
? `<section class="welcome"><div><p class="eyebrow">A LITTLE SPACE FOR YOUR IDEAS</p><h1>Good ideas<br>start here.</h1><p>Keep your notes together, with access you understand and privacy you control.</p><div class="promise"><img src="/fidj-logo.png" alt=""><span>Your account connects through Fidj.<br>Your choices belong to this app.</span></div></div><form id="signin" class="card"><h2>Welcome to ${escape(settings.title)}</h2><p>Sign in with your Fidj account.</p><label for="email">Email</label><input id="email" type="email" value="${escape(signInEmail)}" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" value="${escape(signInPassword)}" autocomplete="current-password" required>${agreementMarkup()}<button class="primary" type="submit">Continue</button>${settings.localDemo ? `<div class="demo"><strong>Try the local example</strong><p>Alex owns the app. Maya and Sam start with the Free role.</p><button type="button" data-demo="alex">Alex · owner</button><button type="button" data-demo="maya">Maya · member</button><button type="button" data-demo="sam">Sam · member</button></div>` : ""}</form></section>`
|
|
89
92
|
: `
|
|
90
93
|
<div class="page-heading"><div><p class="eyebrow">YOUR WORKSPACE</p><h1>A place to think.</h1><p>${escape(session.username)} <span class="roles">${session.roles.map(escape).join(" · ") || "No assigned roles"}</span></p></div><button id="signout">Sign out</button></div>
|
|
91
94
|
<nav><button id="workspace-tab" class="${view === "workspace" ? "selected" : ""}">My notes</button><button id="privacy-tab" class="${view === "privacy" ? "selected" : ""}">My privacy</button><button id="refresh">Refresh access</button></nav>
|
|
@@ -107,15 +110,26 @@ function render() {
|
|
|
107
110
|
}`
|
|
108
111
|
}
|
|
109
112
|
<footer>Built with Fidj · One identity. Separate choices for every app.</footer></main>`;
|
|
110
|
-
void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId);
|
|
113
|
+
void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId, signInAgreementAccepted);
|
|
111
114
|
el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
112
115
|
event.preventDefault();
|
|
113
|
-
const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
|
|
114
|
-
if (!acceptance) return;
|
|
115
116
|
const email = el<HTMLInputElement>("email").value;
|
|
116
117
|
const password = el<HTMLInputElement>("password").value;
|
|
118
|
+
signInEmail = email;
|
|
119
|
+
signInPassword = password;
|
|
120
|
+
signInAgreementAccepted = el<HTMLInputElement>("service-agreement").checked;
|
|
121
|
+
const acceptance = acceptedAgreement(event.currentTarget as HTMLFormElement);
|
|
122
|
+
if (!acceptance) {
|
|
123
|
+
error = "Please accept the service agreement before continuing.";
|
|
124
|
+
render();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
117
127
|
void action(async () => {
|
|
118
|
-
|
|
128
|
+
try {
|
|
129
|
+
await sdk.login(email, password, { autoSignup: false, ...acceptance });
|
|
130
|
+
} catch (reason) {
|
|
131
|
+
throw new Error(signInErrorMessage(reason));
|
|
132
|
+
}
|
|
119
133
|
await load();
|
|
120
134
|
});
|
|
121
135
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function agreementMarkup() {
|
|
2
|
-
return `<div class="signin-agreement"><label class="agreement-choice"><input id="service-agreement" type="checkbox" required disabled><span id="agreement-label">I accept the service agreement for this app.</span></label><button type="button" id="read-agreement" disabled>Read service agreement</button><p id="agreement-status" class="fineprint" role="status">Loading service agreement…</p></div><dialog id="agreement-dialog" aria-labelledby="agreement-heading"><h2 id="agreement-heading">Service agreement</h2><p id="agreement-version"></p><p id="agreement-text"></p><button type="button" id="close-agreement">Close agreement</button></dialog>`;
|
|
2
|
+
return `<div class="signin-agreement"><label class="agreement-choice"><input id="service-agreement" type="checkbox" aria-required="true" disabled><span id="agreement-label">I accept the service agreement for this app.</span></label><button type="button" id="read-agreement" disabled>Read service agreement</button><p id="agreement-status" class="fineprint" role="status">Loading service agreement…</p></div><dialog id="agreement-dialog" aria-labelledby="agreement-heading"><h2 id="agreement-heading">Service agreement</h2><p id="agreement-version"></p><p id="agreement-text"></p><button type="button" id="close-agreement">Close agreement</button></dialog>`;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function acceptedAgreement(form: HTMLFormElement) {
|
|
@@ -8,14 +8,24 @@ export function acceptedAgreement(form: HTMLFormElement) {
|
|
|
8
8
|
return { termsAccepted: true, termsVersion: checkbox.dataset.version };
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export
|
|
11
|
+
export function signInErrorMessage(error: unknown) {
|
|
12
|
+
const detail = error as {code?: number; reason?: unknown; message?: unknown};
|
|
13
|
+
const reason = typeof detail?.reason === "string" ? detail.reason : typeof detail?.message === "string" ? detail.message : "";
|
|
14
|
+
if (detail?.code === 429) return "Too many attempts. Please wait before trying again.";
|
|
15
|
+
if (reason === "unknown-user") return "We could not sign in to this account. Check the email and password.";
|
|
16
|
+
if (reason === "already exists - inconsistent request") return "An account already uses this email. Check the password or sign in instead.";
|
|
17
|
+
if (/ECONNREFUSED|ENOTFOUND|ETIMEDOUT|ECONNRESET|EHOSTUNREACH|network/i.test(reason)) return "We cannot reach Fidj right now. Please try again.";
|
|
18
|
+
return "We could not sign in to this account. Please try again.";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function bindAgreement(form: HTMLFormElement | null, title: string, endpoint: string, appId: string, checked = false) {
|
|
12
22
|
if (!form) return;
|
|
13
23
|
const checkbox = form.querySelector<HTMLInputElement>("#service-agreement")!;
|
|
14
24
|
const read = form.querySelector<HTMLButtonElement>("#read-agreement")!;
|
|
15
25
|
const dialog = form.querySelector<HTMLDialogElement>("#agreement-dialog")!;
|
|
16
26
|
const status = form.querySelector<HTMLElement>("#agreement-status")!;
|
|
17
27
|
const submitButtons = form.querySelectorAll<HTMLButtonElement>('button[type="submit"]');
|
|
18
|
-
const update = () => submitButtons.forEach(button => { button.disabled =
|
|
28
|
+
const update = () => submitButtons.forEach(button => { button.disabled = checkbox.disabled; });
|
|
19
29
|
form.querySelector("#agreement-label")!.textContent = `I accept the service agreement for ${title}.`;
|
|
20
30
|
update();
|
|
21
31
|
checkbox.addEventListener("change", update);
|
|
@@ -29,6 +39,7 @@ export async function bindAgreement(form: HTMLFormElement | null, title: string,
|
|
|
29
39
|
if (!form.isConnected) return;
|
|
30
40
|
checkbox.dataset.version = agreement.version;
|
|
31
41
|
checkbox.disabled = false;
|
|
42
|
+
checkbox.checked = checked;
|
|
32
43
|
read.disabled = false;
|
|
33
44
|
dialog.querySelector("#agreement-version")!.textContent = `Version ${agreement.version}`;
|
|
34
45
|
dialog.querySelector("#agreement-text")!.textContent = agreement.text;
|