@lukoweb/apitogo 0.1.59 → 0.1.61
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 +23 -12
- package/dist/declarations/app/apitogoBrandTheme.d.ts +2 -0
- package/dist/declarations/config/validators/ZudokuConfig.d.ts +2 -0
- package/dist/declarations/index.d.ts +1 -0
- package/dist/declarations/lib/authentication/providers/dev-portal-auth-pages.d.ts +2 -1
- package/dist/declarations/lib/authentication/providers/dev-portal-demo.d.ts +30 -0
- package/dist/declarations/lib/authentication/providers/dev-portal.d.ts +6 -1
- package/dist/flat-config.d.ts +1 -0
- package/docs/configuration/authentication.md +16 -4
- package/docs/customization/colors-theme.mdx +2 -0
- package/package.json +1 -1
- package/src/app/apitogoBrandTheme.ts +46 -0
- package/src/app/defaultTheme.css +64 -64
- package/src/app/main.css +10 -0
- package/src/config/apitogo-config-core.ts +9 -0
- package/src/config/validators/ZudokuConfig.ts +1 -0
- package/src/index.ts +1 -0
- package/src/lib/authentication/hook.ts +9 -0
- package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +80 -9
- package/src/lib/authentication/providers/dev-portal-demo.ts +153 -0
- package/src/lib/authentication/providers/dev-portal.tsx +97 -2
- package/src/lib/core/RouteGuard.tsx +6 -4
- package/src/vite/plugin-theme.ts +13 -11
package/dist/cli/cli.js
CHANGED
|
@@ -419,6 +419,13 @@ function syncAuthentication(normalized) {
|
|
|
419
419
|
if (!Array.isArray(auth.providers)) {
|
|
420
420
|
auth.providers = [];
|
|
421
421
|
}
|
|
422
|
+
const topLevelDemoMode = normalized.demoMode;
|
|
423
|
+
if (auth.demoMode === void 0) {
|
|
424
|
+
auth.demoMode = topLevelDemoMode !== false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (normalized.demoMode !== void 0) {
|
|
428
|
+
auth.demoMode = normalized.demoMode !== false;
|
|
422
429
|
}
|
|
423
430
|
normalized.authentication = auth;
|
|
424
431
|
}
|
|
@@ -1600,7 +1607,7 @@ var init_shadcn_registry = __esm({
|
|
|
1600
1607
|
import { createRequire } from "node:module";
|
|
1601
1608
|
import path4 from "node:path";
|
|
1602
1609
|
import { normalizeTheme } from "shiki";
|
|
1603
|
-
var THEME_VARIABLES, uncamelize, processColorValue, generateCss, generateRegistryCss, processCustomCss, MAIN_REPLACE, DEFAULT_THEME_REPLACE, GOOGLE_FONTS, getGoogleFontUrl, processFont, processFontConfig, processFonts, MODULE_SOURCE_PACKAGES, resolveModuleSourceDirs, virtualModuleId, resolvedVirtualModuleId, viteThemePlugin;
|
|
1610
|
+
var THEME_VARIABLES, uncamelize, processColorValue, generateCss, generateRegistryCss, processCustomCss, MAIN_REPLACE, DEFAULT_THEME_REPLACE, GOOGLE_FONTS, getGoogleFontUrl, processFont, processFontConfig, DEFAULT_FONT_SANS, DEFAULT_FONT_MONO, DEFAULT_FONT_HEADING, processFonts, MODULE_SOURCE_PACKAGES, resolveModuleSourceDirs, virtualModuleId, resolvedVirtualModuleId, viteThemePlugin;
|
|
1604
1611
|
var init_plugin_theme = __esm({
|
|
1605
1612
|
"src/vite/plugin-theme.ts"() {
|
|
1606
1613
|
init_loader();
|
|
@@ -1712,12 +1719,16 @@ var init_plugin_theme = __esm({
|
|
|
1712
1719
|
fontFamily: fontConfig.fontFamily
|
|
1713
1720
|
};
|
|
1714
1721
|
};
|
|
1722
|
+
DEFAULT_FONT_SANS = '"IBM Plex Sans", system-ui, sans-serif';
|
|
1723
|
+
DEFAULT_FONT_MONO = '"JetBrains Mono", monospace';
|
|
1724
|
+
DEFAULT_FONT_HEADING = '"Space Grotesk", system-ui, sans-serif';
|
|
1715
1725
|
processFonts = async (themeConfig) => {
|
|
1716
1726
|
const imports = [];
|
|
1717
1727
|
const families = {
|
|
1718
|
-
sans:
|
|
1728
|
+
sans: DEFAULT_FONT_SANS,
|
|
1719
1729
|
serif: void 0,
|
|
1720
|
-
mono:
|
|
1730
|
+
mono: DEFAULT_FONT_MONO,
|
|
1731
|
+
heading: DEFAULT_FONT_HEADING
|
|
1721
1732
|
};
|
|
1722
1733
|
const userFonts = {
|
|
1723
1734
|
sans: processFontConfig(themeConfig?.fonts?.sans),
|
|
@@ -1754,18 +1765,16 @@ var init_plugin_theme = __esm({
|
|
|
1754
1765
|
} catch {
|
|
1755
1766
|
}
|
|
1756
1767
|
}
|
|
1757
|
-
families.sans = userFonts.sans.fontFamily || registryFonts.sans ||
|
|
1768
|
+
families.sans = userFonts.sans.fontFamily || registryFonts.sans || DEFAULT_FONT_SANS;
|
|
1758
1769
|
families.serif = userFonts.serif.fontFamily || registryFonts.serif;
|
|
1759
|
-
families.mono = userFonts.mono.fontFamily || registryFonts.mono ||
|
|
1770
|
+
families.mono = userFonts.mono.fontFamily || registryFonts.mono || DEFAULT_FONT_MONO;
|
|
1760
1771
|
if (!userFonts.sans.fontFamily && !registryFonts.sans) {
|
|
1761
|
-
imports.push(getGoogleFontUrl("
|
|
1762
|
-
}
|
|
1763
|
-
if (!userFonts.serif.fontFamily && !registryFonts.serif) {
|
|
1764
|
-
imports.push(getGoogleFontUrl("Playfair Display"));
|
|
1772
|
+
imports.push(getGoogleFontUrl("IBM Plex Sans"));
|
|
1765
1773
|
}
|
|
1766
1774
|
if (!userFonts.mono.fontFamily && !registryFonts.mono) {
|
|
1767
|
-
imports.push(getGoogleFontUrl("
|
|
1775
|
+
imports.push(getGoogleFontUrl("JetBrains Mono"));
|
|
1768
1776
|
}
|
|
1777
|
+
imports.push(getGoogleFontUrl("Space Grotesk"));
|
|
1769
1778
|
return { imports, families };
|
|
1770
1779
|
};
|
|
1771
1780
|
MODULE_SOURCE_PACKAGES = [
|
|
@@ -1882,7 +1891,8 @@ ${generateCss(themeConfig.dark)}
|
|
|
1882
1891
|
}
|
|
1883
1892
|
const rootVars = [
|
|
1884
1893
|
` --font-sans: ${fonts.families.sans};`,
|
|
1885
|
-
` --font-mono: ${fonts.families.mono}
|
|
1894
|
+
` --font-mono: ${fonts.families.mono};`,
|
|
1895
|
+
` --font-heading: ${fonts.families.heading};`
|
|
1886
1896
|
];
|
|
1887
1897
|
if (fonts.families.serif) {
|
|
1888
1898
|
rootVars.push(` --font-serif: ${fonts.families.serif};`);
|
|
@@ -4575,6 +4585,7 @@ var init_ZudokuConfig = __esm({
|
|
|
4575
4585
|
z9.object({
|
|
4576
4586
|
type: z9.literal("dev-portal").optional().default("dev-portal"),
|
|
4577
4587
|
apiBaseUrl: z9.string().optional(),
|
|
4588
|
+
demoMode: z9.boolean().optional(),
|
|
4578
4589
|
native: z9.object({
|
|
4579
4590
|
enabled: z9.boolean().optional()
|
|
4580
4591
|
}).optional(),
|
|
@@ -5227,7 +5238,7 @@ import {
|
|
|
5227
5238
|
// package.json
|
|
5228
5239
|
var package_default = {
|
|
5229
5240
|
name: "@lukoweb/apitogo",
|
|
5230
|
-
version: "0.1.
|
|
5241
|
+
version: "0.1.61",
|
|
5231
5242
|
type: "module",
|
|
5232
5243
|
sideEffects: [
|
|
5233
5244
|
"**/*.css",
|
|
@@ -369,6 +369,7 @@ declare const AuthenticationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
369
369
|
}, z.core.$strip>, z.ZodObject<{
|
|
370
370
|
type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"dev-portal">>>;
|
|
371
371
|
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
372
|
+
demoMode: z.ZodOptional<z.ZodBoolean>;
|
|
372
373
|
native: z.ZodOptional<z.ZodObject<{
|
|
373
374
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
374
375
|
}, z.core.$strip>>;
|
|
@@ -6790,6 +6791,7 @@ export declare const ZudokuConfig: z.ZodObject<{
|
|
|
6790
6791
|
}, z.core.$strip>, z.ZodObject<{
|
|
6791
6792
|
type: z.ZodDefault<z.ZodOptional<z.ZodLiteral<"dev-portal">>>;
|
|
6792
6793
|
apiBaseUrl: z.ZodOptional<z.ZodString>;
|
|
6794
|
+
demoMode: z.ZodOptional<z.ZodBoolean>;
|
|
6793
6795
|
native: z.ZodOptional<z.ZodObject<{
|
|
6794
6796
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
6795
6797
|
}, z.core.$strip>>;
|
|
@@ -17,5 +17,6 @@ export { defaultLanguages } from "./lib/shiki.js";
|
|
|
17
17
|
export { cn } from "./lib/ui/util.js";
|
|
18
18
|
export { joinUrl } from "./lib/util/joinUrl.js";
|
|
19
19
|
export { type ProblemJson, throwIfProblemJson, } from "./lib/util/problemJson.js";
|
|
20
|
+
export { apitogoBrandTheme } from "./app/apitogoBrandTheme.js";
|
|
20
21
|
export { buildApitogoConfig } from "./config/build-apitogo-config.js";
|
|
21
22
|
export { APITOGO_CONFIG_FILENAME, applyEnvOverrides, extractManifest, extractSiteConfig, normalizeApitogoConfig, resolveProjectDisplayName, } from "./config/apitogo-config-core.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export declare function DevPortalLoginPage({ apiBaseUrl }: {
|
|
1
|
+
export declare function DevPortalLoginPage({ apiBaseUrl, demoMode, }: {
|
|
2
2
|
apiBaseUrl?: string;
|
|
3
|
+
demoMode?: boolean;
|
|
3
4
|
}): import("react/jsx-runtime").JSX.Element;
|
|
4
5
|
export declare function DevPortalRegisterPage({ apiBaseUrl }: {
|
|
5
6
|
apiBaseUrl?: string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import type { UserProfile } from "../state.js";
|
|
3
|
+
import type { EndUserMeResponse } from "./dev-portal-constants.js";
|
|
4
|
+
export declare const DEMO_EMAIL = "demo@apitogo.local";
|
|
5
|
+
export declare const DEMO_PASSWORD = "password";
|
|
6
|
+
export declare const DEMO_USER_ID = "00000000-0000-0000-0000-000000000001";
|
|
7
|
+
export declare const DEMO_DISPLAY_NAME = "Demo User";
|
|
8
|
+
export declare const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
|
|
9
|
+
export type DemoPlanInput = {
|
|
10
|
+
sku: string;
|
|
11
|
+
displayName?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveDemoMode(config: Pick<DevPortalAuthenticationConfig, "demoMode">): boolean;
|
|
14
|
+
export declare function isDemoCredential(email: string, password: string): boolean;
|
|
15
|
+
export declare function createDemoProfile(): UserProfile;
|
|
16
|
+
export declare function resolveDemoPlanSku(plans: DemoPlanInput[] | undefined): string;
|
|
17
|
+
export declare function createDemoMeResponse(plans?: DemoPlanInput[]): EndUserMeResponse;
|
|
18
|
+
export type DemoCredentialsResponse = {
|
|
19
|
+
idToken: string;
|
|
20
|
+
accessToken: string | null;
|
|
21
|
+
subscriptionKey: string;
|
|
22
|
+
gatewayBaseUrl: string | null;
|
|
23
|
+
planSku: string;
|
|
24
|
+
expiresAt: string | null;
|
|
25
|
+
};
|
|
26
|
+
export declare function createDemoCredentials(plans?: DemoPlanInput[]): DemoCredentialsResponse;
|
|
27
|
+
export declare function readDemoSession(): boolean;
|
|
28
|
+
export declare function writeDemoSession(): void;
|
|
29
|
+
export declare function clearDemoSession(): void;
|
|
30
|
+
export declare function activateDemoSession(apiBaseUrl?: string): void;
|
|
@@ -7,6 +7,8 @@ export type DevPortalProviderData = {
|
|
|
7
7
|
type: "dev-portal";
|
|
8
8
|
apiBaseUrl: string;
|
|
9
9
|
authMode: DevPortalAuthMode;
|
|
10
|
+
isDemoSession?: boolean;
|
|
11
|
+
demoMode?: boolean;
|
|
10
12
|
};
|
|
11
13
|
declare module "../state.js" {
|
|
12
14
|
interface ProviderDataRegistry {
|
|
@@ -15,6 +17,7 @@ declare module "../state.js" {
|
|
|
15
17
|
}
|
|
16
18
|
export declare class DevPortalAuthenticationProvider extends CoreAuthenticationPlugin implements AuthenticationPlugin {
|
|
17
19
|
private readonly apiBaseUrl;
|
|
20
|
+
private readonly demoMode;
|
|
18
21
|
private readonly redirectToAfterSignIn;
|
|
19
22
|
private readonly redirectToAfterSignUp;
|
|
20
23
|
private readonly redirectToAfterSignOut;
|
|
@@ -22,7 +25,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
|
|
|
22
25
|
private refreshGeneration;
|
|
23
26
|
private refreshInFlight;
|
|
24
27
|
private authConfigNativeEnabled;
|
|
25
|
-
constructor({ apiBaseUrl, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
|
|
28
|
+
constructor({ apiBaseUrl, demoMode, redirectToAfterSignIn, redirectToAfterSignUp, redirectToAfterSignOut, }: DevPortalAuthenticationConfig);
|
|
26
29
|
getRoutes(): {
|
|
27
30
|
path: string;
|
|
28
31
|
element: import("react/jsx-runtime").JSX.Element;
|
|
@@ -31,6 +34,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
|
|
|
31
34
|
onPageLoad: () => Promise<void>;
|
|
32
35
|
private syncBackendAvailability;
|
|
33
36
|
private setPreviewState;
|
|
37
|
+
private setDemoAuthenticatedState;
|
|
34
38
|
private ensureConfiguredBackend;
|
|
35
39
|
refreshUserProfile(): Promise<boolean>;
|
|
36
40
|
private refreshUserProfileInternal;
|
|
@@ -39,6 +43,7 @@ export declare class DevPortalAuthenticationProvider extends CoreAuthenticationP
|
|
|
39
43
|
signOut(_: AuthActionContext): Promise<void>;
|
|
40
44
|
hasDistinctSignUpFlow(): boolean;
|
|
41
45
|
signRequest(request: Request): Promise<Request>;
|
|
46
|
+
completeDemoLogin(): void;
|
|
42
47
|
}
|
|
43
48
|
declare const devPortalAuth: AuthenticationProviderInitializer<DevPortalAuthenticationConfig>;
|
|
44
49
|
export default devPortalAuth;
|
package/dist/flat-config.d.ts
CHANGED
|
@@ -47,10 +47,22 @@ VITE_APITOGO_AUTHENTICATION_API_BASE_URL=https://your-dev-portal-api.example.com
|
|
|
47
47
|
You can also set `authentication.apiBaseUrl` in `apitogo.config.json` or
|
|
48
48
|
`APITOGO_CONFIG_authentication__apiBaseUrl` in `.env`.
|
|
49
49
|
|
|
50
|
-
**Local
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
**Local demo mode:** In development (`npm run dev`), `demoMode` is enabled by default when the user
|
|
51
|
+
panel is on. You do not need `VITE_APITOGO_AUTHENTICATION_API_BASE_URL`, OIDC, or a running
|
|
52
|
+
dev-portal backend to explore protected account pages locally. Sign in at `/login` with:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
demo@apitogo.local / password
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Disable with `APITOGO_CONFIG_demoMode=false` in `.env` or `"demoMode": false` in
|
|
59
|
+
`apitogo.config.json`. Demo mode is never active in production builds.
|
|
60
|
+
|
|
61
|
+
**Local preview (demo off):** When the backend URL is a placeholder or unreachable and demo mode is
|
|
62
|
+
disabled, the account area shows a message that sign-in unlocks after publish. Docs and API
|
|
63
|
+
reference remain public. Plans on `/pricing` load from `apitogo.config.json` (overridable via `.env`
|
|
64
|
+
— see [Manifest](./manifest.md), including `includedActionKeys` per plan). Plan edits hot-reload
|
|
65
|
+
without restarting the dev server.
|
|
54
66
|
|
|
55
67
|
### Dev portal configuration files
|
|
56
68
|
|
|
@@ -24,6 +24,8 @@ The theme system is built on [shadcn/ui theming](https://ui.shadcn.com/docs/them
|
|
|
24
24
|
- **Tailwind v4** CSS variable system for modern styling
|
|
25
25
|
- **Theme editors** like [tweakcn](https://tweakcn.com/) work out of the box
|
|
26
26
|
- **Shadcn registries** are supported
|
|
27
|
+
- **Landing homepage**, **portal footer**, and **MCP-generated custom pages** all use the same
|
|
28
|
+
semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`, etc.)
|
|
27
29
|
|
|
28
30
|
## Custom Colors
|
|
29
31
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ZudokuConfig } from "../config/validators/ZudokuConfig.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* APIToGo brand theme for per-site overrides via apitogo.config.
|
|
5
|
+
* Keep in sync with defaultTheme.css (full palette including sidebar/chart tokens).
|
|
6
|
+
*/
|
|
7
|
+
export const apitogoBrandTheme: NonNullable<ZudokuConfig["theme"]> = {
|
|
8
|
+
fonts: {
|
|
9
|
+
sans: "IBM Plex Sans",
|
|
10
|
+
mono: "JetBrains Mono",
|
|
11
|
+
},
|
|
12
|
+
light: {
|
|
13
|
+
background: "#F5F6F9",
|
|
14
|
+
foreground: "#0E1016",
|
|
15
|
+
card: "#FFFFFF",
|
|
16
|
+
cardForeground: "#0E1016",
|
|
17
|
+
primary: "#E11D2A",
|
|
18
|
+
primaryForeground: "#ffffff",
|
|
19
|
+
secondary: "#FAFBFC",
|
|
20
|
+
secondaryForeground: "#0E1016",
|
|
21
|
+
muted: "#FAFBFC",
|
|
22
|
+
mutedForeground: "#565E70",
|
|
23
|
+
accent: "rgba(225, 29, 42, 0.08)",
|
|
24
|
+
accentForeground: "#E11D2A",
|
|
25
|
+
border: "#E6E8EF",
|
|
26
|
+
input: "#D6DAE3",
|
|
27
|
+
ring: "#E11D2A",
|
|
28
|
+
},
|
|
29
|
+
dark: {
|
|
30
|
+
background: "#0B0D12",
|
|
31
|
+
foreground: "#EDEFF5",
|
|
32
|
+
card: "#14171F",
|
|
33
|
+
cardForeground: "#EDEFF5",
|
|
34
|
+
primary: "#F23645",
|
|
35
|
+
primaryForeground: "#ffffff",
|
|
36
|
+
secondary: "#191D27",
|
|
37
|
+
secondaryForeground: "#EDEFF5",
|
|
38
|
+
muted: "#191D27",
|
|
39
|
+
mutedForeground: "#A6AEBF",
|
|
40
|
+
accent: "rgba(242, 54, 69, 0.13)",
|
|
41
|
+
accentForeground: "#F23645",
|
|
42
|
+
border: "#252B37",
|
|
43
|
+
input: "#333A48",
|
|
44
|
+
ring: "#F23645",
|
|
45
|
+
},
|
|
46
|
+
};
|
package/src/app/defaultTheme.css
CHANGED
|
@@ -1,77 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Default theme for
|
|
3
|
-
*
|
|
2
|
+
* Default APIToGo brand theme for scaffolded sites.
|
|
3
|
+
* Color tokens sourced from webapp-template/APIToGo.dc.html (:root dark + light override).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
:root {
|
|
7
7
|
--radius: 0.625rem;
|
|
8
|
-
--background:
|
|
9
|
-
--foreground:
|
|
10
|
-
--card:
|
|
11
|
-
--card-foreground:
|
|
12
|
-
--popover:
|
|
13
|
-
--popover-foreground:
|
|
14
|
-
--primary:
|
|
15
|
-
--primary-foreground:
|
|
16
|
-
--secondary:
|
|
17
|
-
--secondary-foreground:
|
|
18
|
-
--muted:
|
|
19
|
-
--muted-foreground:
|
|
20
|
-
--accent:
|
|
21
|
-
--accent-foreground:
|
|
22
|
-
--destructive:
|
|
8
|
+
--background: #f5f6f9;
|
|
9
|
+
--foreground: #0e1016;
|
|
10
|
+
--card: #ffffff;
|
|
11
|
+
--card-foreground: #0e1016;
|
|
12
|
+
--popover: #ffffff;
|
|
13
|
+
--popover-foreground: #0e1016;
|
|
14
|
+
--primary: #e11d2a;
|
|
15
|
+
--primary-foreground: #ffffff;
|
|
16
|
+
--secondary: #fafbfc;
|
|
17
|
+
--secondary-foreground: #0e1016;
|
|
18
|
+
--muted: #fafbfc;
|
|
19
|
+
--muted-foreground: #565e70;
|
|
20
|
+
--accent: rgba(225, 29, 42, 0.08);
|
|
21
|
+
--accent-foreground: #e11d2a;
|
|
22
|
+
--destructive: #e11d2a;
|
|
23
23
|
--warning: oklch(0.84 0.16 84);
|
|
24
24
|
--warning-foreground: oklch(0.47 0.14 46.2);
|
|
25
|
-
--border:
|
|
26
|
-
--input:
|
|
27
|
-
--ring:
|
|
28
|
-
--chart-1:
|
|
29
|
-
--chart-2:
|
|
30
|
-
--chart-3:
|
|
31
|
-
--chart-4:
|
|
32
|
-
--chart-5:
|
|
33
|
-
--sidebar:
|
|
34
|
-
--sidebar-foreground:
|
|
35
|
-
--sidebar-primary:
|
|
36
|
-
--sidebar-primary-foreground:
|
|
37
|
-
--sidebar-accent:
|
|
38
|
-
--sidebar-accent-foreground:
|
|
39
|
-
--sidebar-border:
|
|
40
|
-
--sidebar-ring:
|
|
25
|
+
--border: #e6e8ef;
|
|
26
|
+
--input: #d6dae3;
|
|
27
|
+
--ring: #e11d2a;
|
|
28
|
+
--chart-1: #e11d2a;
|
|
29
|
+
--chart-2: #0e9e63;
|
|
30
|
+
--chart-3: #4e5ac0;
|
|
31
|
+
--chart-4: #8a92a3;
|
|
32
|
+
--chart-5: #565e70;
|
|
33
|
+
--sidebar: #ffffff;
|
|
34
|
+
--sidebar-foreground: #0e1016;
|
|
35
|
+
--sidebar-primary: #e11d2a;
|
|
36
|
+
--sidebar-primary-foreground: #ffffff;
|
|
37
|
+
--sidebar-accent: #fafbfc;
|
|
38
|
+
--sidebar-accent-foreground: #0e1016;
|
|
39
|
+
--sidebar-border: #e6e8ef;
|
|
40
|
+
--sidebar-ring: #e11d2a;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.dark {
|
|
44
|
-
--background:
|
|
45
|
-
--foreground:
|
|
46
|
-
--card:
|
|
47
|
-
--card-foreground:
|
|
48
|
-
--popover:
|
|
49
|
-
--popover-foreground:
|
|
50
|
-
--primary:
|
|
51
|
-
--primary-foreground:
|
|
52
|
-
--secondary:
|
|
53
|
-
--secondary-foreground:
|
|
54
|
-
--muted:
|
|
55
|
-
--muted-foreground:
|
|
56
|
-
--accent:
|
|
57
|
-
--accent-foreground:
|
|
58
|
-
--destructive:
|
|
44
|
+
--background: #0b0d12;
|
|
45
|
+
--foreground: #edeff5;
|
|
46
|
+
--card: #14171f;
|
|
47
|
+
--card-foreground: #edeff5;
|
|
48
|
+
--popover: #1d222d;
|
|
49
|
+
--popover-foreground: #edeff5;
|
|
50
|
+
--primary: #f23645;
|
|
51
|
+
--primary-foreground: #ffffff;
|
|
52
|
+
--secondary: #191d27;
|
|
53
|
+
--secondary-foreground: #edeff5;
|
|
54
|
+
--muted: #191d27;
|
|
55
|
+
--muted-foreground: #a6aebf;
|
|
56
|
+
--accent: rgba(242, 54, 69, 0.13);
|
|
57
|
+
--accent-foreground: #f23645;
|
|
58
|
+
--destructive: #f23645;
|
|
59
59
|
--warning: oklch(0.41 0.11 46);
|
|
60
60
|
--warning-foreground: oklch(0.99 0.02 95);
|
|
61
|
-
--border:
|
|
62
|
-
--input:
|
|
63
|
-
--ring:
|
|
64
|
-
--chart-1:
|
|
65
|
-
--chart-2:
|
|
66
|
-
--chart-3:
|
|
67
|
-
--chart-4:
|
|
68
|
-
--chart-5:
|
|
69
|
-
--sidebar:
|
|
70
|
-
--sidebar-foreground:
|
|
71
|
-
--sidebar-primary:
|
|
72
|
-
--sidebar-primary-foreground:
|
|
73
|
-
--sidebar-accent:
|
|
74
|
-
--sidebar-accent-foreground:
|
|
75
|
-
--sidebar-border:
|
|
76
|
-
--sidebar-ring:
|
|
61
|
+
--border: #252b37;
|
|
62
|
+
--input: #333a48;
|
|
63
|
+
--ring: #f23645;
|
|
64
|
+
--chart-1: #f23645;
|
|
65
|
+
--chart-2: #16c784;
|
|
66
|
+
--chart-3: #aeb9e8;
|
|
67
|
+
--chart-4: #6b7385;
|
|
68
|
+
--chart-5: #a6aebf;
|
|
69
|
+
--sidebar: #0e1119;
|
|
70
|
+
--sidebar-foreground: #edeff5;
|
|
71
|
+
--sidebar-primary: #f23645;
|
|
72
|
+
--sidebar-primary-foreground: #ffffff;
|
|
73
|
+
--sidebar-accent: #191d27;
|
|
74
|
+
--sidebar-accent-foreground: #edeff5;
|
|
75
|
+
--sidebar-border: #252b37;
|
|
76
|
+
--sidebar-ring: #f23645;
|
|
77
77
|
}
|
package/src/app/main.css
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
--font-sans: var(--font-sans);
|
|
50
50
|
--font-mono: var(--font-mono);
|
|
51
51
|
--font-serif: var(--font-serif);
|
|
52
|
+
--font-heading: var(--font-heading);
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
@utility prose {
|
|
@@ -161,6 +162,15 @@
|
|
|
161
162
|
"calt" 1;
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
h1,
|
|
166
|
+
h2,
|
|
167
|
+
h3,
|
|
168
|
+
h4,
|
|
169
|
+
h5,
|
|
170
|
+
h6 {
|
|
171
|
+
font-family: var(--font-heading);
|
|
172
|
+
}
|
|
173
|
+
|
|
164
174
|
::selection {
|
|
165
175
|
background: var(--color-primary);
|
|
166
176
|
color: var(--color-primary-foreground);
|
|
@@ -452,6 +452,15 @@ function syncAuthentication(normalized: ApitogoJsonConfig): void {
|
|
|
452
452
|
if (!Array.isArray(auth.providers)) {
|
|
453
453
|
auth.providers = [];
|
|
454
454
|
}
|
|
455
|
+
|
|
456
|
+
const topLevelDemoMode = normalized.demoMode;
|
|
457
|
+
if (auth.demoMode === undefined) {
|
|
458
|
+
auth.demoMode = topLevelDemoMode !== false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (normalized.demoMode !== undefined) {
|
|
463
|
+
auth.demoMode = normalized.demoMode !== false;
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
normalized.authentication = auth;
|
|
@@ -494,6 +494,7 @@ const AuthenticationSchema = z.discriminatedUnion("type", [
|
|
|
494
494
|
z.object({
|
|
495
495
|
type: z.literal("dev-portal").optional().default("dev-portal"),
|
|
496
496
|
apiBaseUrl: z.string().optional(),
|
|
497
|
+
demoMode: z.boolean().optional(),
|
|
497
498
|
native: z
|
|
498
499
|
.object({
|
|
499
500
|
enabled: z.boolean().optional(),
|
package/src/index.ts
CHANGED
|
@@ -71,6 +71,7 @@ export {
|
|
|
71
71
|
type ProblemJson,
|
|
72
72
|
throwIfProblemJson,
|
|
73
73
|
} from "./lib/util/problemJson.js";
|
|
74
|
+
export { apitogoBrandTheme } from "./app/apitogoBrandTheme.js";
|
|
74
75
|
export { buildApitogoConfig } from "./config/build-apitogo-config.js";
|
|
75
76
|
export {
|
|
76
77
|
APITOGO_CONFIG_FILENAME,
|
|
@@ -74,11 +74,20 @@ export const useAuth = () => {
|
|
|
74
74
|
|
|
75
75
|
useRefreshUserProfile();
|
|
76
76
|
|
|
77
|
+
const providerData = authState.providerData as
|
|
78
|
+
| { isDemoSession?: boolean; demoMode?: boolean }
|
|
79
|
+
| null
|
|
80
|
+
| undefined;
|
|
81
|
+
const isDemoSession = providerData?.isDemoSession === true;
|
|
82
|
+
const isDemoMode = providerData?.demoMode === true;
|
|
83
|
+
|
|
77
84
|
return {
|
|
78
85
|
isAuthEnabled,
|
|
79
86
|
...authState,
|
|
80
87
|
isBackendAvailable: authState.isBackendAvailable,
|
|
81
88
|
authMode: authState.authMode,
|
|
89
|
+
isDemoSession,
|
|
90
|
+
isDemoMode,
|
|
82
91
|
hasDistinctSignUpFlow: authentication?.hasDistinctSignUpFlow?.() ?? true,
|
|
83
92
|
|
|
84
93
|
login: async (options?: AuthActionOptions) => {
|
|
@@ -12,10 +12,17 @@ import { Label } from "@lukoweb/apitogo/ui/Label.js";
|
|
|
12
12
|
import { useCallback, useEffect, useState } from "react";
|
|
13
13
|
import { Link, useSearchParams } from "react-router";
|
|
14
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";
|
|
15
21
|
import {
|
|
16
22
|
buildDevPortalOidcChallengeUrl,
|
|
17
23
|
fetchDevPortalAuthConfig,
|
|
18
24
|
isNativeAuthEnabled,
|
|
25
|
+
isPlaceholderDevPortalApiUrl,
|
|
19
26
|
postDevPortalAuth,
|
|
20
27
|
resolveDevPortalApiBaseUrl,
|
|
21
28
|
toAbsoluteReturnUrl,
|
|
@@ -100,20 +107,60 @@ function OidcProviderButtons({
|
|
|
100
107
|
);
|
|
101
108
|
}
|
|
102
109
|
|
|
103
|
-
|
|
110
|
+
function DemoLoginHint() {
|
|
111
|
+
return (
|
|
112
|
+
<div className="rounded-lg border border-dashed bg-muted/30 p-3 text-sm text-muted-foreground">
|
|
113
|
+
<p className="font-medium text-foreground">Local demo sign-in</p>
|
|
114
|
+
<p className="mt-1">
|
|
115
|
+
Use <code className="rounded bg-muted px-1 py-0.5">{DEMO_EMAIL}</code>{" "}
|
|
116
|
+
with password{" "}
|
|
117
|
+
<code className="rounded bg-muted px-1 py-0.5">{DEMO_PASSWORD}</code> to
|
|
118
|
+
explore the account area without a dev-portal backend.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function DevPortalLoginPage({
|
|
125
|
+
apiBaseUrl,
|
|
126
|
+
demoMode = false,
|
|
127
|
+
}: {
|
|
128
|
+
apiBaseUrl?: string;
|
|
129
|
+
demoMode?: boolean;
|
|
130
|
+
}) {
|
|
104
131
|
const baseUrl = useApiBaseUrl(apiBaseUrl);
|
|
105
132
|
const returnUrl = useReturnUrl();
|
|
106
133
|
const [config, setConfig] = useState<DevPortalAuthConfigResponse | null>(
|
|
107
134
|
null,
|
|
108
135
|
);
|
|
109
|
-
const [configLoading, setConfigLoading] = useState(
|
|
136
|
+
const [configLoading, setConfigLoading] = useState(
|
|
137
|
+
Boolean(baseUrl) && !isPlaceholderDevPortalApiUrl(baseUrl),
|
|
138
|
+
);
|
|
110
139
|
const [configError, setConfigError] = useState<string | null>(null);
|
|
140
|
+
const [useDemoFallback, setUseDemoFallback] = useState(
|
|
141
|
+
demoMode && (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)),
|
|
142
|
+
);
|
|
143
|
+
const demoAuthAvailable = demoMode && useDemoFallback;
|
|
111
144
|
const [email, setEmail] = useState("");
|
|
112
145
|
const [password, setPassword] = useState("");
|
|
113
146
|
const [error, setError] = useState<string | null>(null);
|
|
114
147
|
const [pending, setPending] = useState(false);
|
|
115
148
|
|
|
116
149
|
useEffect(() => {
|
|
150
|
+
if (!demoMode) {
|
|
151
|
+
setUseDemoFallback(false);
|
|
152
|
+
} else if (!baseUrl || isPlaceholderDevPortalApiUrl(baseUrl)) {
|
|
153
|
+
setUseDemoFallback(true);
|
|
154
|
+
}
|
|
155
|
+
}, [baseUrl, demoMode]);
|
|
156
|
+
|
|
157
|
+
useEffect(() => {
|
|
158
|
+
if (demoAuthAvailable) {
|
|
159
|
+
setConfig({ native: { enabled: true }, email: null, providers: [] });
|
|
160
|
+
setConfigLoading(false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
117
164
|
if (!baseUrl) return;
|
|
118
165
|
setConfigLoading(true);
|
|
119
166
|
setConfigError(null);
|
|
@@ -123,20 +170,41 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
|
123
170
|
setConfigLoading(false);
|
|
124
171
|
})
|
|
125
172
|
.catch(() => {
|
|
173
|
+
if (demoMode) {
|
|
174
|
+
setUseDemoFallback(true);
|
|
175
|
+
setConfig({ native: { enabled: true }, email: null, providers: [] });
|
|
176
|
+
setConfigLoading(false);
|
|
177
|
+
setConfigError(null);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
126
181
|
setConfig(null);
|
|
127
182
|
setConfigLoading(false);
|
|
128
183
|
setConfigError(
|
|
129
184
|
`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.`,
|
|
130
185
|
);
|
|
131
186
|
});
|
|
132
|
-
}, [baseUrl]);
|
|
187
|
+
}, [baseUrl, demoAuthAvailable, demoMode]);
|
|
133
188
|
|
|
134
189
|
const onSubmit = useCallback(
|
|
135
190
|
async (event: React.FormEvent) => {
|
|
136
191
|
event.preventDefault();
|
|
137
|
-
if (!baseUrl) return;
|
|
138
192
|
setPending(true);
|
|
139
193
|
setError(null);
|
|
194
|
+
|
|
195
|
+
if (demoAuthAvailable) {
|
|
196
|
+
if (!isDemoCredential(email, password)) {
|
|
197
|
+
setError("Invalid demo credentials.");
|
|
198
|
+
setPending(false);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
activateDemoSession(baseUrl);
|
|
203
|
+
window.location.assign(toAbsoluteReturnUrl(returnUrl));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!baseUrl) return;
|
|
140
208
|
try {
|
|
141
209
|
await postDevPortalAuth(baseUrl, "/api/v1/auth/login", {
|
|
142
210
|
email,
|
|
@@ -149,10 +217,10 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
|
149
217
|
setPending(false);
|
|
150
218
|
}
|
|
151
219
|
},
|
|
152
|
-
[baseUrl, email, password, returnUrl],
|
|
220
|
+
[baseUrl, demoAuthAvailable, email, password, returnUrl],
|
|
153
221
|
);
|
|
154
222
|
|
|
155
|
-
if (!baseUrl) {
|
|
223
|
+
if (!baseUrl && !demoAuthAvailable) {
|
|
156
224
|
return (
|
|
157
225
|
<AuthPageShell
|
|
158
226
|
title="Sign in"
|
|
@@ -167,7 +235,8 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
|
167
235
|
|
|
168
236
|
return (
|
|
169
237
|
<AuthPageShell title="Sign in" description="Access your developer account.">
|
|
170
|
-
{
|
|
238
|
+
{demoAuthAvailable ? <DemoLoginHint /> : null}
|
|
239
|
+
{isNativeAuthEnabled(config) || demoAuthAvailable ? (
|
|
171
240
|
<form className="space-y-4" onSubmit={onSubmit}>
|
|
172
241
|
<div className="space-y-2">
|
|
173
242
|
<Label htmlFor="email">Email</Label>
|
|
@@ -211,12 +280,14 @@ export function DevPortalLoginPage({ apiBaseUrl }: { apiBaseUrl?: string }) {
|
|
|
211
280
|
</div>
|
|
212
281
|
</form>
|
|
213
282
|
) : null}
|
|
214
|
-
{configError
|
|
283
|
+
{configError && !demoAuthAvailable ? (
|
|
284
|
+
<AuthError message={configError} />
|
|
285
|
+
) : null}
|
|
215
286
|
{configLoading ? (
|
|
216
287
|
<div className="flex justify-center">
|
|
217
288
|
<Spinner />
|
|
218
289
|
</div>
|
|
219
|
-
) : config ? (
|
|
290
|
+
) : config && !demoAuthAvailable ? (
|
|
220
291
|
<>
|
|
221
292
|
<OidcProviderButtons
|
|
222
293
|
apiBaseUrl={baseUrl}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { DevPortalAuthenticationConfig } from "../../../config/config.js";
|
|
2
|
+
import type { UserProfile } from "../state.js";
|
|
3
|
+
import { useAuthState } from "../state.js";
|
|
4
|
+
import type { EndUserMeResponse } from "./dev-portal-constants.js";
|
|
5
|
+
|
|
6
|
+
export const DEMO_EMAIL = "demo@apitogo.local";
|
|
7
|
+
export const DEMO_PASSWORD = "password";
|
|
8
|
+
export const DEMO_USER_ID = "00000000-0000-0000-0000-000000000001";
|
|
9
|
+
export const DEMO_DISPLAY_NAME = "Demo User";
|
|
10
|
+
export const DEMO_SUBSCRIPTION_KEY = "demo-subscription-key";
|
|
11
|
+
|
|
12
|
+
const DEMO_SESSION_STORAGE_KEY = "apitogo-demo-session";
|
|
13
|
+
|
|
14
|
+
export type DemoPlanInput = {
|
|
15
|
+
sku: string;
|
|
16
|
+
displayName?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function resolveDemoMode(
|
|
20
|
+
config: Pick<DevPortalAuthenticationConfig, "demoMode">,
|
|
21
|
+
): boolean {
|
|
22
|
+
if (typeof import.meta !== "undefined" && !import.meta.env.DEV) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return config.demoMode !== false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isDemoCredential(email: string, password: string): boolean {
|
|
30
|
+
return (
|
|
31
|
+
email.trim().toLowerCase() === DEMO_EMAIL && password === DEMO_PASSWORD
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function createDemoProfile(): UserProfile {
|
|
36
|
+
return {
|
|
37
|
+
sub: DEMO_USER_ID,
|
|
38
|
+
email: DEMO_EMAIL,
|
|
39
|
+
emailVerified: true,
|
|
40
|
+
name: DEMO_DISPLAY_NAME,
|
|
41
|
+
pictureUrl: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveDemoPlanSku(plans: DemoPlanInput[] | undefined): string {
|
|
46
|
+
const first = plans?.[0];
|
|
47
|
+
return first?.sku ?? "free";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createDemoMeResponse(
|
|
51
|
+
plans?: DemoPlanInput[],
|
|
52
|
+
): EndUserMeResponse {
|
|
53
|
+
const planSku = resolveDemoPlanSku(plans);
|
|
54
|
+
const periodEnd = new Date();
|
|
55
|
+
periodEnd.setMonth(periodEnd.getMonth() + 1);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
userId: DEMO_USER_ID,
|
|
59
|
+
displayName: DEMO_DISPLAY_NAME,
|
|
60
|
+
email: DEMO_EMAIL,
|
|
61
|
+
emailVerified: true,
|
|
62
|
+
subscriptions: [
|
|
63
|
+
{
|
|
64
|
+
planSku,
|
|
65
|
+
status: "Active",
|
|
66
|
+
entitlementState: "active",
|
|
67
|
+
currentPeriodEnd: periodEnd.toISOString(),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type DemoCredentialsResponse = {
|
|
74
|
+
idToken: string;
|
|
75
|
+
accessToken: string | null;
|
|
76
|
+
subscriptionKey: string;
|
|
77
|
+
gatewayBaseUrl: string | null;
|
|
78
|
+
planSku: string;
|
|
79
|
+
expiresAt: string | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export function createDemoCredentials(
|
|
83
|
+
plans?: DemoPlanInput[],
|
|
84
|
+
): DemoCredentialsResponse {
|
|
85
|
+
const planSku = resolveDemoPlanSku(plans);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
idToken: "demo-id-token",
|
|
89
|
+
accessToken: null,
|
|
90
|
+
subscriptionKey: DEMO_SUBSCRIPTION_KEY,
|
|
91
|
+
gatewayBaseUrl: null,
|
|
92
|
+
planSku,
|
|
93
|
+
expiresAt: null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function readDemoSession(): boolean {
|
|
98
|
+
if (typeof window === "undefined") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
return localStorage.getItem(DEMO_SESSION_STORAGE_KEY) === "1";
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function writeDemoSession(): void {
|
|
110
|
+
if (typeof window === "undefined") {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
localStorage.setItem(DEMO_SESSION_STORAGE_KEY, "1");
|
|
116
|
+
} catch {
|
|
117
|
+
// ignore quota / privacy errors
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function clearDemoSession(): void {
|
|
122
|
+
if (typeof window === "undefined") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
localStorage.removeItem(DEMO_SESSION_STORAGE_KEY);
|
|
128
|
+
} catch {
|
|
129
|
+
// ignore
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function activateDemoSession(apiBaseUrl?: string): void {
|
|
134
|
+
writeDemoSession();
|
|
135
|
+
|
|
136
|
+
const profile = createDemoProfile();
|
|
137
|
+
|
|
138
|
+
useAuthState.setState({
|
|
139
|
+
isAuthenticated: true,
|
|
140
|
+
isPending: false,
|
|
141
|
+
sessionChecked: true,
|
|
142
|
+
profile,
|
|
143
|
+
providerData: {
|
|
144
|
+
type: "dev-portal",
|
|
145
|
+
apiBaseUrl: apiBaseUrl ?? "",
|
|
146
|
+
authMode: "preview",
|
|
147
|
+
isDemoSession: true,
|
|
148
|
+
demoMode: true,
|
|
149
|
+
},
|
|
150
|
+
isBackendAvailable: false,
|
|
151
|
+
authMode: "preview",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
@@ -23,6 +23,12 @@ import type {
|
|
|
23
23
|
DevPortalAuthMode,
|
|
24
24
|
EndUserMeResponse,
|
|
25
25
|
} from "./dev-portal-constants.js";
|
|
26
|
+
import {
|
|
27
|
+
clearDemoSession,
|
|
28
|
+
readDemoSession,
|
|
29
|
+
resolveDemoMode,
|
|
30
|
+
activateDemoSession,
|
|
31
|
+
} from "./dev-portal-demo.js";
|
|
26
32
|
import {
|
|
27
33
|
buildDevPortalLogoutUrl,
|
|
28
34
|
isNativeAuthEnabled,
|
|
@@ -37,6 +43,8 @@ export type DevPortalProviderData = {
|
|
|
37
43
|
type: "dev-portal";
|
|
38
44
|
apiBaseUrl: string;
|
|
39
45
|
authMode: DevPortalAuthMode;
|
|
46
|
+
isDemoSession?: boolean;
|
|
47
|
+
demoMode?: boolean;
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
declare module "../state.js" {
|
|
@@ -50,6 +58,7 @@ export class DevPortalAuthenticationProvider
|
|
|
50
58
|
implements AuthenticationPlugin
|
|
51
59
|
{
|
|
52
60
|
private readonly apiBaseUrl: string | undefined;
|
|
61
|
+
private readonly demoMode: boolean;
|
|
53
62
|
private readonly redirectToAfterSignIn: string | undefined;
|
|
54
63
|
private readonly redirectToAfterSignUp: string | undefined;
|
|
55
64
|
private readonly redirectToAfterSignOut: string;
|
|
@@ -61,19 +70,24 @@ export class DevPortalAuthenticationProvider
|
|
|
61
70
|
|
|
62
71
|
constructor({
|
|
63
72
|
apiBaseUrl,
|
|
73
|
+
demoMode,
|
|
64
74
|
redirectToAfterSignIn,
|
|
65
75
|
redirectToAfterSignUp,
|
|
66
76
|
redirectToAfterSignOut = "/",
|
|
67
77
|
}: DevPortalAuthenticationConfig) {
|
|
68
78
|
super();
|
|
69
79
|
this.apiBaseUrl = resolveDevPortalApiBaseUrl({ apiBaseUrl });
|
|
80
|
+
this.demoMode = resolveDemoMode({ demoMode });
|
|
70
81
|
this.redirectToAfterSignIn = redirectToAfterSignIn;
|
|
71
82
|
this.redirectToAfterSignUp = redirectToAfterSignUp;
|
|
72
83
|
this.redirectToAfterSignOut = redirectToAfterSignOut;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
getRoutes() {
|
|
76
|
-
const pageProps = {
|
|
87
|
+
const pageProps = {
|
|
88
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
89
|
+
demoMode: this.demoMode,
|
|
90
|
+
};
|
|
77
91
|
return [
|
|
78
92
|
...super.getRoutes(),
|
|
79
93
|
{ path: "/login", element: <DevPortalLoginPage {...pageProps} /> },
|
|
@@ -112,6 +126,11 @@ export class DevPortalAuthenticationProvider
|
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
if (!this.backendAvailable) {
|
|
129
|
+
if (this.demoMode && readDemoSession()) {
|
|
130
|
+
this.setDemoAuthenticatedState();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
this.setPreviewState();
|
|
116
135
|
return;
|
|
117
136
|
}
|
|
@@ -127,6 +146,11 @@ export class DevPortalAuthenticationProvider
|
|
|
127
146
|
await waitForAuthStateHydration();
|
|
128
147
|
await this.syncBackendAvailability();
|
|
129
148
|
if (!this.backendAvailable) {
|
|
149
|
+
if (this.demoMode && readDemoSession()) {
|
|
150
|
+
this.setDemoAuthenticatedState();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
this.setPreviewState();
|
|
131
155
|
return;
|
|
132
156
|
}
|
|
@@ -154,15 +178,31 @@ export class DevPortalAuthenticationProvider
|
|
|
154
178
|
type: "dev-portal",
|
|
155
179
|
apiBaseUrl: this.apiBaseUrl,
|
|
156
180
|
authMode: "preview",
|
|
181
|
+
demoMode: this.demoMode,
|
|
157
182
|
}
|
|
158
|
-
:
|
|
183
|
+
: this.demoMode
|
|
184
|
+
? {
|
|
185
|
+
type: "dev-portal",
|
|
186
|
+
apiBaseUrl: "",
|
|
187
|
+
authMode: "preview",
|
|
188
|
+
demoMode: true,
|
|
189
|
+
}
|
|
190
|
+
: null,
|
|
159
191
|
isBackendAvailable: false,
|
|
160
192
|
authMode: "preview",
|
|
161
193
|
});
|
|
162
194
|
}
|
|
163
195
|
|
|
196
|
+
private setDemoAuthenticatedState(): void {
|
|
197
|
+
activateDemoSession(this.apiBaseUrl);
|
|
198
|
+
}
|
|
199
|
+
|
|
164
200
|
private ensureConfiguredBackend(): string {
|
|
165
201
|
if (!this.apiBaseUrl || isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
202
|
+
if (this.demoMode) {
|
|
203
|
+
return "";
|
|
204
|
+
}
|
|
205
|
+
|
|
166
206
|
throw new Error("Dev portal authentication is not available in preview.");
|
|
167
207
|
}
|
|
168
208
|
|
|
@@ -182,12 +222,22 @@ export class DevPortalAuthenticationProvider
|
|
|
182
222
|
}
|
|
183
223
|
|
|
184
224
|
private async refreshUserProfileInternal(): Promise<boolean> {
|
|
225
|
+
if (this.demoMode && readDemoSession() && !this.backendAvailable) {
|
|
226
|
+
this.setDemoAuthenticatedState();
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
185
230
|
if (!this.apiBaseUrl) {
|
|
186
231
|
this.setPreviewState();
|
|
187
232
|
return false;
|
|
188
233
|
}
|
|
189
234
|
|
|
190
235
|
if (isPlaceholderDevPortalApiUrl(this.apiBaseUrl)) {
|
|
236
|
+
if (this.demoMode && readDemoSession()) {
|
|
237
|
+
this.setDemoAuthenticatedState();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
191
241
|
this.setPreviewState();
|
|
192
242
|
return false;
|
|
193
243
|
}
|
|
@@ -195,6 +245,11 @@ export class DevPortalAuthenticationProvider
|
|
|
195
245
|
await waitForAuthStateHydration();
|
|
196
246
|
await this.syncBackendAvailability();
|
|
197
247
|
if (!this.backendAvailable) {
|
|
248
|
+
if (this.demoMode && readDemoSession()) {
|
|
249
|
+
this.setDemoAuthenticatedState();
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
198
253
|
this.setPreviewState();
|
|
199
254
|
return false;
|
|
200
255
|
}
|
|
@@ -229,6 +284,7 @@ export class DevPortalAuthenticationProvider
|
|
|
229
284
|
type: "dev-portal",
|
|
230
285
|
apiBaseUrl: this.apiBaseUrl,
|
|
231
286
|
authMode: "live",
|
|
287
|
+
demoMode: this.demoMode,
|
|
232
288
|
},
|
|
233
289
|
isBackendAvailable: true,
|
|
234
290
|
authMode: "live",
|
|
@@ -257,6 +313,7 @@ export class DevPortalAuthenticationProvider
|
|
|
257
313
|
type: "dev-portal",
|
|
258
314
|
apiBaseUrl: this.apiBaseUrl,
|
|
259
315
|
authMode: "live",
|
|
316
|
+
demoMode: this.demoMode,
|
|
260
317
|
},
|
|
261
318
|
isBackendAvailable: true,
|
|
262
319
|
authMode: "live",
|
|
@@ -276,6 +333,15 @@ export class DevPortalAuthenticationProvider
|
|
|
276
333
|
_: AuthActionContext,
|
|
277
334
|
{ redirectTo }: AuthActionOptions = {},
|
|
278
335
|
): Promise<void> {
|
|
336
|
+
if (!this.backendAvailable && this.demoMode) {
|
|
337
|
+
const target =
|
|
338
|
+
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
339
|
+
const url = new URL("/login", window.location.origin);
|
|
340
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
341
|
+
window.location.assign(url.toString());
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
279
345
|
this.ensureConfiguredBackend();
|
|
280
346
|
const target =
|
|
281
347
|
redirectTo ?? this.redirectToAfterSignIn ?? window.location.href;
|
|
@@ -288,6 +354,15 @@ export class DevPortalAuthenticationProvider
|
|
|
288
354
|
_: AuthActionContext,
|
|
289
355
|
{ redirectTo }: AuthActionOptions = {},
|
|
290
356
|
): Promise<void> {
|
|
357
|
+
if (!this.backendAvailable && this.demoMode) {
|
|
358
|
+
const target =
|
|
359
|
+
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
360
|
+
const url = new URL("/register", window.location.origin);
|
|
361
|
+
url.searchParams.set("redirect", toAbsoluteReturnUrl(target));
|
|
362
|
+
window.location.assign(url.toString());
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
291
366
|
this.ensureConfiguredBackend();
|
|
292
367
|
const target =
|
|
293
368
|
redirectTo ?? this.redirectToAfterSignUp ?? window.location.href;
|
|
@@ -297,6 +372,17 @@ export class DevPortalAuthenticationProvider
|
|
|
297
372
|
}
|
|
298
373
|
|
|
299
374
|
async signOut(_: AuthActionContext): Promise<void> {
|
|
375
|
+
const providerData = useAuthState.getState().providerData as
|
|
376
|
+
| DevPortalProviderData
|
|
377
|
+
| null
|
|
378
|
+
| undefined;
|
|
379
|
+
|
|
380
|
+
if (providerData?.isDemoSession || (this.demoMode && readDemoSession())) {
|
|
381
|
+
clearDemoSession();
|
|
382
|
+
this.setPreviewState();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
300
386
|
const apiBaseUrl = this.ensureConfiguredBackend();
|
|
301
387
|
window.location.assign(
|
|
302
388
|
buildDevPortalLogoutUrl(apiBaseUrl, this.redirectToAfterSignOut),
|
|
@@ -304,12 +390,21 @@ export class DevPortalAuthenticationProvider
|
|
|
304
390
|
}
|
|
305
391
|
|
|
306
392
|
hasDistinctSignUpFlow(): boolean {
|
|
393
|
+
if (this.demoMode && !this.backendAvailable) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
|
|
307
397
|
return this.authConfigNativeEnabled !== false;
|
|
308
398
|
}
|
|
309
399
|
|
|
310
400
|
async signRequest(request: Request): Promise<Request> {
|
|
311
401
|
return request;
|
|
312
402
|
}
|
|
403
|
+
|
|
404
|
+
/** Called from the login page after successful demo credential check. */
|
|
405
|
+
completeDemoLogin(): void {
|
|
406
|
+
activateDemoSession(this.apiBaseUrl);
|
|
407
|
+
}
|
|
313
408
|
}
|
|
314
409
|
|
|
315
410
|
const devPortalAuth: AuthenticationProviderInitializer<
|
|
@@ -171,13 +171,14 @@ export const RouteGuard = () => {
|
|
|
171
171
|
const redirectTo = isBlocked
|
|
172
172
|
? blocker.location.pathname + blocker.location.search
|
|
173
173
|
: location.pathname + location.search;
|
|
174
|
+
const canPromptForSignIn = auth.isBackendAvailable || auth.isDemoMode;
|
|
174
175
|
const showDialog =
|
|
175
|
-
|
|
176
|
+
canPromptForSignIn && needsAuth && auth.hasDistinctSignUpFlow;
|
|
176
177
|
const unifiedRedirectKeyRef = useRef<string | null>(null);
|
|
177
178
|
|
|
178
179
|
useEffect(() => {
|
|
179
180
|
if (
|
|
180
|
-
!
|
|
181
|
+
!canPromptForSignIn ||
|
|
181
182
|
auth.isPending ||
|
|
182
183
|
!auth.sessionChecked ||
|
|
183
184
|
!needsAuth ||
|
|
@@ -195,7 +196,7 @@ export const RouteGuard = () => {
|
|
|
195
196
|
unifiedRedirectKeyRef.current = redirectTo;
|
|
196
197
|
void auth.login({ redirectTo });
|
|
197
198
|
}, [
|
|
198
|
-
|
|
199
|
+
canPromptForSignIn,
|
|
199
200
|
auth.isPending,
|
|
200
201
|
auth.sessionChecked,
|
|
201
202
|
auth.hasDistinctSignUpFlow,
|
|
@@ -238,7 +239,8 @@ export const RouteGuard = () => {
|
|
|
238
239
|
isProtectedRoute &&
|
|
239
240
|
needsToSignIn &&
|
|
240
241
|
auth.isAuthEnabled &&
|
|
241
|
-
!auth.isBackendAvailable
|
|
242
|
+
!auth.isBackendAvailable &&
|
|
243
|
+
!auth.isDemoSession
|
|
242
244
|
) {
|
|
243
245
|
return <ProductionUnlockPage />;
|
|
244
246
|
}
|
package/src/vite/plugin-theme.ts
CHANGED
|
@@ -125,12 +125,17 @@ const processFontConfig = (fontConfig?: FontConfig) => {
|
|
|
125
125
|
};
|
|
126
126
|
};
|
|
127
127
|
|
|
128
|
+
const DEFAULT_FONT_SANS = '"IBM Plex Sans", system-ui, sans-serif';
|
|
129
|
+
const DEFAULT_FONT_MONO = '"JetBrains Mono", monospace';
|
|
130
|
+
const DEFAULT_FONT_HEADING = '"Space Grotesk", system-ui, sans-serif';
|
|
131
|
+
|
|
128
132
|
const processFonts = async (themeConfig: ZudokuConfig["theme"]) => {
|
|
129
133
|
const imports: string[] = [];
|
|
130
134
|
const families = {
|
|
131
|
-
sans:
|
|
135
|
+
sans: DEFAULT_FONT_SANS,
|
|
132
136
|
serif: undefined as string | undefined,
|
|
133
|
-
mono:
|
|
137
|
+
mono: DEFAULT_FONT_MONO,
|
|
138
|
+
heading: DEFAULT_FONT_HEADING,
|
|
134
139
|
};
|
|
135
140
|
|
|
136
141
|
// Process user fonts
|
|
@@ -181,23 +186,19 @@ const processFonts = async (themeConfig: ZudokuConfig["theme"]) => {
|
|
|
181
186
|
|
|
182
187
|
// Determine final font families with priority: user > registry > defaults
|
|
183
188
|
families.sans =
|
|
184
|
-
userFonts.sans.fontFamily || registryFonts.sans ||
|
|
189
|
+
userFonts.sans.fontFamily || registryFonts.sans || DEFAULT_FONT_SANS;
|
|
185
190
|
families.serif = userFonts.serif.fontFamily || registryFonts.serif;
|
|
186
191
|
families.mono =
|
|
187
|
-
userFonts.mono.fontFamily ||
|
|
188
|
-
registryFonts.mono ||
|
|
189
|
-
'"Geist Mono", monospace';
|
|
192
|
+
userFonts.mono.fontFamily || registryFonts.mono || DEFAULT_FONT_MONO;
|
|
190
193
|
|
|
191
194
|
// Add default font imports if no user or registry fonts
|
|
192
195
|
if (!userFonts.sans.fontFamily && !registryFonts.sans) {
|
|
193
|
-
imports.push(getGoogleFontUrl("
|
|
194
|
-
}
|
|
195
|
-
if (!userFonts.serif.fontFamily && !registryFonts.serif) {
|
|
196
|
-
imports.push(getGoogleFontUrl("Playfair Display"));
|
|
196
|
+
imports.push(getGoogleFontUrl("IBM Plex Sans"));
|
|
197
197
|
}
|
|
198
198
|
if (!userFonts.mono.fontFamily && !registryFonts.mono) {
|
|
199
|
-
imports.push(getGoogleFontUrl("
|
|
199
|
+
imports.push(getGoogleFontUrl("JetBrains Mono"));
|
|
200
200
|
}
|
|
201
|
+
imports.push(getGoogleFontUrl("Space Grotesk"));
|
|
201
202
|
|
|
202
203
|
return { imports, families };
|
|
203
204
|
};
|
|
@@ -347,6 +348,7 @@ export const viteThemePlugin = (): Plugin => {
|
|
|
347
348
|
const rootVars = [
|
|
348
349
|
` --font-sans: ${fonts.families.sans};`,
|
|
349
350
|
` --font-mono: ${fonts.families.mono};`,
|
|
351
|
+
` --font-heading: ${fonts.families.heading};`,
|
|
350
352
|
];
|
|
351
353
|
|
|
352
354
|
if (fonts.families.serif) {
|