@ofidj/generator-fidj 1.0.7 → 1.0.9
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/README.md +1 -1
- package/generators/app/templates/typescript/server/index.ts +9 -0
- package/generators/app/templates/typescript/src/content.ts +1 -1
- package/generators/app/templates/typescript/src/main.ts +28 -5
- package/generators/app/templates/typescript/src/version.ts +12 -1
- package/lib/scaffold.cjs +6 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Readiness confirms this handler only. Register its trusted URL and independent s
|
|
|
128
128
|
|
|
129
129
|
## Beta OIDC input
|
|
130
130
|
|
|
131
|
-
`--oidc-issuer` selects the OIDC entry for content
|
|
131
|
+
`--oidc-issuer` selects the OIDC entry for any generated app — a content site, a console module, or an app with its own backend — while preserving branding and anonymous-entry configuration. The entry becomes a single **Continue with Fidj** button, no password field, and the generated `.env` carries `FIDJ_OIDC_ISSUER` so the app's own server can serve it to its page. Register the app's exact callback first (`PUT /v3/apps/:appId/oidc`), or the provider refuses the authorization. This is beta: the API ships an integrated provider from 3.6.26, but it serves no `/oidc` routes until an operator configures an issuer and signing keys, so confirm the issuer you pass actually answers discovery before offering it.
|
|
132
132
|
|
|
133
133
|
A compatible issuer, REST API and registered callback without a fragment are prerequisites. Validate that integration and coordinated SDK publication before offering it to app builders. Notes retains its existing authentication flow; no subject migration is implied.
|
|
134
134
|
|
|
@@ -11,6 +11,7 @@ export interface Settings {
|
|
|
11
11
|
apiEndpoint: string;
|
|
12
12
|
dashboardUrl: string;
|
|
13
13
|
title: string;
|
|
14
|
+
releaseVersion: string;
|
|
14
15
|
localDemo: boolean;
|
|
15
16
|
}
|
|
16
17
|
class HttpError extends Error {
|
|
@@ -319,11 +320,19 @@ if (require.main === module) {
|
|
|
319
320
|
throw new Error("Invalid Fidj API URL.");
|
|
320
321
|
const port = Number(process.env.PORT || 8200);
|
|
321
322
|
const host = process.env.HOST || "127.0.0.1";
|
|
323
|
+
// The page needs the issuer to start a redirect sign-in, and it may only be
|
|
324
|
+
// this API's own provider: an issuer on another origin would send people to
|
|
325
|
+
// type this app's credentials somewhere unrelated.
|
|
326
|
+
const oidcIssuer = process.env.FIDJ_OIDC_ISSUER || "";
|
|
327
|
+
if (oidcIssuer && new URL(oidcIssuer).href !== new URL("/oidc", api).href)
|
|
328
|
+
throw new Error("FIDJ_OIDC_ISSUER must be the Fidj API's own /oidc endpoint.");
|
|
322
329
|
const settings = {
|
|
323
330
|
appId,
|
|
324
331
|
apiEndpoint,
|
|
332
|
+
oidcIssuer,
|
|
325
333
|
dashboardUrl: process.env.FIDJ_DASHBOARD_URL || "https://fidj.ovh",
|
|
326
334
|
title: process.env.APP_TITLE || "My workspace",
|
|
335
|
+
releaseVersion: process.env.APP_VERSION || "",
|
|
327
336
|
localDemo:
|
|
328
337
|
process.env.LOCAL_DEMO === "true" &&
|
|
329
338
|
host === "127.0.0.1" &&
|
|
@@ -7,7 +7,7 @@ import { showVersionBadge } from "./version";
|
|
|
7
7
|
const sdk = new FidjNodeService();
|
|
8
8
|
const oidc = config.oidcIssuer ? new FidjOidcClient({issuer: config.oidcIssuer, clientId: config.appId, redirectUri: window.location.origin + window.location.pathname, apiEndpoint: config.apiEndpoint, storage: sessionStorage}) : null;
|
|
9
9
|
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
10
|
-
showVersionBadge(config.releaseVersion);
|
|
10
|
+
showVersionBadge(config.releaseVersion, config.title === "Fidj" ? config.apiEndpoint : undefined);
|
|
11
11
|
const appPath = `/me/apps/${encodeURIComponent(config.appId)}`;
|
|
12
12
|
let signedIn = false;
|
|
13
13
|
let emailVerified = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {agreementMarkup, bindAgreement, acceptedAgreement, signInErrorMessage} from "./service-agreement";
|
|
2
|
-
import { FidjNodeService } from "@ofidj/node";
|
|
2
|
+
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
3
3
|
import "./style.css";
|
|
4
4
|
import { showVersionBadge } from "./version";
|
|
5
5
|
|
|
@@ -12,9 +12,13 @@ type Settings = {
|
|
|
12
12
|
dashboardUrl: string;
|
|
13
13
|
localDemo: boolean;
|
|
14
14
|
releaseVersion: string;
|
|
15
|
+
oidcIssuer?: string;
|
|
15
16
|
};
|
|
16
17
|
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
17
18
|
const sdk = new FidjNodeService();
|
|
19
|
+
// When the app is configured with a provider it never sees a password: the
|
|
20
|
+
// entry hands the person to Fidj and gets a code back.
|
|
21
|
+
let oidc: FidjOidcClient | null = null;
|
|
18
22
|
let settings: Settings;
|
|
19
23
|
let session: Session | null = null;
|
|
20
24
|
let notes: Note[] = [];
|
|
@@ -90,7 +94,7 @@ function render() {
|
|
|
90
94
|
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
91
95
|
${
|
|
92
96
|
!session
|
|
93
|
-
? `<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
|
|
97
|
+
? `<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>${oidc ? `<p>Continue securely with your Fidj account. Your password stays with Fidj.</p>${agreementMarkup()}<button class="primary" type="submit">Continue with Fidj</button>` : `<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>`
|
|
94
98
|
: `
|
|
95
99
|
<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>
|
|
96
100
|
<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>
|
|
@@ -115,8 +119,8 @@ function render() {
|
|
|
115
119
|
void bindAgreement(el<HTMLFormElement>("signin"), settings.title, settings.apiEndpoint, settings.appId, signInAgreementAccepted);
|
|
116
120
|
el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
117
121
|
event.preventDefault();
|
|
118
|
-
const email = el<HTMLInputElement>("email")
|
|
119
|
-
const password = el<HTMLInputElement>("password")
|
|
122
|
+
const email = el<HTMLInputElement>("email")?.value || "";
|
|
123
|
+
const password = el<HTMLInputElement>("password")?.value || "";
|
|
120
124
|
signInEmail = email;
|
|
121
125
|
signInPassword = password;
|
|
122
126
|
signInAgreementAccepted = el<HTMLInputElement>("service-agreement").checked;
|
|
@@ -127,6 +131,10 @@ function render() {
|
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
void action(async () => {
|
|
134
|
+
if (oidc) {
|
|
135
|
+
window.location.assign(await oidc.beginLogin());
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
130
138
|
try {
|
|
131
139
|
await sdk.login(email, password, { autoSignup: false, ...acceptance });
|
|
132
140
|
} catch (reason) {
|
|
@@ -250,7 +258,22 @@ function render() {
|
|
|
250
258
|
async function start() {
|
|
251
259
|
try {
|
|
252
260
|
settings = await (await fetch("/api/config")).json();
|
|
253
|
-
showVersionBadge(settings.releaseVersion);
|
|
261
|
+
showVersionBadge(settings.releaseVersion, settings.title === "Fidj" ? settings.apiEndpoint : undefined);
|
|
262
|
+
if (settings.oidcIssuer)
|
|
263
|
+
oidc = new FidjOidcClient({
|
|
264
|
+
issuer: settings.oidcIssuer,
|
|
265
|
+
clientId: settings.appId,
|
|
266
|
+
redirectUri: window.location.origin + window.location.pathname,
|
|
267
|
+
apiEndpoint: settings.apiEndpoint,
|
|
268
|
+
storage: sessionStorage,
|
|
269
|
+
});
|
|
270
|
+
// Coming back from Fidj: swap the code for a session, and take the code out
|
|
271
|
+
// of the address bar before anything can reload it.
|
|
272
|
+
if (oidc && new URL(window.location.href).searchParams.has("state")) {
|
|
273
|
+
const callback = new URL(window.location.href);
|
|
274
|
+
window.history.replaceState(null, "", window.location.pathname);
|
|
275
|
+
await oidc.completeLogin(callback);
|
|
276
|
+
}
|
|
254
277
|
await sdk.init(settings.appId, {
|
|
255
278
|
apiEndpoint: settings.apiEndpoint,
|
|
256
279
|
prod: !settings.localDemo,
|
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
export function showVersionBadge(version: string): void {
|
|
1
|
+
export function showVersionBadge(version: string, apiEndpoint?: string): void {
|
|
2
2
|
if (!/^\d{2}\.\d{2}\.\d{2}$/.test(version || "")) return;
|
|
3
3
|
const badge = document.createElement("div");
|
|
4
4
|
badge.className = "fidj-version";
|
|
5
5
|
badge.setAttribute("aria-label", `App version ${version}`);
|
|
6
6
|
badge.textContent = `v${version}`;
|
|
7
7
|
document.body.append(badge);
|
|
8
|
+
if (apiEndpoint) {
|
|
9
|
+
void fetch(`${apiEndpoint.replace(/\/$/, "")}/status`)
|
|
10
|
+
.then(response => response.ok ? response.json() : null)
|
|
11
|
+
.then(status => {
|
|
12
|
+
const apiVersion = status?.version || status?.built;
|
|
13
|
+
if (!apiVersion) return;
|
|
14
|
+
badge.textContent = `v${version} · API ${apiVersion}`;
|
|
15
|
+
badge.setAttribute("aria-label", `App version ${version}, API version ${apiVersion}`);
|
|
16
|
+
})
|
|
17
|
+
.catch(() => undefined);
|
|
18
|
+
}
|
|
8
19
|
}
|
package/lib/scaffold.cjs
CHANGED
|
@@ -36,7 +36,6 @@ function scaffold(destination, options) {
|
|
|
36
36
|
if (options.oidcIssuer) {
|
|
37
37
|
const issuer = new URL(options.oidcIssuer);
|
|
38
38
|
if (issuer.origin !== api.origin || issuer.pathname !== "/oidc" || issuer.hash || issuer.search || issuer.username || issuer.password) throw new Error("OIDC issuer must share the API origin and use /oidc.");
|
|
39
|
-
if (options.content === undefined && !options.module) throw new Error("OIDC currently requires a content app or console module.");
|
|
40
39
|
}
|
|
41
40
|
const title = options.title || name;
|
|
42
41
|
if (/[\r\n]/.test(title)) throw new Error("Title must be one line.");
|
|
@@ -126,7 +125,11 @@ function scaffold(destination, options) {
|
|
|
126
125
|
JSON.stringify(pkg, null, 2) + "\n",
|
|
127
126
|
);
|
|
128
127
|
fs.renameSync(path.join(dir, "gitignore"), path.join(dir, ".gitignore"));
|
|
129
|
-
const
|
|
128
|
+
const releaseVersion = new Date().toISOString().slice(2, 10).replaceAll("-", ".");
|
|
129
|
+
// The app's own server serves this to its page: the entry cannot redirect to
|
|
130
|
+
// a provider it was never told about.
|
|
131
|
+
const issuerSetting = options.oidcIssuer ? `FIDJ_OIDC_ISSUER=${options.oidcIssuer}\n` : "";
|
|
132
|
+
const settings = `FIDJ_APP_ID=${options.appId}\nFIDJ_API_ENDPOINT=${api.href.replace(/\/$/, "")}\n${issuerSetting}FIDJ_DASHBOARD_URL=https://fidj.ovh\nAPP_TITLE=${title}\nAPP_VERSION=${releaseVersion}\nPORT=8200\nHOST=127.0.0.1\nLOCAL_DEMO=false\nFIDJ_DATA_DIR=../.fidj-data/${name}\nFIDJ_PRIVACY_ADAPTER_KEY=\n`;
|
|
130
133
|
const localApi = options.local
|
|
131
134
|
? "http://localhost:3201/v3"
|
|
132
135
|
: api.href.replace(/\/$/, "");
|
|
@@ -136,7 +139,7 @@ function scaffold(destination, options) {
|
|
|
136
139
|
apiEndpoint: localApi,
|
|
137
140
|
dashboardUrl: options.local ? "http://localhost:4200" : "https://fidj.ovh",
|
|
138
141
|
title,
|
|
139
|
-
releaseVersion
|
|
142
|
+
releaseVersion,
|
|
140
143
|
localDemo: Boolean(options.local),
|
|
141
144
|
allowAnonymous: anonymous === true || anonymous === "true",
|
|
142
145
|
welcome: options.welcome || title,
|