@dreamtree-org/landing-kit 0.2.1 → 0.5.0
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/MIGRATION.md +9 -0
- package/README.md +81 -5
- package/auth.css +42 -0
- package/auth.d.ts +3 -2
- package/demo-server.d.ts +41 -0
- package/dist/auth.cjs +176 -32
- package/dist/auth.js +176 -32
- package/dist/demo-server.cjs +121 -0
- package/dist/demo-server.js +88 -0
- package/dist/index.cjs +484 -340
- package/dist/index.js +477 -333
- package/index.d.ts +74 -1
- package/package.json +7 -1
- package/styles.css +42 -0
package/dist/auth.js
CHANGED
|
@@ -2,7 +2,109 @@
|
|
|
2
2
|
|
|
3
3
|
// src/AuthShell.jsx
|
|
4
4
|
import { useId } from "react";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// src/DemoAccounts.jsx
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
|
|
9
|
+
// src/demoFill.js
|
|
10
|
+
var DEMO_ACCOUNTS_URL = "/api/auth/demo-accounts";
|
|
11
|
+
var FILL_KEY = "dt-demo-fill";
|
|
12
|
+
async function fetchDemoAccounts(url = DEMO_ACCOUNTS_URL, init = {}) {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, { credentials: "include", cache: "no-store", ...init, headers: { Accept: "application/json", ...init.headers } });
|
|
15
|
+
if (!res.ok) return { accounts: [] };
|
|
16
|
+
return normalizeDemoAccounts(await res.json());
|
|
17
|
+
} catch {
|
|
18
|
+
return { accounts: [] };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function normalizeDemoAccounts(data) {
|
|
22
|
+
const list = Array.isArray(data) ? data : Array.isArray(data == null ? void 0 : data.accounts) ? data.accounts : [];
|
|
23
|
+
const accounts = list.filter((a) => a && typeof a.label === "string" && typeof a.identifier === "string");
|
|
24
|
+
let sharedPassword = typeof (data == null ? void 0 : data.sharedPassword) === "string" ? data.sharedPassword : void 0;
|
|
25
|
+
if (!sharedPassword && accounts.length > 1 && accounts.every((a) => a.password && a.password === accounts[0].password)) {
|
|
26
|
+
sharedPassword = accounts[0].password;
|
|
27
|
+
}
|
|
28
|
+
return sharedPassword ? { accounts, sharedPassword } : { accounts };
|
|
29
|
+
}
|
|
30
|
+
function stashDemoFill(account) {
|
|
31
|
+
var _a;
|
|
32
|
+
try {
|
|
33
|
+
sessionStorage.setItem(FILL_KEY, JSON.stringify({ identifier: account.identifier, password: (_a = account.password) != null ? _a : "", company: account.company }));
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function takeDemoFill() {
|
|
38
|
+
try {
|
|
39
|
+
const raw = sessionStorage.getItem(FILL_KEY);
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
sessionStorage.removeItem(FILL_KEY);
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/DemoAccounts.jsx
|
|
49
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
50
|
+
function DemoAccounts({ load, onSelect, className = "" }) {
|
|
51
|
+
const [state, setState] = useState({ status: "loading", accounts: [] });
|
|
52
|
+
const [picked, setPicked] = useState(null);
|
|
53
|
+
const loadRef = useRef(load);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let live = true;
|
|
56
|
+
Promise.resolve().then(() => loadRef.current()).then((data) => live && setState({ status: "ready", ...normalizeDemoAccounts(data) })).catch(() => live && setState({ status: "ready", accounts: [] }));
|
|
57
|
+
return () => {
|
|
58
|
+
live = false;
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
if (state.status === "ready" && !state.accounts.length) return null;
|
|
62
|
+
const pick = (account, i) => {
|
|
63
|
+
setPicked(i);
|
|
64
|
+
onSelect == null ? void 0 : onSelect(account);
|
|
65
|
+
};
|
|
66
|
+
return /* @__PURE__ */ jsxs("div", { className: `dt-auth-demo ${className}`, "aria-busy": state.status === "loading" || void 0, children: [
|
|
67
|
+
/* @__PURE__ */ jsx("p", { className: "dt-auth-demo-label", children: "Demo accounts" }),
|
|
68
|
+
state.status === "loading" ? /* @__PURE__ */ jsx("div", { className: "dt-auth-demo-chips", "aria-hidden": "true", children: [0, 1, 2].map((i) => /* @__PURE__ */ jsx("span", { className: "dt-auth-demo-chip dt-auth-demo-chip--skeleton" }, i)) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
69
|
+
/* @__PURE__ */ jsx("div", { className: "dt-auth-demo-chips", children: state.accounts.map((a, i) => /* @__PURE__ */ jsx(
|
|
70
|
+
"button",
|
|
71
|
+
{
|
|
72
|
+
type: "button",
|
|
73
|
+
className: "dt-auth-demo-chip",
|
|
74
|
+
"aria-pressed": picked === i,
|
|
75
|
+
title: a.company ? `${a.identifier} \xB7 ${a.company}` : a.identifier,
|
|
76
|
+
onClick: () => pick(a, i),
|
|
77
|
+
children: a.label
|
|
78
|
+
},
|
|
79
|
+
`${a.label}-${a.identifier}`
|
|
80
|
+
)) }),
|
|
81
|
+
/* @__PURE__ */ jsx("p", { className: "dt-auth-demo-note", children: state.sharedPassword ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
82
|
+
"Shared password ",
|
|
83
|
+
/* @__PURE__ */ jsx("code", { children: state.sharedPassword }),
|
|
84
|
+
" \u2014 tap a role to fill."
|
|
85
|
+
] }) : "Tap a role to fill." })
|
|
86
|
+
] })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/useTapReveal.js
|
|
91
|
+
import { useCallback, useRef as useRef2, useState as useState2 } from "react";
|
|
92
|
+
function useTapReveal({ taps = 4, windowMs = 1500 } = {}) {
|
|
93
|
+
const [revealed, setRevealed] = useState2(false);
|
|
94
|
+
const count = useRef2(0);
|
|
95
|
+
const last = useRef2(0);
|
|
96
|
+
const onClick = useCallback(() => {
|
|
97
|
+
if (revealed) return;
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
count.current = now - last.current <= windowMs ? count.current + 1 : 1;
|
|
100
|
+
last.current = now;
|
|
101
|
+
if (count.current >= taps) setRevealed(true);
|
|
102
|
+
}, [revealed, taps, windowMs]);
|
|
103
|
+
return { revealed, tapProps: { onClick, style: { userSelect: "none", WebkitUserSelect: "none" } } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/AuthShell.jsx
|
|
107
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
6
108
|
var DEFAULT_LEGAL = [
|
|
7
109
|
{ label: "Privacy Policy", href: "/privacy" },
|
|
8
110
|
{ label: "Terms", href: "/terms" },
|
|
@@ -10,10 +112,10 @@ var DEFAULT_LEGAL = [
|
|
|
10
112
|
];
|
|
11
113
|
function BrandMark({ brand, size }) {
|
|
12
114
|
if (brand.logoSrc) {
|
|
13
|
-
return /* @__PURE__ */
|
|
115
|
+
return /* @__PURE__ */ jsx2("img", { className: "dt-auth-mark-img", src: brand.logoSrc, alt: "", width: size, height: size });
|
|
14
116
|
}
|
|
15
|
-
if (brand.mark) return /* @__PURE__ */
|
|
16
|
-
return /* @__PURE__ */
|
|
117
|
+
if (brand.mark) return /* @__PURE__ */ jsx2("span", { className: "dt-auth-mark-node", children: brand.mark });
|
|
118
|
+
return /* @__PURE__ */ jsx2("span", { className: "dt-auth-monogram", style: { width: size, height: size }, "aria-hidden": "true", children: brand.name.charAt(0) });
|
|
17
119
|
}
|
|
18
120
|
function brandStyle(brand) {
|
|
19
121
|
return {
|
|
@@ -31,50 +133,53 @@ function AuthShell({
|
|
|
31
133
|
variant = "tenant",
|
|
32
134
|
legalLinks = DEFAULT_LEGAL,
|
|
33
135
|
linkComponent: LinkComponent,
|
|
34
|
-
className = ""
|
|
136
|
+
className = "",
|
|
137
|
+
demoAccounts
|
|
35
138
|
}) {
|
|
36
|
-
var _a;
|
|
139
|
+
var _a, _b;
|
|
37
140
|
const headingId = useId();
|
|
141
|
+
const { revealed, tapProps } = useTapReveal({ taps: (_a = demoAccounts == null ? void 0 : demoAccounts.taps) != null ? _a : 4 });
|
|
38
142
|
const isPlatform = variant === "platform";
|
|
39
143
|
const headline = isPlatform ? brand.platformHeadline || "Platform administration" : brand.headline;
|
|
40
144
|
const pitch = isPlatform ? brand.platformPitch || `Super administrator console for ${brand.name}. Every action here is audited.` : brand.pitch;
|
|
41
|
-
const renderLink = (l) => LinkComponent ? /* @__PURE__ */
|
|
42
|
-
return /* @__PURE__ */
|
|
145
|
+
const renderLink = (l) => LinkComponent ? /* @__PURE__ */ jsx2(LinkComponent, { to: l.href, href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href) : /* @__PURE__ */ jsx2("a", { href: l.href, className: "dt-auth-legal-link", children: l.label }, l.href);
|
|
146
|
+
return /* @__PURE__ */ jsxs2(
|
|
43
147
|
"div",
|
|
44
148
|
{
|
|
45
149
|
className: `dt-auth ${isPlatform ? "dt-auth--platform" : ""} ${className}`,
|
|
46
150
|
style: brandStyle(brand),
|
|
47
151
|
"data-product": brand.id,
|
|
48
152
|
children: [
|
|
49
|
-
/* @__PURE__ */
|
|
50
|
-
/* @__PURE__ */
|
|
51
|
-
/* @__PURE__ */
|
|
52
|
-
/* @__PURE__ */
|
|
53
|
-
isPlatform ? /* @__PURE__ */
|
|
153
|
+
/* @__PURE__ */ jsxs2("aside", { className: "dt-auth-panel", children: [
|
|
154
|
+
/* @__PURE__ */ jsxs2("a", { href: "/", className: "dt-auth-brand", children: [
|
|
155
|
+
/* @__PURE__ */ jsx2(BrandMark, { brand, size: 28 }),
|
|
156
|
+
/* @__PURE__ */ jsx2("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
157
|
+
isPlatform ? /* @__PURE__ */ jsx2("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
54
158
|
] }),
|
|
55
|
-
/* @__PURE__ */
|
|
56
|
-
/* @__PURE__ */
|
|
57
|
-
pitch ? /* @__PURE__ */
|
|
58
|
-
!isPlatform && ((
|
|
159
|
+
/* @__PURE__ */ jsxs2("div", { className: "dt-auth-pitch", children: [
|
|
160
|
+
/* @__PURE__ */ jsx2("h2", { children: headline }),
|
|
161
|
+
pitch ? /* @__PURE__ */ jsx2("p", { children: pitch }) : null,
|
|
162
|
+
!isPlatform && ((_b = brand.points) == null ? void 0 : _b.length) ? /* @__PURE__ */ jsx2("ul", { children: brand.points.map((p) => /* @__PURE__ */ jsx2("li", { children: p }, p)) }) : null
|
|
59
163
|
] }),
|
|
60
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ jsx2("p", { className: "dt-auth-panel-foot", children: brand.category || "by Dreamtree-Org" })
|
|
61
165
|
] }),
|
|
62
|
-
/* @__PURE__ */
|
|
63
|
-
/* @__PURE__ */
|
|
64
|
-
/* @__PURE__ */
|
|
65
|
-
/* @__PURE__ */
|
|
66
|
-
isPlatform ? /* @__PURE__ */
|
|
166
|
+
/* @__PURE__ */ jsx2("main", { className: "dt-auth-main", children: /* @__PURE__ */ jsxs2("div", { className: "dt-auth-column", children: [
|
|
167
|
+
/* @__PURE__ */ jsxs2("a", { href: "/", className: "dt-auth-brand dt-auth-brand--mobile", children: [
|
|
168
|
+
/* @__PURE__ */ jsx2(BrandMark, { brand, size: 24 }),
|
|
169
|
+
/* @__PURE__ */ jsx2("span", { className: "dt-auth-brand-name", children: brand.name }),
|
|
170
|
+
isPlatform ? /* @__PURE__ */ jsx2("span", { className: "dt-auth-badge", children: "Platform" }) : null
|
|
67
171
|
] }),
|
|
68
|
-
/* @__PURE__ */
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
-
/* @__PURE__ */
|
|
71
|
-
subtitle ? /* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ jsxs2("section", { className: "dt-auth-card", "aria-labelledby": headingId, children: [
|
|
173
|
+
/* @__PURE__ */ jsxs2("header", { className: "dt-auth-head", children: [
|
|
174
|
+
/* @__PURE__ */ jsx2("h1", { id: headingId, ...demoAccounts ? tapProps : {}, children: title }),
|
|
175
|
+
subtitle ? /* @__PURE__ */ jsx2("p", { children: subtitle }) : null
|
|
72
176
|
] }),
|
|
73
|
-
children
|
|
177
|
+
children,
|
|
178
|
+
demoAccounts && revealed ? /* @__PURE__ */ jsx2(DemoAccounts, { load: demoAccounts.load, onSelect: demoAccounts.onSelect }) : null
|
|
74
179
|
] }),
|
|
75
|
-
footer ? /* @__PURE__ */
|
|
76
|
-
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */
|
|
77
|
-
i > 0 ? /* @__PURE__ */
|
|
180
|
+
footer ? /* @__PURE__ */ jsx2("div", { className: "dt-auth-footer", children: footer }) : null,
|
|
181
|
+
(legalLinks == null ? void 0 : legalLinks.length) ? /* @__PURE__ */ jsx2("nav", { className: "dt-auth-legal", "aria-label": "Legal", children: legalLinks.map((l, i) => /* @__PURE__ */ jsxs2("span", { className: "dt-auth-legal-item", children: [
|
|
182
|
+
i > 0 ? /* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "\xB7" }) : null,
|
|
78
183
|
renderLink(l)
|
|
79
184
|
] }, l.href)) }) : null
|
|
80
185
|
] }) })
|
|
@@ -198,14 +303,53 @@ var brands = {
|
|
|
198
303
|
category: "by Dreamtree-Org"
|
|
199
304
|
}
|
|
200
305
|
};
|
|
306
|
+
var warned = false;
|
|
201
307
|
function getBrand(id) {
|
|
308
|
+
if (!warned && typeof console !== "undefined") {
|
|
309
|
+
warned = true;
|
|
310
|
+
console.warn('landing-kit: getBrand() and the built-in brands are deprecated (removed in 0.5.0). Keep your brand in dreamtree.brand.json and use toLandingBrand(require("./dreamtree.brand.json"), copy).');
|
|
311
|
+
}
|
|
202
312
|
const b = brands[id];
|
|
203
313
|
if (!b) throw new Error(`landing-kit: unknown brand "${id}" (known: ${Object.keys(brands).join(", ")})`);
|
|
204
314
|
return b;
|
|
205
315
|
}
|
|
316
|
+
|
|
317
|
+
// src/brandFile.js
|
|
318
|
+
var shellOf = (role) => {
|
|
319
|
+
const shell = role && role.shell;
|
|
320
|
+
return typeof shell === "string" ? shell : shell && shell.light;
|
|
321
|
+
};
|
|
322
|
+
function toLandingBrand(file, copy = {}) {
|
|
323
|
+
if (!file || typeof file !== "object") throw new Error("landing-kit: toLandingBrand needs the parsed dreamtree.brand.json object.");
|
|
324
|
+
const id = file.id || file.name;
|
|
325
|
+
if (!id) throw new Error('landing-kit: the brand file has no "id". Run `npx @dreamtree-org/twreact-ui brand validate`.');
|
|
326
|
+
const scale = file.color && file.color.primary || {};
|
|
327
|
+
const marketing = file.marketing || {};
|
|
328
|
+
const primary = marketing.primary || scale["600"] || scale["700"] || scale["500"];
|
|
329
|
+
if (!primary) throw new Error(`landing-kit: brand "${id}" sets no primary colour (color.primary.600 or marketing.primary).`);
|
|
330
|
+
const brand = {
|
|
331
|
+
id,
|
|
332
|
+
name: (file.id ? file.name : file.label) || id,
|
|
333
|
+
primary,
|
|
334
|
+
accent: marketing.accent || scale["300"] || void 0,
|
|
335
|
+
panel: marketing.panel || shellOf(file.role) || void 0,
|
|
336
|
+
logoSrc: file.logo && file.logo.mark || void 0,
|
|
337
|
+
headline: "",
|
|
338
|
+
...copy
|
|
339
|
+
};
|
|
340
|
+
return Object.fromEntries(Object.entries(brand).filter(([, v]) => v !== void 0));
|
|
341
|
+
}
|
|
206
342
|
export {
|
|
207
343
|
AuthShell,
|
|
344
|
+
DEMO_ACCOUNTS_URL,
|
|
345
|
+
DemoAccounts,
|
|
208
346
|
brandStyle,
|
|
209
347
|
brands,
|
|
210
|
-
|
|
348
|
+
fetchDemoAccounts,
|
|
349
|
+
getBrand,
|
|
350
|
+
normalizeDemoAccounts,
|
|
351
|
+
stashDemoFill,
|
|
352
|
+
takeDemoFill,
|
|
353
|
+
toLandingBrand,
|
|
354
|
+
useTapReveal
|
|
211
355
|
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/demo-server.js
|
|
20
|
+
var demo_server_exports = {};
|
|
21
|
+
__export(demo_server_exports, {
|
|
22
|
+
DEMO_ACCOUNTS_HEADERS: () => DEMO_ACCOUNTS_HEADERS,
|
|
23
|
+
DEMO_COMPANY: () => DEMO_COMPANY,
|
|
24
|
+
DEMO_PASSWORD_MIN: () => DEMO_PASSWORD_MIN,
|
|
25
|
+
demoAccountsEnabled: () => demoAccountsEnabled,
|
|
26
|
+
demoAccountsExpress: () => demoAccountsExpress,
|
|
27
|
+
demoAccountsResponse: () => demoAccountsResponse,
|
|
28
|
+
demoEmail: () => demoEmail,
|
|
29
|
+
demoUsers: () => demoUsers,
|
|
30
|
+
requireDemoPassword: () => requireDemoPassword,
|
|
31
|
+
resolveDemoAccounts: () => resolveDemoAccounts
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(demo_server_exports);
|
|
34
|
+
function fromEnvJson(raw) {
|
|
35
|
+
if (!raw) return null;
|
|
36
|
+
try {
|
|
37
|
+
const data = JSON.parse(raw);
|
|
38
|
+
return Array.isArray(data) ? data : Array.isArray(data?.accounts) ? data.accounts : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function clean(accounts) {
|
|
44
|
+
return (accounts || []).filter((a) => a && a.label && a.identifier && a.password).map(({ label, identifier, password, company, role }) => ({
|
|
45
|
+
label: String(label),
|
|
46
|
+
identifier: String(identifier),
|
|
47
|
+
password: String(password),
|
|
48
|
+
...company ? { company: String(company) } : {},
|
|
49
|
+
...role ? { role: String(role) } : {}
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
|
|
53
|
+
return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
|
|
54
|
+
}
|
|
55
|
+
function result(accounts) {
|
|
56
|
+
if (!accounts) return { status: 404, body: { error: "Not found" } };
|
|
57
|
+
const list = clean(accounts);
|
|
58
|
+
if (!list.length) return { status: 404, body: { error: "Not found" } };
|
|
59
|
+
return { status: 200, body: { accounts: list } };
|
|
60
|
+
}
|
|
61
|
+
function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
|
|
62
|
+
const on = demoAccountsEnabled({ enabled, env });
|
|
63
|
+
if (!on) return result(null);
|
|
64
|
+
const override = fromEnvJson(env.DEMO_ACCOUNTS_JSON);
|
|
65
|
+
if (override) return result(override);
|
|
66
|
+
const list = typeof accounts === "function" ? accounts(env) : accounts;
|
|
67
|
+
return list && typeof list.then === "function" ? list.then((l) => result(l || []), () => result(null)) : result(list || []);
|
|
68
|
+
}
|
|
69
|
+
var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
|
|
70
|
+
function demoAccountsExpress(opts = {}) {
|
|
71
|
+
const send = (res, { status, body }) => {
|
|
72
|
+
res.set(DEMO_ACCOUNTS_HEADERS);
|
|
73
|
+
res.status(status).json(body);
|
|
74
|
+
};
|
|
75
|
+
return (req, res) => {
|
|
76
|
+
const r = resolveDemoAccounts(opts);
|
|
77
|
+
if (typeof r.then === "function") return r.then((x) => send(res, x));
|
|
78
|
+
send(res, r);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function demoAccountsResponse(opts = {}) {
|
|
82
|
+
const toResponse = ({ status, body }) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
|
|
83
|
+
const r = resolveDemoAccounts(opts);
|
|
84
|
+
return typeof r.then === "function" ? r.then(toResponse) : toResponse(r);
|
|
85
|
+
}
|
|
86
|
+
var DEMO_COMPANY = "default";
|
|
87
|
+
var DEMO_PASSWORD_MIN = 12;
|
|
88
|
+
function demoEmail(product, role) {
|
|
89
|
+
const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
90
|
+
return `${slug(role)}@${slug(product)}.demo.test`;
|
|
91
|
+
}
|
|
92
|
+
function demoUsers(manifest, { product, env = process.env, company = DEMO_COMPANY } = {}) {
|
|
93
|
+
const password = env.DEMO_PASSWORD;
|
|
94
|
+
if (!password) return [];
|
|
95
|
+
return (manifest || []).map((m) => ({
|
|
96
|
+
label: m.label,
|
|
97
|
+
identifier: m.email || demoEmail(product, m.role),
|
|
98
|
+
password,
|
|
99
|
+
company,
|
|
100
|
+
role: m.role
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
function requireDemoPassword(env = process.env) {
|
|
104
|
+
const p = env.DEMO_PASSWORD;
|
|
105
|
+
if (!p) throw new Error("DEMO_PASSWORD is not set: demo:seed gives every demo user this password (the login chips show it).");
|
|
106
|
+
if (p.length < DEMO_PASSWORD_MIN) throw new Error(`DEMO_PASSWORD must be at least ${DEMO_PASSWORD_MIN} characters.`);
|
|
107
|
+
return p;
|
|
108
|
+
}
|
|
109
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
110
|
+
0 && (module.exports = {
|
|
111
|
+
DEMO_ACCOUNTS_HEADERS,
|
|
112
|
+
DEMO_COMPANY,
|
|
113
|
+
DEMO_PASSWORD_MIN,
|
|
114
|
+
demoAccountsEnabled,
|
|
115
|
+
demoAccountsExpress,
|
|
116
|
+
demoAccountsResponse,
|
|
117
|
+
demoEmail,
|
|
118
|
+
demoUsers,
|
|
119
|
+
requireDemoPassword,
|
|
120
|
+
resolveDemoAccounts
|
|
121
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// src/demo-server.js
|
|
2
|
+
function fromEnvJson(raw) {
|
|
3
|
+
if (!raw) return null;
|
|
4
|
+
try {
|
|
5
|
+
const data = JSON.parse(raw);
|
|
6
|
+
return Array.isArray(data) ? data : Array.isArray(data?.accounts) ? data.accounts : null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function clean(accounts) {
|
|
12
|
+
return (accounts || []).filter((a) => a && a.label && a.identifier && a.password).map(({ label, identifier, password, company, role }) => ({
|
|
13
|
+
label: String(label),
|
|
14
|
+
identifier: String(identifier),
|
|
15
|
+
password: String(password),
|
|
16
|
+
...company ? { company: String(company) } : {},
|
|
17
|
+
...role ? { role: String(role) } : {}
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
function demoAccountsEnabled({ enabled = true, env = process.env } = {}) {
|
|
21
|
+
return Boolean(enabled) && env.DEMO_ACCOUNTS === "1";
|
|
22
|
+
}
|
|
23
|
+
function result(accounts) {
|
|
24
|
+
if (!accounts) return { status: 404, body: { error: "Not found" } };
|
|
25
|
+
const list = clean(accounts);
|
|
26
|
+
if (!list.length) return { status: 404, body: { error: "Not found" } };
|
|
27
|
+
return { status: 200, body: { accounts: list } };
|
|
28
|
+
}
|
|
29
|
+
function resolveDemoAccounts({ enabled = true, accounts = [], env = process.env } = {}) {
|
|
30
|
+
const on = demoAccountsEnabled({ enabled, env });
|
|
31
|
+
if (!on) return result(null);
|
|
32
|
+
const override = fromEnvJson(env.DEMO_ACCOUNTS_JSON);
|
|
33
|
+
if (override) return result(override);
|
|
34
|
+
const list = typeof accounts === "function" ? accounts(env) : accounts;
|
|
35
|
+
return list && typeof list.then === "function" ? list.then((l) => result(l || []), () => result(null)) : result(list || []);
|
|
36
|
+
}
|
|
37
|
+
var DEMO_ACCOUNTS_HEADERS = { "Cache-Control": "no-store", "X-Robots-Tag": "noindex" };
|
|
38
|
+
function demoAccountsExpress(opts = {}) {
|
|
39
|
+
const send = (res, { status, body }) => {
|
|
40
|
+
res.set(DEMO_ACCOUNTS_HEADERS);
|
|
41
|
+
res.status(status).json(body);
|
|
42
|
+
};
|
|
43
|
+
return (req, res) => {
|
|
44
|
+
const r = resolveDemoAccounts(opts);
|
|
45
|
+
if (typeof r.then === "function") return r.then((x) => send(res, x));
|
|
46
|
+
send(res, r);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function demoAccountsResponse(opts = {}) {
|
|
50
|
+
const toResponse = ({ status, body }) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json", ...DEMO_ACCOUNTS_HEADERS } });
|
|
51
|
+
const r = resolveDemoAccounts(opts);
|
|
52
|
+
return typeof r.then === "function" ? r.then(toResponse) : toResponse(r);
|
|
53
|
+
}
|
|
54
|
+
var DEMO_COMPANY = "default";
|
|
55
|
+
var DEMO_PASSWORD_MIN = 12;
|
|
56
|
+
function demoEmail(product, role) {
|
|
57
|
+
const slug = (s) => String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
58
|
+
return `${slug(role)}@${slug(product)}.demo.test`;
|
|
59
|
+
}
|
|
60
|
+
function demoUsers(manifest, { product, env = process.env, company = DEMO_COMPANY } = {}) {
|
|
61
|
+
const password = env.DEMO_PASSWORD;
|
|
62
|
+
if (!password) return [];
|
|
63
|
+
return (manifest || []).map((m) => ({
|
|
64
|
+
label: m.label,
|
|
65
|
+
identifier: m.email || demoEmail(product, m.role),
|
|
66
|
+
password,
|
|
67
|
+
company,
|
|
68
|
+
role: m.role
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function requireDemoPassword(env = process.env) {
|
|
72
|
+
const p = env.DEMO_PASSWORD;
|
|
73
|
+
if (!p) throw new Error("DEMO_PASSWORD is not set: demo:seed gives every demo user this password (the login chips show it).");
|
|
74
|
+
if (p.length < DEMO_PASSWORD_MIN) throw new Error(`DEMO_PASSWORD must be at least ${DEMO_PASSWORD_MIN} characters.`);
|
|
75
|
+
return p;
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
DEMO_ACCOUNTS_HEADERS,
|
|
79
|
+
DEMO_COMPANY,
|
|
80
|
+
DEMO_PASSWORD_MIN,
|
|
81
|
+
demoAccountsEnabled,
|
|
82
|
+
demoAccountsExpress,
|
|
83
|
+
demoAccountsResponse,
|
|
84
|
+
demoEmail,
|
|
85
|
+
demoUsers,
|
|
86
|
+
requireDemoPassword,
|
|
87
|
+
resolveDemoAccounts
|
|
88
|
+
};
|