@ofidj/generator-fidj 1.0.0 → 1.0.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.
- package/README.md +93 -58
- package/bin/create-fidj.cjs +54 -0
- package/generators/app/index.js +58 -189
- package/generators/app/templates/typescript/README.md +73 -0
- package/generators/app/templates/typescript/app.config.json +10 -0
- package/generators/app/templates/typescript/gitignore +5 -0
- package/generators/app/templates/typescript/package.json +25 -0
- package/generators/app/templates/typescript/public/fidj-logo.png +0 -0
- package/generators/app/templates/typescript/public/index.html +16 -0
- package/generators/app/templates/typescript/scripts/build.mjs +46 -0
- package/generators/app/templates/typescript/scripts/privacy-check.mjs +21 -0
- package/generators/app/templates/typescript/scripts/render-content.mjs +14 -0
- package/generators/app/templates/typescript/server/data-store.ts +123 -0
- package/generators/app/templates/typescript/server/index.ts +329 -0
- package/generators/app/templates/typescript/server/privacy-adapter.ts +21 -0
- package/generators/app/templates/typescript/src/content.ts +416 -0
- package/generators/app/templates/typescript/src/main.ts +243 -0
- package/generators/app/templates/typescript/src/style.css +639 -0
- package/generators/app/templates/typescript/test/server.test.mjs +287 -0
- package/generators/app/templates/typescript/tsconfig.json +15 -0
- package/lib/app-module.cjs +59 -0
- package/lib/scaffold.cjs +145 -0
- package/package.json +18 -12
- package/generators/app/templates/app2018/.travis.yml +0 -35
- package/generators/app/templates/app2018/LICENSE +0 -21
- package/generators/app/templates/app2018/Procfile +0 -1
- package/generators/app/templates/app2018/README.md +0 -49
- package/generators/app/templates/app2018/config.xml +0 -91
- package/generators/app/templates/app2018/gulpfile.js +0 -63
- package/generators/app/templates/app2018/ionic.config.json +0 -9
- package/generators/app/templates/app2018/package.json +0 -90
- package/generators/app/templates/app2018/platforms/README.md +0 -1
- package/generators/app/templates/app2018/resources/README.md +0 -6
- package/generators/app/templates/app2018/resources/icon.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2018/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2018/resources/marketing.json +0 -19
- package/generators/app/templates/app2018/resources/splash.png +0 -0
- package/generators/app/templates/app2018/src/app/app.component.ts +0 -88
- package/generators/app/templates/app2018/src/app/app.html +0 -19
- package/generators/app/templates/app2018/src/app/app.module.ts +0 -40
- package/generators/app/templates/app2018/src/app/app.scss +0 -16
- package/generators/app/templates/app2018/src/app/main.ts +0 -5
- package/generators/app/templates/app2018/src/app/shared/card.model.ts +0 -10
- package/generators/app/templates/app2018/src/app/shared/profile.service.ts +0 -46
- package/generators/app/templates/app2018/src/app/shared/shared.module.ts +0 -35
- package/generators/app/templates/app2018/src/app/shared/storage.service.ts +0 -11
- package/generators/app/templates/app2018/src/app/shared/version.const.ts +0 -5
- package/generators/app/templates/app2018/src/assets/icon/favicon.ico +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/gg.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/github.png +0 -0
- package/generators/app/templates/app2018/src/assets/imgs/login.svg +0 -5668
- package/generators/app/templates/app2018/src/assets/imgs/logo.png +0 -0
- package/generators/app/templates/app2018/src/index.html +0 -49
- package/generators/app/templates/app2018/src/manifest.json +0 -13
- package/generators/app/templates/app2018/src/pages/home/home.html +0 -56
- package/generators/app/templates/app2018/src/pages/home/home.scss +0 -3
- package/generators/app/templates/app2018/src/pages/home/home.ts +0 -126
- package/generators/app/templates/app2018/src/pages/login/login.html +0 -56
- package/generators/app/templates/app2018/src/pages/login/login.scss +0 -44
- package/generators/app/templates/app2018/src/pages/login/login.ts +0 -59
- package/generators/app/templates/app2018/src/pages/profile/profile.html +0 -33
- package/generators/app/templates/app2018/src/pages/profile/profile.scss +0 -3
- package/generators/app/templates/app2018/src/pages/profile/profile.ts +0 -36
- package/generators/app/templates/app2018/src/service-worker.js +0 -31
- package/generators/app/templates/app2018/src/theme/variables.scss +0 -88
- package/generators/app/templates/app2018/tests/karma.unit-tests.conf.js +0 -84
- package/generators/app/templates/app2018/tests/unit/app.test.js +0 -10
- package/generators/app/templates/app2018/tsconfig.json +0 -28
- package/generators/app/templates/app2018/tslint.json +0 -8
- package/generators/app/templates/app2018/web.js +0 -21
- package/generators/app/templates/app2021/.travis.yml +0 -32
- package/generators/app/templates/app2021/LICENSE +0 -21
- package/generators/app/templates/app2021/Procfile +0 -1
- package/generators/app/templates/app2021/README.md +0 -45
- package/generators/app/templates/app2021/angular.json +0 -188
- package/generators/app/templates/app2021/browserslist +0 -12
- package/generators/app/templates/app2021/ionic.config.json +0 -9
- package/generators/app/templates/app2021/karma.conf.js +0 -31
- package/generators/app/templates/app2021/package-lock.json +0 -5
- package/generators/app/templates/app2021/package.json +0 -77
- package/generators/app/templates/app2021/platforms/README.md +0 -1
- package/generators/app/templates/app2021/resources/README.md +0 -6
- package/generators/app/templates/app2021/resources/icon.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-01-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-02-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-ipad.png +0 -0
- package/generators/app/templates/app2021/resources/marketing-03-iphone.png +0 -0
- package/generators/app/templates/app2021/resources/marketing.json +0 -19
- package/generators/app/templates/app2021/resources/splash.png +0 -0
- package/generators/app/templates/app2021/src/app/app-routing.module.ts +0 -25
- package/generators/app/templates/app2021/src/app/app.component.html +0 -3
- package/generators/app/templates/app2021/src/app/app.component.scss +0 -0
- package/generators/app/templates/app2021/src/app/app.component.spec.ts +0 -50
- package/generators/app/templates/app2021/src/app/app.component.ts +0 -28
- package/generators/app/templates/app2021/src/app/app.module.ts +0 -24
- package/generators/app/templates/app2021/src/app/content/content-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/content/content.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/content/content.page.html +0 -31
- package/generators/app/templates/app2021/src/app/content/content.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/content/content.page.ts +0 -26
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.html +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.scss +0 -4
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.spec.ts +0 -24
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.component.ts +0 -16
- package/generators/app/templates/app2021/src/app/explore-container/explore-container.module.ts +0 -14
- package/generators/app/templates/app2021/src/app/my/my-routing.module.ts +0 -36
- package/generators/app/templates/app2021/src/app/my/my.module.ts +0 -19
- package/generators/app/templates/app2021/src/app/my/my.page.html +0 -15
- package/generators/app/templates/app2021/src/app/my/my.page.scss +0 -1
- package/generators/app/templates/app2021/src/app/my/my.page.spec.ts +0 -26
- package/generators/app/templates/app2021/src/app/my/my.page.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/profile/profile.module.ts +0 -20
- package/generators/app/templates/app2021/src/app/profile/profile.page.html +0 -49
- package/generators/app/templates/app2021/src/app/profile/profile.page.scss +0 -0
- package/generators/app/templates/app2021/src/app/profile/profile.page.spec.ts +0 -25
- package/generators/app/templates/app2021/src/app/profile/profile.page.ts +0 -44
- package/generators/app/templates/app2021/src/app/shared/const.ts +0 -3
- package/generators/app/templates/app2021/src/app/signin/signin-routing.module.ts +0 -16
- package/generators/app/templates/app2021/src/app/signin/signin.module.ts +0 -18
- package/generators/app/templates/app2021/src/app/signin/signin.page.html +0 -53
- package/generators/app/templates/app2021/src/app/signin/signin.page.scss +0 -36
- package/generators/app/templates/app2021/src/app/signin/signin.page.ts +0 -69
- package/generators/app/templates/app2021/src/assets/icon/favicon.png +0 -0
- package/generators/app/templates/app2021/src/assets/shapes.svg +0 -1
- package/generators/app/templates/app2021/src/environments/environment.prod.ts +0 -3
- package/generators/app/templates/app2021/src/environments/environment.ts +0 -16
- package/generators/app/templates/app2021/src/global.scss +0 -26
- package/generators/app/templates/app2021/src/index.html +0 -26
- package/generators/app/templates/app2021/src/main.ts +0 -12
- package/generators/app/templates/app2021/src/polyfills.ts +0 -67
- package/generators/app/templates/app2021/src/test.ts +0 -20
- package/generators/app/templates/app2021/src/theme/variables.scss +0 -232
- package/generators/app/templates/app2021/src/zone-flags.ts +0 -5
- package/generators/app/templates/app2021/tests/protractor.conf.js +0 -36
- package/generators/app/templates/app2021/tests/src/app.e2e-spec.ts +0 -14
- package/generators/app/templates/app2021/tests/src/app.po.ts +0 -11
- package/generators/app/templates/app2021/tests/tsconfig.json +0 -13
- package/generators/app/templates/app2021/tsconfig.app.json +0 -18
- package/generators/app/templates/app2021/tsconfig.json +0 -23
- package/generators/app/templates/app2021/tsconfig.spec.json +0 -18
- package/generators/app/templates/app2021/tslint.json +0 -149
- package/generators/app/templates/app2021/web.js +0 -21
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { FidjNodeService, FidjOidcClient } from "@ofidj/node";
|
|
2
|
+
import config from "../app.config.json";
|
|
3
|
+
import "./style.css";
|
|
4
|
+
|
|
5
|
+
const sdk = new FidjNodeService();
|
|
6
|
+
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;
|
|
7
|
+
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
8
|
+
const appPath = `/me/apps/${encodeURIComponent(config.appId)}`;
|
|
9
|
+
let signedIn = false;
|
|
10
|
+
let emailVerified = false;
|
|
11
|
+
let anonymous = false;
|
|
12
|
+
let initialized = false;
|
|
13
|
+
let roles: string[] = [];
|
|
14
|
+
let consent: Record<string, boolean> = {};
|
|
15
|
+
let history: Array<{ type: string; granted: boolean; changedAt: string }> = [];
|
|
16
|
+
const departure = new URLSearchParams(window.location.search).get("departure");
|
|
17
|
+
let message =
|
|
18
|
+
departure === "completed"
|
|
19
|
+
? `You left ${config.title}. Your other memberships and shared identity remain.`
|
|
20
|
+
: departure === "pending"
|
|
21
|
+
? "Access was revoked. Cleanup is queued; follow its progress in Fidj."
|
|
22
|
+
: "";
|
|
23
|
+
let failed = false;
|
|
24
|
+
let busy = false;
|
|
25
|
+
let leaving = false;
|
|
26
|
+
const accountRoutes = ["forgot", "reset", "verify", "account"];
|
|
27
|
+
let linkToken = "";
|
|
28
|
+
let verificationConfirmed = false;
|
|
29
|
+
function currentRoute() {
|
|
30
|
+
const [route, query] = window.location.hash.slice(2).split("?");
|
|
31
|
+
if (["reset", "verify"].includes(route) && query) {
|
|
32
|
+
verificationConfirmed = false;
|
|
33
|
+
linkToken = new URLSearchParams(query).get("token") || "";
|
|
34
|
+
window.history.replaceState(null, "", "#/" + route);
|
|
35
|
+
}
|
|
36
|
+
return route;
|
|
37
|
+
}
|
|
38
|
+
const escape = (value: unknown) =>
|
|
39
|
+
String(value ?? "").replace(
|
|
40
|
+
/[&<>"']/g,
|
|
41
|
+
(char) =>
|
|
42
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
43
|
+
char
|
|
44
|
+
]!,
|
|
45
|
+
);
|
|
46
|
+
const element = <T extends HTMLElement>(id: string) =>
|
|
47
|
+
document.getElementById(id) as T | null;
|
|
48
|
+
async function request(path: string, method = "GET", data?: unknown) {
|
|
49
|
+
const token = await sdk.fidjGetIdToken();
|
|
50
|
+
const response = await fetch(config.apiEndpoint + path, {
|
|
51
|
+
method,
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${token}`,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
57
|
+
signal: AbortSignal.timeout(10000),
|
|
58
|
+
});
|
|
59
|
+
const result = await response.json();
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
if ([401, 403].includes(response.status)) {
|
|
62
|
+
signedIn = false;
|
|
63
|
+
await sdk.logout(true);
|
|
64
|
+
}
|
|
65
|
+
throw new Error(result.message || result.status || "Please retry.");
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
async function refresh() {
|
|
70
|
+
const membership = await request(
|
|
71
|
+
`/apps/${encodeURIComponent(config.appId)}/me`,
|
|
72
|
+
);
|
|
73
|
+
roles = membership.roles.map((role: { type: string }) => role.type);
|
|
74
|
+
[consent, history] = await Promise.all([
|
|
75
|
+
request(appPath + "/consents"),
|
|
76
|
+
request(appPath + "/consents/history").then((result) => result.history),
|
|
77
|
+
]);
|
|
78
|
+
emailVerified = (await request("/me")).user?.verified === true;
|
|
79
|
+
signedIn = true;
|
|
80
|
+
}
|
|
81
|
+
async function action(task: () => Promise<void>) {
|
|
82
|
+
if (busy) return;
|
|
83
|
+
busy = true;
|
|
84
|
+
root.setAttribute("aria-busy", "true");
|
|
85
|
+
root
|
|
86
|
+
.querySelectorAll<HTMLButtonElement | HTMLInputElement>("button, input")
|
|
87
|
+
.forEach((control) => {
|
|
88
|
+
control.disabled = true;
|
|
89
|
+
});
|
|
90
|
+
const submit = root.querySelector<HTMLButtonElement>("button.primary");
|
|
91
|
+
if (submit) submit.textContent = "Please wait…";
|
|
92
|
+
failed = false;
|
|
93
|
+
if (initialized) message = "";
|
|
94
|
+
try {
|
|
95
|
+
await task();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
failed = true;
|
|
98
|
+
const detail = error as {
|
|
99
|
+
code?: number;
|
|
100
|
+
reason?: unknown;
|
|
101
|
+
message?: unknown;
|
|
102
|
+
};
|
|
103
|
+
if (detail.code === 429)
|
|
104
|
+
message = "Too many attempts. Please wait before trying again.";
|
|
105
|
+
else if (detail.message || detail.reason) {
|
|
106
|
+
let reason: any = detail.message || detail.reason;
|
|
107
|
+
try {
|
|
108
|
+
if (typeof reason === "string") reason = JSON.parse(reason);
|
|
109
|
+
} catch {}
|
|
110
|
+
message =
|
|
111
|
+
typeof reason === "string"
|
|
112
|
+
? reason
|
|
113
|
+
: reason.message ||
|
|
114
|
+
reason.status ||
|
|
115
|
+
"The request could not be completed. Please retry.";
|
|
116
|
+
} else
|
|
117
|
+
message =
|
|
118
|
+
error instanceof Error
|
|
119
|
+
? error.message
|
|
120
|
+
: "The request could not be completed. Please retry.";
|
|
121
|
+
if (
|
|
122
|
+
/jwt expired|session revoked|session expired|token.*expired/i.test(
|
|
123
|
+
message,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
message = "Your session has ended. Please sign in again.";
|
|
127
|
+
} finally {
|
|
128
|
+
busy = false;
|
|
129
|
+
initialized = true;
|
|
130
|
+
root.setAttribute("aria-busy", "false");
|
|
131
|
+
render();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function navigate(route: string) {
|
|
135
|
+
window.history.replaceState(null, "", "#/" + route);
|
|
136
|
+
if (!busy) render();
|
|
137
|
+
}
|
|
138
|
+
function moduleRoute() {
|
|
139
|
+
const route = window.location.hash.slice(2).split("?")[0];
|
|
140
|
+
if (
|
|
141
|
+
!config.moduleEntry ||
|
|
142
|
+
!route ||
|
|
143
|
+
["signin", "content", "privacy", ...accountRoutes].includes(route)
|
|
144
|
+
)
|
|
145
|
+
return null;
|
|
146
|
+
const target = new URL(config.moduleEntry, window.location.href);
|
|
147
|
+
target.hash = window.location.hash;
|
|
148
|
+
return target.href;
|
|
149
|
+
}
|
|
150
|
+
function render() {
|
|
151
|
+
if (!initialized) {
|
|
152
|
+
root.innerHTML = '<p role="status">Loading your session…</p>';
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const applicationRoute = moduleRoute();
|
|
156
|
+
if (applicationRoute) {
|
|
157
|
+
root.innerHTML = '<p role="status">Opening your app…</p>';
|
|
158
|
+
window.location.replace(applicationRoute);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let route = currentRoute();
|
|
162
|
+
if (
|
|
163
|
+
!signedIn &&
|
|
164
|
+
!(config.allowAnonymous && anonymous) &&
|
|
165
|
+
!["forgot", "reset", "verify"].includes(route)
|
|
166
|
+
)
|
|
167
|
+
route = "signin";
|
|
168
|
+
else if (!["signin", "content", "privacy", ...accountRoutes].includes(route))
|
|
169
|
+
route = "content";
|
|
170
|
+
if (route === "privacy" && !signedIn) route = "signin";
|
|
171
|
+
window.history.replaceState(null, "", "#/" + route);
|
|
172
|
+
if (accountRoutes.includes(route)) {
|
|
173
|
+
renderAccount(route);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (route === "content" && config.moduleEntry) {
|
|
177
|
+
root.innerHTML = '<p role="status">Opening your app…</p>';
|
|
178
|
+
window.location.assign(config.moduleEntry);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (route === "content") {
|
|
182
|
+
root.innerHTML = `<nav class="content-nav" aria-label="App navigation"><button id="content-tab" class="selected" aria-current="page">Content</button><button id="privacy-tab">${signedIn ? "My privacy" : "Sign in"}</button><button id="account-tab">My account</button><button id="exit">${signedIn ? "Sign out" : "Back to sign in"}</button></nav>${element<HTMLTemplateElement>("public-content")!.innerHTML}`;
|
|
183
|
+
element("account-tab")!.addEventListener("click", () =>
|
|
184
|
+
navigate("account"),
|
|
185
|
+
);
|
|
186
|
+
element("privacy-tab")!.addEventListener("click", () =>
|
|
187
|
+
navigate(signedIn ? "privacy" : "signin"),
|
|
188
|
+
);
|
|
189
|
+
element("exit")!.addEventListener(
|
|
190
|
+
"click",
|
|
191
|
+
() =>
|
|
192
|
+
void action(async () => {
|
|
193
|
+
if (signedIn) await sdk.logout(true);
|
|
194
|
+
signedIn = false;
|
|
195
|
+
anonymous = false;
|
|
196
|
+
navigate("signin");
|
|
197
|
+
}),
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
root.innerHTML = `<section class="${route === "signin" ? "signin-shell" : "card content-account"}">
|
|
202
|
+
${message ? `<p role="${failed ? "alert" : "status"}" class="${failed ? "error" : "notice"}">${escape(message)}</p>` : ""}
|
|
203
|
+
${
|
|
204
|
+
route === "signin"
|
|
205
|
+
? `<div class="signin-intro"><p class="eyebrow">${escape(config.title)}</p><h1>${escape(config.welcome)}</h1><p class="signin-description">${escape(config.description)}</p><div class="signin-trust"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><div><strong>Your account, with Fidj</strong><p>One identity. Your own choices for every app.</p></div></div></div>
|
|
206
|
+
<div class="signin-form"><h2>Welcome back</h2><p>Sign in to continue to ${escape(config.title)}.</p><form id="signin"><label for="email">Email address</label><input id="email" type="email" placeholder="you@example.com" autocomplete="username" required><label for="password">Password</label><input id="password" type="password" autocomplete="current-password" required><button class="primary" type="submit">Sign in</button><button class="secondary" type="submit" name="signup" value="true">Create an account</button></form><a href="#/forgot">Forgot your password?</a>${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>` : ""}
|
|
207
|
+
</div>`
|
|
208
|
+
: `
|
|
209
|
+
<button id="back-content">← Content</button><h2>My privacy in ${escape(config.title)}</h2><p>Roles: ${roles.map(escape).join(" · ") || "No assigned roles"}</p><button id="refresh">Refresh access</button><button id="signout">Sign out</button>
|
|
210
|
+
<p>These choices apply only to this app.${config.allowAnonymous ? " You can also view the public content by entering anonymously." : ""}</p>
|
|
211
|
+
<p>Service agreement: ${consent.terms ? "Accepted" : "Not recorded"}. ${consent.terms ? "Leaving withdraws this agreement." : 'This generated example uses a demo agreement. <button id="terms">Accept demo agreement</button>'}</p>
|
|
212
|
+
${["analytics", "communications", "optionalData"].map((key, i) => `<label class="toggle"><span>${["Analytics", "Communications", "Optional data"][i]}</span><input type="checkbox" data-purpose="${key}" ${consent[key] ? "checked" : ""}></label>`).join("")}
|
|
213
|
+
<h3>Consent history</h3>${
|
|
214
|
+
history.length
|
|
215
|
+
? history
|
|
216
|
+
.slice()
|
|
217
|
+
.reverse()
|
|
218
|
+
.map(
|
|
219
|
+
(entry) =>
|
|
220
|
+
`<p>${escape(entry.type)} · ${entry.granted ? "Accepted" : "Withdrawn"} · ${escape(entry.changedAt)}</p>`,
|
|
221
|
+
)
|
|
222
|
+
.join("")
|
|
223
|
+
: "<p>No changes yet.</p>"
|
|
224
|
+
}
|
|
225
|
+
<button id="export">Export my app data</button>
|
|
226
|
+
<p>This app stores its session in this browser. The export covers Fidj-held records for this membership. There is no separate app database in this static template.</p>
|
|
227
|
+
${roles.includes("Owner") ? "<p>Resolve app ownership before leaving.</p>" : leaving ? '<p>Confirm departure: your membership and its Fidj-held data will be removed. Your other apps remain available.</p><button id="confirm-leave" class="danger">Confirm leaving this app</button><button id="cancel-leave">Keep my membership</button>' : '<button id="leave" class="danger">Leave this app</button>'}
|
|
228
|
+
<p><a href="${escape(config.dashboardUrl)}/#/my">Manage my apps and privacy on Fidj ↗</a></p>`
|
|
229
|
+
}</section>`;
|
|
230
|
+
element("anonymous")?.addEventListener("click", () => {
|
|
231
|
+
if (!config.allowAnonymous) return;
|
|
232
|
+
anonymous = true;
|
|
233
|
+
navigate("content");
|
|
234
|
+
});
|
|
235
|
+
element("back-content")?.addEventListener("click", () => navigate("content"));
|
|
236
|
+
if (oidc && element("signin")) element("signin")!.innerHTML = '<p>Continue securely with your Fidj account. Your password stays with Fidj.</p><button class="primary" type="submit">Continue with Fidj</button>';
|
|
237
|
+
element<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
const email = element<HTMLInputElement>("email")?.value || "";
|
|
240
|
+
const password = element<HTMLInputElement>("password")?.value || "";
|
|
241
|
+
const signup = (event.submitter as HTMLButtonElement)?.name === "signup";
|
|
242
|
+
void action(async () => {
|
|
243
|
+
if (oidc) {window.location.assign(await oidc.beginLogin()); return;}
|
|
244
|
+
await sdk.login(email, password, { autoSignup: signup });
|
|
245
|
+
await refresh();
|
|
246
|
+
anonymous = false;
|
|
247
|
+
navigate(signup ? "account" : "content");
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
element("refresh")?.addEventListener("click", () => void action(refresh));
|
|
251
|
+
element("signout")?.addEventListener(
|
|
252
|
+
"click",
|
|
253
|
+
() =>
|
|
254
|
+
void action(async () => {
|
|
255
|
+
await sdk.logout(true);
|
|
256
|
+
signedIn = false;
|
|
257
|
+
anonymous = false;
|
|
258
|
+
navigate("signin");
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
element("terms")?.addEventListener(
|
|
262
|
+
"click",
|
|
263
|
+
() =>
|
|
264
|
+
void action(async () => {
|
|
265
|
+
await request(appPath + "/consents", "PUT", {
|
|
266
|
+
terms: true,
|
|
267
|
+
termsVersion: "starter-demo-1",
|
|
268
|
+
source: "profile",
|
|
269
|
+
});
|
|
270
|
+
await refresh();
|
|
271
|
+
}),
|
|
272
|
+
);
|
|
273
|
+
root.querySelectorAll<HTMLInputElement>("[data-purpose]").forEach((input) =>
|
|
274
|
+
input.addEventListener(
|
|
275
|
+
"change",
|
|
276
|
+
() =>
|
|
277
|
+
void action(async () => {
|
|
278
|
+
await request(appPath + "/consents", "PUT", {
|
|
279
|
+
[input.dataset.purpose!]: input.checked,
|
|
280
|
+
source: "profile",
|
|
281
|
+
});
|
|
282
|
+
await refresh();
|
|
283
|
+
message = "Saved for this app.";
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
element("export")?.addEventListener(
|
|
288
|
+
"click",
|
|
289
|
+
() =>
|
|
290
|
+
void action(async () => {
|
|
291
|
+
const data = await request(appPath + "/export");
|
|
292
|
+
const url = URL.createObjectURL(
|
|
293
|
+
new Blob([JSON.stringify(data, null, 2)], {
|
|
294
|
+
type: "application/json",
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
const link = document.createElement("a");
|
|
298
|
+
link.href = url;
|
|
299
|
+
link.download = `${config.appId}-my-data.json`;
|
|
300
|
+
link.click();
|
|
301
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
302
|
+
message = "Export downloaded.";
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
element("leave")?.addEventListener("click", () => {
|
|
306
|
+
leaving = true;
|
|
307
|
+
render();
|
|
308
|
+
});
|
|
309
|
+
element("cancel-leave")?.addEventListener("click", () => {
|
|
310
|
+
leaving = false;
|
|
311
|
+
render();
|
|
312
|
+
});
|
|
313
|
+
element("confirm-leave")?.addEventListener("click", () => {
|
|
314
|
+
void action(async () => {
|
|
315
|
+
const result = await request(appPath, "DELETE", {
|
|
316
|
+
confirm: config.appId,
|
|
317
|
+
});
|
|
318
|
+
await sdk.logout(true);
|
|
319
|
+
signedIn = false;
|
|
320
|
+
anonymous = false;
|
|
321
|
+
leaving = false;
|
|
322
|
+
message =
|
|
323
|
+
result.status === "pending"
|
|
324
|
+
? "Access revoked. Storage cleanup is pending; contact the app owner."
|
|
325
|
+
: "You left this app. Your other memberships remain available.";
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function renderAccount(route: string) {
|
|
331
|
+
const notice = message
|
|
332
|
+
? `<p role="${failed ? "alert" : "status"}" class="${failed ? "error" : "notice"}">${escape(message)}</p>`
|
|
333
|
+
: "";
|
|
334
|
+
const form =
|
|
335
|
+
route === "forgot"
|
|
336
|
+
? `<h2>Reset your password</h2><p>We’ll email you a link to choose a new password for your shared Fidj account.</p><form id="recovery"><label for="recovery-email">Email address</label><input id="recovery-email" type="email" autocomplete="email" required><button class="primary">Send reset link</button></form>`
|
|
337
|
+
: route === "reset"
|
|
338
|
+
? `<h2>Choose a new password</h2><p>This changes your Fidj password across all your apps and signs out existing sessions.</p>${linkToken ? '<form id="recovery"><label for="new-password">New password</label><input id="new-password" type="password" autocomplete="new-password" minlength="12" required><label for="confirm-password">Confirm password</label><input id="confirm-password" type="password" autocomplete="new-password" minlength="12" required><p>Use at least 12 characters (up to 72 UTF-8 bytes).</p><button class="primary">Save new password</button></form>' : '<p>Request a new link if you no longer have an active reset link.</p><a href="#/forgot">Request a reset link</a>'}`
|
|
339
|
+
: route === "verify"
|
|
340
|
+
? `<h2>${verificationConfirmed ? "Email verified" : "Verify your email"}</h2>${verificationConfirmed ? "<p>Your account is ready. Return to your app to continue.</p>" : "<p>Confirm that this email address belongs to you.</p>"}${verificationConfirmed ? "" : linkToken ? '<form id="recovery"><button class="primary">Confirm email address</button></form>' : "<p>Sign in to your account to request a new verification email.</p>"}`
|
|
341
|
+
: `<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>`;
|
|
342
|
+
root.innerHTML = `<section class="signin-shell"><div class="signin-intro"><p class="eyebrow">${escape(config.title)}</p><h1>Your account.<br>Your control.</h1><p class="signin-description">Secure access to the apps you use, with one Fidj identity.</p><div class="signin-trust"><img class="signin-logo" src="./fidj-logo.png" alt="Fidj"><p>You choose what you share.</p></div></div><div class="signin-form">${notice}${form}<p><a href="#/signin">Back to sign in</a></p></div></section>`;
|
|
343
|
+
element("continue-app")?.addEventListener("click", () => navigate("content"));
|
|
344
|
+
element("check-verification")?.addEventListener(
|
|
345
|
+
"click",
|
|
346
|
+
() =>
|
|
347
|
+
void action(async () => {
|
|
348
|
+
const result = await request("/me");
|
|
349
|
+
emailVerified = result.user?.verified === true;
|
|
350
|
+
message = emailVerified
|
|
351
|
+
? "Your email address is verified."
|
|
352
|
+
: "Your email is not verified yet. Request a verification email below.";
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
element("resend-verification")?.addEventListener(
|
|
356
|
+
"click",
|
|
357
|
+
() =>
|
|
358
|
+
void action(async () => {
|
|
359
|
+
await sdk.resendVerification();
|
|
360
|
+
message =
|
|
361
|
+
"Verification email sent. Open the link and confirm your address. If it does not arrive, check your spam folder.";
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
element<HTMLFormElement>("recovery")?.addEventListener("submit", (event) => {
|
|
365
|
+
event.preventDefault();
|
|
366
|
+
const email = element<HTMLInputElement>("recovery-email")?.value || "";
|
|
367
|
+
const password = element<HTMLInputElement>("new-password")?.value || "";
|
|
368
|
+
const confirmation =
|
|
369
|
+
element<HTMLInputElement>("confirm-password")?.value || "";
|
|
370
|
+
void action(async () => {
|
|
371
|
+
if (route === "forgot") {
|
|
372
|
+
await sdk.fidjForgotPasswordRequest(email);
|
|
373
|
+
message =
|
|
374
|
+
"If an account matches that email, a reset link is on its way. Check your inbox and spam folder.";
|
|
375
|
+
} else if (route === "reset") {
|
|
376
|
+
if (password !== confirmation)
|
|
377
|
+
throw new Error("The passwords do not match.");
|
|
378
|
+
if (new TextEncoder().encode(password).length > 72)
|
|
379
|
+
throw new Error(
|
|
380
|
+
"Use a shorter password: the limit is 72 UTF-8 bytes.",
|
|
381
|
+
);
|
|
382
|
+
await sdk.resetPassword({ token: linkToken, password });
|
|
383
|
+
linkToken = "";
|
|
384
|
+
signedIn = false;
|
|
385
|
+
anonymous = false;
|
|
386
|
+
message =
|
|
387
|
+
"Your password has been changed. Sign in with your new password.";
|
|
388
|
+
navigate("signin");
|
|
389
|
+
} else {
|
|
390
|
+
await sdk.verifyEmail({ token: linkToken });
|
|
391
|
+
verificationConfirmed = true;
|
|
392
|
+
linkToken = "";
|
|
393
|
+
message =
|
|
394
|
+
"Your email address is now verified. You can return to your app.";
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
window.addEventListener("hashchange", render);
|
|
400
|
+
render();
|
|
401
|
+
void action(async () => {
|
|
402
|
+
if (oidc && new URL(window.location.href).searchParams.has("state")) {
|
|
403
|
+
const callback = new URL(window.location.href);
|
|
404
|
+
window.history.replaceState(null, "", window.location.pathname + "#/content");
|
|
405
|
+
await oidc.completeLogin(callback);
|
|
406
|
+
}
|
|
407
|
+
await sdk.init(config.appId, {
|
|
408
|
+
apiEndpoint: config.apiEndpoint,
|
|
409
|
+
prod: !config.localDemo,
|
|
410
|
+
});
|
|
411
|
+
if (sdk.isLoggedIn()) {
|
|
412
|
+
await refresh();
|
|
413
|
+
if (!moduleRoute() && !accountRoutes.includes(currentRoute()))
|
|
414
|
+
navigate("content");
|
|
415
|
+
}
|
|
416
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { FidjNodeService } from "@ofidj/node";
|
|
2
|
+
import "./style.css";
|
|
3
|
+
|
|
4
|
+
type Session = { username: string; roles: string[] };
|
|
5
|
+
type Note = { id: string; title: string; body: string; createdAt: string };
|
|
6
|
+
type Settings = {
|
|
7
|
+
title: string;
|
|
8
|
+
appId: string;
|
|
9
|
+
apiEndpoint: string;
|
|
10
|
+
dashboardUrl: string;
|
|
11
|
+
localDemo: boolean;
|
|
12
|
+
};
|
|
13
|
+
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
14
|
+
const sdk = new FidjNodeService();
|
|
15
|
+
let settings: Settings;
|
|
16
|
+
let session: Session | null = null;
|
|
17
|
+
let notes: Note[] = [];
|
|
18
|
+
let privacy: {
|
|
19
|
+
consent: Record<string, boolean>;
|
|
20
|
+
history: Array<{ type: string; granted: boolean; changedAt: string }>;
|
|
21
|
+
} | null = null;
|
|
22
|
+
let view = "workspace";
|
|
23
|
+
let notice = "";
|
|
24
|
+
let error = "";
|
|
25
|
+
let busy = false;
|
|
26
|
+
const escape = (value: unknown) =>
|
|
27
|
+
String(value ?? "").replace(
|
|
28
|
+
/[&<>"']/g,
|
|
29
|
+
(char) =>
|
|
30
|
+
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
|
|
31
|
+
char
|
|
32
|
+
]!,
|
|
33
|
+
);
|
|
34
|
+
const el = <T extends HTMLElement>(id: string) =>
|
|
35
|
+
document.getElementById(id) as T;
|
|
36
|
+
async function api(path: string, method = "GET", data?: unknown) {
|
|
37
|
+
const token = await sdk.fidjGetIdToken();
|
|
38
|
+
const response = await fetch("/api/" + path, {
|
|
39
|
+
method,
|
|
40
|
+
headers: {
|
|
41
|
+
Authorization: `Bearer ${token}`,
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
},
|
|
44
|
+
body: data === undefined ? undefined : JSON.stringify(data),
|
|
45
|
+
});
|
|
46
|
+
const result = await response.json();
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
if (response.status === 401) {
|
|
49
|
+
session = null;
|
|
50
|
+
await sdk.logout(true);
|
|
51
|
+
}
|
|
52
|
+
throw new Error(result.message || "Please retry.");
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
async function load() {
|
|
57
|
+
session = await api("session");
|
|
58
|
+
[notes, privacy] = await Promise.all([
|
|
59
|
+
api("notes").then((result) => result.notes),
|
|
60
|
+
api("privacy"),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
async function action(task: () => Promise<void>) {
|
|
64
|
+
if (busy) return;
|
|
65
|
+
busy = true;
|
|
66
|
+
error = "";
|
|
67
|
+
notice = "";
|
|
68
|
+
try {
|
|
69
|
+
await task();
|
|
70
|
+
} catch (reason) {
|
|
71
|
+
error = reason instanceof Error ? reason.message : String(reason);
|
|
72
|
+
} finally {
|
|
73
|
+
busy = false;
|
|
74
|
+
render();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function render() {
|
|
78
|
+
document.title = settings.title + " · Fidj";
|
|
79
|
+
const canWrite = session?.roles.some((role) =>
|
|
80
|
+
["Owner", "Editor"].includes(role),
|
|
81
|
+
);
|
|
82
|
+
root.innerHTML = `<header class="topbar"><a class="brand" href="/"><span class="app-symbol">✳</span>${escape(settings.title)}</a><a class="fidj-brand" href="${escape(settings.dashboardUrl)}" target="_blank" rel="noopener"><img src="/fidj-logo.png" alt="Fidj">Privacy with Fidj ↗</a></header>
|
|
83
|
+
<main>${notice ? `<p class="notice" role="status">${escape(notice)}</p>` : ""}${error ? `<p class="error" role="alert">${escape(error)}</p>` : ""}
|
|
84
|
+
${
|
|
85
|
+
!session
|
|
86
|
+
? `<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><button class="primary" type="submit">Sign in</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>`
|
|
87
|
+
: `
|
|
88
|
+
<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>
|
|
89
|
+
<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>
|
|
90
|
+
${
|
|
91
|
+
view === "workspace"
|
|
92
|
+
? `<section class="workspace"><div><div class="section-heading"><h2>Your notes</h2><span>${notes.length} saved</span></div>${notes.length ? notes.map((note) => `<article class="card note"><small>${escape(new Date(note.createdAt).toLocaleString())}</small><h3>${escape(note.title)}</h3><p>${escape(note.body)}</p></article>`).join("") : `<article class="card empty"><span>✳</span><h3>Room for your next idea.</h3><p>Your saved notes will appear here. Only you can read your notes.</p></article>`}</div><form id="note" class="card"><p class="eyebrow">CAPTURE SOMETHING</p><h2>A fresh note</h2><p>${canWrite ? "You have permission to save notes." : "Ask your app owner for Editor access to save notes."}</p><label for="note-title">Title</label><input id="note-title" maxlength="120" required ${!canWrite ? "disabled" : ""}><label for="note-body">Your note</label><textarea id="note-body" rows="6" maxlength="5000" ${!canWrite ? "disabled" : ""}></textarea><button class="primary" type="submit" ${!canWrite ? "disabled" : ""}>Save note</button><small>Your notes are saved by this app and survive a server restart.</small></form></section>`
|
|
93
|
+
: `<section class="privacy-grid"><article class="card"><p class="eyebrow">ONLY ${escape(settings.title).toUpperCase()}</p><h2>Your choices in this app</h2><p>These preferences are independent of Fidj and your other apps.</p><div class="agreement"><strong>Service agreement</strong><span>${privacy?.consent.terms ? "Accepted" : "Not recorded"}</span>${!privacy?.consent.terms ? '<p>This starter uses a demo agreement. Accept it to record your choice.</p><button id="accept-terms">Accept demo agreement</button>' : "<p>Leaving this app withdraws its required agreement.</p>"}</div>${["analytics", "communications", "optionalData"].map((key, index) => `<label class="toggle"><span><strong>${["Analytics", "Communications", "Optional data"][index]}</strong><small>${["Help improve this app.", "Receive optional news and updates.", "Allow data beyond the essential service."][index]}</small></span><input type="checkbox" data-purpose="${key}" ${privacy?.consent[key] ? "checked" : ""}></label>`).join("")}<button id="export">Export my data</button></article><article class="card"><h2>Consent history</h2>${
|
|
94
|
+
privacy?.history.length
|
|
95
|
+
? privacy.history
|
|
96
|
+
.slice()
|
|
97
|
+
.reverse()
|
|
98
|
+
.map(
|
|
99
|
+
(entry) =>
|
|
100
|
+
`<p><strong>${escape(entry.type)}</strong> · ${entry.granted ? "Accepted" : "Withdrawn"}<br><small>${escape(new Date(entry.changedAt).toLocaleString())}</small></p>`,
|
|
101
|
+
)
|
|
102
|
+
.join("")
|
|
103
|
+
: "<p>No changes yet.</p>"
|
|
104
|
+
}<hr><h2>Leave this app</h2><p>This removes your app membership and this starter’s notes. Your Fidj account and other memberships remain.</p>${session.roles.includes("Owner") ? "<p>As the app owner, resolve ownership before leaving.</p>" : '<button id="leave" class="danger">Leave and erase my app data</button>'}<p class="fineprint">Exports here include your Fidj membership and this starter’s notes. The registered app-data handler lets Fidj export and erase these notes too. If cleanup is pending, retry from My privacy on Fidj. Minimal completion receipts are retained; backups and unregistered systems are outside this operation.</p></article></section>`
|
|
105
|
+
}`
|
|
106
|
+
}
|
|
107
|
+
<footer>Built with Fidj · One identity. Separate choices for every app.</footer></main>`;
|
|
108
|
+
el<HTMLFormElement>("signin")?.addEventListener("submit", (event) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
const email = el<HTMLInputElement>("email").value;
|
|
111
|
+
const password = el<HTMLInputElement>("password").value;
|
|
112
|
+
void action(async () => {
|
|
113
|
+
await sdk.login(email, password, { autoSignup: false });
|
|
114
|
+
await load();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
root.querySelectorAll<HTMLButtonElement>("[data-demo]").forEach((button) =>
|
|
118
|
+
button.addEventListener("click", () => {
|
|
119
|
+
el<HTMLInputElement>("email").value = button.dataset.demo + "@fidj.local";
|
|
120
|
+
el<HTMLInputElement>("password").value =
|
|
121
|
+
button.dataset.demo === "alex"
|
|
122
|
+
? "local-demo-only"
|
|
123
|
+
: "local-member-only";
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
el("signout")?.addEventListener(
|
|
127
|
+
"click",
|
|
128
|
+
() =>
|
|
129
|
+
void action(async () => {
|
|
130
|
+
await sdk.logout(true);
|
|
131
|
+
session = null;
|
|
132
|
+
notes = [];
|
|
133
|
+
privacy = null;
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
el("workspace-tab")?.addEventListener("click", () => {
|
|
137
|
+
view = "workspace";
|
|
138
|
+
render();
|
|
139
|
+
});
|
|
140
|
+
el("privacy-tab")?.addEventListener("click", () => {
|
|
141
|
+
view = "privacy";
|
|
142
|
+
render();
|
|
143
|
+
});
|
|
144
|
+
el("refresh")?.addEventListener(
|
|
145
|
+
"click",
|
|
146
|
+
() =>
|
|
147
|
+
void action(async () => {
|
|
148
|
+
await load();
|
|
149
|
+
notice = "Access and preferences refreshed.";
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
el<HTMLFormElement>("note")?.addEventListener("submit", (event) => {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
const data = {
|
|
155
|
+
title: el<HTMLInputElement>("note-title").value,
|
|
156
|
+
body: el<HTMLTextAreaElement>("note-body").value,
|
|
157
|
+
};
|
|
158
|
+
void action(async () => {
|
|
159
|
+
await api("notes", "POST", data);
|
|
160
|
+
await load();
|
|
161
|
+
notice = "Note saved.";
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
root.querySelectorAll<HTMLInputElement>("[data-purpose]").forEach((input) =>
|
|
165
|
+
input.addEventListener(
|
|
166
|
+
"change",
|
|
167
|
+
() =>
|
|
168
|
+
void action(async () => {
|
|
169
|
+
await api("privacy", "PUT", {
|
|
170
|
+
[input.dataset.purpose!]: input.checked,
|
|
171
|
+
});
|
|
172
|
+
await load();
|
|
173
|
+
notice = "Saved for this app. Your other apps are unchanged.";
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
el("accept-terms")?.addEventListener(
|
|
178
|
+
"click",
|
|
179
|
+
() =>
|
|
180
|
+
void action(async () => {
|
|
181
|
+
await api("privacy", "PUT", { terms: true });
|
|
182
|
+
await load();
|
|
183
|
+
notice = "Demo agreement accepted.";
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
el("export")?.addEventListener(
|
|
187
|
+
"click",
|
|
188
|
+
() =>
|
|
189
|
+
void action(async () => {
|
|
190
|
+
const data = await api("privacy/export");
|
|
191
|
+
const url = URL.createObjectURL(
|
|
192
|
+
new Blob([JSON.stringify(data, null, 2)], {
|
|
193
|
+
type: "application/json",
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
const link = document.createElement("a");
|
|
197
|
+
link.href = url;
|
|
198
|
+
link.download = `${settings.appId}-my-data.json`;
|
|
199
|
+
link.click();
|
|
200
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
201
|
+
notice = "Exported your membership, choices and notes.";
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
el("leave")?.addEventListener("click", () => {
|
|
205
|
+
if (
|
|
206
|
+
!confirm(
|
|
207
|
+
`Leave ${settings.title}? Your membership, consent and notes here will be removed. Other apps remain available.`,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
return;
|
|
211
|
+
void action(async () => {
|
|
212
|
+
const result = await api("privacy/leave", "DELETE", {
|
|
213
|
+
confirm: settings.appId,
|
|
214
|
+
});
|
|
215
|
+
await sdk.logout(true);
|
|
216
|
+
session = null;
|
|
217
|
+
notes = [];
|
|
218
|
+
privacy = null;
|
|
219
|
+
notice =
|
|
220
|
+
result.status === "pending"
|
|
221
|
+
? "Access revoked. Cleanup is pending. Open My privacy on Fidj and choose Retry cleanup."
|
|
222
|
+
: "You left this app. Its membership and registered app data were erased. Your other apps are unchanged.";
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async function start() {
|
|
227
|
+
try {
|
|
228
|
+
settings = await (await fetch("/api/config")).json();
|
|
229
|
+
await sdk.init(settings.appId, {
|
|
230
|
+
apiEndpoint: settings.apiEndpoint,
|
|
231
|
+
prod: !settings.localDemo,
|
|
232
|
+
});
|
|
233
|
+
if (sdk.isLoggedIn()) await load();
|
|
234
|
+
} catch (reason) {
|
|
235
|
+
error =
|
|
236
|
+
reason instanceof Error ? reason.message : "Cannot connect to Fidj.";
|
|
237
|
+
}
|
|
238
|
+
if (settings) render();
|
|
239
|
+
else
|
|
240
|
+
root.textContent =
|
|
241
|
+
"This app could not load. Please check the server and retry.";
|
|
242
|
+
}
|
|
243
|
+
void start();
|