@ofidj/generator-fidj 1.0.3 → 1.0.4

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.
@@ -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 cannot load, the form stays blocked. The API records the accepted
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.
@@ -15,7 +15,7 @@
15
15
  "privacy:rehearse": "npm run build && node --test test/server.test.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@ofidj/node": "^3.6.24"
18
+ "@ofidj/node": "^3.6.26"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^22.0.0",
@@ -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" disabled>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>` : ""}
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
- await sdk.login(email, password, { autoSignup: signup, ...acceptance });
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" disabled>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>`
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
- await sdk.login(email, password, { autoSignup: false, ...acceptance });
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 async function bindAgreement(form: HTMLFormElement | null, title: string, endpoint: string, appId: string) {
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 = !acceptedAgreement(form); });
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ofidj/generator-fidj",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Generate a TypeScript app with Fidj sign-in, live server-side roles and per-app privacy.",
5
5
  "homepage": "https://fidj.ovh",
6
6
  "bugs": {