@mehdad67/apitogo 0.1.27 → 0.1.28
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 +16 -1
- package/dist/declarations/config/config.d.ts +3 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +12 -0
- package/dist/declarations/lib/authentication/components/ProductionUnlockPage.d.ts +1 -0
- package/dist/declarations/lib/authentication/hook.d.ts +2 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-constants.d.ts +16 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-utils.d.ts +9 -0
- package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +36 -0
- package/dist/declarations/lib/authentication/state.d.ts +3 -0
- package/dist/flat-config.d.ts +6 -0
- package/docs/configuration/authentication.md +32 -2
- package/package.json +6 -1
- package/src/config/config.ts +4 -0
- package/src/config/validators/ZudokuConfig.ts +7 -0
- package/src/lib/auth/issuer.ts +3 -0
- package/src/lib/authentication/components/ProductionUnlockPage.tsx +27 -0
- package/src/lib/authentication/hook.ts +2 -0
- package/src/lib/authentication/providers/dev-portal-constants.ts +21 -0
- package/src/lib/authentication/providers/dev-portal-utils.ts +107 -0
- package/src/lib/authentication/providers/dev-portal.tsx +218 -0
- package/src/lib/authentication/state.ts +12 -0
- package/src/lib/components/Header.tsx +2 -2
- package/src/lib/components/MobileTopNavigation.tsx +3 -2
- package/src/lib/core/RouteGuard.tsx +11 -1
package/dist/cli/cli.js
CHANGED
|
@@ -3545,6 +3545,13 @@ var init_ZudokuConfig = __esm({
|
|
|
3545
3545
|
redirectToAfterSignUp: z8.string().optional(),
|
|
3546
3546
|
redirectToAfterSignIn: z8.string().optional(),
|
|
3547
3547
|
redirectToAfterSignOut: z8.string().optional()
|
|
3548
|
+
}),
|
|
3549
|
+
z8.object({
|
|
3550
|
+
type: z8.literal("dev-portal"),
|
|
3551
|
+
apiBaseUrl: z8.string().optional(),
|
|
3552
|
+
redirectToAfterSignUp: z8.string().optional(),
|
|
3553
|
+
redirectToAfterSignIn: z8.string().optional(),
|
|
3554
|
+
redirectToAfterSignOut: z8.string().optional()
|
|
3548
3555
|
})
|
|
3549
3556
|
]);
|
|
3550
3557
|
MetadataSchema = z8.object({
|
|
@@ -4086,6 +4093,9 @@ var getIssuer = async (config2) => {
|
|
|
4086
4093
|
case "firebase": {
|
|
4087
4094
|
return `https://securetoken.google.com/${config2.authentication.projectId}`;
|
|
4088
4095
|
}
|
|
4096
|
+
case "dev-portal": {
|
|
4097
|
+
return void 0;
|
|
4098
|
+
}
|
|
4089
4099
|
case void 0: {
|
|
4090
4100
|
return void 0;
|
|
4091
4101
|
}
|
|
@@ -4111,7 +4121,7 @@ import {
|
|
|
4111
4121
|
// package.json
|
|
4112
4122
|
var package_default = {
|
|
4113
4123
|
name: "@mehdad67/apitogo",
|
|
4114
|
-
version: "0.1.
|
|
4124
|
+
version: "0.1.28",
|
|
4115
4125
|
type: "module",
|
|
4116
4126
|
sideEffects: [
|
|
4117
4127
|
"**/*.css",
|
|
@@ -4157,6 +4167,7 @@ var package_default = {
|
|
|
4157
4167
|
"./auth/supabase": "./src/lib/authentication/providers/supabase.tsx",
|
|
4158
4168
|
"./auth/azureb2c": "./src/lib/authentication/providers/azureb2c.tsx",
|
|
4159
4169
|
"./auth/firebase": "./src/lib/authentication/providers/firebase.tsx",
|
|
4170
|
+
"./auth/dev-portal": "./src/lib/authentication/providers/dev-portal.tsx",
|
|
4160
4171
|
"./plugins": "./src/lib/core/plugins.ts",
|
|
4161
4172
|
"./plugins/api-keys": "./src/lib/plugins/api-keys/index.tsx",
|
|
4162
4173
|
"./plugins/markdown": "./src/lib/plugins/markdown/index.tsx",
|
|
@@ -4406,6 +4417,10 @@ var package_default = {
|
|
|
4406
4417
|
types: "./dist/declarations/lib/authentication/providers/firebase.d.ts",
|
|
4407
4418
|
default: "./src/lib/authentication/providers/firebase.tsx"
|
|
4408
4419
|
},
|
|
4420
|
+
"./auth/dev-portal": {
|
|
4421
|
+
types: "./dist/declarations/lib/authentication/providers/dev-portal.d.ts",
|
|
4422
|
+
default: "./src/lib/authentication/providers/dev-portal.tsx"
|
|
4423
|
+
},
|
|
4409
4424
|
"./plugins": {
|
|
4410
4425
|
types: "./dist/declarations/lib/core/plugins.d.ts",
|
|
4411
4426
|
default: "./src/lib/core/plugins.ts"
|
|
@@ -24,3 +24,6 @@ export type FirebaseAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
|
24
24
|
export type AzureB2CAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
25
25
|
type: "azureb2c";
|
|
26
26
|
}>;
|
|
27
|
+
export type DevPortalAuthenticationConfig = Extract<AuthenticationConfig, {
|
|
28
|
+
type: "dev-portal";
|
|
29
|
+
}>;
|
|
@@ -366,6 +366,12 @@ declare const AuthenticationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
366
366
|
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
367
367
|
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
368
368
|
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
369
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
370
|
+
type: z.ZodLiteral<"dev-portal">;
|
|
371
|
+
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
372
|
+
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
373
|
+
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
374
|
+
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
369
375
|
}, z.core.$strip>], "type">;
|
|
370
376
|
declare const FontConfigSchema: z.ZodUnion<readonly [z.ZodEnum<{
|
|
371
377
|
Inter: "Inter";
|
|
@@ -6728,6 +6734,12 @@ export declare const ZudokuConfig: z.ZodObject<{
|
|
|
6728
6734
|
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
6729
6735
|
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
6730
6736
|
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
6737
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
6738
|
+
type: z.ZodLiteral<"dev-portal">;
|
|
6739
|
+
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
6740
|
+
redirectToAfterSignUp: z.ZodOptional<z.ZodString>;
|
|
6741
|
+
redirectToAfterSignIn: z.ZodOptional<z.ZodString>;
|
|
6742
|
+
redirectToAfterSignOut: z.ZodOptional<z.ZodString>;
|
|
6731
6743
|
}, z.core.$strip>], "type">>;
|
|
6732
6744
|
search: z.ZodOptional<z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6733
6745
|
type: z.ZodLiteral<"inkeep">;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ProductionUnlockPage: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -11,6 +11,8 @@ export declare const useVerifiedEmail: () => {
|
|
|
11
11
|
requestEmailVerification: (options?: AuthActionOptions) => Promise<void>;
|
|
12
12
|
};
|
|
13
13
|
export declare const useAuth: () => {
|
|
14
|
+
isBackendAvailable: boolean;
|
|
15
|
+
authMode: import("./state.js").AuthMode;
|
|
14
16
|
login: (options?: AuthActionOptions) => Promise<void>;
|
|
15
17
|
logout: () => Promise<void>;
|
|
16
18
|
signup: (options?: AuthActionOptions) => Promise<void>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const DEV_PORTAL_PLACEHOLDER_API_URL = "https://dp-example.azurecontainerapps.io";
|
|
2
|
+
export type DevPortalAuthMode = "live" | "preview";
|
|
3
|
+
export type EndUserMeResponse = {
|
|
4
|
+
userId: string;
|
|
5
|
+
displayName: string | null;
|
|
6
|
+
subscriptions: Array<{
|
|
7
|
+
planSku: string;
|
|
8
|
+
status: string;
|
|
9
|
+
entitlementState: string | null;
|
|
10
|
+
currentPeriodEnd: string | null;
|
|
11
|
+
}>;
|
|
12
|
+
};
|
|
13
|
+
export type DevPortalAuthStatusResponse = {
|
|
14
|
+
available: boolean;
|
|
15
|
+
oidcConfigured: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import { type EndUserMeResponse } from "./dev-portal-constants.js";
|
|
3
|
+
export declare function resolveDevPortalApiBaseUrl(config: Pick<DevPortalAuthenticationConfig, "apiBaseUrl">): string | undefined;
|
|
4
|
+
export declare function isPlaceholderDevPortalApiUrl(apiBaseUrl: string | undefined): boolean;
|
|
5
|
+
export declare function toAbsoluteReturnUrl(pathOrUrl: string): string;
|
|
6
|
+
export declare function buildDevPortalLoginUrl(apiBaseUrl: string, returnUrl: string): string;
|
|
7
|
+
export declare function buildDevPortalLogoutUrl(apiBaseUrl: string, returnUrl: string): string;
|
|
8
|
+
export declare function probeDevPortalBackend(apiBaseUrl: string, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
9
|
+
export declare function mapEndUserMeToProfile(me: EndUserMeResponse): import("../state.js").UserProfile;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import type { ZudokuContext } from "../../core/ZudokuContext.js";
|
|
3
|
+
import type { AuthActionContext, AuthActionOptions, AuthenticationPlugin, AuthenticationProviderInitializer } from "../authentication.js";
|
|
4
|
+
import { CoreAuthenticationPlugin } from "../AuthenticationPlugin.js";
|
|
5
|
+
import type { DevPortalAuthMode } from "./dev-portal-constants.js";
|
|
6
|
+
export type DevPortalProviderData = {
|
|
7
|
+
type: "dev-portal";
|
|
8
|
+
apiBaseUrl: string;
|
|
9
|
+
authMode: DevPortalAuthMode;
|
|
10
|
+
};
|
|
11
|
+
declare module "../state.js" {
|
|
12
|
+
interface ProviderDataRegistry {
|
|
13
|
+
"dev-portal": DevPortalProviderData;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export declare class DevPortalAuthenticationProvider extends CoreAuthenticationPlugin implements AuthenticationPlugin {
|
|
17
|
+
private readonly apiBaseUrl;
|
|
18
|
+
private readonly redirectToAfterSignIn;
|
|
19
|
+
private readonly redirectToAfterSignUp;
|
|
20
|
+
private readonly redirectToAfterSignOut;
|
|
21
|
+
private authMode;
|
|
22
|
+
private backendAvailable;
|
|
23
|
+
constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
|
|
24
|
+
initialize(_context: ZudokuContext): Promise<void>;
|
|
25
|
+
onPageLoad: () => Promise<void>;
|
|
26
|
+
private syncBackendAvailability;
|
|
27
|
+
private setPreviewState;
|
|
28
|
+
private ensureLiveBackend;
|
|
29
|
+
refreshUserProfile(): Promise<boolean>;
|
|
30
|
+
signIn(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
|
|
31
|
+
signUp(_: AuthActionContext, { redirectTo }?: AuthActionOptions): Promise<void>;
|
|
32
|
+
signOut(_: AuthActionContext): Promise<void>;
|
|
33
|
+
signRequest(request: Request): Promise<Request>;
|
|
34
|
+
}
|
|
35
|
+
declare const devPortalAuth: AuthenticationProviderInitializer<DevPortalAuthenticationConfig>;
|
|
36
|
+
export default devPortalAuth;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export interface ProviderDataRegistry {
|
|
2
2
|
}
|
|
3
3
|
export type ProviderData = [keyof ProviderDataRegistry] extends [never] ? unknown : ProviderDataRegistry[keyof ProviderDataRegistry];
|
|
4
|
+
export type AuthMode = "live" | "preview";
|
|
4
5
|
export interface AuthState {
|
|
5
6
|
isAuthenticated: boolean;
|
|
6
7
|
isPending: boolean;
|
|
7
8
|
profile: UserProfile | null;
|
|
8
9
|
providerData: ProviderData | null;
|
|
10
|
+
isBackendAvailable: boolean;
|
|
11
|
+
authMode: AuthMode;
|
|
9
12
|
setAuthenticationPending: () => void;
|
|
10
13
|
setLoggedOut: () => void;
|
|
11
14
|
setLoggedIn: (args: {
|
package/dist/flat-config.d.ts
CHANGED
|
@@ -305,6 +305,12 @@ export interface FlatZudokuConfig {
|
|
|
305
305
|
redirectToAfterSignUp?: string
|
|
306
306
|
redirectToAfterSignIn?: string
|
|
307
307
|
redirectToAfterSignOut?: string
|
|
308
|
+
} | {
|
|
309
|
+
type: "dev-portal"
|
|
310
|
+
apiBaseUrl?: string
|
|
311
|
+
redirectToAfterSignUp?: string
|
|
312
|
+
redirectToAfterSignIn?: string
|
|
313
|
+
redirectToAfterSignOut?: string
|
|
308
314
|
})
|
|
309
315
|
search?: ({
|
|
310
316
|
type: "inkeep"
|
|
@@ -15,12 +15,42 @@ the authentication provider you use.
|
|
|
15
15
|
|
|
16
16
|
## Authentication Providers
|
|
17
17
|
|
|
18
|
-
APIToGo supports Clerk, Auth0, Supabase, Azure B2C,
|
|
19
|
-
Connect protocol
|
|
18
|
+
APIToGo supports Clerk, Auth0, Supabase, Azure B2C, any OpenID provider that supports the OpenID
|
|
19
|
+
Connect protocol, and **dev-portal** (backend-delegated OIDC for APItoGo provisioned developer
|
|
20
|
+
portals).
|
|
20
21
|
|
|
21
22
|
Not seeing your authentication provider?
|
|
22
23
|
[Let us know](https://github.com/lukoweb/apitogo-doc-tool/issues)
|
|
23
24
|
|
|
25
|
+
### Dev portal (APItoGo)
|
|
26
|
+
|
|
27
|
+
For sites provisioned through APItoGo MCP, use backend-delegated authentication. The static site
|
|
28
|
+
redirects sign-in to the provisioned dev-portal API; OIDC client credentials stay on the backend
|
|
29
|
+
only.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
{
|
|
33
|
+
// ...
|
|
34
|
+
authentication: {
|
|
35
|
+
type: "dev-portal",
|
|
36
|
+
},
|
|
37
|
+
// ...
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Set the backend API URL via environment variable (MCP writes this at scaffold/publish):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
VITE_APITOGO_DEV_PORTAL_API_URL=https://your-dev-portal-api.example.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Local preview:** When the backend URL is a placeholder or unreachable, the account area shows a
|
|
48
|
+
message that sign-in unlocks after publish. Docs and API reference remain public.
|
|
49
|
+
|
|
50
|
+
**Production:** Register one OIDC app with redirect URI `https://{backend}/signin-oidc` (see MCP
|
|
51
|
+
`manualSteps.oidcRedirectUri`). The frontend uses cookie sessions via `credentials: "include"` on
|
|
52
|
+
`/api/v1/auth/me`.
|
|
53
|
+
|
|
24
54
|
### Auth0
|
|
25
55
|
|
|
26
56
|
For Auth0, you will need the `clientId` associated with the domain you are using.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mehdad67/apitogo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"./auth/supabase": "./src/lib/authentication/providers/supabase.tsx",
|
|
47
47
|
"./auth/azureb2c": "./src/lib/authentication/providers/azureb2c.tsx",
|
|
48
48
|
"./auth/firebase": "./src/lib/authentication/providers/firebase.tsx",
|
|
49
|
+
"./auth/dev-portal": "./src/lib/authentication/providers/dev-portal.tsx",
|
|
49
50
|
"./plugins": "./src/lib/core/plugins.ts",
|
|
50
51
|
"./plugins/api-keys": "./src/lib/plugins/api-keys/index.tsx",
|
|
51
52
|
"./plugins/markdown": "./src/lib/plugins/markdown/index.tsx",
|
|
@@ -295,6 +296,10 @@
|
|
|
295
296
|
"types": "./dist/declarations/lib/authentication/providers/firebase.d.ts",
|
|
296
297
|
"default": "./src/lib/authentication/providers/firebase.tsx"
|
|
297
298
|
},
|
|
299
|
+
"./auth/dev-portal": {
|
|
300
|
+
"types": "./dist/declarations/lib/authentication/providers/dev-portal.d.ts",
|
|
301
|
+
"default": "./src/lib/authentication/providers/dev-portal.tsx"
|
|
302
|
+
},
|
|
298
303
|
"./plugins": {
|
|
299
304
|
"types": "./dist/declarations/lib/core/plugins.d.ts",
|
|
300
305
|
"default": "./src/lib/core/plugins.ts"
|
package/src/config/config.ts
CHANGED
|
@@ -491,6 +491,13 @@ const AuthenticationSchema = z.discriminatedUnion("type", [
|
|
|
491
491
|
redirectToAfterSignIn: z.string().optional(),
|
|
492
492
|
redirectToAfterSignOut: z.string().optional(),
|
|
493
493
|
}),
|
|
494
|
+
z.object({
|
|
495
|
+
type: z.literal("dev-portal"),
|
|
496
|
+
apiBaseUrl: z.string().optional(),
|
|
497
|
+
redirectToAfterSignUp: z.string().optional(),
|
|
498
|
+
redirectToAfterSignIn: z.string().optional(),
|
|
499
|
+
redirectToAfterSignOut: z.string().optional(),
|
|
500
|
+
}),
|
|
494
501
|
]);
|
|
495
502
|
|
|
496
503
|
const MetadataSchema = z
|
package/src/lib/auth/issuer.ts
CHANGED
|
@@ -21,6 +21,9 @@ export const getIssuer = async (config: ZudokuConfig) => {
|
|
|
21
21
|
case "firebase": {
|
|
22
22
|
return `https://securetoken.google.com/${config.authentication.projectId}`;
|
|
23
23
|
}
|
|
24
|
+
case "dev-portal": {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
24
27
|
case undefined: {
|
|
25
28
|
return undefined;
|
|
26
29
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button } from "@mehdad67/apitogo/ui/Button.js";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { Layout } from "../../components/Layout.js";
|
|
4
|
+
|
|
5
|
+
export const ProductionUnlockPage = () => (
|
|
6
|
+
<Layout>
|
|
7
|
+
<div className="mx-auto flex min-h-[50vh] max-w-lg flex-col items-center justify-center gap-4 px-4 text-center">
|
|
8
|
+
<h1 className="text-2xl font-semibold tracking-tight">
|
|
9
|
+
Account sign-in unlocks after publish
|
|
10
|
+
</h1>
|
|
11
|
+
<p className="text-muted-foreground">
|
|
12
|
+
Your developer portal account area uses live authentication backed by
|
|
13
|
+
APItoGo. Preview this site locally for docs and API reference; sign-in,
|
|
14
|
+
plans, and subscriptions activate once the portal is published to
|
|
15
|
+
production.
|
|
16
|
+
</p>
|
|
17
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
18
|
+
<Button asChild variant="default">
|
|
19
|
+
<Link to="/introduction">Browse documentation</Link>
|
|
20
|
+
</Button>
|
|
21
|
+
<Button asChild variant="outline">
|
|
22
|
+
<Link to="/">Back to home</Link>
|
|
23
|
+
</Button>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</Layout>
|
|
27
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Placeholder backend URL written by MCP before publish. */
|
|
2
|
+
export const DEV_PORTAL_PLACEHOLDER_API_URL =
|
|
3
|
+
"https://dp-example.azurecontainerapps.io";
|
|
4
|
+
|
|
5
|
+
export type DevPortalAuthMode = "live" | "preview";
|
|
6
|
+
|
|
7
|
+
export type EndUserMeResponse = {
|
|
8
|
+
userId: string;
|
|
9
|
+
displayName: string | null;
|
|
10
|
+
subscriptions: Array<{
|
|
11
|
+
planSku: string;
|
|
12
|
+
status: string;
|
|
13
|
+
entitlementState: string | null;
|
|
14
|
+
currentPeriodEnd: string | null;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type DevPortalAuthStatusResponse = {
|
|
19
|
+
available: boolean;
|
|
20
|
+
oidcConfigured: boolean;
|
|
21
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import {
|
|
3
|
+
DEV_PORTAL_PLACEHOLDER_API_URL,
|
|
4
|
+
type DevPortalAuthStatusResponse,
|
|
5
|
+
type EndUserMeResponse,
|
|
6
|
+
} from "./dev-portal-constants.js";
|
|
7
|
+
|
|
8
|
+
declare const VITE_APITOGO_DEV_PORTAL_API_URL: string | undefined;
|
|
9
|
+
|
|
10
|
+
export function resolveDevPortalApiBaseUrl(
|
|
11
|
+
config: Pick<DevPortalAuthenticationConfig, "apiBaseUrl">,
|
|
12
|
+
): string | undefined {
|
|
13
|
+
const fromConfig = config.apiBaseUrl?.trim();
|
|
14
|
+
if (fromConfig) {
|
|
15
|
+
return fromConfig.replace(/\/+$/, "");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const fromEnv =
|
|
19
|
+
typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string"
|
|
20
|
+
? VITE_APITOGO_DEV_PORTAL_API_URL.trim()
|
|
21
|
+
: "";
|
|
22
|
+
|
|
23
|
+
return fromEnv ? fromEnv.replace(/\/+$/, "") : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isPlaceholderDevPortalApiUrl(
|
|
27
|
+
apiBaseUrl: string | undefined,
|
|
28
|
+
): boolean {
|
|
29
|
+
if (!apiBaseUrl) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
apiBaseUrl === DEV_PORTAL_PLACEHOLDER_API_URL ||
|
|
35
|
+
apiBaseUrl.includes("dp-example.azurecontainerapps.io")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function toAbsoluteReturnUrl(pathOrUrl: string): string {
|
|
40
|
+
if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) {
|
|
41
|
+
return pathOrUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof window === "undefined") {
|
|
45
|
+
return pathOrUrl;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return new URL(pathOrUrl, window.location.origin).href;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildDevPortalLoginUrl(
|
|
52
|
+
apiBaseUrl: string,
|
|
53
|
+
returnUrl: string,
|
|
54
|
+
): string {
|
|
55
|
+
const url = new URL("/api/v1/auth/login", apiBaseUrl);
|
|
56
|
+
url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
|
|
57
|
+
return url.toString();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildDevPortalLogoutUrl(
|
|
61
|
+
apiBaseUrl: string,
|
|
62
|
+
returnUrl: string,
|
|
63
|
+
): string {
|
|
64
|
+
const url = new URL("/api/v1/auth/logout", apiBaseUrl);
|
|
65
|
+
url.searchParams.set("returnUrl", toAbsoluteReturnUrl(returnUrl));
|
|
66
|
+
return url.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function probeDevPortalBackend(
|
|
70
|
+
apiBaseUrl: string,
|
|
71
|
+
fetchImpl: typeof fetch = fetch,
|
|
72
|
+
): Promise<boolean> {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeout = setTimeout(() => controller.abort(), 8_000);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetchImpl(`${apiBaseUrl}/api/v1/auth/status`, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
headers: { Accept: "application/json" },
|
|
80
|
+
credentials: "include",
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const body = (await response.json()) as DevPortalAuthStatusResponse;
|
|
89
|
+
return body.available === true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
} finally {
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function mapEndUserMeToProfile(
|
|
98
|
+
me: EndUserMeResponse,
|
|
99
|
+
): import("../state.js").UserProfile {
|
|
100
|
+
return {
|
|
101
|
+
sub: me.userId,
|
|
102
|
+
email: undefined,
|
|
103
|
+
emailVerified: false,
|
|
104
|
+
name: me.displayName ?? undefined,
|
|
105
|
+
pictureUrl: undefined,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
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 { type UserProfile, useAuthState } from "../state.js";
|
|
11
|
+
import type {
|
|
12
|
+
DevPortalAuthMode,
|
|
13
|
+
EndUserMeResponse,
|
|
14
|
+
} from "./dev-portal-constants.js";
|
|
15
|
+
import {
|
|
16
|
+
buildDevPortalLoginUrl,
|
|
17
|
+
buildDevPortalLogoutUrl,
|
|
18
|
+
isPlaceholderDevPortalApiUrl,
|
|
19
|
+
mapEndUserMeToProfile,
|
|
20
|
+
probeDevPortalBackend,
|
|
21
|
+
resolveDevPortalApiBaseUrl,
|
|
22
|
+
} from "./dev-portal-utils.js";
|
|
23
|
+
|
|
24
|
+
export type DevPortalProviderData = {
|
|
25
|
+
type: "dev-portal";
|
|
26
|
+
apiBaseUrl: string;
|
|
27
|
+
authMode: DevPortalAuthMode;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
declare module "../state.js" {
|
|
31
|
+
interface ProviderDataRegistry {
|
|
32
|
+
"dev-portal": DevPortalProviderData;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class DevPortalAuthenticationProvider
|
|
37
|
+
extends CoreAuthenticationPlugin
|
|
38
|
+
implements AuthenticationPlugin
|
|
39
|
+
{
|
|
40
|
+
private readonly apiBaseUrl: string | undefined;
|
|
41
|
+
private readonly redirectToAfterSignIn: string | undefined;
|
|
42
|
+
private readonly redirectToAfterSignUp: string | undefined;
|
|
43
|
+
private readonly redirectToAfterSignOut: string;
|
|
44
|
+
private authMode: DevPortalAuthMode = "preview";
|
|
45
|
+
private backendAvailable = false;
|
|
46
|
+
|
|
47
|
+
constructor({
|
|
48
|
+
apiBaseUrl,
|
|
49
|
+
redirectToAfterSignIn,
|
|
50
|
+
redirectToAfterSignUp,
|
|
51
|
+
redirectToAfterSignOut = "/",
|
|
52
|
+
}: DevPortalAuthenticationConfig) {
|
|
53
|
+
super();
|
|
54
|
+
this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
55
|
+
this.redirectToAfterSignIn = redirectToAfterSignIn;
|
|
56
|
+
this.redirectToAfterSignUp = redirectToAfterSignUp;
|
|
57
|
+
this.redirectToAfterSignOut = redirectToAfterSignOut;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async initialize(_context: ZudokuContext): Promise<void> {
|
|
61
|
+
await this.syncBackendAvailability();
|
|
62
|
+
if (!this.backendAvailable) {
|
|
63
|
+
this.setPreviewState();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await this.refreshUserProfile();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
onPageLoad = async () => {
|
|
71
|
+
if (!this.backendAvailable) {
|
|
72
|
+
this.setPreviewState();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await this.refreshUserProfile();
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private async syncBackendAvailability(): Promise<void> {
|
|
80
|
+
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
81
|
+
this.backendAvailable = false;
|
|
82
|
+
this.authMode = "preview";
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.backendAvailable = await probeDevPortalBackend(this.apiBaseUrl);
|
|
87
|
+
this.authMode = this.backendAvailable ? "live" : "preview";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private setPreviewState(): void {
|
|
91
|
+
useAuthState.setState({
|
|
92
|
+
isAuthenticated: false,
|
|
93
|
+
isPending: false,
|
|
94
|
+
profile: null,
|
|
95
|
+
providerData: this.apiBaseUrl
|
|
96
|
+
? {
|
|
97
|
+
type: "dev-portal",
|
|
98
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
99
|
+
authMode: "preview",
|
|
100
|
+
}
|
|
101
|
+
: null,
|
|
102
|
+
isBackendAvailable: false,
|
|
103
|
+
authMode: "preview",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private ensureLiveBackend(): string {
|
|
108
|
+
if (!this.apiBaseUrl || !this.backendAvailable) {
|
|
109
|
+
throw new Error("Dev portal authentication is not available in preview.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return this.apiBaseUrl;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async refreshUserProfile(): Promise<boolean> {
|
|
116
|
+
if (!this.apiBaseUrl) {
|
|
117
|
+
this.setPreviewState();
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
122
|
+
this.setPreviewState();
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.syncBackendAvailability();
|
|
127
|
+
if (!this.backendAvailable) {
|
|
128
|
+
this.setPreviewState();
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(`${this.apiBaseUrl}/api/v1/auth/me`, {
|
|
134
|
+
method: "GET",
|
|
135
|
+
headers: { Accept: "application/json" },
|
|
136
|
+
credentials: "include",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (response.status === 401) {
|
|
140
|
+
useAuthState.setState({
|
|
141
|
+
isAuthenticated: false,
|
|
142
|
+
isPending: false,
|
|
143
|
+
profile: null,
|
|
144
|
+
providerData: {
|
|
145
|
+
type: "dev-portal",
|
|
146
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
147
|
+
authMode: "live",
|
|
148
|
+
},
|
|
149
|
+
isBackendAvailable: true,
|
|
150
|
+
authMode: "live",
|
|
151
|
+
});
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
this.setPreviewState();
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const me = (await response.json()) as EndUserMeResponse;
|
|
161
|
+
const profile: UserProfile = mapEndUserMeToProfile(me);
|
|
162
|
+
|
|
163
|
+
useAuthState.getState().setLoggedIn({
|
|
164
|
+
profile,
|
|
165
|
+
providerData: {
|
|
166
|
+
type: "dev-portal",
|
|
167
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
168
|
+
authMode: "live",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
useAuthState.setState({
|
|
172
|
+
isBackendAvailable: true,
|
|
173
|
+
authMode: "live",
|
|
174
|
+
});
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
this.setPreviewState();
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async signIn(
|
|
183
|
+
_: AuthActionContext,
|
|
184
|
+
{ redirectTo }: AuthActionOptions = {},
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
const apiBaseUrl = this.ensureLiveBackend();
|
|
187
|
+
const target =
|
|
188
|
+
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
189
|
+
window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async signUp(
|
|
193
|
+
_: AuthActionContext,
|
|
194
|
+
{ redirectTo }: AuthActionOptions = {},
|
|
195
|
+
): Promise<void> {
|
|
196
|
+
const apiBaseUrl = this.ensureLiveBackend();
|
|
197
|
+
const target =
|
|
198
|
+
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
199
|
+
window.location.assign(buildDevPortalLoginUrl(apiBaseUrl, target));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async signOut(_: AuthActionContext): Promise<void> {
|
|
203
|
+
const apiBaseUrl = this.ensureLiveBackend();
|
|
204
|
+
window.location.assign(
|
|
205
|
+
buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async signRequest(request: Request): Promise<Request> {
|
|
210
|
+
return request;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const devPortalAuth: AuthenticationProviderInitializer<
|
|
215
|
+
DevPortalAuthenticationConfig
|
|
216
|
+
> = (options) => new DevPortalAuthenticationProvider(options);
|
|
217
|
+
|
|
218
|
+
export default devPortalAuth;
|
|
@@ -21,11 +21,17 @@ export type ProviderData = [keyof ProviderDataRegistry] extends [never]
|
|
|
21
21
|
? unknown
|
|
22
22
|
: ProviderDataRegistry[keyof ProviderDataRegistry];
|
|
23
23
|
|
|
24
|
+
export type AuthMode = "live" | "preview";
|
|
25
|
+
|
|
24
26
|
export interface AuthState {
|
|
25
27
|
isAuthenticated: boolean;
|
|
26
28
|
isPending: boolean;
|
|
27
29
|
profile: UserProfile | null;
|
|
28
30
|
providerData: ProviderData | null;
|
|
31
|
+
/** False when dev-portal backend is unavailable (local preview). Defaults true. */
|
|
32
|
+
isBackendAvailable: boolean;
|
|
33
|
+
/** Live when backend auth is reachable; preview otherwise. */
|
|
34
|
+
authMode: AuthMode;
|
|
29
35
|
setAuthenticationPending: () => void;
|
|
30
36
|
setLoggedOut: () => void;
|
|
31
37
|
setLoggedIn: (args: {
|
|
@@ -41,12 +47,16 @@ export const authState = create<AuthState>()(
|
|
|
41
47
|
isPending: true,
|
|
42
48
|
profile: null,
|
|
43
49
|
providerData: null,
|
|
50
|
+
isBackendAvailable: true,
|
|
51
|
+
authMode: "live",
|
|
44
52
|
setAuthenticationPending: () =>
|
|
45
53
|
set(() => ({
|
|
46
54
|
isAuthenticated: false,
|
|
47
55
|
isPending: false,
|
|
48
56
|
profile: null,
|
|
49
57
|
providerData: null,
|
|
58
|
+
isBackendAvailable: true,
|
|
59
|
+
authMode: "live",
|
|
50
60
|
})),
|
|
51
61
|
setLoggedOut: () =>
|
|
52
62
|
set(() => ({
|
|
@@ -54,6 +64,8 @@ export const authState = create<AuthState>()(
|
|
|
54
64
|
isPending: false,
|
|
55
65
|
profile: null,
|
|
56
66
|
providerData: null,
|
|
67
|
+
isBackendAvailable: true,
|
|
68
|
+
authMode: "live",
|
|
57
69
|
})),
|
|
58
70
|
setLoggedIn: ({ profile, providerData }) =>
|
|
59
71
|
set(() => ({
|
|
@@ -73,9 +73,9 @@ const ProfileMenu = () => {
|
|
|
73
73
|
const context = useZudoku();
|
|
74
74
|
const profileItems = context.getProfileMenuItems();
|
|
75
75
|
const auth = useAuth();
|
|
76
|
-
const { isAuthEnabled, isAuthenticated, profile } = auth;
|
|
76
|
+
const { isAuthEnabled, isAuthenticated, profile, isBackendAvailable } = auth;
|
|
77
77
|
|
|
78
|
-
if (!isAuthEnabled) return null;
|
|
78
|
+
if (!isAuthEnabled || !isBackendAvailable) return null;
|
|
79
79
|
|
|
80
80
|
return (
|
|
81
81
|
<ClientOnly fallback={<Skeleton className="rounded-sm h-5 w-24 mr-4" />}>
|
|
@@ -130,7 +130,8 @@ export const MobileTopNavigation = () => {
|
|
|
130
130
|
getProfileMenuItems,
|
|
131
131
|
} = context;
|
|
132
132
|
const headerNavigation = header?.navigation ?? [];
|
|
133
|
-
const { isAuthenticated, profile, isAuthEnabled } =
|
|
133
|
+
const { isAuthenticated, profile, isAuthEnabled, isBackendAvailable } =
|
|
134
|
+
authState;
|
|
134
135
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
135
136
|
|
|
136
137
|
const accountItems = getProfileMenuItems();
|
|
@@ -232,7 +233,7 @@ export const MobileTopNavigation = () => {
|
|
|
232
233
|
</div>
|
|
233
234
|
<div className="border-t shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.05)] px-4 pt-3 flex flex-col gap-2">
|
|
234
235
|
<div className="flex items-center justify-between">
|
|
235
|
-
{isAuthEnabled && (
|
|
236
|
+
{isAuthEnabled && isBackendAvailable && (
|
|
236
237
|
<ClientOnly
|
|
237
238
|
fallback={<Skeleton className="rounded-sm h-8 w-16" />}
|
|
238
239
|
>
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
useNavigate,
|
|
18
18
|
} from "react-router";
|
|
19
19
|
import { REASON_CODES } from "../../config/validators/reason-codes.js";
|
|
20
|
+
import { ProductionUnlockPage } from "../authentication/components/ProductionUnlockPage.js";
|
|
20
21
|
import { useAuth } from "../authentication/hook.js";
|
|
21
22
|
import { RenderContext } from "../components/context/RenderContext.js";
|
|
22
23
|
import { useZudoku } from "../components/context/ZudokuContext.js";
|
|
@@ -174,7 +175,16 @@ export const RouteGuard = () => {
|
|
|
174
175
|
return null;
|
|
175
176
|
}
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
if (
|
|
179
|
+
isProtectedRoute &&
|
|
180
|
+
needsToSignIn &&
|
|
181
|
+
auth.isAuthEnabled &&
|
|
182
|
+
!auth.isBackendAvailable
|
|
183
|
+
) {
|
|
184
|
+
return <ProductionUnlockPage />;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const showDialog = auth.isBackendAvailable && (needsToSignIn || isBlocked);
|
|
178
188
|
const redirectTo = isBlocked
|
|
179
189
|
? blocker.location.pathname + blocker.location.search
|
|
180
190
|
: location.pathname + location.search;
|