@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,602 +1,667 @@
|
|
|
1
|
-
import { Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
-
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
9
|
-
} from "@lukoweb/apitogo/ui/Card.js";
|
|
10
|
-
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
11
|
-
import { Label } from "@lukoweb/apitogo/ui/Label.js";
|
|
12
|
-
import { useCallback, useEffect, useState, type ReactNode } from "react";
|
|
13
|
-
import { Link, useSearchParams } from "react-router";
|
|
14
|
-
import type { DevPortalAuthConfigResponse } from "./dev-portal-constants.js";
|
|
15
|
-
import {
|
|
16
|
-
DEMO_EMAIL,
|
|
17
|
-
DEMO_PASSWORD,
|
|
18
|
-
activateDemoSession,
|
|
19
|
-
isDemoCredential,
|
|
20
|
-
} from "./dev-portal-demo.js";
|
|
21
|
-
import {
|
|
22
|
-
buildDevPortalOidcChallengeUrl,
|
|
23
|
-
fetchDevPortalAuthConfig,
|
|
24
|
-
isNativeAuthEnabled,
|
|
25
|
-
isPlaceholderDevPortalApiUrl,
|
|
26
|
-
postDevPortalAuth,
|
|
27
|
-
resolveDevPortalApiBaseUrl,
|
|
28
|
-
toAbsoluteReturnUrl,
|
|
29
|
-
} from "./dev-portal-utils.js";
|
|
30
|
-
|
|
31
|
-
type AuthPageShellProps = {
|
|
32
|
-
title: string;
|
|
33
|
-
description?: string;
|
|
34
|
-
badge?: ReactNode;
|
|
35
|
-
children: ReactNode;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function AuthPageShell({
|
|
39
|
-
title,
|
|
40
|
-
description,
|
|
41
|
-
badge,
|
|
42
|
-
children,
|
|
43
|
-
}: AuthPageShellProps) {
|
|
44
|
-
return (
|
|
45
|
-
<div className="flex items-center justify-center bg-muted px-5 py-10">
|
|
46
|
-
<Card className="w-full max-w-md gap-0 bg-card py-8">
|
|
47
|
-
<CardHeader className="gap-0 px-8 pb-0">
|
|
48
|
-
{badge ? <div className="mb-3.5">{badge}</div> : null}
|
|
49
|
-
<CardTitle className="text-2xl font-bold tracking-tight">
|
|
50
|
-
{title}
|
|
51
|
-
</CardTitle>
|
|
52
|
-
{description ? (
|
|
53
|
-
<CardDescription className="mt-1 mb-[22px] text-sm">
|
|
54
|
-
{description}
|
|
55
|
-
</CardDescription>
|
|
56
|
-
) : null}
|
|
57
|
-
</CardHeader>
|
|
58
|
-
<CardContent className="space-y-4 px-8">{children}</CardContent>
|
|
59
|
-
</Card>
|
|
60
|
-
</div>
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function useReturnUrl(defaultReturnUrl = "/"): string {
|
|
65
|
-
const [search] = useSearchParams();
|
|
66
|
-
return search.get("redirect") ?? search.get("returnUrl") ?? defaultReturnUrl;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function useApiBaseUrl(apiBaseUrl?: string): string | undefined {
|
|
70
|
-
return resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function AuthError({ message }: { message: string | null }) {
|
|
74
|
-
if (!message) return null;
|
|
75
|
-
return <p className="text-sm text-destructive">{message}</p>;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function OidcProviderButtons({
|
|
79
|
-
apiBaseUrl,
|
|
80
|
-
config,
|
|
81
|
-
returnUrl,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
);
|
|
149
|
-
const [
|
|
150
|
-
|
|
151
|
-
);
|
|
152
|
-
const [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
) : null}
|
|
299
|
-
{
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
apiBaseUrl
|
|
326
|
-
defaultReturnUrl
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
const [
|
|
334
|
-
const [
|
|
335
|
-
const [
|
|
336
|
-
|
|
337
|
-
const
|
|
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
|
-
<p className="text-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
>
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
const [
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
1
|
+
import { Spinner } from "@lukoweb/apitogo/components";
|
|
2
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from "@lukoweb/apitogo/ui/Card.js";
|
|
10
|
+
import { Input } from "@lukoweb/apitogo/ui/Input.js";
|
|
11
|
+
import { Label } from "@lukoweb/apitogo/ui/Label.js";
|
|
12
|
+
import { useCallback, useEffect, useState, type ReactNode } from "react";
|
|
13
|
+
import { Link, useSearchParams } from "react-router";
|
|
14
|
+
import type { DevPortalAuthConfigResponse } from "./dev-portal-constants.js";
|
|
15
|
+
import {
|
|
16
|
+
DEMO_EMAIL,
|
|
17
|
+
DEMO_PASSWORD,
|
|
18
|
+
activateDemoSession,
|
|
19
|
+
isDemoCredential,
|
|
20
|
+
} from "./dev-portal-demo.js";
|
|
21
|
+
import {
|
|
22
|
+
buildDevPortalOidcChallengeUrl,
|
|
23
|
+
fetchDevPortalAuthConfig,
|
|
24
|
+
isNativeAuthEnabled,
|
|
25
|
+
isPlaceholderDevPortalApiUrl,
|
|
26
|
+
postDevPortalAuth,
|
|
27
|
+
resolveDevPortalApiBaseUrl,
|
|
28
|
+
toAbsoluteReturnUrl,
|
|
29
|
+
} from "./dev-portal-utils.js";
|
|
30
|
+
|
|
31
|
+
type AuthPageShellProps = {
|
|
32
|
+
title: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
badge?: ReactNode;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function AuthPageShell({
|
|
39
|
+
title,
|
|
40
|
+
description,
|
|
41
|
+
badge,
|
|
42
|
+
children,
|
|
43
|
+
}: AuthPageShellProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex items-center justify-center bg-muted px-5 py-10">
|
|
46
|
+
<Card className="w-full max-w-md gap-0 bg-card py-8">
|
|
47
|
+
<CardHeader className="gap-0 px-8 pb-0">
|
|
48
|
+
{badge ? <div className="mb-3.5">{badge}</div> : null}
|
|
49
|
+
<CardTitle className="text-2xl font-bold tracking-tight">
|
|
50
|
+
{title}
|
|
51
|
+
</CardTitle>
|
|
52
|
+
{description ? (
|
|
53
|
+
<CardDescription className="mt-1 mb-[22px] text-sm">
|
|
54
|
+
{description}
|
|
55
|
+
</CardDescription>
|
|
56
|
+
) : null}
|
|
57
|
+
</CardHeader>
|
|
58
|
+
<CardContent className="space-y-4 px-8">{children}</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function useReturnUrl(defaultReturnUrl = "/"): string {
|
|
65
|
+
const [search] = useSearchParams();
|
|
66
|
+
return search.get("redirect") ?? search.get("returnUrl") ?? defaultReturnUrl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function useApiBaseUrl(apiBaseUrl?: string): string | undefined {
|
|
70
|
+
return resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function AuthError({ message }: { message: string | null }) {
|
|
74
|
+
if (!message) return null;
|
|
75
|
+
return <p className="text-sm text-destructive">{message}</p>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function OidcProviderButtons({
|
|
79
|
+
apiBaseUrl,
|
|
80
|
+
config,
|
|
81
|
+
returnUrl,
|
|
82
|
+
label,
|
|
83
|
+
}: {
|
|
84
|
+
apiBaseUrl: string;
|
|
85
|
+
config: DevPortalAuthConfigResponse;
|
|
86
|
+
returnUrl: string;
|
|
87
|
+
/** Replaces the provider's own name on the button, for example on the sign-up page. */
|
|
88
|
+
label?: string;
|
|
89
|
+
}) {
|
|
90
|
+
if (!config.providers.length) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-2">
|
|
96
|
+
{isNativeAuthEnabled(config) ? (
|
|
97
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
98
|
+
or continue with
|
|
99
|
+
</p>
|
|
100
|
+
) : null}
|
|
101
|
+
{config.providers.map((provider) => (
|
|
102
|
+
<Button
|
|
103
|
+
key={provider.id}
|
|
104
|
+
type="button"
|
|
105
|
+
variant="outline"
|
|
106
|
+
className="w-full"
|
|
107
|
+
onClick={() => {
|
|
108
|
+
window.location.assign(
|
|
109
|
+
buildDevPortalOidcChallengeUrl(
|
|
110
|
+
apiBaseUrl,
|
|
111
|
+
provider.id,
|
|
112
|
+
returnUrl,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{label ?? (provider.displayName || provider.id)}
|
|
118
|
+
</Button>
|
|
119
|
+
))}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function DemoLoginHint() {
|
|
125
|
+
return (
|
|
126
|
+
<div className="rounded-lg border border-dashed bg-muted/30 p-3 text-sm text-muted-foreground">
|
|
127
|
+
<p className="font-medium text-foreground">Local demo sign-in</p>
|
|
128
|
+
<p className="mt-1">
|
|
129
|
+
Use <code className="rounded bg-muted px-1 py-0.5">{DEMO_EMAIL}</code>{" "}
|
|
130
|
+
with password{" "}
|
|
131
|
+
<code className="rounded bg-muted px-1 py-0.5">{DEMO_PASSWORD}</code> to
|
|
132
|
+
explore the account area without a dev-portal backend.
|
|
133
|
+
</p>
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function DevPortalLoginPage({
|
|
139
|
+
apiBaseUrl,
|
|
140
|
+
demoMode = false,
|
|
141
|
+
defaultReturnUrl = "/",
|
|
142
|
+
}: {
|
|
143
|
+
apiBaseUrl?: string;
|
|
144
|
+
demoMode?: boolean;
|
|
145
|
+
defaultReturnUrl?: string;
|
|
146
|
+
}) {
|
|
147
|
+
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
148
|
+
const returnUrl = useReturnUrl(defaultReturnUrl);
|
|
149
|
+
const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
|
|
150
|
+
null,
|
|
151
|
+
);
|
|
152
|
+
const [configLoading, setConfigLoading] = useState(
|
|
153
|
+
Boolean(baseUrl) && !isPlaceholderDevPortalApiUrl(baseUrl),
|
|
154
|
+
);
|
|
155
|
+
const [configError, setConfigError] = useState<string | null>(null);
|
|
156
|
+
const [useDemoFallback, setUseDemoFallback] = useState(
|
|
157
|
+
demoMode && (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)),
|
|
158
|
+
);
|
|
159
|
+
const demoAuthAvailable = demoMode && useDemoFallback;
|
|
160
|
+
const [email, setEmail] = useState("");
|
|
161
|
+
const [password, setPassword] = useState("");
|
|
162
|
+
const [error, setError] = useState<string | null>(null);
|
|
163
|
+
const [pending, setPending] = useState(false);
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (!demoMode) {
|
|
167
|
+
setUseDemoFallback(false);
|
|
168
|
+
} else if (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)) {
|
|
169
|
+
setUseDemoFallback(true);
|
|
170
|
+
}
|
|
171
|
+
}, [baseUrl, demoMode]);
|
|
172
|
+
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (demoAuthAvailable) {
|
|
175
|
+
setConfig({ native: { enabled: true }, email: null, providers: [] });
|
|
176
|
+
setConfigLoading(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!baseUrl) return;
|
|
181
|
+
setConfigLoading(true);
|
|
182
|
+
setConfigError(null);
|
|
183
|
+
void fetchDevPortalAuthConfig(baseUrl)
|
|
184
|
+
.then((loaded) => {
|
|
185
|
+
setConfig(loaded);
|
|
186
|
+
setConfigLoading(false);
|
|
187
|
+
})
|
|
188
|
+
.catch(() => {
|
|
189
|
+
if (demoMode) {
|
|
190
|
+
setUseDemoFallback(true);
|
|
191
|
+
setConfig({ native: { enabled: true }, email: null, providers: [] });
|
|
192
|
+
setConfigLoading(false);
|
|
193
|
+
setConfigError(null);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
setConfig(null);
|
|
198
|
+
setConfigLoading(false);
|
|
199
|
+
setConfigError(
|
|
200
|
+
`Could not reach the dev portal API at ${baseUrl}. If you use a local HTTPS certificate, open ${baseUrl}/api/v1/auth/status in your browser first to trust it.`,
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
}, [baseUrl, demoAuthAvailable, demoMode]);
|
|
204
|
+
|
|
205
|
+
const onSubmit = useCallback(
|
|
206
|
+
async (event: React.FormEvent) => {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
setPending(true);
|
|
209
|
+
setError(null);
|
|
210
|
+
|
|
211
|
+
if (demoAuthAvailable) {
|
|
212
|
+
if (!isDemoCredential(email, password)) {
|
|
213
|
+
setError("Invalid demo credentials.");
|
|
214
|
+
setPending(false);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
activateDemoSession(baseUrl);
|
|
219
|
+
window.location.assign(toAbsoluteReturnUrl(returnUrl));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!baseUrl) return;
|
|
224
|
+
try {
|
|
225
|
+
await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
|
|
226
|
+
email,
|
|
227
|
+
password,
|
|
228
|
+
});
|
|
229
|
+
window.location.assign(toAbsoluteReturnUrl(returnUrl));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
setError(err instanceof Error ? err.message : "Login failed.");
|
|
232
|
+
} finally {
|
|
233
|
+
setPending(false);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[baseUrl, demoAuthAvailable, email, password, returnUrl],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!baseUrl && !demoAuthAvailable) {
|
|
240
|
+
return (
|
|
241
|
+
<AuthPageShell
|
|
242
|
+
title="Sign in"
|
|
243
|
+
description="Dev portal API is not configured."
|
|
244
|
+
>
|
|
245
|
+
<p className="text-sm text-muted-foreground">
|
|
246
|
+
Set authentication.apiBaseUrl to continue.
|
|
247
|
+
</p>
|
|
248
|
+
</AuthPageShell>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<AuthPageShell title="Sign in" description="Access your developer account.">
|
|
254
|
+
{demoAuthAvailable ? <DemoLoginHint /> : null}
|
|
255
|
+
{isNativeAuthEnabled(config) || demoAuthAvailable ? (
|
|
256
|
+
<form className="space-y-4" onSubmit={onSubmit}>
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
<Label htmlFor="email">Email</Label>
|
|
259
|
+
<Input
|
|
260
|
+
id="email"
|
|
261
|
+
type="email"
|
|
262
|
+
autoComplete="email"
|
|
263
|
+
value={email}
|
|
264
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
265
|
+
required
|
|
266
|
+
/>
|
|
267
|
+
</div>
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<Label htmlFor="password">Password</Label>
|
|
270
|
+
<Input
|
|
271
|
+
id="password"
|
|
272
|
+
type="password"
|
|
273
|
+
autoComplete="current-password"
|
|
274
|
+
value={password}
|
|
275
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
276
|
+
required
|
|
277
|
+
/>
|
|
278
|
+
</div>
|
|
279
|
+
<AuthError message={error} />
|
|
280
|
+
<Button type="submit" className="w-full" disabled={pending}>
|
|
281
|
+
{pending ? <Spinner /> : "Sign in"}
|
|
282
|
+
</Button>
|
|
283
|
+
<div className="flex justify-between text-sm">
|
|
284
|
+
<Link
|
|
285
|
+
className="text-primary hover:underline"
|
|
286
|
+
to="/forgot-password"
|
|
287
|
+
>
|
|
288
|
+
Forgot password?
|
|
289
|
+
</Link>
|
|
290
|
+
<Link
|
|
291
|
+
className="text-primary hover:underline"
|
|
292
|
+
to={`/register?redirect=${encodeURIComponent(returnUrl)}`}
|
|
293
|
+
>
|
|
294
|
+
Create account
|
|
295
|
+
</Link>
|
|
296
|
+
</div>
|
|
297
|
+
</form>
|
|
298
|
+
) : null}
|
|
299
|
+
{configError && !demoAuthAvailable ? (
|
|
300
|
+
<AuthError message={configError} />
|
|
301
|
+
) : null}
|
|
302
|
+
{configLoading ? (
|
|
303
|
+
<div className="flex justify-center">
|
|
304
|
+
<Spinner />
|
|
305
|
+
</div>
|
|
306
|
+
) : config && !demoAuthAvailable && baseUrl ? (
|
|
307
|
+
<>
|
|
308
|
+
<OidcProviderButtons
|
|
309
|
+
apiBaseUrl={baseUrl}
|
|
310
|
+
config={config}
|
|
311
|
+
returnUrl={returnUrl}
|
|
312
|
+
/>
|
|
313
|
+
{!isNativeAuthEnabled(config) && !config.providers.length ? (
|
|
314
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
315
|
+
No sign-in methods are configured.
|
|
316
|
+
</p>
|
|
317
|
+
) : null}
|
|
318
|
+
</>
|
|
319
|
+
) : null}
|
|
320
|
+
</AuthPageShell>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function DevPortalRegisterPage({
|
|
325
|
+
apiBaseUrl,
|
|
326
|
+
defaultReturnUrl = "/",
|
|
327
|
+
}: {
|
|
328
|
+
apiBaseUrl?: string;
|
|
329
|
+
defaultReturnUrl?: string;
|
|
330
|
+
}) {
|
|
331
|
+
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
332
|
+
const returnUrl = useReturnUrl(defaultReturnUrl);
|
|
333
|
+
const [email, setEmail] = useState("");
|
|
334
|
+
const [password, setPassword] = useState("");
|
|
335
|
+
const [displayName, setDisplayName] = useState("");
|
|
336
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
337
|
+
const [error, setError] = useState<string | null>(null);
|
|
338
|
+
const [pending, setPending] = useState(false);
|
|
339
|
+
const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
|
|
340
|
+
null,
|
|
341
|
+
);
|
|
342
|
+
const [configLoading, setConfigLoading] = useState(
|
|
343
|
+
Boolean(baseUrl) && !isPlaceholderDevPortalApiUrl(baseUrl),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// Native sign-up only exists when the backend says so. Otherwise sign-up happens at the identity provider, the same
|
|
347
|
+
// way sign-in does, and the form below would only end in "Native authentication is not enabled".
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)) {
|
|
350
|
+
setConfigLoading(false);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
setConfigLoading(true);
|
|
355
|
+
void fetchDevPortalAuthConfig(baseUrl)
|
|
356
|
+
.then((loaded) => setConfig(loaded))
|
|
357
|
+
.catch(() => setConfig(null))
|
|
358
|
+
.finally(() => setConfigLoading(false));
|
|
359
|
+
}, [baseUrl]);
|
|
360
|
+
|
|
361
|
+
const onSubmit = useCallback(
|
|
362
|
+
async (event: React.FormEvent) => {
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
if (!baseUrl) return;
|
|
365
|
+
setPending(true);
|
|
366
|
+
setError(null);
|
|
367
|
+
setMessage(null);
|
|
368
|
+
try {
|
|
369
|
+
const response = await postDevPortalAuth<{ message: string }>(
|
|
370
|
+
baseUrl,
|
|
371
|
+
"/api/v1/auth/register",
|
|
372
|
+
{ email, password, displayName: displayName || null },
|
|
373
|
+
);
|
|
374
|
+
setMessage(response.message);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
setError(err instanceof Error ? err.message : "Registration failed.");
|
|
377
|
+
} finally {
|
|
378
|
+
setPending(false);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
[baseUrl, displayName, email, password],
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (!baseUrl) {
|
|
385
|
+
return (
|
|
386
|
+
<AuthPageShell
|
|
387
|
+
title="Create your account"
|
|
388
|
+
description="Dev portal API is not configured."
|
|
389
|
+
>
|
|
390
|
+
<p className="text-sm text-muted-foreground">
|
|
391
|
+
Set authentication.apiBaseUrl to continue.
|
|
392
|
+
</p>
|
|
393
|
+
</AuthPageShell>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (configLoading) {
|
|
398
|
+
return (
|
|
399
|
+
<AuthPageShell title="Create your account">
|
|
400
|
+
<div className="flex justify-center">
|
|
401
|
+
<Spinner />
|
|
402
|
+
</div>
|
|
403
|
+
</AuthPageShell>
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (config && !isNativeAuthEnabled(config)) {
|
|
408
|
+
return (
|
|
409
|
+
<AuthPageShell
|
|
410
|
+
title="Create your account"
|
|
411
|
+
description="Accounts are created on our secure sign-in provider."
|
|
412
|
+
>
|
|
413
|
+
{config.providers.length ? (
|
|
414
|
+
<OidcProviderButtons
|
|
415
|
+
apiBaseUrl={baseUrl}
|
|
416
|
+
config={config}
|
|
417
|
+
returnUrl={returnUrl}
|
|
418
|
+
label="Continue to create your account"
|
|
419
|
+
/>
|
|
420
|
+
) : (
|
|
421
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
422
|
+
Sign-up is not available.
|
|
423
|
+
</p>
|
|
424
|
+
)}
|
|
425
|
+
<p className="text-sm text-muted-foreground">
|
|
426
|
+
Already have an account?{" "}
|
|
427
|
+
<Link
|
|
428
|
+
className="font-semibold text-primary no-underline hover:underline"
|
|
429
|
+
to={`/login?redirect=${encodeURIComponent(returnUrl)}`}
|
|
430
|
+
>
|
|
431
|
+
Log in
|
|
432
|
+
</Link>
|
|
433
|
+
</p>
|
|
434
|
+
</AuthPageShell>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<AuthPageShell title="Create your account">
|
|
440
|
+
<form className="flex flex-col gap-4" onSubmit={onSubmit}>
|
|
441
|
+
<div className="space-y-2">
|
|
442
|
+
<Label htmlFor="displayName">Full name</Label>
|
|
443
|
+
<Input
|
|
444
|
+
id="displayName"
|
|
445
|
+
placeholder="Priya Shah"
|
|
446
|
+
value={displayName}
|
|
447
|
+
onChange={(event) => setDisplayName(event.target.value)}
|
|
448
|
+
className="h-10"
|
|
449
|
+
/>
|
|
450
|
+
</div>
|
|
451
|
+
<div className="space-y-2">
|
|
452
|
+
<Label htmlFor="email">Email</Label>
|
|
453
|
+
<Input
|
|
454
|
+
id="email"
|
|
455
|
+
type="email"
|
|
456
|
+
autoComplete="email"
|
|
457
|
+
placeholder="you@company.com"
|
|
458
|
+
value={email}
|
|
459
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
460
|
+
required
|
|
461
|
+
className="h-10"
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
<div className="space-y-2">
|
|
465
|
+
<Label htmlFor="password">Password</Label>
|
|
466
|
+
<Input
|
|
467
|
+
id="password"
|
|
468
|
+
type="password"
|
|
469
|
+
autoComplete="new-password"
|
|
470
|
+
placeholder="••••••••"
|
|
471
|
+
value={password}
|
|
472
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
473
|
+
required
|
|
474
|
+
className="h-10"
|
|
475
|
+
/>
|
|
476
|
+
<p className="text-xs text-muted-foreground">At least 8 characters</p>
|
|
477
|
+
</div>
|
|
478
|
+
<AuthError message={error} />
|
|
479
|
+
{message ? (
|
|
480
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
481
|
+
) : null}
|
|
482
|
+
<Button
|
|
483
|
+
type="submit"
|
|
484
|
+
size="lg"
|
|
485
|
+
className="h-12 w-full text-base"
|
|
486
|
+
disabled={pending}
|
|
487
|
+
>
|
|
488
|
+
{pending ? <Spinner /> : "Create account"}
|
|
489
|
+
</Button>
|
|
490
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
491
|
+
By creating an account you agree to our Terms and Privacy Policy.
|
|
492
|
+
</p>
|
|
493
|
+
<p className="text-sm text-muted-foreground">
|
|
494
|
+
Already have an account?{" "}
|
|
495
|
+
<Link
|
|
496
|
+
className="font-semibold text-primary no-underline hover:underline"
|
|
497
|
+
to={`/login?redirect=${encodeURIComponent(returnUrl)}`}
|
|
498
|
+
>
|
|
499
|
+
Log in
|
|
500
|
+
</Link>
|
|
501
|
+
</p>
|
|
502
|
+
</form>
|
|
503
|
+
</AuthPageShell>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function DevPortalVerifyEmailPage({
|
|
508
|
+
apiBaseUrl,
|
|
509
|
+
}: {
|
|
510
|
+
apiBaseUrl?: string;
|
|
511
|
+
}) {
|
|
512
|
+
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
513
|
+
const [search] = useSearchParams();
|
|
514
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
515
|
+
const [error, setError] = useState<string | null>(null);
|
|
516
|
+
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
const token = search.get("token");
|
|
519
|
+
if (!baseUrl || !token) return;
|
|
520
|
+
void postDevPortalAuth<{ message: string }>(
|
|
521
|
+
baseUrl,
|
|
522
|
+
"/api/v1/auth/verify-email",
|
|
523
|
+
{
|
|
524
|
+
token,
|
|
525
|
+
},
|
|
526
|
+
)
|
|
527
|
+
.then((response) => setMessage(response.message))
|
|
528
|
+
.catch((err) =>
|
|
529
|
+
setError(err instanceof Error ? err.message : "Verification failed."),
|
|
530
|
+
);
|
|
531
|
+
}, [baseUrl, search]);
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<AuthPageShell title="Verify email">
|
|
535
|
+
<AuthError message={error} />
|
|
536
|
+
{message ? (
|
|
537
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
538
|
+
) : (
|
|
539
|
+
<Spinner />
|
|
540
|
+
)}
|
|
541
|
+
<Link className="text-sm text-primary hover:underline" to="/login">
|
|
542
|
+
Sign in
|
|
543
|
+
</Link>
|
|
544
|
+
</AuthPageShell>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function DevPortalForgotPasswordPage({
|
|
549
|
+
apiBaseUrl,
|
|
550
|
+
}: {
|
|
551
|
+
apiBaseUrl?: string;
|
|
552
|
+
}) {
|
|
553
|
+
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
554
|
+
const [email, setEmail] = useState("");
|
|
555
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
556
|
+
const [error, setError] = useState<string | null>(null);
|
|
557
|
+
const [pending, setPending] = useState(false);
|
|
558
|
+
|
|
559
|
+
const onSubmit = useCallback(
|
|
560
|
+
async (event: React.FormEvent) => {
|
|
561
|
+
event.preventDefault();
|
|
562
|
+
if (!baseUrl) return;
|
|
563
|
+
setPending(true);
|
|
564
|
+
setError(null);
|
|
565
|
+
try {
|
|
566
|
+
const response = await postDevPortalAuth<{ message: string }>(
|
|
567
|
+
baseUrl,
|
|
568
|
+
"/api/v1/auth/forgot-password",
|
|
569
|
+
{ email },
|
|
570
|
+
);
|
|
571
|
+
setMessage(response.message);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
setError(err instanceof Error ? err.message : "Request failed.");
|
|
574
|
+
} finally {
|
|
575
|
+
setPending(false);
|
|
576
|
+
}
|
|
577
|
+
},
|
|
578
|
+
[baseUrl, email],
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
return (
|
|
582
|
+
<AuthPageShell
|
|
583
|
+
title="Reset password"
|
|
584
|
+
description="We'll email you a reset link."
|
|
585
|
+
>
|
|
586
|
+
<form className="space-y-4" onSubmit={onSubmit}>
|
|
587
|
+
<div className="space-y-2">
|
|
588
|
+
<Label htmlFor="email">Email</Label>
|
|
589
|
+
<Input
|
|
590
|
+
id="email"
|
|
591
|
+
type="email"
|
|
592
|
+
value={email}
|
|
593
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
594
|
+
required
|
|
595
|
+
/>
|
|
596
|
+
</div>
|
|
597
|
+
<AuthError message={error} />
|
|
598
|
+
{message ? (
|
|
599
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
600
|
+
) : null}
|
|
601
|
+
<Button type="submit" className="w-full" disabled={pending}>
|
|
602
|
+
{pending ? <Spinner /> : "Send reset link"}
|
|
603
|
+
</Button>
|
|
604
|
+
</form>
|
|
605
|
+
</AuthPageShell>
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function DevPortalResetPasswordPage({
|
|
610
|
+
apiBaseUrl,
|
|
611
|
+
}: {
|
|
612
|
+
apiBaseUrl?: string;
|
|
613
|
+
}) {
|
|
614
|
+
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
615
|
+
const [search] = useSearchParams();
|
|
616
|
+
const [password, setPassword] = useState("");
|
|
617
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
618
|
+
const [error, setError] = useState<string | null>(null);
|
|
619
|
+
const [pending, setPending] = useState(false);
|
|
620
|
+
|
|
621
|
+
const onSubmit = useCallback(
|
|
622
|
+
async (event: React.FormEvent) => {
|
|
623
|
+
event.preventDefault();
|
|
624
|
+
const token = search.get("token");
|
|
625
|
+
if (!baseUrl || !token) return;
|
|
626
|
+
setPending(true);
|
|
627
|
+
setError(null);
|
|
628
|
+
try {
|
|
629
|
+
const response = await postDevPortalAuth<{ message: string }>(
|
|
630
|
+
baseUrl,
|
|
631
|
+
"/api/v1/auth/reset-password",
|
|
632
|
+
{ token, newPassword: password },
|
|
633
|
+
);
|
|
634
|
+
setMessage(response.message);
|
|
635
|
+
} catch (err) {
|
|
636
|
+
setError(err instanceof Error ? err.message : "Reset failed.");
|
|
637
|
+
} finally {
|
|
638
|
+
setPending(false);
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
[baseUrl, password, search],
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<AuthPageShell title="Choose a new password">
|
|
646
|
+
<form className="space-y-4" onSubmit={onSubmit}>
|
|
647
|
+
<div className="space-y-2">
|
|
648
|
+
<Label htmlFor="password">New password</Label>
|
|
649
|
+
<Input
|
|
650
|
+
id="password"
|
|
651
|
+
type="password"
|
|
652
|
+
value={password}
|
|
653
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
654
|
+
required
|
|
655
|
+
/>
|
|
656
|
+
</div>
|
|
657
|
+
<AuthError message={error} />
|
|
658
|
+
{message ? (
|
|
659
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
660
|
+
) : null}
|
|
661
|
+
<Button type="submit" className="w-full" disabled={pending}>
|
|
662
|
+
{pending ? <Spinner /> : "Update password"}
|
|
663
|
+
</Button>
|
|
664
|
+
</form>
|
|
665
|
+
</AuthPageShell>
|
|
666
|
+
);
|
|
667
|
+
}
|