@lukoweb/apitogo 0.1.54 → 0.1.55
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 +604 -376
- package/dist/declarations/config/landing-manifest.d.ts +76 -0
- package/dist/declarations/config/local-manifest.d.ts +76 -0
- package/dist/declarations/config/validators/ModulesSchema.d.ts +227 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +50 -0
- package/dist/declarations/lib/authentication/header-nav-filter.d.ts +4 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +5 -5
- package/dist/declarations/lib/components/HeaderAuthActions.d.ts +3 -0
- package/dist/declarations/lib/components/SiteTitle.d.ts +6 -0
- package/dist/declarations/lib/components/ThemeSwitch.d.ts +3 -1
- package/dist/declarations/lib/components/TopNavigation.d.ts +1 -1
- package/dist/declarations/lib/core/ZudokuContext.d.ts +1 -0
- package/dist/flat-config.d.ts +44 -0
- package/package.json +1 -1
- package/src/app/main.css +2 -2
- package/src/config/apitogo-config-core.ts +13 -0
- package/src/config/apitogo-config-loader.browser.ts +17 -0
- package/src/config/build-apitogo-config.ts +13 -4
- package/src/config/landing-manifest.ts +11 -0
- package/src/config/landing-openapi-showcase.ts +115 -0
- package/src/config/loader.ts +17 -3
- package/src/config/local-manifest.ts +114 -10
- package/src/config/resolve-modules.ts +22 -1
- package/src/config/validators/ModulesSchema.ts +84 -0
- package/src/config/validators/ZudokuConfig.ts +1 -0
- package/src/lib/authentication/auth-header-nav.ts +5 -15
- package/src/lib/authentication/header-nav-filter.ts +36 -0
- package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +95 -35
- package/src/lib/authentication/providers/dev-portal-utils.ts +17 -7
- package/src/lib/authentication/providers/dev-portal.tsx +18 -8
- package/src/lib/components/Header.tsx +49 -39
- package/src/lib/components/HeaderAuthActions.tsx +36 -0
- package/src/lib/components/HeaderNavigation.tsx +11 -9
- package/src/lib/components/MobileTopNavigation.tsx +43 -24
- package/src/lib/components/SiteTitle.tsx +20 -0
- package/src/lib/components/ThemeSwitch.tsx +36 -2
- package/src/lib/components/TopNavigation.tsx +20 -33
- package/src/lib/core/ZudokuContext.ts +1 -0
- package/src/vite/config.ts +21 -0
- package/src/vite/plugin-auth.ts +4 -1
- package/src/vite/plugin-config.ts +39 -4
|
@@ -27,22 +27,12 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
|
|
|
27
27
|
const authNavEnabled = config.__meta?.authenticationEnabled === true;
|
|
28
28
|
const navigation = config.header?.navigation ?? [];
|
|
29
29
|
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
if (filtered.length === navigation.length) {
|
|
33
|
-
return config;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
...config,
|
|
38
|
-
header: {
|
|
39
|
-
...config.header,
|
|
40
|
-
navigation: filtered,
|
|
41
|
-
},
|
|
42
|
-
};
|
|
30
|
+
if (authNavEnabled) {
|
|
31
|
+
return config;
|
|
43
32
|
}
|
|
44
33
|
|
|
45
|
-
|
|
34
|
+
const filtered = withoutLoginNavigation(navigation);
|
|
35
|
+
if (filtered.length === navigation.length) {
|
|
46
36
|
return config;
|
|
47
37
|
}
|
|
48
38
|
|
|
@@ -50,7 +40,7 @@ export function applyAuthenticationHeaderNav<T extends ZudokuConfig>(
|
|
|
50
40
|
...config,
|
|
51
41
|
header: {
|
|
52
42
|
...config.header,
|
|
53
|
-
navigation:
|
|
43
|
+
navigation: filtered,
|
|
54
44
|
},
|
|
55
45
|
};
|
|
56
46
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { HeaderNavigation } from "../../config/validators/HeaderNavigationSchema.js";
|
|
2
|
+
import { LOGIN_NAV_PATH } from "./auth-header-nav.js";
|
|
3
|
+
|
|
4
|
+
const AUTH_HEADER_PATHS = new Set([
|
|
5
|
+
LOGIN_NAV_PATH,
|
|
6
|
+
"/signin",
|
|
7
|
+
"/login",
|
|
8
|
+
"/signup",
|
|
9
|
+
"/register",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const PRICING_HEADER_PATHS = new Set(["/pricing"]);
|
|
13
|
+
|
|
14
|
+
export function withoutAuthHeaderNavigation(
|
|
15
|
+
navigation: HeaderNavigation,
|
|
16
|
+
): HeaderNavigation {
|
|
17
|
+
return navigation.filter(
|
|
18
|
+
(item) => !("to" in item) || !AUTH_HEADER_PATHS.has(item.to),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function withoutPricingHeaderNavigation(
|
|
23
|
+
navigation: HeaderNavigation,
|
|
24
|
+
): HeaderNavigation {
|
|
25
|
+
return navigation.filter(
|
|
26
|
+
(item) => !("to" in item) || !PRICING_HEADER_PATHS.has(item.to),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function withoutMarketingCtaHeaderNavigation(
|
|
31
|
+
navigation: HeaderNavigation,
|
|
32
|
+
): HeaderNavigation {
|
|
33
|
+
return withoutPricingHeaderNavigation(
|
|
34
|
+
withoutAuthHeaderNavigation(navigation),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -33,7 +33,9 @@ function AuthPageShell({ title, description, children }: AuthPageShellProps) {
|
|
|
33
33
|
<Card className="max-w-md w-full">
|
|
34
34
|
<CardHeader>
|
|
35
35
|
<CardTitle className="text-lg">{title}</CardTitle>
|
|
36
|
-
{description ?
|
|
36
|
+
{description ? (
|
|
37
|
+
<CardDescription>{description}</CardDescription>
|
|
38
|
+
) : null}
|
|
37
39
|
</CardHeader>
|
|
38
40
|
<CardContent className="space-y-4">{children}</CardContent>
|
|
39
41
|
</Card>
|
|
@@ -71,7 +73,9 @@ function OidcProviderButtons({
|
|
|
71
73
|
return (
|
|
72
74
|
<div className="space-y-2">
|
|
73
75
|
{isNativeAuthEnabled(config) ? (
|
|
74
|
-
<p className="text-sm text-muted-foreground text-center">
|
|
76
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
77
|
+
or continue with
|
|
78
|
+
</p>
|
|
75
79
|
) : null}
|
|
76
80
|
{config.providers.map((provider) => (
|
|
77
81
|
<Button
|
|
@@ -81,7 +85,11 @@ function OidcProviderButtons({
|
|
|
81
85
|
className="w-full"
|
|
82
86
|
onClick={() => {
|
|
83
87
|
window.location.assign(
|
|
84
|
-
buildDevPortalOidcChallengeUrl(
|
|
88
|
+
buildDevPortalOidcChallengeUrl(
|
|
89
|
+
apiBaseUrl,
|
|
90
|
+
provider.id,
|
|
91
|
+
returnUrl,
|
|
92
|
+
),
|
|
85
93
|
);
|
|
86
94
|
}}
|
|
87
95
|
>
|
|
@@ -92,14 +100,12 @@ function OidcProviderButtons({
|
|
|
92
100
|
);
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
export function DevPortalLoginPage({
|
|
96
|
-
apiBaseUrl,
|
|
97
|
-
}: {
|
|
98
|
-
apiBaseUrl?: string;
|
|
99
|
-
}) {
|
|
103
|
+
export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
100
104
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
101
105
|
const returnUrl = useReturnUrl();
|
|
102
|
-
const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
|
|
106
|
+
const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
|
|
107
|
+
null,
|
|
108
|
+
);
|
|
103
109
|
const [configLoading, setConfigLoading] = useState(true);
|
|
104
110
|
const [configError, setConfigError] = useState<string | null>(null);
|
|
105
111
|
const [email, setEmail] = useState("");
|
|
@@ -132,7 +138,10 @@ export function DevPortalLoginPage({
|
|
|
132
138
|
setPending(true);
|
|
133
139
|
setError(null);
|
|
134
140
|
try {
|
|
135
|
-
await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
|
|
141
|
+
await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
|
|
142
|
+
email,
|
|
143
|
+
password,
|
|
144
|
+
});
|
|
136
145
|
window.location.assign(toAbsoluteReturnUrl(returnUrl));
|
|
137
146
|
} catch (err) {
|
|
138
147
|
setError(err instanceof Error ? err.message : "Login failed.");
|
|
@@ -145,8 +154,13 @@ export function DevPortalLoginPage({
|
|
|
145
154
|
|
|
146
155
|
if (!baseUrl) {
|
|
147
156
|
return (
|
|
148
|
-
<AuthPageShell
|
|
149
|
-
|
|
157
|
+
<AuthPageShell
|
|
158
|
+
title="Sign in"
|
|
159
|
+
description="Dev portal API is not configured."
|
|
160
|
+
>
|
|
161
|
+
<p className="text-sm text-muted-foreground">
|
|
162
|
+
Set authentication.apiBaseUrl to continue.
|
|
163
|
+
</p>
|
|
150
164
|
</AuthPageShell>
|
|
151
165
|
);
|
|
152
166
|
}
|
|
@@ -182,10 +196,16 @@ export function DevPortalLoginPage({
|
|
|
182
196
|
{pending ? <Spinner /> : "Sign in"}
|
|
183
197
|
</Button>
|
|
184
198
|
<div className="flex justify-between text-sm">
|
|
185
|
-
<Link
|
|
199
|
+
<Link
|
|
200
|
+
className="text-primary hover:underline"
|
|
201
|
+
to="/forgot-password"
|
|
202
|
+
>
|
|
186
203
|
Forgot password?
|
|
187
204
|
</Link>
|
|
188
|
-
<Link
|
|
205
|
+
<Link
|
|
206
|
+
className="text-primary hover:underline"
|
|
207
|
+
to={`/register?redirect=${encodeURIComponent(returnUrl)}`}
|
|
208
|
+
>
|
|
189
209
|
Create account
|
|
190
210
|
</Link>
|
|
191
211
|
</div>
|
|
@@ -198,7 +218,11 @@ export function DevPortalLoginPage({
|
|
|
198
218
|
</div>
|
|
199
219
|
) : config ? (
|
|
200
220
|
<>
|
|
201
|
-
<OidcProviderButtons
|
|
221
|
+
<OidcProviderButtons
|
|
222
|
+
apiBaseUrl={baseUrl}
|
|
223
|
+
config={config}
|
|
224
|
+
returnUrl={returnUrl}
|
|
225
|
+
/>
|
|
202
226
|
{!isNativeAuthEnabled(config) && !config.providers.length ? (
|
|
203
227
|
<p className="text-sm text-muted-foreground text-center">
|
|
204
228
|
No sign-in methods are configured.
|
|
@@ -210,11 +234,7 @@ export function DevPortalLoginPage({
|
|
|
210
234
|
);
|
|
211
235
|
}
|
|
212
236
|
|
|
213
|
-
export function DevPortalRegisterPage({
|
|
214
|
-
apiBaseUrl,
|
|
215
|
-
}: {
|
|
216
|
-
apiBaseUrl?: string;
|
|
217
|
-
}) {
|
|
237
|
+
export function DevPortalRegisterPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
218
238
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
219
239
|
const returnUrl = useReturnUrl();
|
|
220
240
|
const [email, setEmail] = useState("");
|
|
@@ -249,14 +269,22 @@ export function DevPortalRegisterPage({
|
|
|
249
269
|
|
|
250
270
|
if (!baseUrl) {
|
|
251
271
|
return (
|
|
252
|
-
<AuthPageShell
|
|
253
|
-
|
|
272
|
+
<AuthPageShell
|
|
273
|
+
title="Create account"
|
|
274
|
+
description="Dev portal API is not configured."
|
|
275
|
+
>
|
|
276
|
+
<p className="text-sm text-muted-foreground">
|
|
277
|
+
Set authentication.apiBaseUrl to continue.
|
|
278
|
+
</p>
|
|
254
279
|
</AuthPageShell>
|
|
255
280
|
);
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
return (
|
|
259
|
-
<AuthPageShell
|
|
284
|
+
<AuthPageShell
|
|
285
|
+
title="Create account"
|
|
286
|
+
description="Register for API access."
|
|
287
|
+
>
|
|
260
288
|
<form className="space-y-4" onSubmit={onSubmit}>
|
|
261
289
|
<div className="space-y-2">
|
|
262
290
|
<Label htmlFor="displayName">Name</Label>
|
|
@@ -289,12 +317,17 @@ export function DevPortalRegisterPage({
|
|
|
289
317
|
/>
|
|
290
318
|
</div>
|
|
291
319
|
<AuthError message={error} />
|
|
292
|
-
{message ?
|
|
320
|
+
{message ? (
|
|
321
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
322
|
+
) : null}
|
|
293
323
|
<Button type="submit" className="w-full" disabled={pending}>
|
|
294
324
|
{pending ? <Spinner /> : "Create account"}
|
|
295
325
|
</Button>
|
|
296
326
|
<p className="text-sm text-center">
|
|
297
|
-
<Link
|
|
327
|
+
<Link
|
|
328
|
+
className="text-primary hover:underline"
|
|
329
|
+
to={`/login?redirect=${encodeURIComponent(returnUrl)}`}
|
|
330
|
+
>
|
|
298
331
|
Back to sign in
|
|
299
332
|
</Link>
|
|
300
333
|
</p>
|
|
@@ -303,7 +336,11 @@ export function DevPortalRegisterPage({
|
|
|
303
336
|
);
|
|
304
337
|
}
|
|
305
338
|
|
|
306
|
-
export function DevPortalVerifyEmailPage({
|
|
339
|
+
export function DevPortalVerifyEmailPage({
|
|
340
|
+
apiBaseUrl,
|
|
341
|
+
}: {
|
|
342
|
+
apiBaseUrl?: string;
|
|
343
|
+
}) {
|
|
307
344
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
308
345
|
const [search] = useSearchParams();
|
|
309
346
|
const [message, setMessage] = useState<string | null>(null);
|
|
@@ -312,9 +349,13 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
|
|
|
312
349
|
useEffect(() => {
|
|
313
350
|
const token = search.get("token");
|
|
314
351
|
if (!baseUrl || !token) return;
|
|
315
|
-
void postDevPortalAuth<{ message: string }>(
|
|
316
|
-
|
|
317
|
-
|
|
352
|
+
void postDevPortalAuth<{ message: string }>(
|
|
353
|
+
baseUrl,
|
|
354
|
+
"/api/v1/auth/verify-email",
|
|
355
|
+
{
|
|
356
|
+
token,
|
|
357
|
+
},
|
|
358
|
+
)
|
|
318
359
|
.then((response) => setMessage(response.message))
|
|
319
360
|
.catch((err) =>
|
|
320
361
|
setError(err instanceof Error ? err.message : "Verification failed."),
|
|
@@ -324,7 +365,11 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
|
|
|
324
365
|
return (
|
|
325
366
|
<AuthPageShell title="Verify email">
|
|
326
367
|
<AuthError message={error} />
|
|
327
|
-
{message ?
|
|
368
|
+
{message ? (
|
|
369
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
370
|
+
) : (
|
|
371
|
+
<Spinner />
|
|
372
|
+
)}
|
|
328
373
|
<Link className="text-sm text-primary hover:underline" to="/login">
|
|
329
374
|
Sign in
|
|
330
375
|
</Link>
|
|
@@ -332,7 +377,11 @@ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }
|
|
|
332
377
|
);
|
|
333
378
|
}
|
|
334
379
|
|
|
335
|
-
export function DevPortalForgotPasswordPage({
|
|
380
|
+
export function DevPortalForgotPasswordPage({
|
|
381
|
+
apiBaseUrl,
|
|
382
|
+
}: {
|
|
383
|
+
apiBaseUrl?: string;
|
|
384
|
+
}) {
|
|
336
385
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
337
386
|
const [email, setEmail] = useState("");
|
|
338
387
|
const [message, setMessage] = useState<string | null>(null);
|
|
@@ -362,7 +411,10 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
|
|
|
362
411
|
);
|
|
363
412
|
|
|
364
413
|
return (
|
|
365
|
-
<AuthPageShell
|
|
414
|
+
<AuthPageShell
|
|
415
|
+
title="Reset password"
|
|
416
|
+
description="We'll email you a reset link."
|
|
417
|
+
>
|
|
366
418
|
<form className="space-y-4" onSubmit={onSubmit}>
|
|
367
419
|
<div className="space-y-2">
|
|
368
420
|
<Label htmlFor="email">Email</Label>
|
|
@@ -375,7 +427,9 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
|
|
|
375
427
|
/>
|
|
376
428
|
</div>
|
|
377
429
|
<AuthError message={error} />
|
|
378
|
-
{message ?
|
|
430
|
+
{message ? (
|
|
431
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
432
|
+
) : null}
|
|
379
433
|
<Button type="submit" className="w-full" disabled={pending}>
|
|
380
434
|
{pending ? <Spinner /> : "Send reset link"}
|
|
381
435
|
</Button>
|
|
@@ -384,7 +438,11 @@ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: strin
|
|
|
384
438
|
);
|
|
385
439
|
}
|
|
386
440
|
|
|
387
|
-
export function DevPortalResetPasswordPage({
|
|
441
|
+
export function DevPortalResetPasswordPage({
|
|
442
|
+
apiBaseUrl,
|
|
443
|
+
}: {
|
|
444
|
+
apiBaseUrl?: string;
|
|
445
|
+
}) {
|
|
388
446
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
389
447
|
const [search] = useSearchParams();
|
|
390
448
|
const [password, setPassword] = useState("");
|
|
@@ -429,7 +487,9 @@ export function DevPortalResetPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string
|
|
|
429
487
|
/>
|
|
430
488
|
</div>
|
|
431
489
|
<AuthError message={error} />
|
|
432
|
-
{message ?
|
|
490
|
+
{message ? (
|
|
491
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
492
|
+
) : null}
|
|
433
493
|
<Button type="submit" className="w-full" disabled={pending}>
|
|
434
494
|
{pending ? <Spinner /> : "Update password"}
|
|
435
495
|
</Button>
|
|
@@ -58,7 +58,10 @@ export function buildDevPortalOidcChallengeUrl(
|
|
|
58
58
|
providerId: string,
|
|
59
59
|
returnUrl: string,
|
|
60
60
|
): string {
|
|
61
|
-
const url = new URL(
|
|
61
|
+
const url = new URL(
|
|
62
|
+
`/api/v1/auth/oidc/${encodeURIComponent(providerId)}/challenge`,
|
|
63
|
+
apiBaseUrl,
|
|
64
|
+
);
|
|
62
65
|
url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
|
|
63
66
|
return url.toString();
|
|
64
67
|
}
|
|
@@ -122,15 +125,19 @@ export function normalizeDevPortalAuthConfig(
|
|
|
122
125
|
}
|
|
123
126
|
|
|
124
127
|
export function isNativeAuthEnabled(
|
|
125
|
-
config:
|
|
128
|
+
config:
|
|
129
|
+
| import("./dev-portal-constants.js").DevPortalAuthConfigResponse
|
|
130
|
+
| null
|
|
131
|
+
| undefined,
|
|
126
132
|
): boolean {
|
|
127
133
|
if (!config) {
|
|
128
134
|
return false;
|
|
129
135
|
}
|
|
130
136
|
|
|
131
|
-
const raw =
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
const raw =
|
|
138
|
+
config as import("./dev-portal-constants.js").DevPortalAuthConfigResponse & {
|
|
139
|
+
nativeEnabled?: boolean;
|
|
140
|
+
};
|
|
134
141
|
|
|
135
142
|
return raw.native?.enabled === true || raw.nativeEnabled === true;
|
|
136
143
|
}
|
|
@@ -175,7 +182,10 @@ export async function postDevPortalAuth<T>(
|
|
|
175
182
|
if (!response.ok) {
|
|
176
183
|
let detail = "Request failed.";
|
|
177
184
|
try {
|
|
178
|
-
const problem = (await response.json()) as {
|
|
185
|
+
const problem = (await response.json()) as {
|
|
186
|
+
detail?: string;
|
|
187
|
+
title?: string;
|
|
188
|
+
};
|
|
179
189
|
detail = problem.detail ?? problem.title ?? detail;
|
|
180
190
|
} catch {
|
|
181
191
|
// ignore parse errors
|
|
@@ -252,7 +262,7 @@ export function mapEndUserMeToProfile(
|
|
|
252
262
|
raw.emailVerified,
|
|
253
263
|
raw.EmailVerified,
|
|
254
264
|
raw.email_verified,
|
|
255
|
-
) ??
|
|
265
|
+
) ?? email != null;
|
|
256
266
|
|
|
257
267
|
return {
|
|
258
268
|
sub: me.userId,
|
|
@@ -12,10 +12,6 @@ import {
|
|
|
12
12
|
useAuthState,
|
|
13
13
|
waitForAuthStateHydration,
|
|
14
14
|
} from "../state.js";
|
|
15
|
-
import type {
|
|
16
|
-
DevPortalAuthMode,
|
|
17
|
-
EndUserMeResponse,
|
|
18
|
-
} from "./dev-portal-constants.js";
|
|
19
15
|
import {
|
|
20
16
|
DevPortalForgotPasswordPage,
|
|
21
17
|
DevPortalLoginPage,
|
|
@@ -23,6 +19,10 @@ import {
|
|
|
23
19
|
DevPortalResetPasswordPage,
|
|
24
20
|
DevPortalVerifyEmailPage,
|
|
25
21
|
} from "./dev-portal-auth-pages.js";
|
|
22
|
+
import type {
|
|
23
|
+
DevPortalAuthMode,
|
|
24
|
+
EndUserMeResponse,
|
|
25
|
+
} from "./dev-portal-constants.js";
|
|
26
26
|
import {
|
|
27
27
|
buildDevPortalLogoutUrl,
|
|
28
28
|
isNativeAuthEnabled,
|
|
@@ -78,9 +78,18 @@ export class DevPortalAuthenticationProvider
|
|
|
78
78
|
...super.getRoutes(),
|
|
79
79
|
{ path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
|
|
80
80
|
{ path: "/register", element: <DevPortalRegisterPage {...pageProps} /> },
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
{
|
|
82
|
+
path: "/verify-email",
|
|
83
|
+
element: <DevPortalVerifyEmailPage {...pageProps} />,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: "/forgot-password",
|
|
87
|
+
element: <DevPortalForgotPasswordPage {...pageProps} />,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
path: "/reset-password",
|
|
91
|
+
element: <DevPortalResetPasswordPage {...pageProps} />,
|
|
92
|
+
},
|
|
84
93
|
];
|
|
85
94
|
}
|
|
86
95
|
|
|
@@ -94,7 +103,8 @@ export class DevPortalAuthenticationProvider
|
|
|
94
103
|
await this.syncBackendAvailability();
|
|
95
104
|
if (this.backendAvailable && this.apiBaseUrl) {
|
|
96
105
|
try {
|
|
97
|
-
const { fetchDevPortalAuthConfig } =
|
|
106
|
+
const { fetchDevPortalAuthConfig } =
|
|
107
|
+
await import("./dev-portal-utils.js");
|
|
98
108
|
const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
|
|
99
109
|
this.authConfigNativeEnabled = isNativeAuthEnabled(config);
|
|
100
110
|
} catch {
|
|
@@ -20,9 +20,11 @@ import { cn } from "../util/cn.js";
|
|
|
20
20
|
import { joinUrl } from "../util/joinUrl.js";
|
|
21
21
|
import { Banner } from "./Banner.js";
|
|
22
22
|
import { useZudoku } from "./context/ZudokuContext.js";
|
|
23
|
+
import { HeaderAuthActions } from "./HeaderAuthActions.js";
|
|
23
24
|
import { MobileTopNavigation } from "./MobileTopNavigation.js";
|
|
24
25
|
import { PageProgress } from "./PageProgress.js";
|
|
25
26
|
import { Search } from "./Search.js";
|
|
27
|
+
import { SiteTitle } from "./SiteTitle.js";
|
|
26
28
|
import { Slot } from "./Slot.js";
|
|
27
29
|
import { ThemeSwitch } from "./ThemeSwitch.js";
|
|
28
30
|
import { TopNavigation } from "./TopNavigation.js";
|
|
@@ -78,8 +80,17 @@ const ProfileMenu = () => {
|
|
|
78
80
|
return (
|
|
79
81
|
<DropdownMenu modal={false}>
|
|
80
82
|
<DropdownMenuTrigger asChild>
|
|
81
|
-
<Button
|
|
82
|
-
|
|
83
|
+
<Button
|
|
84
|
+
size="lg"
|
|
85
|
+
variant="outline"
|
|
86
|
+
className="gap-2 rounded-[10px] border-border bg-muted/40 px-2.5"
|
|
87
|
+
>
|
|
88
|
+
<span className="flex size-7 items-center justify-center rounded-[7px] bg-primary text-xs font-bold text-primary-foreground">
|
|
89
|
+
{(profile?.name ?? "A").slice(0, 1).toUpperCase()}
|
|
90
|
+
</span>
|
|
91
|
+
<span className="max-w-32 truncate font-semibold">
|
|
92
|
+
{profile?.name ?? "My Account"}
|
|
93
|
+
</span>
|
|
83
94
|
</Button>
|
|
84
95
|
</DropdownMenuTrigger>
|
|
85
96
|
<DropdownMenuContent className="w-56">
|
|
@@ -116,24 +127,25 @@ const ProfileMenu = () => {
|
|
|
116
127
|
))}
|
|
117
128
|
<DropdownMenuSeparator />
|
|
118
129
|
<Link to="/signout">
|
|
119
|
-
<DropdownMenuItem className="flex gap-2">
|
|
130
|
+
<DropdownMenuItem className="flex gap-2 text-primary">
|
|
120
131
|
<LogOutIcon size={16} strokeWidth={1} absoluteStrokeWidth />
|
|
121
|
-
|
|
132
|
+
Log out
|
|
122
133
|
</DropdownMenuItem>
|
|
123
134
|
</Link>
|
|
124
135
|
</DropdownMenuContent>
|
|
125
136
|
</DropdownMenu>
|
|
126
137
|
);
|
|
127
138
|
};
|
|
139
|
+
|
|
128
140
|
export const Header = memo(function HeaderInner() {
|
|
129
141
|
const context = useZudoku();
|
|
130
142
|
const {
|
|
131
143
|
options: { site, header, basePath },
|
|
132
144
|
} = context;
|
|
133
145
|
|
|
134
|
-
const searchPosition = header?.placements?.search ?? "
|
|
135
|
-
const navPosition = header?.placements?.navigation ?? "
|
|
136
|
-
const authPlacement = header?.placements?.auth ?? "
|
|
146
|
+
const searchPosition = header?.placements?.search ?? "end";
|
|
147
|
+
const navPosition = header?.placements?.navigation ?? "start";
|
|
148
|
+
const authPlacement = header?.placements?.auth ?? "end";
|
|
137
149
|
const authPosition =
|
|
138
150
|
authPlacement === "navigation" ? navPosition : authPlacement;
|
|
139
151
|
|
|
@@ -148,18 +160,18 @@ export const Header = memo(function HeaderInner() {
|
|
|
148
160
|
: joinUrl(basePath, site.logo.src.dark)
|
|
149
161
|
: undefined;
|
|
150
162
|
|
|
151
|
-
const borderBottom = "
|
|
163
|
+
const borderBottom = "border-b border-border";
|
|
152
164
|
|
|
153
165
|
return (
|
|
154
166
|
<header
|
|
155
|
-
className="sticky lg:top-0 z-10 bg-background/
|
|
167
|
+
className="sticky lg:top-0 z-10 w-full bg-background/82 backdrop-blur-[14px]"
|
|
156
168
|
data-pagefind-ignore="all"
|
|
157
169
|
>
|
|
158
170
|
<Banner />
|
|
159
171
|
<div className={cn(borderBottom, "relative")}>
|
|
160
172
|
<PageProgress />
|
|
161
|
-
<div className="
|
|
162
|
-
<div className="flex
|
|
173
|
+
<div className="mx-auto flex h-(--top-header-height) max-w-[1200px] items-center justify-between gap-6 px-6">
|
|
174
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
163
175
|
<Link
|
|
164
176
|
to={site?.logo?.href ?? "/"}
|
|
165
177
|
reloadDocument={site?.logo?.reloadDocument ?? true}
|
|
@@ -182,69 +194,67 @@ export const Header = memo(function HeaderInner() {
|
|
|
182
194
|
className="max-h-(--top-header-height) hidden dark:block"
|
|
183
195
|
loading="lazy"
|
|
184
196
|
/>
|
|
197
|
+
{site.title && site.showTitleInHeader !== false ? (
|
|
198
|
+
<SiteTitle title={site.title} />
|
|
199
|
+
) : null}
|
|
185
200
|
</>
|
|
186
201
|
) : (
|
|
187
|
-
<span className="font-
|
|
202
|
+
<span className="text-xl font-bold tracking-tight">
|
|
203
|
+
{site?.title}
|
|
204
|
+
</span>
|
|
188
205
|
)}
|
|
189
206
|
</div>
|
|
190
207
|
</Link>
|
|
191
208
|
<Slot.Target name="head-navigation-start" />
|
|
209
|
+
<div className="hidden lg:flex min-w-0 items-center ml-2">
|
|
210
|
+
<Slot.Target name="top-navigation-before" />
|
|
211
|
+
<TopNavigation />
|
|
212
|
+
<Slot.Target name="top-navigation-after" />
|
|
213
|
+
</div>
|
|
192
214
|
{searchPosition === "start" && (
|
|
193
215
|
<Search className="hidden lg:flex" />
|
|
194
216
|
)}
|
|
195
|
-
{authPosition === "start" && (
|
|
196
|
-
<div className="hidden lg:flex">
|
|
197
|
-
<ProfileMenu />
|
|
198
|
-
</div>
|
|
199
|
-
)}
|
|
200
217
|
{navPosition === "start" && (
|
|
201
|
-
<div className="hidden lg:block min-w-0">
|
|
218
|
+
<div className="hidden lg:block min-w-0 ml-2">
|
|
202
219
|
<SuspendedHeaderNavigation />
|
|
203
220
|
</div>
|
|
204
221
|
)}
|
|
205
222
|
</div>
|
|
206
223
|
|
|
207
|
-
<div className="flex items-center justify-center">
|
|
224
|
+
<div className="hidden lg:flex items-center justify-center">
|
|
208
225
|
<Search
|
|
209
|
-
className={cn(
|
|
210
|
-
searchPosition === "center" ? "flex" : "flex lg:hidden",
|
|
211
|
-
)}
|
|
226
|
+
className={cn(searchPosition === "center" ? "flex" : "hidden")}
|
|
212
227
|
/>
|
|
213
228
|
{navPosition === "center" && (
|
|
214
|
-
<div className="
|
|
229
|
+
<div className="min-w-0">
|
|
215
230
|
<SuspendedHeaderNavigation />
|
|
216
231
|
</div>
|
|
217
232
|
)}
|
|
218
|
-
{authPosition === "center" && (
|
|
219
|
-
<div className="hidden lg:flex">
|
|
220
|
-
<ProfileMenu />
|
|
221
|
-
</div>
|
|
222
|
-
)}
|
|
223
233
|
</div>
|
|
224
234
|
|
|
225
|
-
<div className="flex items-center gap-2 justify-self-end">
|
|
235
|
+
<div className="flex items-center gap-2.5 justify-self-end">
|
|
226
236
|
<MobileTopNavigation />
|
|
227
|
-
<div className="hidden lg:flex items-center text-sm
|
|
237
|
+
<div className="hidden lg:flex items-center gap-2.5 text-sm">
|
|
228
238
|
{navPosition === "end" && (
|
|
229
239
|
<div className="min-w-0">
|
|
230
240
|
<SuspendedHeaderNavigation />
|
|
231
241
|
</div>
|
|
232
242
|
)}
|
|
233
|
-
{authPosition === "end" && <ProfileMenu />}
|
|
234
243
|
{searchPosition === "end" && <Search />}
|
|
235
244
|
<Slot.Target name="head-navigation-end" />
|
|
236
|
-
<ThemeSwitch />
|
|
245
|
+
<ThemeSwitch variant="compact" />
|
|
246
|
+
{authPosition === "end" ? (
|
|
247
|
+
<>
|
|
248
|
+
<HeaderAuthActions />
|
|
249
|
+
<ProfileMenu />
|
|
250
|
+
</>
|
|
251
|
+
) : (
|
|
252
|
+
<ProfileMenu />
|
|
253
|
+
)}
|
|
237
254
|
</div>
|
|
238
255
|
</div>
|
|
239
256
|
</div>
|
|
240
257
|
</div>
|
|
241
|
-
<div className={cn("hidden lg:block", borderBottom)}>
|
|
242
|
-
<div className="max-w-screen-2xl mx-auto border-transparent relative">
|
|
243
|
-
<Slot.Target name="top-navigation-before" />
|
|
244
|
-
<TopNavigation />
|
|
245
|
-
<Slot.Target name="top-navigation-after" />
|
|
246
|
-
</div>
|
|
247
|
-
</div>
|
|
248
258
|
</header>
|
|
249
259
|
);
|
|
250
260
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Button } from "@lukoweb/apitogo/ui/Button.js";
|
|
2
|
+
import { ArrowRightIcon } from "lucide-react";
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
|
+
import { useAuth } from "../authentication/hook.js";
|
|
5
|
+
import { cn } from "../util/cn.js";
|
|
6
|
+
|
|
7
|
+
export const HeaderAuthActions = ({ className }: { className?: string }) => {
|
|
8
|
+
const auth = useAuth();
|
|
9
|
+
const location = useLocation();
|
|
10
|
+
const { isAuthEnabled, isAuthenticated } = auth;
|
|
11
|
+
|
|
12
|
+
if (!isAuthEnabled || isAuthenticated) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className={cn("hidden lg:flex items-center gap-2.5", className)}>
|
|
18
|
+
<Button
|
|
19
|
+
variant="ghost"
|
|
20
|
+
size="lg"
|
|
21
|
+
className="font-semibold text-foreground hover:text-primary px-3"
|
|
22
|
+
onClick={() => void auth.login({ redirectTo: location.pathname })}
|
|
23
|
+
>
|
|
24
|
+
Sign in
|
|
25
|
+
</Button>
|
|
26
|
+
<Button
|
|
27
|
+
size="lg"
|
|
28
|
+
className="gap-1.5 rounded-[9px] px-4 font-semibold shadow-[0_4px_14px_-4px] shadow-primary/30"
|
|
29
|
+
onClick={() => void auth.signup({ redirectTo: location.pathname })}
|
|
30
|
+
>
|
|
31
|
+
Start free
|
|
32
|
+
<ArrowRightIcon size={15} strokeWidth={2} />
|
|
33
|
+
</Button>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
};
|