@lukoweb/apitogo 0.1.53 → 0.1.54

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.
@@ -0,0 +1,439 @@
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 } from "react";
13
+ import { Link, useSearchParams } from "react-router";
14
+ import type { DevPortalAuthConfigResponse } from "./dev-portal-constants.js";
15
+ import {
16
+ buildDevPortalOidcChallengeUrl,
17
+ fetchDevPortalAuthConfig,
18
+ isNativeAuthEnabled,
19
+ postDevPortalAuth,
20
+ resolveDevPortalApiBaseUrl,
21
+ toAbsoluteReturnUrl,
22
+ } from "./dev-portal-utils.js";
23
+
24
+ type AuthPageShellProps = {
25
+ title: string;
26
+ description?: string;
27
+ children: React.ReactNode;
28
+ };
29
+
30
+ function AuthPageShell({ title, description, children }: AuthPageShellProps) {
31
+ return (
32
+ <div className="flex items-center justify-center mt-8 px-4">
33
+ <Card className="max-w-md w-full">
34
+ <CardHeader>
35
+ <CardTitle className="text-lg">{title}</CardTitle>
36
+ {description ? <CardDescription>{description}</CardDescription> : null}
37
+ </CardHeader>
38
+ <CardContent className="space-y-4">{children}</CardContent>
39
+ </Card>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ function useReturnUrl(): string {
45
+ const [search] = useSearchParams();
46
+ return search.get("redirect") ?? search.get("returnUrl") ?? "/";
47
+ }
48
+
49
+ function useApiBaseUrl(apiBaseUrl?: string): string | undefined {
50
+ return resolveDevPortalApiBaseUrl({ apiBaseUrl });
51
+ }
52
+
53
+ function AuthError({ message }: { message: string | null }) {
54
+ if (!message) return null;
55
+ return <p className="text-sm text-destructive">{message}</p>;
56
+ }
57
+
58
+ function OidcProviderButtons({
59
+ apiBaseUrl,
60
+ config,
61
+ returnUrl,
62
+ }: {
63
+ apiBaseUrl: string;
64
+ config: DevPortalAuthConfigResponse;
65
+ returnUrl: string;
66
+ }) {
67
+ if (!config.providers.length) {
68
+ return null;
69
+ }
70
+
71
+ return (
72
+ <div className="space-y-2">
73
+ {isNativeAuthEnabled(config) ? (
74
+ <p className="text-sm text-muted-foreground text-center">or continue with</p>
75
+ ) : null}
76
+ {config.providers.map((provider) => (
77
+ <Button
78
+ key={provider.id}
79
+ type="button"
80
+ variant="outline"
81
+ className="w-full"
82
+ onClick={() => {
83
+ window.location.assign(
84
+ buildDevPortalOidcChallengeUrl(apiBaseUrl, provider.id, returnUrl),
85
+ );
86
+ }}
87
+ >
88
+ {provider.displayName || provider.id}
89
+ </Button>
90
+ ))}
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export function DevPortalLoginPage({
96
+ apiBaseUrl,
97
+ }: {
98
+ apiBaseUrl?: string;
99
+ }) {
100
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
101
+ const returnUrl = useReturnUrl();
102
+ const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(null);
103
+ const [configLoading, setConfigLoading] = useState(true);
104
+ const [configError, setConfigError] = useState<string | null>(null);
105
+ const [email, setEmail] = useState("");
106
+ const [password, setPassword] = useState("");
107
+ const [error, setError] = useState<string | null>(null);
108
+ const [pending, setPending] = useState(false);
109
+
110
+ useEffect(() => {
111
+ if (!baseUrl) return;
112
+ setConfigLoading(true);
113
+ setConfigError(null);
114
+ void fetchDevPortalAuthConfig(baseUrl)
115
+ .then((loaded) => {
116
+ setConfig(loaded);
117
+ setConfigLoading(false);
118
+ })
119
+ .catch(() => {
120
+ setConfig(null);
121
+ setConfigLoading(false);
122
+ setConfigError(
123
+ `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.`,
124
+ );
125
+ });
126
+ }, [baseUrl]);
127
+
128
+ const onSubmit = useCallback(
129
+ async (event: React.FormEvent) => {
130
+ event.preventDefault();
131
+ if (!baseUrl) return;
132
+ setPending(true);
133
+ setError(null);
134
+ try {
135
+ await postDevPortalAuth(baseUrl, "/api/v1/auth/login", { email, password });
136
+ window.location.assign(toAbsoluteReturnUrl(returnUrl));
137
+ } catch (err) {
138
+ setError(err instanceof Error ? err.message : "Login failed.");
139
+ } finally {
140
+ setPending(false);
141
+ }
142
+ },
143
+ [baseUrl, email, password, returnUrl],
144
+ );
145
+
146
+ if (!baseUrl) {
147
+ return (
148
+ <AuthPageShell title="Sign in" description="Dev portal API is not configured.">
149
+ <p className="text-sm text-muted-foreground">Set authentication.apiBaseUrl to continue.</p>
150
+ </AuthPageShell>
151
+ );
152
+ }
153
+
154
+ return (
155
+ <AuthPageShell title="Sign in" description="Access your developer account.">
156
+ {isNativeAuthEnabled(config) ? (
157
+ <form className="space-y-4" onSubmit={onSubmit}>
158
+ <div className="space-y-2">
159
+ <Label htmlFor="email">Email</Label>
160
+ <Input
161
+ id="email"
162
+ type="email"
163
+ autoComplete="email"
164
+ value={email}
165
+ onChange={(event) => setEmail(event.target.value)}
166
+ required
167
+ />
168
+ </div>
169
+ <div className="space-y-2">
170
+ <Label htmlFor="password">Password</Label>
171
+ <Input
172
+ id="password"
173
+ type="password"
174
+ autoComplete="current-password"
175
+ value={password}
176
+ onChange={(event) => setPassword(event.target.value)}
177
+ required
178
+ />
179
+ </div>
180
+ <AuthError message={error} />
181
+ <Button type="submit" className="w-full" disabled={pending}>
182
+ {pending ? <Spinner /> : "Sign in"}
183
+ </Button>
184
+ <div className="flex justify-between text-sm">
185
+ <Link className="text-primary hover:underline" to="/forgot-password">
186
+ Forgot password?
187
+ </Link>
188
+ <Link className="text-primary hover:underline" to={`/register?redirect=${encodeURIComponent(returnUrl)}`}>
189
+ Create account
190
+ </Link>
191
+ </div>
192
+ </form>
193
+ ) : null}
194
+ {configError ? <AuthError message={configError} /> : null}
195
+ {configLoading ? (
196
+ <div className="flex justify-center">
197
+ <Spinner />
198
+ </div>
199
+ ) : config ? (
200
+ <>
201
+ <OidcProviderButtons apiBaseUrl={baseUrl} config={config} returnUrl={returnUrl} />
202
+ {!isNativeAuthEnabled(config) && !config.providers.length ? (
203
+ <p className="text-sm text-muted-foreground text-center">
204
+ No sign-in methods are configured.
205
+ </p>
206
+ ) : null}
207
+ </>
208
+ ) : null}
209
+ </AuthPageShell>
210
+ );
211
+ }
212
+
213
+ export function DevPortalRegisterPage({
214
+ apiBaseUrl,
215
+ }: {
216
+ apiBaseUrl?: string;
217
+ }) {
218
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
219
+ const returnUrl = useReturnUrl();
220
+ const [email, setEmail] = useState("");
221
+ const [password, setPassword] = useState("");
222
+ const [displayName, setDisplayName] = useState("");
223
+ const [message, setMessage] = useState<string | null>(null);
224
+ const [error, setError] = useState<string | null>(null);
225
+ const [pending, setPending] = useState(false);
226
+
227
+ const onSubmit = useCallback(
228
+ async (event: React.FormEvent) => {
229
+ event.preventDefault();
230
+ if (!baseUrl) return;
231
+ setPending(true);
232
+ setError(null);
233
+ setMessage(null);
234
+ try {
235
+ const response = await postDevPortalAuth<{ message: string }>(
236
+ baseUrl,
237
+ "/api/v1/auth/register",
238
+ { email, password, displayName: displayName || null },
239
+ );
240
+ setMessage(response.message);
241
+ } catch (err) {
242
+ setError(err instanceof Error ? err.message : "Registration failed.");
243
+ } finally {
244
+ setPending(false);
245
+ }
246
+ },
247
+ [baseUrl, displayName, email, password],
248
+ );
249
+
250
+ if (!baseUrl) {
251
+ return (
252
+ <AuthPageShell title="Create account" description="Dev portal API is not configured.">
253
+ <p className="text-sm text-muted-foreground">Set authentication.apiBaseUrl to continue.</p>
254
+ </AuthPageShell>
255
+ );
256
+ }
257
+
258
+ return (
259
+ <AuthPageShell title="Create account" description="Register for API access.">
260
+ <form className="space-y-4" onSubmit={onSubmit}>
261
+ <div className="space-y-2">
262
+ <Label htmlFor="displayName">Name</Label>
263
+ <Input
264
+ id="displayName"
265
+ value={displayName}
266
+ onChange={(event) => setDisplayName(event.target.value)}
267
+ />
268
+ </div>
269
+ <div className="space-y-2">
270
+ <Label htmlFor="email">Email</Label>
271
+ <Input
272
+ id="email"
273
+ type="email"
274
+ autoComplete="email"
275
+ value={email}
276
+ onChange={(event) => setEmail(event.target.value)}
277
+ required
278
+ />
279
+ </div>
280
+ <div className="space-y-2">
281
+ <Label htmlFor="password">Password</Label>
282
+ <Input
283
+ id="password"
284
+ type="password"
285
+ autoComplete="new-password"
286
+ value={password}
287
+ onChange={(event) => setPassword(event.target.value)}
288
+ required
289
+ />
290
+ </div>
291
+ <AuthError message={error} />
292
+ {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
293
+ <Button type="submit" className="w-full" disabled={pending}>
294
+ {pending ? <Spinner /> : "Create account"}
295
+ </Button>
296
+ <p className="text-sm text-center">
297
+ <Link className="text-primary hover:underline" to={`/login?redirect=${encodeURIComponent(returnUrl)}`}>
298
+ Back to sign in
299
+ </Link>
300
+ </p>
301
+ </form>
302
+ </AuthPageShell>
303
+ );
304
+ }
305
+
306
+ export function DevPortalVerifyEmailPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
307
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
308
+ const [search] = useSearchParams();
309
+ const [message, setMessage] = useState<string | null>(null);
310
+ const [error, setError] = useState<string | null>(null);
311
+
312
+ useEffect(() => {
313
+ const token = search.get("token");
314
+ if (!baseUrl || !token) return;
315
+ void postDevPortalAuth<{ message: string }>(baseUrl, "/api/v1/auth/verify-email", {
316
+ token,
317
+ })
318
+ .then((response) => setMessage(response.message))
319
+ .catch((err) =>
320
+ setError(err instanceof Error ? err.message : "Verification failed."),
321
+ );
322
+ }, [baseUrl, search]);
323
+
324
+ return (
325
+ <AuthPageShell title="Verify email">
326
+ <AuthError message={error} />
327
+ {message ? <p className="text-sm text-muted-foreground">{message}</p> : <Spinner />}
328
+ <Link className="text-sm text-primary hover:underline" to="/login">
329
+ Sign in
330
+ </Link>
331
+ </AuthPageShell>
332
+ );
333
+ }
334
+
335
+ export function DevPortalForgotPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
336
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
337
+ const [email, setEmail] = useState("");
338
+ const [message, setMessage] = useState<string | null>(null);
339
+ const [error, setError] = useState<string | null>(null);
340
+ const [pending, setPending] = useState(false);
341
+
342
+ const onSubmit = useCallback(
343
+ async (event: React.FormEvent) => {
344
+ event.preventDefault();
345
+ if (!baseUrl) return;
346
+ setPending(true);
347
+ setError(null);
348
+ try {
349
+ const response = await postDevPortalAuth<{ message: string }>(
350
+ baseUrl,
351
+ "/api/v1/auth/forgot-password",
352
+ { email },
353
+ );
354
+ setMessage(response.message);
355
+ } catch (err) {
356
+ setError(err instanceof Error ? err.message : "Request failed.");
357
+ } finally {
358
+ setPending(false);
359
+ }
360
+ },
361
+ [baseUrl, email],
362
+ );
363
+
364
+ return (
365
+ <AuthPageShell title="Reset password" description="We'll email you a reset link.">
366
+ <form className="space-y-4" onSubmit={onSubmit}>
367
+ <div className="space-y-2">
368
+ <Label htmlFor="email">Email</Label>
369
+ <Input
370
+ id="email"
371
+ type="email"
372
+ value={email}
373
+ onChange={(event) => setEmail(event.target.value)}
374
+ required
375
+ />
376
+ </div>
377
+ <AuthError message={error} />
378
+ {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
379
+ <Button type="submit" className="w-full" disabled={pending}>
380
+ {pending ? <Spinner /> : "Send reset link"}
381
+ </Button>
382
+ </form>
383
+ </AuthPageShell>
384
+ );
385
+ }
386
+
387
+ export function DevPortalResetPasswordPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
388
+ const baseUrl = useApiBaseUrl(apiBaseUrl);
389
+ const [search] = useSearchParams();
390
+ const [password, setPassword] = useState("");
391
+ const [message, setMessage] = useState<string | null>(null);
392
+ const [error, setError] = useState<string | null>(null);
393
+ const [pending, setPending] = useState(false);
394
+
395
+ const onSubmit = useCallback(
396
+ async (event: React.FormEvent) => {
397
+ event.preventDefault();
398
+ const token = search.get("token");
399
+ if (!baseUrl || !token) return;
400
+ setPending(true);
401
+ setError(null);
402
+ try {
403
+ const response = await postDevPortalAuth<{ message: string }>(
404
+ baseUrl,
405
+ "/api/v1/auth/reset-password",
406
+ { token, newPassword: password },
407
+ );
408
+ setMessage(response.message);
409
+ } catch (err) {
410
+ setError(err instanceof Error ? err.message : "Reset failed.");
411
+ } finally {
412
+ setPending(false);
413
+ }
414
+ },
415
+ [baseUrl, password, search],
416
+ );
417
+
418
+ return (
419
+ <AuthPageShell title="Choose a new password">
420
+ <form className="space-y-4" onSubmit={onSubmit}>
421
+ <div className="space-y-2">
422
+ <Label htmlFor="password">New password</Label>
423
+ <Input
424
+ id="password"
425
+ type="password"
426
+ value={password}
427
+ onChange={(event) => setPassword(event.target.value)}
428
+ required
429
+ />
430
+ </div>
431
+ <AuthError message={error} />
432
+ {message ? <p className="text-sm text-muted-foreground">{message}</p> : null}
433
+ <Button type="submit" className="w-full" disabled={pending}>
434
+ {pending ? <Spinner /> : "Update password"}
435
+ </Button>
436
+ </form>
437
+ </AuthPageShell>
438
+ );
439
+ }
@@ -19,5 +19,19 @@ export type EndUserMeResponse = {
19
19
 
20
20
  export type DevPortalAuthStatusResponse = {
21
21
  available: boolean;
22
- oidcConfigured: boolean;
22
+ nativeEnabled?: boolean;
23
+ oidcConfigured?: boolean;
24
+ providerCount?: number;
25
+ };
26
+
27
+ export type DevPortalAuthConfigResponse = {
28
+ native: { enabled: boolean };
29
+ email?: { provider: string; from: string } | null;
30
+ providers: Array<{
31
+ id: string;
32
+ displayName: string;
33
+ authority: string;
34
+ clientId: string;
35
+ scopes: string[];
36
+ }>;
23
37
  };
@@ -53,11 +53,12 @@ export function toAbsoluteReturnUrl(pathOrUrl: string): string {
53
53
  return new URL(pathOrUrl, window.location.origin).href;
54
54
  }
55
55
 
56
- export function buildDevPortalLoginUrl(
56
+ export function buildDevPortalOidcChallengeUrl(
57
57
  apiBaseUrl: string,
58
+ providerId: string,
58
59
  returnUrl: string,
59
60
  ): string {
60
- const url = new URL("/api/v1/auth/login", apiBaseUrl);
61
+ const url = new URL(`/api/v1/auth/oidc/${encodeURIComponent(providerId)}/challenge`, apiBaseUrl);
61
62
  url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
62
63
  return url.toString();
63
64
  }
@@ -71,6 +72,120 @@ export function buildDevPortalLogoutUrl(
71
72
  return url.toString();
72
73
  }
73
74
 
75
+ export function normalizeDevPortalAuthConfig(
76
+ raw: unknown,
77
+ ): import("./dev-portal-constants.js").DevPortalAuthConfigResponse {
78
+ const data = (raw ?? {}) as Record<string, unknown>;
79
+ const nestedNative = data.native as { enabled?: boolean } | undefined;
80
+ const nativeEnabled =
81
+ typeof nestedNative?.enabled === "boolean"
82
+ ? nestedNative.enabled
83
+ : typeof data.nativeEnabled === "boolean"
84
+ ? data.nativeEnabled
85
+ : false;
86
+
87
+ const emailRaw = data.email;
88
+ const email =
89
+ emailRaw &&
90
+ typeof emailRaw === "object" &&
91
+ typeof (emailRaw as { provider?: unknown }).provider === "string" &&
92
+ typeof (emailRaw as { from?: unknown }).from === "string"
93
+ ? {
94
+ provider: (emailRaw as { provider: string }).provider,
95
+ from: (emailRaw as { from: string }).from,
96
+ }
97
+ : null;
98
+
99
+ const providers = Array.isArray(data.providers)
100
+ ? data.providers
101
+ .filter(
102
+ (provider): provider is Record<string, unknown> =>
103
+ provider != null && typeof provider === "object",
104
+ )
105
+ .map((provider) => ({
106
+ id: String(provider.id ?? ""),
107
+ displayName: String(provider.displayName ?? provider.id ?? ""),
108
+ authority: String(provider.authority ?? ""),
109
+ clientId: String(provider.clientId ?? ""),
110
+ scopes: Array.isArray(provider.scopes)
111
+ ? provider.scopes.map((scope) => String(scope))
112
+ : [],
113
+ }))
114
+ .filter((provider) => provider.id.length > 0)
115
+ : [];
116
+
117
+ return {
118
+ native: { enabled: nativeEnabled },
119
+ email,
120
+ providers,
121
+ };
122
+ }
123
+
124
+ export function isNativeAuthEnabled(
125
+ config: import("./dev-portal-constants.js").DevPortalAuthConfigResponse | null | undefined,
126
+ ): boolean {
127
+ if (!config) {
128
+ return false;
129
+ }
130
+
131
+ const raw = config as import("./dev-portal-constants.js").DevPortalAuthConfigResponse & {
132
+ nativeEnabled?: boolean;
133
+ };
134
+
135
+ return raw.native?.enabled === true || raw.nativeEnabled === true;
136
+ }
137
+
138
+ export async function fetchDevPortalAuthConfig(
139
+ apiBaseUrl: string,
140
+ fetchImpl: typeof fetch = fetch,
141
+ ): Promise<import("./dev-portal-constants.js").DevPortalAuthConfigResponse> {
142
+ const response = await fetchImpl(`${apiBaseUrl}/api/v1/auth/config`, {
143
+ method: "GET",
144
+ headers: { Accept: "application/json" },
145
+ credentials: "include",
146
+ });
147
+
148
+ if (!response.ok) {
149
+ throw new Error("Failed to load auth configuration.");
150
+ }
151
+
152
+ return normalizeDevPortalAuthConfig(await response.json());
153
+ }
154
+
155
+ export async function postDevPortalAuth<T>(
156
+ apiBaseUrl: string,
157
+ path: string,
158
+ body: unknown,
159
+ fetchImpl: typeof fetch = fetch,
160
+ ): Promise<T> {
161
+ const response = await fetchImpl(`${apiBaseUrl}${path}`, {
162
+ method: "POST",
163
+ headers: {
164
+ Accept: "application/json",
165
+ "Content-Type": "application/json",
166
+ },
167
+ credentials: "include",
168
+ body: JSON.stringify(body),
169
+ });
170
+
171
+ if (response.status === 204) {
172
+ return {} as T;
173
+ }
174
+
175
+ if (!response.ok) {
176
+ let detail = "Request failed.";
177
+ try {
178
+ const problem = (await response.json()) as { detail?: string; title?: string };
179
+ detail = problem.detail ?? problem.title ?? detail;
180
+ } catch {
181
+ // ignore parse errors
182
+ }
183
+ throw new Error(detail);
184
+ }
185
+
186
+ return (await response.json()) as T;
187
+ }
188
+
74
189
  export async function probeDevPortalBackend(
75
190
  apiBaseUrl: string,
76
191
  fetchImpl: typeof fetch = fetch,
@@ -17,12 +17,20 @@ import type {
17
17
  EndUserMeResponse,
18
18
  } from "./dev-portal-constants.js";
19
19
  import {
20
- buildDevPortalLoginUrl,
20
+ DevPortalForgotPasswordPage,
21
+ DevPortalLoginPage,
22
+ DevPortalRegisterPage,
23
+ DevPortalResetPasswordPage,
24
+ DevPortalVerifyEmailPage,
25
+ } from "./dev-portal-auth-pages.js";
26
+ import {
21
27
  buildDevPortalLogoutUrl,
28
+ isNativeAuthEnabled,
22
29
  isPlaceholderDevPortalApiUrl,
23
30
  mapEndUserMeToProfile,
24
31
  probeDevPortalBackend,
25
32
  resolveDevPortalApiBaseUrl,
33
+ toAbsoluteReturnUrl,
26
34
  } from "./dev-portal-utils.js";
27
35
 
28
36
  export type DevPortalProviderData = {
@@ -49,6 +57,8 @@ export class DevPortalAuthenticationProvider
49
57
  private refreshGeneration = 0;
50
58
  private refreshInFlight: Promise<boolean> | null = null;
51
59
 
60
+ private authConfigNativeEnabled: boolean | null = null;
61
+
52
62
  constructor({
53
63
  apiBaseUrl,
54
64
  redirectToAfterSignIn,
@@ -62,6 +72,18 @@ export class DevPortalAuthenticationProvider
62
72
  this.redirectToAfterSignOut = redirectToAfterSignOut;
63
73
  }
64
74
 
75
+ getRoutes() {
76
+ const pageProps = { apiBaseUrl: this.apiBaseUrl };
77
+ return [
78
+ ...super.getRoutes(),
79
+ { path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
80
+ { path: "/register", element: <DevPortalRegisterPage {...pageProps} /> },
81
+ { path: "/verify-email", element: <DevPortalVerifyEmailPage {...pageProps} /> },
82
+ { path: "/forgot-password", element: <DevPortalForgotPasswordPage {...pageProps} /> },
83
+ { path: "/reset-password", element: <DevPortalResetPasswordPage {...pageProps} /> },
84
+ ];
85
+ }
86
+
65
87
  async initialize(_context: ZudokuContext): Promise<void> {
66
88
  // Node SSR cannot reach local HTTPS backends with self-signed certs.
67
89
  if (typeof window === "undefined") {
@@ -70,6 +92,15 @@ export class DevPortalAuthenticationProvider
70
92
 
71
93
  await waitForAuthStateHydration();
72
94
  await this.syncBackendAvailability();
95
+ if (this.backendAvailable && this.apiBaseUrl) {
96
+ try {
97
+ const { fetchDevPortalAuthConfig } = await import("./dev-portal-utils.js");
98
+ const config = await fetchDevPortalAuthConfig(this.apiBaseUrl);
99
+ this.authConfigNativeEnabled = isNativeAuthEnabled(config);
100
+ } catch {
101
+ this.authConfigNativeEnabled = null;
102
+ }
103
+ }
73
104
  if (!this.backendAvailable) {
74
105
  this.setPreviewState();
75
106
  return;
@@ -235,20 +266,24 @@ export class DevPortalAuthenticationProvider
235
266
  _: AuthActionContext,
236
267
  { redirectTo }: AuthActionOptions = {},
237
268
  ): Promise<void> {
238
- const apiBaseUrl = this.ensureConfiguredBackend();
269
+ this.ensureConfiguredBackend();
239
270
  const target =
240
271
  redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
241
- window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
272
+ const url = new URL("/login", window.location.origin);
273
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
274
+ window.location.assign(url.toString());
242
275
  }
243
276
 
244
277
  async signUp(
245
278
  _: AuthActionContext,
246
279
  { redirectTo }: AuthActionOptions = {},
247
280
  ): Promise<void> {
248
- const apiBaseUrl = this.ensureConfiguredBackend();
281
+ this.ensureConfiguredBackend();
249
282
  const target =
250
283
  redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
251
- window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
284
+ const url = new URL("/register", window.location.origin);
285
+ url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
286
+ window.location.assign(url.toString());
252
287
  }
253
288
 
254
289
  async signOut(_: AuthActionContext): Promise<void> {
@@ -259,7 +294,7 @@ export class DevPortalAuthenticationProvider
259
294
  }
260
295
 
261
296
  hasDistinctSignUpFlow(): boolean {
262
- return false;
297
+ return this.authConfigNativeEnabled !== false;
263
298
  }
264
299
 
265
300
  async signRequest(request: Request): Promise<Request> {