@nsxbet/admin-sdk 0.7.0 → 0.8.0
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/CHECKLIST.md +48 -13
- package/README.md +24 -74
- package/dist/auth/client/bff.d.ts +38 -0
- package/dist/auth/client/bff.js +270 -0
- package/dist/auth/client/in-memory.d.ts +1 -1
- package/dist/auth/client/in-memory.js +2 -2
- package/dist/auth/client/index.d.ts +1 -1
- package/dist/auth/client/index.js +2 -2
- package/dist/auth/client/interface.d.ts +4 -4
- package/dist/auth/client/interface.js +1 -1
- package/dist/auth/client/private-network-guidance.d.ts +2 -0
- package/dist/auth/client/private-network-guidance.js +38 -0
- package/dist/auth/components/LoginPage.d.ts +8 -0
- package/dist/auth/components/LoginPage.js +32 -0
- package/dist/auth/components/UserSelector.d.ts +29 -0
- package/dist/auth/components/UserSelector.js +38 -10
- package/dist/auth/components/UserSelector.stories.d.ts +9 -0
- package/dist/auth/components/UserSelector.stories.js +70 -0
- package/dist/auth/components/index.d.ts +2 -0
- package/dist/auth/components/index.js +1 -0
- package/dist/auth/index.d.ts +3 -2
- package/dist/auth/index.js +2 -2
- package/dist/components/AuthProvider.d.ts +3 -3
- package/dist/components/AuthProvider.js +35 -17
- package/dist/env.d.ts +17 -0
- package/dist/env.js +50 -0
- package/dist/hooks/useAuth.d.ts +3 -3
- package/dist/hooks/useAuth.js +26 -21
- package/dist/hooks/useCallbackRef.d.ts +7 -0
- package/dist/hooks/useCallbackRef.js +14 -0
- package/dist/hooks/useFetch.js +6 -1
- package/dist/hooks/useI18n.js +2 -2
- package/dist/hooks/usePlatformAPI.d.ts +0 -3
- package/dist/hooks/usePlatformAPI.js +6 -4
- package/dist/i18n/config.d.ts +2 -1
- package/dist/i18n/config.js +4 -3
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +7 -0
- package/dist/i18n/locales/es.json +7 -0
- package/dist/i18n/locales/pt-BR.json +7 -0
- package/dist/i18n/locales/ro.json +7 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +6 -2
- package/dist/registry/AdminShellRegistry.js +4 -3
- package/dist/registry/client/http.js +6 -1
- package/dist/registry/client/in-memory.js +20 -5
- package/dist/registry/types/manifest.d.ts +5 -0
- package/dist/registry/types/manifest.js +4 -1
- package/dist/registry/types/module.d.ts +6 -2
- package/dist/sdk-version.d.ts +5 -0
- package/dist/sdk-version.js +5 -0
- package/dist/shell/AdminShell.d.ts +12 -9
- package/dist/shell/AdminShell.js +56 -70
- package/dist/shell/components/ModuleOverview.js +1 -5
- package/dist/shell/components/RegistryPage.js +1 -1
- package/dist/shell/components/TopBar.js +2 -2
- package/dist/shell/components/theme-provider.js +6 -8
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/polling-config.d.ts +4 -3
- package/dist/shell/polling-config.js +11 -9
- package/dist/shell/types.d.ts +3 -1
- package/dist/types/platform.d.ts +2 -11
- package/dist/vite/config.d.ts +4 -9
- package/dist/vite/config.js +85 -27
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/vite/plugins.js +6 -1
- package/package.json +20 -6
- package/scripts/write-sdk-version.mjs +21 -0
- package/dist/auth/client/keycloak.d.ts +0 -18
- package/dist/auth/client/keycloak.js +0 -129
- package/dist/shell/BackofficeShell.d.ts +0 -37
- package/dist/shell/BackofficeShell.js +0 -339
- package/dist/types/keycloak.d.ts +0 -25
- package/dist/types/keycloak.js +0 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Users can be selected from a predefined list or created custom.
|
|
6
6
|
*/
|
|
7
7
|
import { fetchGatewayToken } from './gateway-token';
|
|
8
|
+
import { env } from '../../env';
|
|
8
9
|
export { GatewayTimeoutError, GatewayFetchError } from './gateway-token';
|
|
9
10
|
/**
|
|
10
11
|
* Create mock users from a role configuration
|
|
@@ -71,10 +72,9 @@ const DEFAULT_STORAGE_KEY = '@nsxbet/auth';
|
|
|
71
72
|
export function createInMemoryAuthClient(options) {
|
|
72
73
|
const { users, storageKey = DEFAULT_STORAGE_KEY, tokenTimeout = 5000 } = options;
|
|
73
74
|
const predefinedUsers = users;
|
|
74
|
-
// Resolve gatewayUrl: explicit option → env var → null (disabled)
|
|
75
75
|
const resolvedGatewayUrl = options.gatewayUrl !== undefined
|
|
76
76
|
? options.gatewayUrl ?? null
|
|
77
|
-
: (
|
|
77
|
+
: (env.adminGatewayUrl ?? null);
|
|
78
78
|
// State
|
|
79
79
|
let selectedUser = null;
|
|
80
80
|
let tokenCache = null;
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './interface';
|
|
5
5
|
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, type InMemoryAuthClientOptions, type MockUserRoles, } from './in-memory';
|
|
6
|
-
export {
|
|
6
|
+
export { createBffAuthClient, clearLoggedOutFlag, isLoggedOutFlag, setLoggedOutFlag, type BffAuthClient, type BffAuthClientOptions, type BffMeResponse, } from './bff';
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
// In-memory client
|
|
5
5
|
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, } from './in-memory';
|
|
6
|
-
//
|
|
7
|
-
export {
|
|
6
|
+
// BFF / Okta cookie auth
|
|
7
|
+
export { createBffAuthClient, clearLoggedOutFlag, isLoggedOutFlag, setLoggedOutFlag, } from './bff';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Auth Client Interface
|
|
3
3
|
*
|
|
4
4
|
* Defines the interface for authentication operations.
|
|
5
|
-
* Can be implemented by in-memory (mock) or
|
|
5
|
+
* Can be implemented by in-memory (mock) or BFF cookie auth.
|
|
6
6
|
*/
|
|
7
7
|
import type { User } from '../../types/platform';
|
|
8
8
|
/**
|
|
@@ -54,7 +54,7 @@ export interface AuthClient {
|
|
|
54
54
|
/**
|
|
55
55
|
* Client type identifier
|
|
56
56
|
*/
|
|
57
|
-
readonly type: 'in-memory' | '
|
|
57
|
+
readonly type: 'in-memory' | 'bff';
|
|
58
58
|
/**
|
|
59
59
|
* Initialize the auth client
|
|
60
60
|
* @returns true if user is authenticated, false if login/selection is needed
|
|
@@ -69,9 +69,9 @@ export interface AuthClient {
|
|
|
69
69
|
*/
|
|
70
70
|
getUser(): User | null;
|
|
71
71
|
/**
|
|
72
|
-
* Get access token for API calls
|
|
72
|
+
* Get access token for API calls. BFF cookie auth returns `null` (HttpOnly cookie — use credentialed fetch).
|
|
73
73
|
*/
|
|
74
|
-
getAccessToken(): Promise<string>;
|
|
74
|
+
getAccessToken(): Promise<string | null>;
|
|
75
75
|
/**
|
|
76
76
|
* Check if user has a specific permission/role
|
|
77
77
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const LOVABLE_DOMAINS = [".lovableproject.com", ".lovable.app"];
|
|
2
|
+
export function isLovableContext() {
|
|
3
|
+
try {
|
|
4
|
+
const { hostname } = window.location;
|
|
5
|
+
return LOVABLE_DOMAINS.some((domain) => hostname.endsWith(domain));
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function logPrivateNetworkGuidance(previewUrl) {
|
|
12
|
+
try {
|
|
13
|
+
const flagsUrl = "chrome://flags/#block-insecure-private-network-requests";
|
|
14
|
+
console.groupCollapsed("%c⚠️ Private Network Access — Lovable Preview", "color: #f59e0b; font-weight: bold; font-size: 14px");
|
|
15
|
+
console.warn([
|
|
16
|
+
"Chrome is blocking requests from this Lovable iframe to your local/private network API.",
|
|
17
|
+
"The gateway token fetch failed because the browser's Private Network Access policy",
|
|
18
|
+
"prevents public origins from reaching private IPs without explicit permission.",
|
|
19
|
+
"",
|
|
20
|
+
"━━━ Option 1 (Recommended) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
21
|
+
`Open this preview URL directly in a new tab:`,
|
|
22
|
+
` 👉 ${previewUrl}`,
|
|
23
|
+
"",
|
|
24
|
+
"This takes the page out of the iframe so Chrome can show the local network",
|
|
25
|
+
"access permission prompt.",
|
|
26
|
+
"",
|
|
27
|
+
"━━━ Option 2 (Alternative) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
|
|
28
|
+
"Disable the Chrome flag (less secure, but works globally):",
|
|
29
|
+
` 🔧 ${flagsUrl}`,
|
|
30
|
+
"",
|
|
31
|
+
'Set "Block insecure private network requests" to Disabled, then relaunch Chrome.',
|
|
32
|
+
].join("\n"));
|
|
33
|
+
console.groupEnd();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// best-effort — never throw from guidance logging
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BFF login landing — explicit user action starts OAuth (avoids IdP silent re-login after logout).
|
|
3
|
+
*/
|
|
4
|
+
export interface LoginPageProps {
|
|
5
|
+
/** Base URL of admin-bff (no trailing slash). */
|
|
6
|
+
bffBaseUrl: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function LoginPage({ bffBaseUrl }: LoginPageProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* BFF login landing — explicit user action starts OAuth (avoids IdP silent re-login after logout).
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useCallback } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { ShieldCheck, LogIn, Check, Globe } from 'lucide-react';
|
|
8
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@nsxbet/admin-ui';
|
|
9
|
+
import { clearLoggedOutFlag } from '../client/bff';
|
|
10
|
+
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_FLAGS, LOCALE_NAMES, saveLocale, } from '../../i18n/config';
|
|
11
|
+
function normalizeBaseUrl(url) {
|
|
12
|
+
return url.replace(/\/+$/, '');
|
|
13
|
+
}
|
|
14
|
+
function NsxLogo() {
|
|
15
|
+
return (_jsx("div", { className: "inline-flex items-center justify-center w-[72px] h-[72px] rounded-2xl bg-primary text-primary-foreground shadow-lg shadow-primary/25", children: _jsx("span", { className: "text-2xl font-bold tracking-tighter select-none", "aria-hidden": true, children: "NSX" }) }));
|
|
16
|
+
}
|
|
17
|
+
export function LoginPage({ bffBaseUrl }) {
|
|
18
|
+
const base = normalizeBaseUrl(bffBaseUrl);
|
|
19
|
+
const { t, i18n } = useTranslation('shell');
|
|
20
|
+
const [locale, setLocale] = useState(i18n.language || DEFAULT_LOCALE);
|
|
21
|
+
const handleLocaleChange = useCallback((newLocale) => {
|
|
22
|
+
setLocale(newLocale);
|
|
23
|
+
i18n.changeLanguage(newLocale);
|
|
24
|
+
saveLocale(newLocale);
|
|
25
|
+
}, [i18n]);
|
|
26
|
+
const handleLogin = () => {
|
|
27
|
+
clearLoggedOutFlag();
|
|
28
|
+
const redirect = encodeURIComponent(window.location.origin);
|
|
29
|
+
window.location.assign(`${base}/auth/login?redirect_url=${redirect}`);
|
|
30
|
+
};
|
|
31
|
+
return (_jsxs("div", { className: "min-h-screen bg-muted/40 text-foreground flex flex-col", children: [_jsx("div", { className: "flex items-center justify-end px-4 py-3", children: _jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { className: "flex items-center gap-1.5 rounded-md border border-border bg-background px-2.5 py-1.5 text-sm text-muted-foreground transition-colors hover:text-foreground focus:outline-none", "data-testid": "login-language-selector", children: [_jsx(Globe, { className: "h-4 w-4" }), _jsx("span", { children: LOCALE_FLAGS[locale] })] }), _jsx(DropdownMenuContent, { align: "end", className: "z-[101] min-w-[180px]", children: SUPPORTED_LOCALES.map((opt) => (_jsxs(DropdownMenuItem, { onClick: () => handleLocaleChange(opt), className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center", children: [_jsx("span", { className: "mr-2 text-lg", children: LOCALE_FLAGS[opt] }), _jsx("span", { children: LOCALE_NAMES[opt] })] }), locale === opt && _jsx(Check, { className: "h-4 w-4 text-primary" })] }, opt))) })] }) }), _jsx("div", { className: "flex flex-1 items-center justify-center px-4 pb-14", children: _jsxs("div", { className: "w-full max-w-[420px] flex flex-col items-center text-center gap-8", children: [_jsx(NsxLogo, {}), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("h1", { className: "text-2xl font-semibold tracking-tight", children: [t('loginPage.welcomeTo'), ' ', _jsx("span", { className: "text-primary", children: t('loginPage.brandName') })] }), _jsx("p", { className: "text-muted-foreground text-[15px]", children: t('loginPage.subtitle') })] }), _jsxs("div", { className: "w-full flex flex-col gap-3", children: [_jsxs(Button, { type: "button", size: "lg", className: "w-full h-12 text-[15px] gap-2.5 font-medium", onClick: handleLogin, children: [_jsx(LogIn, { className: "h-[18px] w-[18px]", "aria-hidden": true }), t('loginPage.loginButton')] }), _jsxs("div", { className: "w-full flex items-center justify-center gap-2 rounded-lg border border-border bg-background py-2.5 px-4 text-sm text-muted-foreground", children: [_jsx(ShieldCheck, { className: "h-4 w-4 text-muted-foreground/70 shrink-0", "aria-hidden": true }), t('loginPage.secureAuth')] })] })] }) })] }));
|
|
32
|
+
}
|
|
@@ -11,6 +11,35 @@ interface UserSelectorProps {
|
|
|
11
11
|
/** Callback when user is selected */
|
|
12
12
|
onUserSelected?: () => void;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Loading screen shown while fetching a BFF token.
|
|
16
|
+
*/
|
|
17
|
+
export declare function LoginLoadingScreen({ userName }: {
|
|
18
|
+
userName: string;
|
|
19
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
/**
|
|
21
|
+
* Error screen shown when the gateway returns an error or is unreachable.
|
|
22
|
+
*/
|
|
23
|
+
export declare function LoginErrorScreen({ errorMessage, gatewayUrl, isLovable, onRetry, onFallback, onBack, retrying, }: {
|
|
24
|
+
errorMessage: string;
|
|
25
|
+
gatewayUrl: string | null;
|
|
26
|
+
isLovable: boolean;
|
|
27
|
+
onRetry: () => void;
|
|
28
|
+
onFallback: () => void;
|
|
29
|
+
onBack: () => void;
|
|
30
|
+
retrying: boolean;
|
|
31
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
/**
|
|
33
|
+
* Timeout screen shown when the gateway doesn't respond in time.
|
|
34
|
+
*/
|
|
35
|
+
export declare function LoginTimeoutScreen({ gatewayUrl, isLovable, onRetry, onFallback, onBack, retrying, }: {
|
|
36
|
+
gatewayUrl: string | null;
|
|
37
|
+
isLovable: boolean;
|
|
38
|
+
onRetry: () => void;
|
|
39
|
+
onFallback: () => void;
|
|
40
|
+
onBack: () => void;
|
|
41
|
+
retrying: boolean;
|
|
42
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
14
43
|
/**
|
|
15
44
|
* Main User Selector Component
|
|
16
45
|
*/
|
|
@@ -6,8 +6,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
6
6
|
* Uses Brasa Design System tokens for consistent styling.
|
|
7
7
|
*/
|
|
8
8
|
import { useState } from 'react';
|
|
9
|
-
import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight, Loader2, AlertCircle, Clock, ArrowLeft, RefreshCw, ShieldAlert, } from '@nsxbet/admin-ui';
|
|
9
|
+
import { Crown, Ban, Eye, User, Users, Sparkles, Wrench, Trash2, Plus, ChevronRight, Loader2, AlertCircle, Clock, ArrowLeft, RefreshCw, ShieldAlert, Info, Copy, Check, ChevronDown, } from '@nsxbet/admin-ui';
|
|
10
|
+
import { ExternalLink } from 'lucide-react';
|
|
10
11
|
import { GatewayTimeoutError } from '../client/in-memory';
|
|
12
|
+
import { isLovableContext, logPrivateNetworkGuidance } from '../client/private-network-guidance';
|
|
11
13
|
/**
|
|
12
14
|
* Get user icon based on roles
|
|
13
15
|
*/
|
|
@@ -65,7 +67,7 @@ function UserCard({ user, isCustom, onClick, onDelete, }) {
|
|
|
65
67
|
onDelete();
|
|
66
68
|
}
|
|
67
69
|
};
|
|
68
|
-
return (_jsxs("div", { className: "relative group", children: [_jsx("button", { onClick: onClick, className: "w-full p-4 rounded-xl border border-border bg-card hover:bg-muted/50 hover:border-muted-foreground/30 transition-all duration-200 text-left", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0", children: icon }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "font-semibold text-foreground group-hover:text-foreground transition-colors", children: user.displayName }), isCustom && (_jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-primary/20 text-primary border border-primary/30", children: "custom" }))] }), _jsx("p", { className: "text-sm text-muted-foreground truncate", children: user.email }), _jsxs("div", { className: "flex flex-wrap gap-1.5 mt-2", children: [user.roles.length === 0 ? (_jsx("span", { className: "text-xs px-2 py-0.5 rounded-full border bg-destructive/20 text-destructive border-destructive/30", children: "no roles" })) : (user.roles.slice(0, 4).map((role) => (_jsx("span", { className: `text-xs px-2 py-0.5 rounded-full border ${getRoleBadgeColor(role)}`, children: role }, role)))), user.roles.length > 4 && (_jsxs("span", { className: "text-xs px-2 py-0.5 rounded-full border bg-muted text-muted-foreground border-border", children: ["+", user.roles.length - 4, " more"] }))] })] }), _jsx("div", { className: "text-muted-foreground group-hover:text-foreground transition-colors", children: _jsx(ChevronRight, { className: "h-5 w-5" }) })] }) }), isCustom && onDelete && (_jsx("button", { onClick: handleDelete, className: "absolute top-2 right-2 p-1.5 rounded-lg bg-destructive/10 text-destructive opacity-0 group-hover:opacity-100 hover:bg-destructive/20 transition-all", title: "Delete user", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }));
|
|
70
|
+
return (_jsxs("div", { className: "relative group", children: [_jsx("button", { type: "button", "data-testid": `user-${user.id}`, onClick: onClick, className: "w-full p-4 rounded-xl border border-border bg-card hover:bg-muted/50 hover:border-muted-foreground/30 transition-all duration-200 text-left", children: _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0", children: icon }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("h3", { className: "font-semibold text-foreground group-hover:text-foreground transition-colors", children: user.displayName }), isCustom && (_jsx("span", { className: "text-xs px-1.5 py-0.5 rounded bg-primary/20 text-primary border border-primary/30", children: "custom" }))] }), _jsx("p", { className: "text-sm text-muted-foreground truncate", children: user.email }), _jsxs("div", { className: "flex flex-wrap gap-1.5 mt-2", children: [user.roles.length === 0 ? (_jsx("span", { className: "text-xs px-2 py-0.5 rounded-full border bg-destructive/20 text-destructive border-destructive/30", children: "no roles" })) : (user.roles.slice(0, 4).map((role) => (_jsx("span", { className: `text-xs px-2 py-0.5 rounded-full border ${getRoleBadgeColor(role)}`, children: role }, role)))), user.roles.length > 4 && (_jsxs("span", { className: "text-xs px-2 py-0.5 rounded-full border bg-muted text-muted-foreground border-border", children: ["+", user.roles.length - 4, " more"] }))] })] }), _jsx("div", { className: "text-muted-foreground group-hover:text-foreground transition-colors", children: _jsx(ChevronRight, { className: "h-5 w-5" }) })] }) }), isCustom && onDelete && (_jsx("button", { onClick: handleDelete, className: "absolute top-2 right-2 p-1.5 rounded-lg bg-destructive/10 text-destructive opacity-0 group-hover:opacity-100 hover:bg-destructive/20 transition-all", title: "Delete user", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }));
|
|
69
71
|
}
|
|
70
72
|
/**
|
|
71
73
|
* Custom User Form Component
|
|
@@ -88,9 +90,31 @@ function CustomUserForm({ onSubmit, onCancel, }) {
|
|
|
88
90
|
/**
|
|
89
91
|
* Loading screen shown while fetching a BFF token.
|
|
90
92
|
*/
|
|
91
|
-
function LoginLoadingScreen({ userName }) {
|
|
93
|
+
export function LoginLoadingScreen({ userName }) {
|
|
92
94
|
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 mb-6", children: _jsx(Loader2, { className: "h-8 w-8 text-primary animate-spin" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authenticating\u2026" }), _jsxs("p", { className: "text-muted-foreground", children: ["Fetching token for ", _jsx("span", { className: "font-medium text-foreground", children: userName })] })] }) }));
|
|
93
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Guidance shown on error/timeout screens when running inside a Lovable preview iframe.
|
|
98
|
+
* Explains why Chrome's Private Network Access policy blocks the gateway request
|
|
99
|
+
* and offers two resolution paths.
|
|
100
|
+
*/
|
|
101
|
+
function LovableNetworkGuidance() {
|
|
102
|
+
const [showAlternative, setShowAlternative] = useState(false);
|
|
103
|
+
const [copied, setCopied] = useState(false);
|
|
104
|
+
const previewUrl = typeof window !== "undefined" ? window.location.href : "";
|
|
105
|
+
const flagsUrl = "chrome://flags/#block-insecure-private-network-requests";
|
|
106
|
+
const handleCopy = async () => {
|
|
107
|
+
try {
|
|
108
|
+
await navigator.clipboard.writeText(flagsUrl);
|
|
109
|
+
setCopied(true);
|
|
110
|
+
setTimeout(() => setCopied(false), 2000);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// clipboard API may not be available in all contexts
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return (_jsx("div", { "data-testid": "lovable-network-guidance", className: "w-full mt-4 rounded-xl border border-info/30 bg-info/5 p-4 text-left", children: _jsxs("div", { className: "flex items-start gap-3", children: [_jsx(Info, { className: "h-5 w-5 text-info flex-shrink-0 mt-0.5" }), _jsxs("div", { className: "flex-1 space-y-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Lovable Preview \u2014 Private Network Access" }), _jsx("p", { className: "text-sm text-muted-foreground mt-1", children: "Chrome blocks requests from Lovable's iframe to local/private network APIs. Open this preview directly in a new tab to grant permission." })] }), _jsxs("a", { href: previewUrl, target: "_blank", rel: "noopener noreferrer", className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-info text-info-foreground font-medium text-sm hover:bg-info/90 transition-colors", children: [_jsx(ExternalLink, { className: "h-4 w-4" }), "Open preview directly"] }), _jsxs("div", { children: [_jsxs("button", { onClick: () => setShowAlternative(!showAlternative), className: "flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors", children: [_jsx(ChevronDown, { className: `h-3.5 w-3.5 transition-transform ${showAlternative ? "rotate-180" : ""}` }), "Alternative: disable Chrome flag"] }), showAlternative && (_jsxs("div", { className: "mt-2 space-y-2", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Disable the Private Network Access check in Chrome flags (less secure, but works globally):" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs px-3 py-1.5 rounded-md bg-muted text-foreground font-mono break-all", children: flagsUrl }), _jsx("button", { onClick: handleCopy, className: "flex-shrink-0 p-1.5 rounded-md hover:bg-muted transition-colors", title: "Copy to clipboard", children: copied ? (_jsx(Check, { className: "h-3.5 w-3.5 text-success" })) : (_jsx(Copy, { className: "h-3.5 w-3.5 text-muted-foreground" })) })] }), _jsxs("p", { className: "text-xs text-muted-foreground/70", children: ["Set \u201CBlock insecure private network requests\u201D to ", _jsx("strong", { children: "Disabled" }), ", then relaunch Chrome."] })] }))] })] })] }) }));
|
|
117
|
+
}
|
|
94
118
|
/**
|
|
95
119
|
* Shared action bar for error / timeout screens.
|
|
96
120
|
*/
|
|
@@ -100,14 +124,14 @@ function LoginRecoveryActions({ onRetry, onFallback, onBack, retrying, }) {
|
|
|
100
124
|
/**
|
|
101
125
|
* Error screen shown when the gateway returns an error or is unreachable.
|
|
102
126
|
*/
|
|
103
|
-
function LoginErrorScreen({ errorMessage, gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
|
|
104
|
-
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-destructive/10 mb-6", children: _jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authentication Failed" }), _jsx("p", { className: "text-muted-foreground mb-1", children: errorMessage }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
127
|
+
export function LoginErrorScreen({ errorMessage, gatewayUrl, isLovable, onRetry, onFallback, onBack, retrying, }) {
|
|
128
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-destructive/10 mb-6", children: _jsx(AlertCircle, { className: "h-8 w-8 text-destructive" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Authentication Failed" }), _jsx("p", { className: "text-muted-foreground mb-1", children: errorMessage }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), isLovable && _jsx(LovableNetworkGuidance, {}), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
105
129
|
}
|
|
106
130
|
/**
|
|
107
131
|
* Timeout screen shown when the gateway doesn't respond in time.
|
|
108
132
|
*/
|
|
109
|
-
function LoginTimeoutScreen({ gatewayUrl, onRetry, onFallback, onBack, retrying, }) {
|
|
110
|
-
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-amber-500/10 mb-6", children: _jsx(Clock, { className: "h-8 w-8 text-amber-500" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Gateway Timeout" }), _jsx("p", { className: "text-muted-foreground mb-1", children: "The gateway did not respond in time." }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
133
|
+
export function LoginTimeoutScreen({ gatewayUrl, isLovable, onRetry, onFallback, onBack, retrying, }) {
|
|
134
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md flex flex-col items-center text-center", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-amber-500/10 mb-6", children: _jsx(Clock, { className: "h-8 w-8 text-amber-500" }) }), _jsx("h2", { className: "text-xl font-semibold text-foreground mb-2", children: "Gateway Timeout" }), _jsx("p", { className: "text-muted-foreground mb-1", children: "The gateway did not respond in time." }), gatewayUrl && (_jsx("p", { className: "text-xs text-muted-foreground/70 font-mono break-all", children: gatewayUrl })), isLovable && _jsx(LovableNetworkGuidance, {}), _jsx(LoginRecoveryActions, { onRetry: onRetry, onFallback: onFallback, onBack: onBack, retrying: retrying })] }) }));
|
|
111
135
|
}
|
|
112
136
|
/**
|
|
113
137
|
* Main User Selector Component
|
|
@@ -117,6 +141,7 @@ export function UserSelector({ authClient, onUserSelected }) {
|
|
|
117
141
|
const [loginState, setLoginState] = useState({ status: 'idle' });
|
|
118
142
|
const [, forceUpdate] = useState(0);
|
|
119
143
|
const users = authClient.getAvailableUsers();
|
|
144
|
+
const isLovable = isLovableContext();
|
|
120
145
|
const findUserName = (userId) => users.find((u) => u.id === userId)?.displayName ?? userId;
|
|
121
146
|
const handleSelectUser = async (userId) => {
|
|
122
147
|
if (!authClient.gatewayUrl) {
|
|
@@ -130,6 +155,9 @@ export function UserSelector({ authClient, onUserSelected }) {
|
|
|
130
155
|
onUserSelected?.();
|
|
131
156
|
}
|
|
132
157
|
catch (error) {
|
|
158
|
+
if (isLovable) {
|
|
159
|
+
logPrivateNetworkGuidance(window.location.href);
|
|
160
|
+
}
|
|
133
161
|
if (error instanceof GatewayTimeoutError) {
|
|
134
162
|
setLoginState({ status: 'timeout', userId });
|
|
135
163
|
}
|
|
@@ -181,12 +209,12 @@ export function UserSelector({ authClient, onUserSelected }) {
|
|
|
181
209
|
return _jsx(LoginLoadingScreen, { userName: findUserName(loginState.userId) });
|
|
182
210
|
}
|
|
183
211
|
if (loginState.status === 'error') {
|
|
184
|
-
return (_jsx(LoginErrorScreen, { errorMessage: loginState.error, gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
212
|
+
return (_jsx(LoginErrorScreen, { errorMessage: loginState.error, gatewayUrl: authClient.gatewayUrl, isLovable: isLovable, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
185
213
|
}
|
|
186
214
|
if (loginState.status === 'timeout') {
|
|
187
|
-
return (_jsx(LoginTimeoutScreen, { gatewayUrl: authClient.gatewayUrl, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
215
|
+
return (_jsx(LoginTimeoutScreen, { gatewayUrl: authClient.gatewayUrl, isLovable: isLovable, onRetry: handleRetry, onFallback: handleFallbackToMock, onBack: handleBack, retrying: false }));
|
|
188
216
|
}
|
|
189
|
-
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", children: _jsxs("div", { className: "w-full max-w-md", children: [_jsxs("div", { className: "text-center mb-8", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/25", children: _jsx(Users, { className: "h-8 w-8 text-primary-foreground" }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground mb-2", children: "Select User" }), _jsx("p", { className: "text-muted-foreground", children: "Choose a mock user to continue in development mode" })] }), _jsx("div", { className: "space-y-3 mb-6", children: users.map((user) => {
|
|
217
|
+
return (_jsx("div", { className: "min-h-screen bg-background flex items-center justify-center p-4", "data-testid": "user-selector", children: _jsxs("div", { className: "w-full max-w-md", children: [_jsxs("div", { className: "text-center mb-8", children: [_jsx("div", { className: "inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary mb-4 shadow-lg shadow-primary/25", children: _jsx(Users, { className: "h-8 w-8 text-primary-foreground" }) }), _jsx("h1", { className: "text-2xl font-bold text-foreground mb-2", children: "Select User" }), _jsx("p", { className: "text-muted-foreground", children: "Choose a mock user to continue in development mode" })] }), _jsx("div", { className: "space-y-3 mb-6", children: users.map((user) => {
|
|
190
218
|
const isCustom = authClient.isCustomUser(user.id);
|
|
191
219
|
return (_jsx(UserCard, { user: user, isCustom: isCustom, onClick: () => handleSelectUser(user.id), onDelete: isCustom ? () => handleDeleteUser(user.id) : undefined }, user.id));
|
|
192
220
|
}) }), _jsxs("div", { className: "relative my-6", children: [_jsx("div", { className: "absolute inset-0 flex items-center", children: _jsx("div", { className: "w-full border-t border-border" }) }), _jsx("div", { className: "relative flex justify-center text-sm", children: _jsx("span", { className: "px-3 bg-background text-muted-foreground", children: "or" }) })] }), showCustomForm ? (_jsx(CustomUserForm, { onSubmit: handleCreateCustomUser, onCancel: () => setShowCustomForm(false) })) : (_jsxs("button", { onClick: () => setShowCustomForm(true), className: "w-full p-4 rounded-xl border border-dashed border-border text-muted-foreground hover:text-foreground hover:border-muted-foreground transition-colors flex items-center justify-center gap-2", children: [_jsx(Plus, { className: "h-5 w-5" }), "Create Custom User"] })), _jsxs("p", { className: "text-center text-xs text-muted-foreground mt-8 flex items-center justify-center gap-1", children: [_jsx(Wrench, { className: "h-3 w-3" }), "Development Mode \u2022 Data stored in localStorage"] })] }) }));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
declare const meta: Meta;
|
|
3
|
+
export default meta;
|
|
4
|
+
export declare const LoginErrorDefault: StoryObj;
|
|
5
|
+
export declare const LoginErrorLovable: StoryObj;
|
|
6
|
+
export declare const LoginTimeoutDefault: StoryObj;
|
|
7
|
+
export declare const LoginTimeoutLovable: StoryObj;
|
|
8
|
+
export declare const Loading: StoryObj;
|
|
9
|
+
export declare const Idle: StoryObj;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { LoginErrorScreen, LoginTimeoutScreen, LoginLoadingScreen, UserSelector } from "./UserSelector";
|
|
3
|
+
const noop = () => { };
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Auth/UserSelector",
|
|
6
|
+
parameters: {
|
|
7
|
+
layout: "fullscreen",
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// LoginErrorScreen
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
export const LoginErrorDefault = {
|
|
15
|
+
name: "LoginError — Default",
|
|
16
|
+
render: () => (_jsx(LoginErrorScreen, { errorMessage: "Failed to fetch: NetworkError when attempting to fetch resource.", gatewayUrl: "https://admin-bff-stg.nsx.dev", isLovable: false, onRetry: noop, onFallback: noop, onBack: noop, retrying: false })),
|
|
17
|
+
};
|
|
18
|
+
export const LoginErrorLovable = {
|
|
19
|
+
name: "LoginError — Lovable Context",
|
|
20
|
+
render: () => (_jsx(LoginErrorScreen, { errorMessage: "Failed to fetch: NetworkError when attempting to fetch resource.", gatewayUrl: "https://admin-bff-stg.nsx.dev", isLovable: true, onRetry: noop, onFallback: noop, onBack: noop, retrying: false })),
|
|
21
|
+
};
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// LoginTimeoutScreen
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export const LoginTimeoutDefault = {
|
|
26
|
+
name: "LoginTimeout — Default",
|
|
27
|
+
render: () => (_jsx(LoginTimeoutScreen, { gatewayUrl: "https://admin-bff-stg.nsx.dev", isLovable: false, onRetry: noop, onFallback: noop, onBack: noop, retrying: false })),
|
|
28
|
+
};
|
|
29
|
+
export const LoginTimeoutLovable = {
|
|
30
|
+
name: "LoginTimeout — Lovable Context",
|
|
31
|
+
render: () => (_jsx(LoginTimeoutScreen, { gatewayUrl: "https://admin-bff-stg.nsx.dev", isLovable: true, onRetry: noop, onFallback: noop, onBack: noop, retrying: false })),
|
|
32
|
+
};
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// LoginLoadingScreen
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export const Loading = {
|
|
37
|
+
name: "UserSelector — Loading",
|
|
38
|
+
render: () => _jsx(LoginLoadingScreen, { userName: "Admin User" }),
|
|
39
|
+
};
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// UserSelector — Idle (full component)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
const mockUsers = [
|
|
44
|
+
{ id: "admin", displayName: "Admin User", email: "admin@nsx.bet", roles: ["admin"] },
|
|
45
|
+
{ id: "editor", displayName: "Editor User", email: "editor@nsx.bet", roles: ["admin.tasks.edit", "admin.users.view"] },
|
|
46
|
+
{ id: "viewer", displayName: "Viewer User", email: "viewer@nsx.bet", roles: ["admin.tasks.view", "admin.users.view"] },
|
|
47
|
+
{ id: "no-access", displayName: "No Access", email: "noaccess@nsx.bet", roles: [] },
|
|
48
|
+
];
|
|
49
|
+
function createStoryClient() {
|
|
50
|
+
return {
|
|
51
|
+
type: "in-memory",
|
|
52
|
+
gatewayUrl: null,
|
|
53
|
+
getAvailableUsers: () => mockUsers,
|
|
54
|
+
isCustomUser: () => false,
|
|
55
|
+
login: async () => { },
|
|
56
|
+
logout: async () => { },
|
|
57
|
+
initialize: async () => false,
|
|
58
|
+
isAuthenticated: () => false,
|
|
59
|
+
hasPermission: () => false,
|
|
60
|
+
getUser: () => null,
|
|
61
|
+
getAccessToken: async () => "mock-token",
|
|
62
|
+
subscribe: () => () => { },
|
|
63
|
+
createCustomUser: async (user) => ({ id: "custom", ...user }),
|
|
64
|
+
deleteCustomUser: () => false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export const Idle = {
|
|
68
|
+
name: "UserSelector — Idle",
|
|
69
|
+
render: () => _jsx(UserSelector, { authClient: createStoryClient() }),
|
|
70
|
+
};
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
* Authentication module exports
|
|
3
3
|
*/
|
|
4
4
|
export type { AuthClient, InMemoryAuthClient, MockUser, AuthState, AuthStateCallback, } from './client';
|
|
5
|
-
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles,
|
|
6
|
-
export { UserSelector } from './components';
|
|
5
|
+
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, createBffAuthClient, clearLoggedOutFlag, isLoggedOutFlag, setLoggedOutFlag, type InMemoryAuthClientOptions, type MockUserRoles, type BffAuthClient, type BffAuthClientOptions, type BffMeResponse, } from './client';
|
|
6
|
+
export { UserSelector, LoginPage } from './components';
|
|
7
|
+
export type { LoginPageProps } from './components';
|
package/dist/auth/index.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* Authentication module exports
|
|
3
3
|
*/
|
|
4
4
|
// Auth client factories
|
|
5
|
-
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles,
|
|
5
|
+
export { createInMemoryAuthClient, clearInMemoryAuth, createMockUsersFromRoles, createBffAuthClient, clearLoggedOutFlag, isLoggedOutFlag, setLoggedOutFlag, } from './client';
|
|
6
6
|
// Auth components
|
|
7
|
-
export { UserSelector } from './components';
|
|
7
|
+
export { UserSelector, LoginPage } from './components';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AuthProvider - Manages authentication state using AuthClient
|
|
3
3
|
*
|
|
4
|
-
* Works with both in-memory (mock) and
|
|
4
|
+
* Works with both in-memory (mock) and BFF cookie auth clients.
|
|
5
5
|
* Shows user selection screen when in-memory client has no selected user.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
@@ -15,8 +15,8 @@ interface AuthContextValue {
|
|
|
15
15
|
isAuthenticated: boolean;
|
|
16
16
|
/** Current user (null if not authenticated) */
|
|
17
17
|
user: User | null;
|
|
18
|
-
/** Get access token for API calls */
|
|
19
|
-
getAccessToken: () => Promise<string>;
|
|
18
|
+
/** Get access token for API calls (null for BFF HttpOnly cookie auth) */
|
|
19
|
+
getAccessToken: () => Promise<string | null>;
|
|
20
20
|
/** Check if user has a permission */
|
|
21
21
|
hasPermission: (permission: string) => boolean;
|
|
22
22
|
/** Log out current user */
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* AuthProvider - Manages authentication state using AuthClient
|
|
4
4
|
*
|
|
5
|
-
* Works with both in-memory (mock) and
|
|
5
|
+
* Works with both in-memory (mock) and BFF cookie auth clients.
|
|
6
6
|
* Shows user selection screen when in-memory client has no selected user.
|
|
7
7
|
*/
|
|
8
|
-
import { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
|
8
|
+
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
|
|
9
|
+
import { useCallbackRef } from '../hooks/useCallbackRef';
|
|
10
|
+
import { isLoggedOutFlag } from '../auth/client/bff';
|
|
9
11
|
import { UserSelector } from '../auth/components/UserSelector';
|
|
12
|
+
import { LoginPage } from '../auth/components/LoginPage';
|
|
10
13
|
const AuthContext = createContext(null);
|
|
11
14
|
/**
|
|
12
15
|
* Loading screen component - uses Brasa Design System tokens
|
|
@@ -22,8 +25,10 @@ export function AuthProvider({ children, authClient }) {
|
|
|
22
25
|
isAuthenticated: false,
|
|
23
26
|
user: null,
|
|
24
27
|
});
|
|
25
|
-
const
|
|
28
|
+
const bffLoggedOut = authClient.type === 'bff' && isLoggedOutFlag();
|
|
29
|
+
const [isInitializing, setIsInitializing] = useState(!bffLoggedOut);
|
|
26
30
|
const [needsUserSelection, setNeedsUserSelection] = useState(false);
|
|
31
|
+
const [needsLogin, setNeedsLogin] = useState(bffLoggedOut);
|
|
27
32
|
// Initialize auth client
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
let mounted = true;
|
|
@@ -38,15 +43,22 @@ export function AuthProvider({ children, authClient }) {
|
|
|
38
43
|
user: authClient.getUser(),
|
|
39
44
|
});
|
|
40
45
|
setNeedsUserSelection(false);
|
|
46
|
+
setNeedsLogin(false);
|
|
41
47
|
}
|
|
42
48
|
else if (authClient.type === 'in-memory') {
|
|
43
|
-
// In-memory client needs user selection
|
|
44
49
|
setNeedsUserSelection(true);
|
|
50
|
+
setNeedsLogin(false);
|
|
51
|
+
}
|
|
52
|
+
else if (authClient.type === 'bff') {
|
|
53
|
+
setNeedsLogin(true);
|
|
54
|
+
setNeedsUserSelection(false);
|
|
45
55
|
}
|
|
46
|
-
// Keycloak will redirect to login page, so we don't need to handle it here
|
|
47
56
|
}
|
|
48
57
|
catch (error) {
|
|
49
58
|
console.error('[AuthProvider] Initialization failed:', error);
|
|
59
|
+
if (authClient.type === 'bff') {
|
|
60
|
+
setNeedsLogin(true);
|
|
61
|
+
}
|
|
50
62
|
}
|
|
51
63
|
finally {
|
|
52
64
|
if (mounted) {
|
|
@@ -61,9 +73,15 @@ export function AuthProvider({ children, authClient }) {
|
|
|
61
73
|
setAuthState(state);
|
|
62
74
|
if (!state.isAuthenticated && authClient.type === 'in-memory') {
|
|
63
75
|
setNeedsUserSelection(true);
|
|
76
|
+
setNeedsLogin(false);
|
|
77
|
+
}
|
|
78
|
+
else if (!state.isAuthenticated && authClient.type === 'bff') {
|
|
79
|
+
setNeedsLogin(true);
|
|
80
|
+
setNeedsUserSelection(false);
|
|
64
81
|
}
|
|
65
82
|
else {
|
|
66
83
|
setNeedsUserSelection(false);
|
|
84
|
+
setNeedsLogin(false);
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
});
|
|
@@ -80,24 +98,24 @@ export function AuthProvider({ children, authClient }) {
|
|
|
80
98
|
});
|
|
81
99
|
setNeedsUserSelection(false);
|
|
82
100
|
}, [authClient]);
|
|
83
|
-
|
|
84
|
-
const
|
|
101
|
+
const getAccessToken = useCallbackRef(() => authClient.getAccessToken());
|
|
102
|
+
const hasPermission = useCallbackRef((permission) => authClient.hasPermission(permission));
|
|
103
|
+
const logout = useCallbackRef(() => authClient.logout());
|
|
104
|
+
const contextValue = useMemo(() => ({
|
|
85
105
|
isAuthenticated: authState.isAuthenticated,
|
|
86
106
|
user: authState.user,
|
|
87
|
-
getAccessToken
|
|
88
|
-
hasPermission
|
|
89
|
-
logout
|
|
107
|
+
getAccessToken,
|
|
108
|
+
hasPermission,
|
|
109
|
+
logout,
|
|
90
110
|
authClient,
|
|
91
|
-
};
|
|
111
|
+
}), [authState.isAuthenticated, authState.user, authClient, getAccessToken, hasPermission, logout]);
|
|
92
112
|
// Show loading during initialization
|
|
93
113
|
if (isInitializing) {
|
|
94
114
|
return _jsx(LoadingScreen, {});
|
|
95
115
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return (_jsx(AuthContext.Provider, { value: contextValue, children: children }));
|
|
116
|
+
const bffLoginOverlay = needsLogin && authClient.type === 'bff' ? (_jsx("div", { className: "fixed inset-0 z-[100] bg-background", "data-testid": "bff-login-overlay", children: _jsx(LoginPage, { bffBaseUrl: authClient.bffBaseUrl }) })) : null;
|
|
117
|
+
const content = needsUserSelection && authClient.type === 'in-memory' ? (_jsx(UserSelector, { authClient: authClient, onUserSelected: handleUserSelected })) : (_jsxs(_Fragment, { children: [bffLoginOverlay, children] }));
|
|
118
|
+
return _jsx(AuthContext.Provider, { value: contextValue, children: content });
|
|
101
119
|
}
|
|
102
120
|
/**
|
|
103
121
|
* Hook to access auth context
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime environment from `window.__ENV__` (set by `/env.js` from the shell or adminModule() dev server).
|
|
3
|
+
*/
|
|
4
|
+
declare global {
|
|
5
|
+
interface Window {
|
|
6
|
+
__ENV__?: Record<string, string | undefined>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export interface EnvConfig {
|
|
10
|
+
/** Validated absolute URL, or `undefined` if missing/invalid */
|
|
11
|
+
adminGatewayUrl: string | undefined;
|
|
12
|
+
mockAuth: boolean;
|
|
13
|
+
environment: string;
|
|
14
|
+
/** Parsed from `REGISTRY_POLL_INTERVAL` in `window.__ENV__`, or `undefined` if missing/invalid */
|
|
15
|
+
registryPollInterval: number | undefined;
|
|
16
|
+
}
|
|
17
|
+
export declare const env: EnvConfig;
|