@lukoweb/apitogo 0.2.31 → 0.2.34
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/dist/cli/cli.js +436 -153
- package/dist/declarations/app/entry.server.d.ts +2 -2
- package/dist/declarations/app/main.d.ts +3 -1
- package/dist/declarations/cli/platform/client/platform-api.d.ts +0 -4
- package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
- package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
- package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
- package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
- package/dist/declarations/config/effective-redirects.d.ts +6 -0
- package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
- package/dist/declarations/config/resolve-modules.d.ts +1 -4
- package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
- package/docs/cli/commands.md +19 -0
- package/docs/configuration/gateway.md +52 -0
- package/docs/guides/customer-account.md +11 -1
- package/docs/mcp/tools.md +101 -102
- package/package.json +2 -1
- package/src/app/demo.tsx +78 -77
- package/src/app/entry.client.tsx +115 -107
- package/src/app/entry.server.tsx +164 -160
- package/src/app/main.tsx +175 -167
- package/src/app/standalone.tsx +61 -60
- package/src/config/effective-redirects.ts +31 -0
- package/src/config/header-navigation-guard.ts +13 -0
- package/src/config/resolve-modules.ts +331 -354
- package/src/config/v2/home-redirect.ts +104 -98
- package/src/config/v2/index.ts +127 -126
- package/src/config/v2/ledger.ts +242 -235
- package/src/config/v2/resolve.ts +12 -4
- package/src/config/v2/rules.ts +716 -714
- package/src/config/v2/services.ts +799 -663
- package/src/config/v2/workspace.ts +17 -1
- package/src/config/validators/HeaderNavigationSchema.ts +42 -44
- package/src/lib/assets/powered-by-icon.png +0 -0
- package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
- package/src/lib/authentication/providers/dev-portal.tsx +476 -448
- package/src/lib/components/HeaderNavigation.tsx +176 -176
- package/src/lib/components/MobileTopNavigation.tsx +283 -283
- package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
- package/src/vite/plugin-offroot-resolve.ts +68 -67
- package/src/vite/prerender/prerender.ts +242 -242
|
@@ -1,448 +1,476 @@
|
|
|
1
|
-
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
-
import type { ZudokuContext } from "../../core/ZudokuContext.js";
|
|
3
|
-
import type {
|
|
4
|
-
AuthActionContext,
|
|
5
|
-
AuthActionOptions,
|
|
6
|
-
AuthenticationPlugin,
|
|
7
|
-
AuthenticationProviderInitializer,
|
|
8
|
-
} from "../authentication.js";
|
|
9
|
-
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
10
|
-
import {
|
|
11
|
-
type UserProfile,
|
|
12
|
-
useAuthState,
|
|
13
|
-
waitForAuthStateHydration,
|
|
14
|
-
} from "../state.js";
|
|
15
|
-
import {
|
|
16
|
-
DevPortalForgotPasswordPage,
|
|
17
|
-
DevPortalLoginPage,
|
|
18
|
-
DevPortalRegisterPage,
|
|
19
|
-
DevPortalResetPasswordPage,
|
|
20
|
-
DevPortalVerifyEmailPage,
|
|
21
|
-
} from "./dev-portal-auth-pages.js";
|
|
22
|
-
import type {
|
|
23
|
-
DevPortalAuthMode,
|
|
24
|
-
UserMeResponse,
|
|
25
|
-
} from "./dev-portal-constants.js";
|
|
26
|
-
import {
|
|
27
|
-
clearDemoSession,
|
|
28
|
-
readDemoSession,
|
|
29
|
-
resolveDemoMode,
|
|
30
|
-
activateDemoSession,
|
|
31
|
-
} from "./dev-portal-demo.js";
|
|
32
|
-
import {
|
|
33
|
-
buildDevPortalLogoutUrl,
|
|
34
|
-
isNativeAuthEnabled,
|
|
35
|
-
isPlaceholderDevPortalApiUrl,
|
|
36
|
-
mapUserMeToProfile,
|
|
37
|
-
probeDevPortalBackend,
|
|
38
|
-
resolveDevPortalApiBaseUrl,
|
|
39
|
-
toAbsoluteReturnUrl,
|
|
40
|
-
} from "./dev-portal-utils.js";
|
|
41
|
-
|
|
42
|
-
export type DevPortalProviderData = {
|
|
43
|
-
type: "dev-portal";
|
|
44
|
-
apiBaseUrl: string;
|
|
45
|
-
authMode: DevPortalAuthMode;
|
|
46
|
-
isDemoSession?: boolean;
|
|
47
|
-
demoMode?: boolean;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
declare module "../state.js" {
|
|
51
|
-
interface ProviderDataRegistry {
|
|
52
|
-
"dev-portal": DevPortalProviderData;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export class DevPortalAuthenticationProvider
|
|
57
|
-
extends CoreAuthenticationPlugin
|
|
58
|
-
implements AuthenticationPlugin
|
|
59
|
-
{
|
|
60
|
-
private readonly apiBaseUrl: string | undefined;
|
|
61
|
-
private readonly demoMode: boolean;
|
|
62
|
-
private readonly redirectToAfterSignIn: string | undefined;
|
|
63
|
-
private readonly redirectToAfterSignUp: string | undefined;
|
|
64
|
-
private readonly redirectToAfterSignOut: string;
|
|
65
|
-
private backendAvailable = false;
|
|
66
|
-
private refreshGeneration = 0;
|
|
67
|
-
private refreshInFlight: Promise<boolean> | null = null;
|
|
68
|
-
|
|
69
|
-
private authConfigNativeEnabled: boolean | null = null;
|
|
70
|
-
|
|
71
|
-
constructor({
|
|
72
|
-
apiBaseUrl,
|
|
73
|
-
demoMode,
|
|
74
|
-
redirectToAfterSignIn,
|
|
75
|
-
redirectToAfterSignUp,
|
|
76
|
-
redirectToAfterSignOut = "/",
|
|
77
|
-
}: DevPortalAuthenticationConfig) {
|
|
78
|
-
super();
|
|
79
|
-
this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
80
|
-
this.demoMode = resolveDemoMode({ demoMode });
|
|
81
|
-
this.redirectToAfterSignIn = redirectToAfterSignIn;
|
|
82
|
-
this.redirectToAfterSignUp = redirectToAfterSignUp;
|
|
83
|
-
this.redirectToAfterSignOut = redirectToAfterSignOut;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
getRoutes() {
|
|
87
|
-
const defaultReturnUrl =
|
|
88
|
-
this.redirectToAfterSignIn ?? this.redirectToAfterSignUp ?? "/";
|
|
89
|
-
const pageProps = {
|
|
90
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
91
|
-
demoMode: this.demoMode,
|
|
92
|
-
};
|
|
93
|
-
return [
|
|
94
|
-
...super.getRoutes(),
|
|
95
|
-
{
|
|
96
|
-
path: "/login",
|
|
97
|
-
element: (
|
|
98
|
-
<DevPortalLoginPage
|
|
99
|
-
{...pageProps}
|
|
100
|
-
defaultReturnUrl={defaultReturnUrl}
|
|
101
|
-
/>
|
|
102
|
-
),
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
path: "/register",
|
|
106
|
-
element: (
|
|
107
|
-
<DevPortalRegisterPage
|
|
108
|
-
apiBaseUrl={this.apiBaseUrl}
|
|
109
|
-
defaultReturnUrl={
|
|
110
|
-
this.redirectToAfterSignUp ?? this.redirectToAfterSignIn ?? "/"
|
|
111
|
-
}
|
|
112
|
-
/>
|
|
113
|
-
),
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
path: "/verify-email",
|
|
117
|
-
element: <DevPortalVerifyEmailPage {...pageProps} />,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
path: "/forgot-password",
|
|
121
|
-
element: <DevPortalForgotPasswordPage {...pageProps} />,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
path: "/reset-password",
|
|
125
|
-
element: <DevPortalResetPasswordPage {...pageProps} />,
|
|
126
|
-
},
|
|
127
|
-
];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async initialize(_context: ZudokuContext): Promise<void> {
|
|
131
|
-
// Node SSR cannot reach local HTTPS backends with self-signed certs.
|
|
132
|
-
if (typeof window === "undefined") {
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
await waitForAuthStateHydration();
|
|
137
|
-
await this.syncBackendAvailability();
|
|
138
|
-
if (this.backendAvailable && this.apiBaseUrl) {
|
|
139
|
-
try {
|
|
140
|
-
const { fetchDevPortalAuthConfig } =
|
|
141
|
-
await import("./dev-portal-utils.js");
|
|
142
|
-
const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
|
|
143
|
-
this.authConfigNativeEnabled = isNativeAuthEnabled(config);
|
|
144
|
-
} catch {
|
|
145
|
-
this.authConfigNativeEnabled = null;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!this.backendAvailable) {
|
|
149
|
-
if (this.demoMode && readDemoSession()) {
|
|
150
|
-
this.setDemoAuthenticatedState();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
this.setPreviewState();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
await this.refreshUserProfile();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
onPageLoad = async () => {
|
|
162
|
-
if (typeof window === "undefined") {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
await waitForAuthStateHydration();
|
|
167
|
-
await this.syncBackendAvailability();
|
|
168
|
-
if (!this.backendAvailable) {
|
|
169
|
-
if (this.demoMode && readDemoSession()) {
|
|
170
|
-
this.setDemoAuthenticatedState();
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
this.setPreviewState();
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
await this.refreshUserProfile();
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
private async syncBackendAvailability(): Promise<void> {
|
|
182
|
-
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
183
|
-
this.backendAvailable = false;
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
this.backendAvailable = await probeDevPortalBackend(this.apiBaseUrl);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private setPreviewState(): void {
|
|
191
|
-
useAuthState.setState({
|
|
192
|
-
isAuthenticated: false,
|
|
193
|
-
isPending: false,
|
|
194
|
-
sessionChecked: true,
|
|
195
|
-
profile: null,
|
|
196
|
-
providerData: this.apiBaseUrl
|
|
197
|
-
? {
|
|
198
|
-
type: "dev-portal",
|
|
199
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
200
|
-
authMode: "preview",
|
|
201
|
-
demoMode: this.demoMode,
|
|
202
|
-
}
|
|
203
|
-
: this.demoMode
|
|
204
|
-
? {
|
|
205
|
-
type: "dev-portal",
|
|
206
|
-
apiBaseUrl: "",
|
|
207
|
-
authMode: "preview",
|
|
208
|
-
demoMode: true,
|
|
209
|
-
}
|
|
210
|
-
: null,
|
|
211
|
-
isBackendAvailable: false,
|
|
212
|
-
authMode: "preview",
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private setLiveUnauthenticatedState(): void {
|
|
217
|
-
if (!this.apiBaseUrl) {
|
|
218
|
-
this.setPreviewState();
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
useAuthState.setState({
|
|
223
|
-
isAuthenticated: false,
|
|
224
|
-
isPending: false,
|
|
225
|
-
sessionChecked: true,
|
|
226
|
-
profile: null,
|
|
227
|
-
providerData: {
|
|
228
|
-
type: "dev-portal",
|
|
229
|
-
apiBaseUrl: this.apiBaseUrl,
|
|
230
|
-
authMode: "live",
|
|
231
|
-
demoMode: this.demoMode,
|
|
232
|
-
},
|
|
233
|
-
isBackendAvailable: true,
|
|
234
|
-
authMode: "live",
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private setDemoAuthenticatedState(): void {
|
|
239
|
-
activateDemoSession(this.apiBaseUrl);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
private ensureConfiguredBackend(): string {
|
|
243
|
-
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
244
|
-
if (this.demoMode) {
|
|
245
|
-
return "";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
throw new Error("Dev portal authentication is not available in preview.");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return this.apiBaseUrl;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async refreshUserProfile(): Promise<boolean> {
|
|
255
|
-
if (this.refreshInFlight) {
|
|
256
|
-
return this.refreshInFlight;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
|
|
260
|
-
this.refreshInFlight = null;
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
return this.refreshInFlight;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
private async refreshUserProfileInternal(): Promise<boolean> {
|
|
267
|
-
if (this.demoMode && readDemoSession() && !this.backendAvailable) {
|
|
268
|
-
this.setDemoAuthenticatedState();
|
|
269
|
-
return true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!this.apiBaseUrl) {
|
|
273
|
-
this.setPreviewState();
|
|
274
|
-
return false;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
278
|
-
if (this.demoMode && readDemoSession()) {
|
|
279
|
-
this.setDemoAuthenticatedState();
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
this.setPreviewState();
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
await waitForAuthStateHydration();
|
|
288
|
-
await this.syncBackendAvailability();
|
|
289
|
-
if (!this.backendAvailable) {
|
|
290
|
-
if (this.demoMode && readDemoSession()) {
|
|
291
|
-
this.setDemoAuthenticatedState();
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
this.setPreviewState();
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const generation = ++this.refreshGeneration;
|
|
300
|
-
useAuthState.setState({ isPending: true });
|
|
301
|
-
|
|
302
|
-
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
response
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import type { ZudokuContext } from "../../core/ZudokuContext.js";
|
|
3
|
+
import type {
|
|
4
|
+
AuthActionContext,
|
|
5
|
+
AuthActionOptions,
|
|
6
|
+
AuthenticationPlugin,
|
|
7
|
+
AuthenticationProviderInitializer,
|
|
8
|
+
} from "../authentication.js";
|
|
9
|
+
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
10
|
+
import {
|
|
11
|
+
type UserProfile,
|
|
12
|
+
useAuthState,
|
|
13
|
+
waitForAuthStateHydration,
|
|
14
|
+
} from "../state.js";
|
|
15
|
+
import {
|
|
16
|
+
DevPortalForgotPasswordPage,
|
|
17
|
+
DevPortalLoginPage,
|
|
18
|
+
DevPortalRegisterPage,
|
|
19
|
+
DevPortalResetPasswordPage,
|
|
20
|
+
DevPortalVerifyEmailPage,
|
|
21
|
+
} from "./dev-portal-auth-pages.js";
|
|
22
|
+
import type {
|
|
23
|
+
DevPortalAuthMode,
|
|
24
|
+
UserMeResponse,
|
|
25
|
+
} from "./dev-portal-constants.js";
|
|
26
|
+
import {
|
|
27
|
+
clearDemoSession,
|
|
28
|
+
readDemoSession,
|
|
29
|
+
resolveDemoMode,
|
|
30
|
+
activateDemoSession,
|
|
31
|
+
} from "./dev-portal-demo.js";
|
|
32
|
+
import {
|
|
33
|
+
buildDevPortalLogoutUrl,
|
|
34
|
+
isNativeAuthEnabled,
|
|
35
|
+
isPlaceholderDevPortalApiUrl,
|
|
36
|
+
mapUserMeToProfile,
|
|
37
|
+
probeDevPortalBackend,
|
|
38
|
+
resolveDevPortalApiBaseUrl,
|
|
39
|
+
toAbsoluteReturnUrl,
|
|
40
|
+
} from "./dev-portal-utils.js";
|
|
41
|
+
|
|
42
|
+
export type DevPortalProviderData = {
|
|
43
|
+
type: "dev-portal";
|
|
44
|
+
apiBaseUrl: string;
|
|
45
|
+
authMode: DevPortalAuthMode;
|
|
46
|
+
isDemoSession?: boolean;
|
|
47
|
+
demoMode?: boolean;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
declare module "../state.js" {
|
|
51
|
+
interface ProviderDataRegistry {
|
|
52
|
+
"dev-portal": DevPortalProviderData;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class DevPortalAuthenticationProvider
|
|
57
|
+
extends CoreAuthenticationPlugin
|
|
58
|
+
implements AuthenticationPlugin
|
|
59
|
+
{
|
|
60
|
+
private readonly apiBaseUrl: string | undefined;
|
|
61
|
+
private readonly demoMode: boolean;
|
|
62
|
+
private readonly redirectToAfterSignIn: string | undefined;
|
|
63
|
+
private readonly redirectToAfterSignUp: string | undefined;
|
|
64
|
+
private readonly redirectToAfterSignOut: string;
|
|
65
|
+
private backendAvailable = false;
|
|
66
|
+
private refreshGeneration = 0;
|
|
67
|
+
private refreshInFlight: Promise<boolean> | null = null;
|
|
68
|
+
|
|
69
|
+
private authConfigNativeEnabled: boolean | null = null;
|
|
70
|
+
|
|
71
|
+
constructor({
|
|
72
|
+
apiBaseUrl,
|
|
73
|
+
demoMode,
|
|
74
|
+
redirectToAfterSignIn,
|
|
75
|
+
redirectToAfterSignUp,
|
|
76
|
+
redirectToAfterSignOut = "/",
|
|
77
|
+
}: DevPortalAuthenticationConfig) {
|
|
78
|
+
super();
|
|
79
|
+
this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
80
|
+
this.demoMode = resolveDemoMode({ demoMode });
|
|
81
|
+
this.redirectToAfterSignIn = redirectToAfterSignIn;
|
|
82
|
+
this.redirectToAfterSignUp = redirectToAfterSignUp;
|
|
83
|
+
this.redirectToAfterSignOut = redirectToAfterSignOut;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getRoutes() {
|
|
87
|
+
const defaultReturnUrl =
|
|
88
|
+
this.redirectToAfterSignIn ?? this.redirectToAfterSignUp ?? "/";
|
|
89
|
+
const pageProps = {
|
|
90
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
91
|
+
demoMode: this.demoMode,
|
|
92
|
+
};
|
|
93
|
+
return [
|
|
94
|
+
...super.getRoutes(),
|
|
95
|
+
{
|
|
96
|
+
path: "/login",
|
|
97
|
+
element: (
|
|
98
|
+
<DevPortalLoginPage
|
|
99
|
+
{...pageProps}
|
|
100
|
+
defaultReturnUrl={defaultReturnUrl}
|
|
101
|
+
/>
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
path: "/register",
|
|
106
|
+
element: (
|
|
107
|
+
<DevPortalRegisterPage
|
|
108
|
+
apiBaseUrl={this.apiBaseUrl}
|
|
109
|
+
defaultReturnUrl={
|
|
110
|
+
this.redirectToAfterSignUp ?? this.redirectToAfterSignIn ?? "/"
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
113
|
+
),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
path: "/verify-email",
|
|
117
|
+
element: <DevPortalVerifyEmailPage {...pageProps} />,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
path: "/forgot-password",
|
|
121
|
+
element: <DevPortalForgotPasswordPage {...pageProps} />,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
path: "/reset-password",
|
|
125
|
+
element: <DevPortalResetPasswordPage {...pageProps} />,
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async initialize(_context: ZudokuContext): Promise<void> {
|
|
131
|
+
// Node SSR cannot reach local HTTPS backends with self-signed certs.
|
|
132
|
+
if (typeof window === "undefined") {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await waitForAuthStateHydration();
|
|
137
|
+
await this.syncBackendAvailability();
|
|
138
|
+
if (this.backendAvailable && this.apiBaseUrl) {
|
|
139
|
+
try {
|
|
140
|
+
const { fetchDevPortalAuthConfig } =
|
|
141
|
+
await import("./dev-portal-utils.js");
|
|
142
|
+
const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
|
|
143
|
+
this.authConfigNativeEnabled = isNativeAuthEnabled(config);
|
|
144
|
+
} catch {
|
|
145
|
+
this.authConfigNativeEnabled = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (!this.backendAvailable) {
|
|
149
|
+
if (this.demoMode && readDemoSession()) {
|
|
150
|
+
this.setDemoAuthenticatedState();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.setPreviewState();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
await this.refreshUserProfile();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
onPageLoad = async () => {
|
|
162
|
+
if (typeof window === "undefined") {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await waitForAuthStateHydration();
|
|
167
|
+
await this.syncBackendAvailability();
|
|
168
|
+
if (!this.backendAvailable) {
|
|
169
|
+
if (this.demoMode && readDemoSession()) {
|
|
170
|
+
this.setDemoAuthenticatedState();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.setPreviewState();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.refreshUserProfile();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
private async syncBackendAvailability(): Promise<void> {
|
|
182
|
+
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
183
|
+
this.backendAvailable = false;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.backendAvailable = await probeDevPortalBackend(this.apiBaseUrl);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private setPreviewState(): void {
|
|
191
|
+
useAuthState.setState({
|
|
192
|
+
isAuthenticated: false,
|
|
193
|
+
isPending: false,
|
|
194
|
+
sessionChecked: true,
|
|
195
|
+
profile: null,
|
|
196
|
+
providerData: this.apiBaseUrl
|
|
197
|
+
? {
|
|
198
|
+
type: "dev-portal",
|
|
199
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
200
|
+
authMode: "preview",
|
|
201
|
+
demoMode: this.demoMode,
|
|
202
|
+
}
|
|
203
|
+
: this.demoMode
|
|
204
|
+
? {
|
|
205
|
+
type: "dev-portal",
|
|
206
|
+
apiBaseUrl: "",
|
|
207
|
+
authMode: "preview",
|
|
208
|
+
demoMode: true,
|
|
209
|
+
}
|
|
210
|
+
: null,
|
|
211
|
+
isBackendAvailable: false,
|
|
212
|
+
authMode: "preview",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private setLiveUnauthenticatedState(): void {
|
|
217
|
+
if (!this.apiBaseUrl) {
|
|
218
|
+
this.setPreviewState();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
useAuthState.setState({
|
|
223
|
+
isAuthenticated: false,
|
|
224
|
+
isPending: false,
|
|
225
|
+
sessionChecked: true,
|
|
226
|
+
profile: null,
|
|
227
|
+
providerData: {
|
|
228
|
+
type: "dev-portal",
|
|
229
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
230
|
+
authMode: "live",
|
|
231
|
+
demoMode: this.demoMode,
|
|
232
|
+
},
|
|
233
|
+
isBackendAvailable: true,
|
|
234
|
+
authMode: "live",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private setDemoAuthenticatedState(): void {
|
|
239
|
+
activateDemoSession(this.apiBaseUrl);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private ensureConfiguredBackend(): string {
|
|
243
|
+
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
244
|
+
if (this.demoMode) {
|
|
245
|
+
return "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
throw new Error("Dev portal authentication is not available in preview.");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return this.apiBaseUrl;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async refreshUserProfile(): Promise<boolean> {
|
|
255
|
+
if (this.refreshInFlight) {
|
|
256
|
+
return this.refreshInFlight;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.refreshInFlight = this.refreshUserProfileInternal().finally(() => {
|
|
260
|
+
this.refreshInFlight = null;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return this.refreshInFlight;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async refreshUserProfileInternal(): Promise<boolean> {
|
|
267
|
+
if (this.demoMode && readDemoSession() && !this.backendAvailable) {
|
|
268
|
+
this.setDemoAuthenticatedState();
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!this.apiBaseUrl) {
|
|
273
|
+
this.setPreviewState();
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
278
|
+
if (this.demoMode && readDemoSession()) {
|
|
279
|
+
this.setDemoAuthenticatedState();
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.setPreviewState();
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await waitForAuthStateHydration();
|
|
288
|
+
await this.syncBackendAvailability();
|
|
289
|
+
if (!this.backendAvailable) {
|
|
290
|
+
if (this.demoMode && readDemoSession()) {
|
|
291
|
+
this.setDemoAuthenticatedState();
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.setPreviewState();
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const generation = ++this.refreshGeneration;
|
|
300
|
+
useAuthState.setState({ isPending: true });
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
// /auth/session answers 200 for everyone, so a visitor does not put a failed request in the console. A backend
|
|
304
|
+
// from before it existed answers 404: ask /auth/me there, which answers 401 for a visitor.
|
|
305
|
+
const requestInit: RequestInit = {
|
|
306
|
+
method: "GET",
|
|
307
|
+
headers: { Accept: "application/json" },
|
|
308
|
+
credentials: "include",
|
|
309
|
+
redirect: "manual",
|
|
310
|
+
};
|
|
311
|
+
let response = await fetch(
|
|
312
|
+
`${this.apiBaseUrl}/api/v1/auth/session`,
|
|
313
|
+
requestInit,
|
|
314
|
+
);
|
|
315
|
+
const hasSession = response.status !== 404;
|
|
316
|
+
if (!hasSession) {
|
|
317
|
+
response = await fetch(
|
|
318
|
+
`${this.apiBaseUrl}/api/v1/auth/me`,
|
|
319
|
+
requestInit,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (generation !== this.refreshGeneration) {
|
|
324
|
+
return useAuthState.getState().isAuthenticated;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Backends may still respond with 302 to the IdP; treat as unauthenticated.
|
|
328
|
+
if (
|
|
329
|
+
response.type === "opaqueredirect" ||
|
|
330
|
+
response.status === 302 ||
|
|
331
|
+
response.status === 401
|
|
332
|
+
) {
|
|
333
|
+
this.setLiveUnauthenticatedState();
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
this.setLiveUnauthenticatedState();
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
let me: UserMeResponse;
|
|
343
|
+
if (hasSession) {
|
|
344
|
+
const session = (await response.json()) as {
|
|
345
|
+
authenticated?: boolean;
|
|
346
|
+
user?: UserMeResponse | null;
|
|
347
|
+
};
|
|
348
|
+
if (!session.authenticated || !session.user) {
|
|
349
|
+
if (generation === this.refreshGeneration) {
|
|
350
|
+
this.setLiveUnauthenticatedState();
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
me = session.user;
|
|
355
|
+
} else {
|
|
356
|
+
me = (await response.json()) as UserMeResponse;
|
|
357
|
+
}
|
|
358
|
+
if (generation !== this.refreshGeneration) {
|
|
359
|
+
return useAuthState.getState().isAuthenticated;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const profile: UserProfile = mapUserMeToProfile(me);
|
|
363
|
+
|
|
364
|
+
useAuthState.setState({
|
|
365
|
+
isAuthenticated: true,
|
|
366
|
+
isPending: false,
|
|
367
|
+
sessionChecked: true,
|
|
368
|
+
profile,
|
|
369
|
+
providerData: {
|
|
370
|
+
type: "dev-portal",
|
|
371
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
372
|
+
authMode: "live",
|
|
373
|
+
demoMode: this.demoMode,
|
|
374
|
+
},
|
|
375
|
+
isBackendAvailable: true,
|
|
376
|
+
authMode: "live",
|
|
377
|
+
});
|
|
378
|
+
return true;
|
|
379
|
+
} catch {
|
|
380
|
+
if (generation !== this.refreshGeneration) {
|
|
381
|
+
return useAuthState.getState().isAuthenticated;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (this.backendAvailable) {
|
|
385
|
+
this.setLiveUnauthenticatedState();
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.setPreviewState();
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async signIn(
|
|
395
|
+
_: AuthActionContext,
|
|
396
|
+
{ redirectTo }: AuthActionOptions = {},
|
|
397
|
+
): Promise<void> {
|
|
398
|
+
if (!this.backendAvailable && this.demoMode) {
|
|
399
|
+
const target =
|
|
400
|
+
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
401
|
+
const url = new URL("/login", window.location.origin);
|
|
402
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
403
|
+
window.location.assign(url.toString());
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.ensureConfiguredBackend();
|
|
408
|
+
const target =
|
|
409
|
+
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
410
|
+
const url = new URL("/login", window.location.origin);
|
|
411
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
412
|
+
window.location.assign(url.toString());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async signUp(
|
|
416
|
+
_: AuthActionContext,
|
|
417
|
+
{ redirectTo }: AuthActionOptions = {},
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
if (!this.backendAvailable && this.demoMode) {
|
|
420
|
+
const target =
|
|
421
|
+
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
422
|
+
const url = new URL("/register", window.location.origin);
|
|
423
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
424
|
+
window.location.assign(url.toString());
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.ensureConfiguredBackend();
|
|
429
|
+
const target =
|
|
430
|
+
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
431
|
+
const url = new URL("/register", window.location.origin);
|
|
432
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
433
|
+
window.location.assign(url.toString());
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async signOut(_: AuthActionContext): Promise<void> {
|
|
437
|
+
const providerData = useAuthState.getState().providerData as
|
|
438
|
+
| DevPortalProviderData
|
|
439
|
+
| null
|
|
440
|
+
| undefined;
|
|
441
|
+
|
|
442
|
+
if (providerData?.isDemoSession || (this.demoMode && readDemoSession())) {
|
|
443
|
+
clearDemoSession();
|
|
444
|
+
this.setPreviewState();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const apiBaseUrl = this.ensureConfiguredBackend();
|
|
449
|
+
window.location.assign(
|
|
450
|
+
buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
hasDistinctSignUpFlow(): boolean {
|
|
455
|
+
if (this.demoMode && !this.backendAvailable) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
return this.authConfigNativeEnabled !== false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async signRequest(request: Request): Promise<Request> {
|
|
463
|
+
return request;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Called from the login page after successful demo credential check. */
|
|
467
|
+
completeDemoLogin(): void {
|
|
468
|
+
activateDemoSession(this.apiBaseUrl);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const devPortalAuth: AuthenticationProviderInitializer<
|
|
473
|
+
DevPortalAuthenticationConfig
|
|
474
|
+
> = (options) => new DevPortalAuthenticationProvider(options);
|
|
475
|
+
|
|
476
|
+
export default devPortalAuth;
|