@burdenoff/microfe-auth 2026.1031.1 → 2026.1101.1
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/auth/AuthRoot.js +14 -11
- package/dist/auth/AuthRoot.js.map +1 -1
- package/dist/auth/AuthRoutes.js +1 -2
- package/dist/auth/AuthRoutes.js.map +1 -1
- package/dist/auth/components/AuthIcons.js +1 -2
- package/dist/auth/components/AuthIcons.js.map +1 -1
- package/dist/auth/components/AuthLayout.js +1 -2
- package/dist/auth/components/AuthLayout.js.map +1 -1
- package/dist/auth/components/CaptchaV2.js +9 -10
- package/dist/auth/components/CaptchaV2.js.map +1 -1
- package/dist/auth/components/ConfirmActionDialog.js +1 -2
- package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
- package/dist/auth/components/profile/UserProfileMenu.js +7 -6
- package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
- package/dist/auth/components/workspace/WorkspaceSelector.js +1 -2
- package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
- package/dist/auth/constants/permissions.js +1 -2
- package/dist/auth/constants/permissions.js.map +1 -1
- package/dist/auth/constants/sessionKeys.js +1 -2
- package/dist/auth/constants/sessionKeys.js.map +1 -1
- package/dist/auth/hooks/contextDeepLink.js +1 -2
- package/dist/auth/hooks/contextDeepLink.js.map +1 -1
- package/dist/auth/hooks/index.js +12 -11
- package/dist/auth/hooks/useAccountOperations.js +1 -2
- package/dist/auth/hooks/useAccountOperations.js.map +1 -1
- package/dist/auth/hooks/useActiveContextBootstrapMirror.js +1 -2
- package/dist/auth/hooks/useActiveContextBootstrapMirror.js.map +1 -1
- package/dist/auth/hooks/useAuthOperations.js +37 -37
- package/dist/auth/hooks/useAuthOperations.js.map +1 -1
- package/dist/auth/hooks/useAuthPermissions.js +1 -2
- package/dist/auth/hooks/useAuthPermissions.js.map +1 -1
- package/dist/auth/hooks/useCaptchaV3.js +1 -2
- package/dist/auth/hooks/useCaptchaV3.js.map +1 -1
- package/dist/auth/hooks/useContextSwitcherData.js +81 -78
- package/dist/auth/hooks/useContextSwitcherData.js.map +1 -1
- package/dist/auth/hooks/useMfaOperations.js +4 -5
- package/dist/auth/hooks/useMfaOperations.js.map +1 -1
- package/dist/auth/hooks/useMyWorkspaces.js +4 -5
- package/dist/auth/hooks/useMyWorkspaces.js.map +1 -1
- package/dist/auth/hooks/useOAuthOperations.js +11 -12
- package/dist/auth/hooks/useOAuthOperations.js.map +1 -1
- package/dist/auth/hooks/usePasswordOperations.js +1 -2
- package/dist/auth/hooks/usePasswordOperations.js.map +1 -1
- package/dist/auth/hooks/useSessionOperations.js +3 -4
- package/dist/auth/hooks/useSessionOperations.js.map +1 -1
- package/dist/auth/hooks/useSignupCaptcha.js +1 -2
- package/dist/auth/hooks/useSignupCaptcha.js.map +1 -1
- package/dist/auth/hooks/useSsoOperations.js +80 -81
- package/dist/auth/hooks/useSsoOperations.js.map +1 -1
- package/dist/auth/hooks/useTokenRefresh.js +8 -12
- package/dist/auth/hooks/useTokenRefresh.js.map +1 -1
- package/dist/auth/hooks/useUserOperations.js +1 -2
- package/dist/auth/hooks/useUserOperations.js.map +1 -1
- package/dist/auth/hooks/useWorkspaceOperations.js +1 -2
- package/dist/auth/hooks/useWorkspaceOperations.js.map +1 -1
- package/dist/auth/hooks/useWorkspaceToken.js +1 -2
- package/dist/auth/hooks/useWorkspaceToken.js.map +1 -1
- package/dist/auth/index.js +19 -18
- package/dist/auth/pages/AuditLogPage.js +21 -22
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.js +15 -17
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.js +89 -105
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.js +30 -31
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.js +18 -20
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.js +31 -35
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/OAuthCallbackPage.js +4 -11
- package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.js +20 -23
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.js +14 -18
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.js +180 -181
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SSOClearPage.js +9 -10
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +9 -13
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js +3 -7
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SSORelayPage.js +11 -10
- package/dist/auth/pages/SSORelayPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.js +19 -21
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionBridgePage.js +9 -16
- package/dist/auth/pages/SessionBridgePage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.js +17 -18
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.js +17 -22
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.js +69 -69
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.js +9 -10
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.js +16 -23
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.js +140 -141
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +446 -453
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/auth/providers/AuthProvider.js +1 -2
- package/dist/auth/providers/AuthProvider.js.map +1 -1
- package/dist/auth/providers/CaptchaProvider.js +1 -2
- package/dist/auth/providers/CaptchaProvider.js.map +1 -1
- package/dist/auth/store/authStore.js +5 -12
- package/dist/auth/store/authStore.js.map +1 -1
- package/dist/auth/utils/captchaConfig.js +5 -8
- package/dist/auth/utils/captchaConfig.js.map +1 -1
- package/dist/auth/utils/formatters.js +1 -2
- package/dist/auth/utils/formatters.js.map +1 -1
- package/dist/auth/utils/gatewayBaseUrls.js +1 -2
- package/dist/auth/utils/gatewayBaseUrls.js.map +1 -1
- package/dist/auth/utils/legalUrls.js +2 -3
- package/dist/auth/utils/legalUrls.js.map +1 -1
- package/dist/auth/utils/redirect.js +1 -2
- package/dist/auth/utils/redirect.js.map +1 -1
- package/dist/auth/utils/ssoOrigins.js +2 -3
- package/dist/auth/utils/ssoOrigins.js.map +1 -1
- package/dist/auth/utils/validation.js +34 -36
- package/dist/auth/utils/validation.js.map +1 -1
- package/dist/generated/global-operations.js +2 -3
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/wspace-operations.js +2 -3
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/microfe-auth.css +1 -1
- package/dist/shared/hooks/useTr.js +1 -2
- package/dist/shared/hooks/useTr.js.map +1 -1
- package/dist/translations/en.js +1 -2
- package/dist/translations/en.js.map +1 -1
- package/dist/utils/nativeBridge.js +1 -2
- package/package.json +7 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMfaOperations.js","names":[],"sources":["../../../src/auth/hooks/useMfaOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport {\n loginProfile,\n type ProfileUser,\n} from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport {\n SetupMfaDocument,\n EnableMfaDocument,\n VerifyMfaDocument,\n DisableMfaDocument,\n} from \"../../generated/global-operations\";\nimport type {\n MFAMethod,\n MFASetupResult,\n MFAVerifyInput,\n AuthResult,\n User,\n} from \"../types\";\n\nfunction toProfileUser(user: User): ProfileUser {\n return { ...user };\n}\n\nfunction setShellSsoCookie(userId: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({\n id: userId,\n origin: window.location.origin,\n });\n document.cookie = `bf-sso=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n document.cookie = `bf-sso-profile=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n // Best effort only.\n }\n}\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface SetupMFAResponse {\n setupMFA: MFASetupResult;\n}\n\ninterface SetupMFAVariables {\n method: MFAMethod;\n}\n\ninterface EnableMFAResponse {\n enableMFA: boolean;\n}\n\ninterface EnableMFAVariables {\n code: string;\n}\n\ninterface VerifyMFAResponse {\n verifyMFA: AuthResult;\n}\n\ninterface VerifyMFAVariables {\n input: MFAVerifyInput;\n}\n\ninterface DisableMFAResponse {\n disableMFA: boolean;\n}\n\n// GraphQL Operations\n\n/**\n * Hook for MFA operations\n */\nexport function useMfaOperations() {\n const store = useAuthStore();\n const context = useAuth();\n const eventBus = useEventBus();\n const [setupMfaMutation] = useMutation<SetupMFAResponse, SetupMFAVariables>(\n SetupMfaDocument,\n );\n const [enableMfaMutation] = useMutation<\n EnableMFAResponse,\n EnableMFAVariables\n >(EnableMfaDocument);\n const [verifyMfaMutation] = useMutation<\n VerifyMFAResponse,\n VerifyMFAVariables\n >(VerifyMfaDocument);\n const [disableMfaMutation] =\n useMutation<DisableMFAResponse>(DisableMfaDocument);\n\n /**\n * Setup MFA for the user\n */\n const setupMFA = async (method: MFAMethod): Promise<MFASetupResult> => {\n try {\n store.setLoading(true);\n\n const { data } = await setupMfaMutation({\n variables: { method },\n });\n\n return data!.setupMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"MFA setup failed:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Enable MFA after setup with verification code\n */\n const enableMFA = async (code: string): Promise<boolean> => {\n try {\n store.setLoading(true);\n\n const { data } = await enableMfaMutation({\n variables: { code },\n });\n\n if (data!.enableMFA && store.user) {\n store.setUser({ ...store.user, mfaEnabled: true });\n }\n\n return data!.enableMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"Failed to enable MFA:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Verify MFA code during login\n */\n const verifyMFA = async (input: MFAVerifyInput): Promise<AuthResult> => {\n try {\n store.setLoading(true);\n\n const { data, error } = await verifyMfaMutation({\n variables: { input },\n });\n\n const result = data?.verifyMFA;\n\n // Server returned no data (GraphQL errors, network failure, or null payload).\n // Surface a friendly message instead of letting `result.success` throw a\n // \"Cannot read properties of null\" error to the UI. Apollo Client v4\n // exposes a single `error` (ApolloError) on MutateResult, not an array.\n if (!result) {\n const gqlMessage = error?.message;\n return {\n success: false,\n error: gqlMessage || \"Invalid verification code. Please try again.\",\n } as AuthResult;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n const expiresAt = result.expiresIn\n ? Date.now() + result.expiresIn * 1000\n : undefined;\n loginProfile(toProfileUser(result.user), {\n accessToken: result.token,\n refreshToken: result.refreshToken || \"\",\n expiresAt,\n });\n setShellSsoCookie(result.user.id);\n\n eventBus.emit(\n \"auth.signed_in\",\n {\n userId: result.user.id,\n tenantId: result.user.tenantId,\n workspaceId: result.user.defaultWorkspaceId,\n mfaEnabled: result.user.mfaEnabled,\n emailVerified: result.user.emailVerified,\n },\n { component: \"useMfaOperations\" },\n );\n\n context.onAuthSuccess?.(result.user, result.token, result.refreshToken);\n } else {\n console.warn(\n \"[useMfaOperations] NOT saving tokens - condition failed:\",\n {\n success: result.success,\n hasUser: !!result.user,\n hasToken: !!result.token,\n },\n );\n }\n\n return result;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"MFA verification failed:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Disable MFA\n */\n const disableMFA = async (): Promise<boolean> => {\n try {\n store.setLoading(true);\n\n const { data } = await disableMfaMutation();\n\n if (data!.disableMFA && store.user) {\n store.setUser({ ...store.user, mfaEnabled: false });\n }\n\n return data!.disableMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"Failed to disable MFA:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n // Legacy aliases\n const enableMfa = enableMFA;\n const verifyMfa = verifyMFA;\n const disableMfa = disableMFA;\n\n return {\n setupMFA,\n enableMFA,\n verifyMFA,\n disableMFA,\n // Legacy aliases\n enableMfa,\n verifyMfa,\n disableMfa,\n // State\n mfaRequired: store.mfaRequired,\n mfaChallengeId: store.mfaChallengeId,\n isLoading: store.isLoading,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useMfaOperations.js","names":["toProfileUser","user","setShellSsoCookie","domain","domainPart","securePart","ssoValue","userId","useMfaOperations","store","useAuthStore","context","useAuth","eventBus","useEventBus","setupMfaMutation","useMutation","SetupMfaDocument","enableMfaMutation","EnableMfaDocument","verifyMfaMutation","VerifyMfaDocument","disableMfaMutation","DisableMfaDocument","setupMFA","data","error","enableMFA","verifyMFA","result","expiresAt","loginProfile","disableMFA"],"sources":["../../../src/auth/hooks/useMfaOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useEventBus } from \"@burdenoff/fe-libs/shared/events\";\nimport {\n loginProfile,\n type ProfileUser,\n} from \"@burdenoff/fe-libs/shared/providers/shell/ProfileStorageManager\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport {\n SetupMfaDocument,\n EnableMfaDocument,\n VerifyMfaDocument,\n DisableMfaDocument,\n} from \"../../generated/global-operations\";\nimport type {\n MFAMethod,\n MFASetupResult,\n MFAVerifyInput,\n AuthResult,\n User,\n} from \"../types\";\n\nfunction toProfileUser(user: User): ProfileUser {\n return { ...user };\n}\n\nfunction setShellSsoCookie(userId: string): void {\n try {\n const hostname = window.location.hostname;\n const domain = hostname.endsWith(\".local.burdenoff.com\")\n ? \".local.burdenoff.com\"\n : undefined;\n const domainPart = domain ? `; domain=${domain}` : \"\";\n const securePart = window.location.protocol === \"https:\" ? \"; secure\" : \"\";\n const ssoValue = JSON.stringify({\n id: userId,\n origin: window.location.origin,\n });\n document.cookie = `bf-sso=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n document.cookie = `bf-sso-profile=${encodeURIComponent(ssoValue)}${domainPart}; path=/; samesite=lax${securePart}; max-age=86400`;\n } catch {\n // Best effort only.\n }\n}\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface SetupMFAResponse {\n setupMFA: MFASetupResult;\n}\n\ninterface SetupMFAVariables {\n method: MFAMethod;\n}\n\ninterface EnableMFAResponse {\n enableMFA: boolean;\n}\n\ninterface EnableMFAVariables {\n code: string;\n}\n\ninterface VerifyMFAResponse {\n verifyMFA: AuthResult;\n}\n\ninterface VerifyMFAVariables {\n input: MFAVerifyInput;\n}\n\ninterface DisableMFAResponse {\n disableMFA: boolean;\n}\n\n// GraphQL Operations\n\n/**\n * Hook for MFA operations\n */\nexport function useMfaOperations() {\n const store = useAuthStore();\n const context = useAuth();\n const eventBus = useEventBus();\n const [setupMfaMutation] = useMutation<SetupMFAResponse, SetupMFAVariables>(\n SetupMfaDocument,\n );\n const [enableMfaMutation] = useMutation<\n EnableMFAResponse,\n EnableMFAVariables\n >(EnableMfaDocument);\n const [verifyMfaMutation] = useMutation<\n VerifyMFAResponse,\n VerifyMFAVariables\n >(VerifyMfaDocument);\n const [disableMfaMutation] =\n useMutation<DisableMFAResponse>(DisableMfaDocument);\n\n /**\n * Setup MFA for the user\n */\n const setupMFA = async (method: MFAMethod): Promise<MFASetupResult> => {\n try {\n store.setLoading(true);\n\n const { data } = await setupMfaMutation({\n variables: { method },\n });\n\n return data!.setupMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"MFA setup failed:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Enable MFA after setup with verification code\n */\n const enableMFA = async (code: string): Promise<boolean> => {\n try {\n store.setLoading(true);\n\n const { data } = await enableMfaMutation({\n variables: { code },\n });\n\n if (data!.enableMFA && store.user) {\n store.setUser({ ...store.user, mfaEnabled: true });\n }\n\n return data!.enableMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"Failed to enable MFA:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Verify MFA code during login\n */\n const verifyMFA = async (input: MFAVerifyInput): Promise<AuthResult> => {\n try {\n store.setLoading(true);\n\n const { data, error } = await verifyMfaMutation({\n variables: { input },\n });\n\n const result = data?.verifyMFA;\n\n // Server returned no data (GraphQL errors, network failure, or null payload).\n // Surface a friendly message instead of letting `result.success` throw a\n // \"Cannot read properties of null\" error to the UI. Apollo Client v4\n // exposes a single `error` (ApolloError) on MutateResult, not an array.\n if (!result) {\n const gqlMessage = error?.message;\n return {\n success: false,\n error: gqlMessage || \"Invalid verification code. Please try again.\",\n } as AuthResult;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n const expiresAt = result.expiresIn\n ? Date.now() + result.expiresIn * 1000\n : undefined;\n loginProfile(toProfileUser(result.user), {\n accessToken: result.token,\n refreshToken: result.refreshToken || \"\",\n expiresAt,\n });\n setShellSsoCookie(result.user.id);\n\n eventBus.emit(\n \"auth.signed_in\",\n {\n userId: result.user.id,\n tenantId: result.user.tenantId,\n workspaceId: result.user.defaultWorkspaceId,\n mfaEnabled: result.user.mfaEnabled,\n emailVerified: result.user.emailVerified,\n },\n { component: \"useMfaOperations\" },\n );\n\n context.onAuthSuccess?.(result.user, result.token, result.refreshToken);\n } else {\n console.warn(\n \"[useMfaOperations] NOT saving tokens - condition failed:\",\n {\n success: result.success,\n hasUser: !!result.user,\n hasToken: !!result.token,\n },\n );\n }\n\n return result;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"MFA verification failed:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Disable MFA\n */\n const disableMFA = async (): Promise<boolean> => {\n try {\n store.setLoading(true);\n\n const { data } = await disableMfaMutation();\n\n if (data!.disableMFA && store.user) {\n store.setUser({ ...store.user, mfaEnabled: false });\n }\n\n return data!.disableMFA;\n } catch (error) {\n if (import.meta.env.DEV) {\n console.error(\"Failed to disable MFA:\", error);\n }\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n // Legacy aliases\n const enableMfa = enableMFA;\n const verifyMfa = verifyMFA;\n const disableMfa = disableMFA;\n\n return {\n setupMFA,\n enableMFA,\n verifyMFA,\n disableMFA,\n // Legacy aliases\n enableMfa,\n verifyMfa,\n disableMfa,\n // State\n mfaRequired: store.mfaRequired,\n mfaChallengeId: store.mfaChallengeId,\n isLoading: store.isLoading,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { resolveGatewayBaseUrls as t } from "../utils/gatewayBaseUrls.js";
|
|
3
|
+
import { MyWorkspacesDocument as n } from "../../generated/global-operations.js";
|
|
4
4
|
import { useCallback as r, useEffect as i, useMemo as a, useRef as o, useState as s } from "react";
|
|
5
5
|
import { useQuery as c } from "@apollo/client/react";
|
|
6
6
|
import { graphqlFetch as l } from "@burdenoff/fe-libs/shared/graphql";
|
|
7
|
-
|
|
7
|
+
// src/auth/hooks/useMyWorkspaces.ts
|
|
8
8
|
var u = "\n query MyWorkspaces {\n myWorkspaces {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n }\n }\n }\n", d = "\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n workspaceId\n organizationId\n expiresAt\n scopes\n error {\n code\n message\n }\n }\n }\n";
|
|
9
9
|
function f() {
|
|
10
|
-
let f = e(), [p, m] = s(!1), [h, g] = s(!1), { globalBaseUrl: _, workspaceBaseUrl: v } = a(() =>
|
|
10
|
+
let f = e(), [p, m] = s(!1), [h, g] = s(!1), { globalBaseUrl: _, workspaceBaseUrl: v } = a(() => t(), []), y = o(!1), b = f.accessToken, x = a(() => b ? { headers: { authorization: `Bearer ${b}` } } : void 0, [b]), { data: S, loading: C, error: w, refetch: T } = c(n, {
|
|
11
11
|
skip: !f.isAuthenticated || !f.accessToken || y.current,
|
|
12
12
|
fetchPolicy: "cache-and-network",
|
|
13
13
|
context: x
|
|
@@ -134,7 +134,6 @@ function f() {
|
|
|
134
134
|
refetch: T
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
-
//#endregion
|
|
138
137
|
export { f as useMyWorkspaces };
|
|
139
138
|
|
|
140
139
|
//# sourceMappingURL=useMyWorkspaces.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMyWorkspaces.js","names":[],"sources":["../../../src/auth/hooks/useMyWorkspaces.ts"],"sourcesContent":["import { useCallback, useState, useEffect, useRef, useMemo } from \"react\";\nimport { useQuery } from \"@apollo/client/react\";\nimport { graphqlFetch } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { Workspace } from \"../types\";\nimport { resolveGatewayBaseUrls } from \"../utils/gatewayBaseUrls\";\n\nimport { MyWorkspacesDocument } from \"../../generated/global-operations\";\n\n// GraphQL Response Types\ninterface MyWorkspacesResponse {\n myWorkspaces: {\n total: number;\n items: Workspace[];\n };\n}\n\ninterface IssueTokenError {\n code: string;\n message: string;\n}\n\ninterface IssueWorkspaceTokenResult {\n success: boolean;\n token: string | null;\n workspaceId: string | null;\n organizationId: string | null;\n expiresAt: string | null;\n scopes: string[];\n error: IssueTokenError | null;\n}\n\n// GraphQL Operations\n\n// String versions for graphqlFetch\nconst MY_WORKSPACES_QUERY_STRING = `\n query MyWorkspaces {\n myWorkspaces {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n }\n }\n }\n`;\n\nconst ISSUE_WORKSPACE_TOKEN_MUTATION_STRING = `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n workspaceId\n organizationId\n expiresAt\n scopes\n error {\n code\n message\n }\n }\n }\n`;\n\n/**\n * Hook for fetching user's workspaces and issuing workspace tokens\n *\n * This hook provides:\n * - Fetch workspaces: Query to get all workspaces the user has access to\n * - Issue workspace token: Get a workspace-scoped token for API access\n * - Complete auth flow: Fetch workspaces + auto-select + issue token\n *\n * Uses the shell's Apollo client (via GatewayRoute) for useQuery, and\n * graphqlFetch from fe-libs for imperative calls that need explicit tokens.\n */\nexport function useMyWorkspaces() {\n const store = useAuthStore();\n const [tokenLoading, setTokenLoading] = useState(false);\n const [workspacesLoading, setWorkspacesLoading] = useState(false);\n const { globalBaseUrl, workspaceBaseUrl } = useMemo(\n () => resolveGatewayBaseUrls(),\n [],\n );\n\n // Track whether completeAuthFlow is in progress to prevent the automatic\n // useQuery from racing with the imperative fetchWorkspaces call inside it.\n const authFlowInProgressRef = useRef(false);\n\n // Memoize Apollo context to prevent unstable object references from\n // causing Apollo to re-execute the query on every render.\n const accessToken = store.accessToken;\n const queryContext = useMemo(\n () =>\n accessToken\n ? { headers: { authorization: `Bearer ${accessToken}` } }\n : undefined,\n [accessToken],\n );\n\n // Query for fetching workspaces (uses shell's global Apollo client via GatewayRoute)\n // Skip during completeAuthFlow — it handles fetching imperatively via fetchWorkspaces().\n // This query is only for subsequent page loads or manual refetches.\n const {\n data,\n loading: queryLoading,\n error: queryError,\n refetch,\n } = useQuery<MyWorkspacesResponse>(MyWorkspacesDocument, {\n skip:\n !store.isAuthenticated ||\n !store.accessToken ||\n authFlowInProgressRef.current,\n fetchPolicy: \"cache-and-network\",\n context: queryContext,\n });\n\n // Handle workspaces data updates — only sync to store when we have data\n // and auth flow is NOT in progress (to avoid competing writes).\n useEffect(() => {\n if (data?.myWorkspaces?.items && !authFlowInProgressRef.current) {\n useAuthStore.getState().setWorkspaces(data.myWorkspaces.items);\n }\n }, [data]);\n\n // Handle workspaces fetch error (suppress auth errors during login transition)\n useEffect(() => {\n if (queryError) {\n const isAuthError =\n queryError.message?.toLowerCase().includes(\"unauthenticated\") ||\n queryError.message?.toLowerCase().includes(\"unauthorized\") ||\n queryError.message?.toLowerCase().includes(\"not authenticated\");\n if (!isAuthError) {\n console.error(\"Failed to fetch workspaces:\", queryError);\n }\n }\n }, [queryError]);\n\n /**\n * Manually fetch workspaces using graphqlFetch with explicit Authorization header.\n * Used when the token is freshly obtained (e.g. right after login) and may not\n * yet be in profile storage.\n */\n const fetchWorkspaces = useCallback(\n async (accessToken?: string): Promise<Workspace[]> => {\n const token = accessToken || useAuthStore.getState().accessToken;\n\n if (!token) {\n console.error(\"Cannot fetch workspaces: No access token available\");\n return [];\n }\n\n store.setLoadingWorkspaces(true);\n setWorkspacesLoading(true);\n try {\n const result = await graphqlFetch<{\n myWorkspaces: { items: Workspace[] };\n }>({\n gateway: \"global\",\n query: MY_WORKSPACES_QUERY_STRING,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n console.error(\"Failed to fetch workspaces:\", result.errors);\n throw new Error(errorMessage);\n }\n\n const workspaces: Workspace[] = result.data?.myWorkspaces?.items ?? [];\n store.setWorkspaces(workspaces);\n return workspaces;\n } catch (error) {\n console.error(\"Failed to fetch workspaces:\", error);\n return [];\n } finally {\n store.setLoadingWorkspaces(false);\n setWorkspacesLoading(false);\n }\n },\n [store, globalBaseUrl],\n );\n\n /**\n * Issue workspace token for a specific workspace.\n * Uses graphqlFetch to call the workspace gateway.\n */\n const issueWorkspaceToken = useCallback(\n async (\n workspaceId: string,\n organizationId: string,\n accessToken?: string,\n ): Promise<{ success: boolean; token?: string; error?: string }> => {\n const authToken =\n accessToken || useAuthStore.getState().accessToken || undefined;\n\n if (!authToken) {\n return { success: false, error: \"No access token available\" };\n }\n\n setTokenLoading(true);\n try {\n const result = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION_STRING,\n variables: {\n input: {\n organizationId,\n workspaceId,\n },\n },\n authToken,\n workspaceToken: false,\n baseUrl: workspaceBaseUrl,\n workspaceId,\n organizationId,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n console.error(\"GraphQL errors:\", result.errors);\n return { success: false, error: errorMessage };\n }\n\n const tokenResult = result.data?.issueWorkspaceToken;\n\n if (tokenResult?.success && tokenResult.token) {\n store.setCurrentWorkspaceId(workspaceId);\n store.setWorkspaceToken(tokenResult.token);\n return { success: true, token: tokenResult.token };\n }\n\n return {\n success: false,\n error:\n tokenResult?.error?.message ?? \"Failed to issue workspace token\",\n };\n } catch (error) {\n console.error(\"Failed to issue workspace token:\", error);\n return {\n success: false,\n error:\n error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n } finally {\n setTokenLoading(false);\n }\n },\n [store, workspaceBaseUrl],\n );\n\n /**\n * Select a workspace and issue token for it\n */\n const selectWorkspace = useCallback(\n async (\n workspace: Workspace,\n ): Promise<{ success: boolean; error?: string }> => {\n return issueWorkspaceToken(workspace.id, workspace.organizationId);\n },\n [issueWorkspaceToken],\n );\n\n /**\n * Complete the post-login auth flow:\n * 1. Fetch user's workspaces\n * 2. Auto-select default workspace (user's defaultWorkspaceId or first workspace)\n * 3. Issue workspace token\n */\n const completeAuthFlow = useCallback(\n async (\n accessToken?: string,\n ): Promise<{\n success: boolean;\n workspace?: Workspace;\n error?: string;\n }> => {\n // Signal that the auth flow is handling workspace fetching imperatively.\n // This prevents the useQuery from firing a duplicate request.\n authFlowInProgressRef.current = true;\n store.setLoadingWorkspaces(true);\n try {\n const workspaces = await fetchWorkspaces(accessToken);\n\n if (workspaces.length === 0) {\n return {\n success: false,\n error: \"No workspaces found. Please create a workspace first.\",\n };\n }\n\n const user = store.user;\n let selectedWorkspace: Workspace | undefined;\n\n if (user?.defaultWorkspaceId) {\n selectedWorkspace = workspaces.find(\n (ws) => ws.id === user.defaultWorkspaceId,\n );\n }\n\n if (!selectedWorkspace) {\n selectedWorkspace = workspaces[0];\n }\n\n if (!selectedWorkspace) {\n return { success: false, error: \"Could not select a workspace\" };\n }\n\n const tokenResult = await issueWorkspaceToken(\n selectedWorkspace.id,\n selectedWorkspace.organizationId,\n accessToken,\n );\n\n if (!tokenResult.success) {\n console.warn(\n \"Workspace selected but token issue failed:\",\n tokenResult.error,\n );\n return {\n success: true,\n workspace: selectedWorkspace,\n error: `Workspace selected but token issue failed: ${tokenResult.error}`,\n };\n }\n\n // Set workspace/org context so downstream hooks (useProjectSyncEffect)\n // can fetch projects for the context switcher dropdown.\n store.setCurrentWorkspaceId(selectedWorkspace.id);\n store.setCurrentOrganizationId(selectedWorkspace.organizationId);\n\n return {\n success: true,\n workspace: selectedWorkspace,\n };\n } catch (error) {\n console.error(\"Failed to complete auth flow:\", error);\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n } finally {\n store.setLoadingWorkspaces(false);\n // Allow the useQuery to operate normally after the auth flow completes.\n // Use a short delay so that the navigation to \"/\" happens first,\n // preventing the query from firing on the login page.\n setTimeout(() => {\n authFlowInProgressRef.current = false;\n }, 500);\n }\n },\n [store, fetchWorkspaces, issueWorkspaceToken],\n );\n\n return {\n // Data\n workspaces: data?.myWorkspaces?.items ?? store.workspaces,\n totalWorkspaces: data?.myWorkspaces?.total ?? store.workspaces.length,\n currentWorkspaceId: store.currentWorkspaceId,\n workspaceToken: store.workspaceToken,\n\n // State\n isLoading:\n queryLoading ||\n tokenLoading ||\n workspacesLoading ||\n store.isLoadingWorkspaces,\n error: queryError?.message ?? null,\n\n // Actions\n fetchWorkspaces,\n issueWorkspaceToken,\n selectWorkspace,\n completeAuthFlow,\n refetch,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useMyWorkspaces.js","names":["MY_WORKSPACES_QUERY_STRING","ISSUE_WORKSPACE_TOKEN_MUTATION_STRING","useMyWorkspaces","store","useAuthStore","tokenLoading","setTokenLoading","useState","workspacesLoading","setWorkspacesLoading","useMemo","resolveGatewayBaseUrls","authFlowInProgressRef","useRef","accessToken","queryContext","queryLoading","queryError","useQuery","MyWorkspacesDocument","useEffect","data","fetchWorkspaces","useCallback","token","result","graphqlFetch","globalBaseUrl","errorMessage","workspaces","error","issueWorkspaceToken","authToken","workspaceBaseUrl","tokenResult","workspaceId","selectWorkspace","workspace","completeAuthFlow","user","selectedWorkspace","ws"],"sources":["../../../src/auth/hooks/useMyWorkspaces.ts"],"sourcesContent":["import { useCallback, useState, useEffect, useRef, useMemo } from \"react\";\nimport { useQuery } from \"@apollo/client/react\";\nimport { graphqlFetch } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { Workspace } from \"../types\";\nimport { resolveGatewayBaseUrls } from \"../utils/gatewayBaseUrls\";\n\nimport { MyWorkspacesDocument } from \"../../generated/global-operations\";\n\n// GraphQL Response Types\ninterface MyWorkspacesResponse {\n myWorkspaces: {\n total: number;\n items: Workspace[];\n };\n}\n\ninterface IssueTokenError {\n code: string;\n message: string;\n}\n\ninterface IssueWorkspaceTokenResult {\n success: boolean;\n token: string | null;\n workspaceId: string | null;\n organizationId: string | null;\n expiresAt: string | null;\n scopes: string[];\n error: IssueTokenError | null;\n}\n\n// GraphQL Operations\n\n// String versions for graphqlFetch\nconst MY_WORKSPACES_QUERY_STRING = `\n query MyWorkspaces {\n myWorkspaces {\n total\n items {\n id\n name\n slug\n tenantId\n organizationId\n }\n }\n }\n`;\n\nconst ISSUE_WORKSPACE_TOKEN_MUTATION_STRING = `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n workspaceId\n organizationId\n expiresAt\n scopes\n error {\n code\n message\n }\n }\n }\n`;\n\n/**\n * Hook for fetching user's workspaces and issuing workspace tokens\n *\n * This hook provides:\n * - Fetch workspaces: Query to get all workspaces the user has access to\n * - Issue workspace token: Get a workspace-scoped token for API access\n * - Complete auth flow: Fetch workspaces + auto-select + issue token\n *\n * Uses the shell's Apollo client (via GatewayRoute) for useQuery, and\n * graphqlFetch from fe-libs for imperative calls that need explicit tokens.\n */\nexport function useMyWorkspaces() {\n const store = useAuthStore();\n const [tokenLoading, setTokenLoading] = useState(false);\n const [workspacesLoading, setWorkspacesLoading] = useState(false);\n const { globalBaseUrl, workspaceBaseUrl } = useMemo(\n () => resolveGatewayBaseUrls(),\n [],\n );\n\n // Track whether completeAuthFlow is in progress to prevent the automatic\n // useQuery from racing with the imperative fetchWorkspaces call inside it.\n const authFlowInProgressRef = useRef(false);\n\n // Memoize Apollo context to prevent unstable object references from\n // causing Apollo to re-execute the query on every render.\n const accessToken = store.accessToken;\n const queryContext = useMemo(\n () =>\n accessToken\n ? { headers: { authorization: `Bearer ${accessToken}` } }\n : undefined,\n [accessToken],\n );\n\n // Query for fetching workspaces (uses shell's global Apollo client via GatewayRoute)\n // Skip during completeAuthFlow — it handles fetching imperatively via fetchWorkspaces().\n // This query is only for subsequent page loads or manual refetches.\n const {\n data,\n loading: queryLoading,\n error: queryError,\n refetch,\n } = useQuery<MyWorkspacesResponse>(MyWorkspacesDocument, {\n skip:\n !store.isAuthenticated ||\n !store.accessToken ||\n authFlowInProgressRef.current,\n fetchPolicy: \"cache-and-network\",\n context: queryContext,\n });\n\n // Handle workspaces data updates — only sync to store when we have data\n // and auth flow is NOT in progress (to avoid competing writes).\n useEffect(() => {\n if (data?.myWorkspaces?.items && !authFlowInProgressRef.current) {\n useAuthStore.getState().setWorkspaces(data.myWorkspaces.items);\n }\n }, [data]);\n\n // Handle workspaces fetch error (suppress auth errors during login transition)\n useEffect(() => {\n if (queryError) {\n const isAuthError =\n queryError.message?.toLowerCase().includes(\"unauthenticated\") ||\n queryError.message?.toLowerCase().includes(\"unauthorized\") ||\n queryError.message?.toLowerCase().includes(\"not authenticated\");\n if (!isAuthError) {\n console.error(\"Failed to fetch workspaces:\", queryError);\n }\n }\n }, [queryError]);\n\n /**\n * Manually fetch workspaces using graphqlFetch with explicit Authorization header.\n * Used when the token is freshly obtained (e.g. right after login) and may not\n * yet be in profile storage.\n */\n const fetchWorkspaces = useCallback(\n async (accessToken?: string): Promise<Workspace[]> => {\n const token = accessToken || useAuthStore.getState().accessToken;\n\n if (!token) {\n console.error(\"Cannot fetch workspaces: No access token available\");\n return [];\n }\n\n store.setLoadingWorkspaces(true);\n setWorkspacesLoading(true);\n try {\n const result = await graphqlFetch<{\n myWorkspaces: { items: Workspace[] };\n }>({\n gateway: \"global\",\n query: MY_WORKSPACES_QUERY_STRING,\n authToken: token,\n baseUrl: globalBaseUrl,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n console.error(\"Failed to fetch workspaces:\", result.errors);\n throw new Error(errorMessage);\n }\n\n const workspaces: Workspace[] = result.data?.myWorkspaces?.items ?? [];\n store.setWorkspaces(workspaces);\n return workspaces;\n } catch (error) {\n console.error(\"Failed to fetch workspaces:\", error);\n return [];\n } finally {\n store.setLoadingWorkspaces(false);\n setWorkspacesLoading(false);\n }\n },\n [store, globalBaseUrl],\n );\n\n /**\n * Issue workspace token for a specific workspace.\n * Uses graphqlFetch to call the workspace gateway.\n */\n const issueWorkspaceToken = useCallback(\n async (\n workspaceId: string,\n organizationId: string,\n accessToken?: string,\n ): Promise<{ success: boolean; token?: string; error?: string }> => {\n const authToken =\n accessToken || useAuthStore.getState().accessToken || undefined;\n\n if (!authToken) {\n return { success: false, error: \"No access token available\" };\n }\n\n setTokenLoading(true);\n try {\n const result = await graphqlFetch<{\n issueWorkspaceToken: IssueWorkspaceTokenResult;\n }>({\n gateway: \"workspace\",\n query: ISSUE_WORKSPACE_TOKEN_MUTATION_STRING,\n variables: {\n input: {\n organizationId,\n workspaceId,\n },\n },\n authToken,\n workspaceToken: false,\n baseUrl: workspaceBaseUrl,\n workspaceId,\n organizationId,\n });\n\n if (result.errors?.length) {\n const errorMessage = result.errors[0]?.message ?? \"GraphQL error\";\n console.error(\"GraphQL errors:\", result.errors);\n return { success: false, error: errorMessage };\n }\n\n const tokenResult = result.data?.issueWorkspaceToken;\n\n if (tokenResult?.success && tokenResult.token) {\n store.setCurrentWorkspaceId(workspaceId);\n store.setWorkspaceToken(tokenResult.token);\n return { success: true, token: tokenResult.token };\n }\n\n return {\n success: false,\n error:\n tokenResult?.error?.message ?? \"Failed to issue workspace token\",\n };\n } catch (error) {\n console.error(\"Failed to issue workspace token:\", error);\n return {\n success: false,\n error:\n error instanceof Error ? error.message : \"Unknown error occurred\",\n };\n } finally {\n setTokenLoading(false);\n }\n },\n [store, workspaceBaseUrl],\n );\n\n /**\n * Select a workspace and issue token for it\n */\n const selectWorkspace = useCallback(\n async (\n workspace: Workspace,\n ): Promise<{ success: boolean; error?: string }> => {\n return issueWorkspaceToken(workspace.id, workspace.organizationId);\n },\n [issueWorkspaceToken],\n );\n\n /**\n * Complete the post-login auth flow:\n * 1. Fetch user's workspaces\n * 2. Auto-select default workspace (user's defaultWorkspaceId or first workspace)\n * 3. Issue workspace token\n */\n const completeAuthFlow = useCallback(\n async (\n accessToken?: string,\n ): Promise<{\n success: boolean;\n workspace?: Workspace;\n error?: string;\n }> => {\n // Signal that the auth flow is handling workspace fetching imperatively.\n // This prevents the useQuery from firing a duplicate request.\n authFlowInProgressRef.current = true;\n store.setLoadingWorkspaces(true);\n try {\n const workspaces = await fetchWorkspaces(accessToken);\n\n if (workspaces.length === 0) {\n return {\n success: false,\n error: \"No workspaces found. Please create a workspace first.\",\n };\n }\n\n const user = store.user;\n let selectedWorkspace: Workspace | undefined;\n\n if (user?.defaultWorkspaceId) {\n selectedWorkspace = workspaces.find(\n (ws) => ws.id === user.defaultWorkspaceId,\n );\n }\n\n if (!selectedWorkspace) {\n selectedWorkspace = workspaces[0];\n }\n\n if (!selectedWorkspace) {\n return { success: false, error: \"Could not select a workspace\" };\n }\n\n const tokenResult = await issueWorkspaceToken(\n selectedWorkspace.id,\n selectedWorkspace.organizationId,\n accessToken,\n );\n\n if (!tokenResult.success) {\n console.warn(\n \"Workspace selected but token issue failed:\",\n tokenResult.error,\n );\n return {\n success: true,\n workspace: selectedWorkspace,\n error: `Workspace selected but token issue failed: ${tokenResult.error}`,\n };\n }\n\n // Set workspace/org context so downstream hooks (useProjectSyncEffect)\n // can fetch projects for the context switcher dropdown.\n store.setCurrentWorkspaceId(selectedWorkspace.id);\n store.setCurrentOrganizationId(selectedWorkspace.organizationId);\n\n return {\n success: true,\n workspace: selectedWorkspace,\n };\n } catch (error) {\n console.error(\"Failed to complete auth flow:\", error);\n return {\n success: false,\n error: error instanceof Error ? error.message : \"Unknown error\",\n };\n } finally {\n store.setLoadingWorkspaces(false);\n // Allow the useQuery to operate normally after the auth flow completes.\n // Use a short delay so that the navigation to \"/\" happens first,\n // preventing the query from firing on the login page.\n setTimeout(() => {\n authFlowInProgressRef.current = false;\n }, 500);\n }\n },\n [store, fetchWorkspaces, issueWorkspaceToken],\n );\n\n return {\n // Data\n workspaces: data?.myWorkspaces?.items ?? store.workspaces,\n totalWorkspaces: data?.myWorkspaces?.total ?? store.workspaces.length,\n currentWorkspaceId: store.currentWorkspaceId,\n workspaceToken: store.workspaceToken,\n\n // State\n isLoading:\n queryLoading ||\n tokenLoading ||\n workspacesLoading ||\n store.isLoadingWorkspaces,\n error: queryError?.message ?? null,\n\n // Actions\n fetchWorkspaces,\n issueWorkspaceToken,\n selectWorkspace,\n completeAuthFlow,\n refetch,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
|
+
import { useAuth as t } from "../providers/AuthProvider.js";
|
|
3
3
|
import { AuthenticateOAuthDocument as n } from "../../generated/global-operations.js";
|
|
4
4
|
import { useMutation as r } from "@apollo/client/react";
|
|
5
|
-
|
|
5
|
+
// src/auth/hooks/useOAuthOperations.ts
|
|
6
6
|
var i = {
|
|
7
7
|
google: {
|
|
8
8
|
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
@@ -29,7 +29,7 @@ var i = {
|
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
31
|
function a() {
|
|
32
|
-
let e = new Uint8Array(32);
|
|
32
|
+
let e = /* @__PURE__ */ new Uint8Array(32);
|
|
33
33
|
return crypto.getRandomValues(e), Array.from(e, (e) => e.toString(16).padStart(2, "0")).join("");
|
|
34
34
|
}
|
|
35
35
|
function o(e) {
|
|
@@ -63,7 +63,7 @@ async function l(e) {
|
|
|
63
63
|
window.location.href = e;
|
|
64
64
|
}
|
|
65
65
|
function u() {
|
|
66
|
-
let u =
|
|
66
|
+
let u = t(), [d, { loading: f }] = r(n), p = (e) => u.oauthClientIds?.[e] || o(i[e].clientIdEnvKey), m = (e) => {
|
|
67
67
|
let t = i[e], n = p(e);
|
|
68
68
|
if (!n) throw console.error(`OAuth client ID not configured for ${e}`), Error(`${e} OAuth is not configured. Please set ${t.clientIdEnvKey} environment variable.`);
|
|
69
69
|
let r = a();
|
|
@@ -83,8 +83,8 @@ function u() {
|
|
|
83
83
|
};
|
|
84
84
|
return {
|
|
85
85
|
initiateOAuth: m,
|
|
86
|
-
completeOAuth: async (
|
|
87
|
-
let r =
|
|
86
|
+
completeOAuth: async (t, n) => {
|
|
87
|
+
let r = e.getState(), i = localStorage.getItem("oauth_state");
|
|
88
88
|
if (!i || i !== n) throw console.error("[OAuth Complete] State mismatch!", {
|
|
89
89
|
storedState: i,
|
|
90
90
|
urlState: n
|
|
@@ -94,16 +94,16 @@ function u() {
|
|
|
94
94
|
localStorage.removeItem("oauth_state"), localStorage.removeItem("oauth_provider"), localStorage.removeItem("oauth_redirect_uri");
|
|
95
95
|
try {
|
|
96
96
|
r.setLoading(!0), r.setError(null);
|
|
97
|
-
let { data:
|
|
97
|
+
let { data: e, error: n } = await d({ variables: { input: {
|
|
98
98
|
provider: a.toUpperCase(),
|
|
99
|
-
code:
|
|
99
|
+
code: t,
|
|
100
100
|
redirectUri: o
|
|
101
101
|
} } });
|
|
102
|
-
if (!
|
|
102
|
+
if (!e?.authenticateOAuth) {
|
|
103
103
|
let e = n?.message ?? "OAuth authentication failed. Please try again.";
|
|
104
104
|
throw Error(e);
|
|
105
105
|
}
|
|
106
|
-
let i =
|
|
106
|
+
let i = e.authenticateOAuth;
|
|
107
107
|
return i.requiresMFA ? (r.setMfaRequired(!0, i.mfaChallengeId || void 0), i) : (i.success && i.user && i.token ? (r.setUser(i.user), r.setAuthTokens(i.token, i.refreshToken || ""), r.setAuthenticated(!0), r.setMfaRequired(!1), u.onAuthSuccess?.(i.user, i.token, i.refreshToken || void 0)) : i.error && r.setError(i.error), i);
|
|
108
108
|
} catch (e) {
|
|
109
109
|
let t = e instanceof Error ? e.message : "OAuth authentication failed";
|
|
@@ -119,7 +119,6 @@ function u() {
|
|
|
119
119
|
isLoading: f
|
|
120
120
|
};
|
|
121
121
|
}
|
|
122
|
-
//#endregion
|
|
123
122
|
export { u as useOAuthOperations };
|
|
124
123
|
|
|
125
124
|
//# sourceMappingURL=useOAuthOperations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOAuthOperations.js","names":[],"sources":["../../../src/auth/hooks/useOAuthOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport type { AuthResult, OAuthInput } from \"../types\";\n\nimport { AuthenticateOAuthDocument } from \"../../generated/global-operations\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface AuthenticateOAuthResponse {\n authenticateOAuth: AuthResult;\n}\n\ninterface AuthenticateOAuthVariables {\n input: OAuthInput;\n}\n\n/**\n * Supported OAuth providers\n */\nexport type OAuthProvider = \"google\" | \"github\" | \"microsoft\";\n\n/**\n * OAuth configuration for each provider\n */\ninterface OAuthConfig {\n authorizationUrl: string;\n scopes: string[];\n clientIdEnvKey: string;\n}\n\nconst OAUTH_CONFIGS: Record<OAuthProvider, OAuthConfig> = {\n google: {\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_GOOGLE_CLIENT_ID\",\n },\n github: {\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n scopes: [\"user:email\", \"read:user\"],\n clientIdEnvKey: \"VITE_GITHUB_CLIENT_ID\",\n },\n microsoft: {\n authorizationUrl:\n \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_MICROSOFT_CLIENT_ID\",\n },\n};\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n}\n\n/**\n * Read a Vite env var by key via a dynamic (non-statically-analyzable) lookup.\n *\n * Build-time composition compiles THIS package into its own npm dist first —\n * where `VITE_GOOGLE_CLIENT_ID` / `VITE_MICROSOFT_CLIENT_ID` are unset — and\n * only later bundles that dist into a host app shell. A direct\n * `import.meta.env.VITE_GOOGLE_CLIENT_ID` member read is statically replaced\n * with `undefined` during this package's build, so it ships dead and the host\n * shell's build value can never reach it (this is exactly why the SSO buttons\n * rendered permanently disabled).\n *\n * Reading the whole `import.meta.env` object and indexing it dynamically leaves\n * the lookup to runtime, so the value baked into `import.meta.env` by the HOST\n * app shell's build wins. Same pattern fe-libs uses for `VITE_GATEWAY_TARGET`.\n */\nfunction readViteEnv(key: string): string | undefined {\n try {\n const env = (import.meta as { env?: Record<string, string | undefined> })\n .env;\n return env?.[key];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Mirrors the helper in LoginPage so OAuth\n * initiation can swap to a custom-scheme redirect_uri + system-browser\n * hand-off — the only flow OAuth providers will accept from a single-device\n * install.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\n/**\n * Custom-scheme redirect URI used in native shells. The scheme is registered\n * by the host shell (Electron `setAsDefaultProtocolClient`, iOS Info.plist\n * CFBundleURLTypes, Android intent-filter). The OS routes the provider's\n * callback back to the running app, which forwards it to React Router via\n * the host's deep-link handler.\n *\n * IMPORTANT: This redirect URI MUST be registered for the OAuth client in\n * each provider's console:\n * - Google Cloud Console → Credentials → OAuth client → add\n * `vibecontrols://app/auth/oauth/callback` (Installed App).\n * - Microsoft Entra App registration → Authentication → register the\n * same custom-scheme redirect URI as a mobile/desktop redirect.\n * The backend `authenticateOAuth` mutation must also accept this redirectUri\n * verbatim during the code exchange.\n */\nconst NATIVE_OAUTH_REDIRECT_URI = \"vibecontrols://app/auth/oauth/callback\";\n\nasync function openOAuthAuthorizationUrl(authUrl: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n const opener = (\n window as typeof window & {\n __burdenoffOpenExternal?: (url: string) => Promise<boolean>;\n }\n ).__burdenoffOpenExternal;\n if (typeof opener === \"function\") {\n try {\n const ok: unknown = await opener(authUrl);\n if (ok === true) return;\n } catch {\n /* fall through to legacy hand-off */\n }\n }\n // Browser fallback: same-window navigation (current production behavior).\n window.location.href = authUrl;\n}\n\n/**\n * Hook for OAuth authentication operations\n */\nexport function useOAuthOperations() {\n const context = useAuth();\n const [authenticateOAuthMutation, { loading }] = useMutation<\n AuthenticateOAuthResponse,\n AuthenticateOAuthVariables\n >(AuthenticateOAuthDocument);\n\n /**\n * Resolve a provider's OAuth client ID.\n *\n * Priority:\n * 1. `oauthClientIds` injected by the host app shell — the only source that\n * works in published builds (see AuthRootProps.oauthClientIds for why a\n * direct `import.meta.env` read is baked dead at this package's build).\n * 2. `import.meta.env.VITE_*_CLIENT_ID` — fallback for when this package is\n * consumed from source in local dev (where the host's Vite build inlines\n * it into the same bundle).\n */\n const resolveClientId = (provider: OAuthProvider): string | undefined => {\n const fromHost = context.oauthClientIds?.[provider];\n if (fromHost) return fromHost;\n return readViteEnv(OAUTH_CONFIGS[provider].clientIdEnvKey);\n };\n\n /**\n * Initiate OAuth flow by redirecting to the provider\n * @param provider - The OAuth provider to use\n */\n const initiateOAuth = (provider: OAuthProvider) => {\n const config = OAUTH_CONFIGS[provider];\n\n // Client ID comes from the host app shell (see resolveClientId).\n const clientId = resolveClientId(provider);\n\n if (!clientId) {\n console.error(`OAuth client ID not configured for ${provider}`);\n throw new Error(\n `${provider} OAuth is not configured. Please set ${config.clientIdEnvKey} environment variable.`,\n );\n }\n\n // Generate state for CSRF protection and store it\n // Use localStorage instead of sessionStorage for better persistence across redirects\n const state = generateState();\n localStorage.setItem(\"oauth_state\", state);\n localStorage.setItem(\"oauth_provider\", provider);\n\n // Native shells (Electron, Capacitor) can't use the current origin as the\n // redirect_uri — `vibecontrols://app` / `https://localhost` aren't valid\n // registered URIs at OAuth providers. Use a custom-scheme URL that the\n // host shell has registered as a deep-link target; the OS will route the\n // provider's callback back into the running app.\n const redirectUri = isNativeShell()\n ? NATIVE_OAUTH_REDIRECT_URI\n : `${window.location.origin}/auth/oauth/callback`;\n localStorage.setItem(\"oauth_redirect_uri\", redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: config.scopes.join(\" \"),\n state: state,\n // Google-specific: force account selection\n ...(provider === \"google\" && { prompt: \"select_account\" }),\n // Google-specific: enable offline access for refresh token\n ...(provider === \"google\" && { access_type: \"offline\" }),\n });\n\n const authUrl = `${config.authorizationUrl}?${params.toString()}`;\n\n // Hand off to the system browser in native shells (keeps the embedded\n // webview in place so the deep-link callback can return cleanly). On the\n // web this falls back to same-window navigation.\n void openOAuthAuthorizationUrl(authUrl);\n };\n\n /**\n * Complete OAuth flow by exchanging authorization code for tokens\n * Called from the callback page after provider redirects back\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter for CSRF validation\n */\n const completeOAuth = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n // Validate state parameter\n const storedState = localStorage.getItem(\"oauth_state\");\n if (!storedState || storedState !== state) {\n console.error(\"[OAuth Complete] State mismatch!\", {\n storedState,\n urlState: state,\n });\n throw new Error(\"Invalid OAuth state. Please try again.\");\n }\n\n // Get provider and redirect URI from localStorage\n const provider = localStorage.getItem(\"oauth_provider\") as OAuthProvider;\n const redirectUri = localStorage.getItem(\"oauth_redirect_uri\");\n\n if (!provider || !redirectUri) {\n console.error(\"[OAuth Complete] Missing provider or redirectUri\");\n throw new Error(\"OAuth session data missing. Please try again.\");\n }\n\n // Clear localStorage\n localStorage.removeItem(\"oauth_state\");\n localStorage.removeItem(\"oauth_provider\");\n localStorage.removeItem(\"oauth_redirect_uri\");\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Call backend mutation to exchange code for tokens\n const { data, error } = await authenticateOAuthMutation({\n variables: {\n input: {\n provider: provider.toUpperCase(),\n code,\n redirectUri,\n },\n },\n });\n\n if (!data?.authenticateOAuth) {\n const errMsg =\n error?.message ?? \"OAuth authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateOAuth;\n\n if (result.requiresMFA) {\n store.setMfaRequired(true, result.mfaChallengeId || undefined);\n return result;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n context.onAuthSuccess?.(\n result.user,\n result.token,\n result.refreshToken || undefined,\n );\n } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"OAuth authentication failed\";\n store.setError(message);\n throw err;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Convenience method to initiate Google OAuth\n */\n const initiateGoogleOAuth = () => initiateOAuth(\"google\");\n\n /**\n * Convenience method to initiate GitHub OAuth\n */\n const initiateGitHubOAuth = () => initiateOAuth(\"github\");\n\n /**\n * Convenience method to initiate Microsoft OAuth\n */\n const initiateMicrosoftOAuth = () => initiateOAuth(\"microsoft\");\n\n /**\n * Check if a provider is configured (has client ID)\n */\n const isProviderConfigured = (provider: OAuthProvider): boolean => {\n return !!resolveClientId(provider);\n };\n\n return {\n initiateOAuth,\n completeOAuth,\n initiateGoogleOAuth,\n initiateGitHubOAuth,\n initiateMicrosoftOAuth,\n isProviderConfigured,\n isLoading: loading,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useOAuthOperations.js","names":["OAUTH_CONFIGS","generateState","array","byte","readViteEnv","\"BASE_URL\"","\"DEV\"","\"MODE\"","\"PROD\"","\"SSR\"","key","isNativeShell","w","NATIVE_OAUTH_REDIRECT_URI","openOAuthAuthorizationUrl","opener","authUrl","useOAuthOperations","context","useAuth","authenticateOAuthMutation","useMutation","AuthenticateOAuthDocument","resolveClientId","provider","initiateOAuth","config","clientId","state","redirectUri","params","store","useAuthStore","storedState","data","errMsg","error","result","err","message","loading"],"sources":["../../../src/auth/hooks/useOAuthOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport type { AuthResult, OAuthInput } from \"../types\";\n\nimport { AuthenticateOAuthDocument } from \"../../generated/global-operations\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface AuthenticateOAuthResponse {\n authenticateOAuth: AuthResult;\n}\n\ninterface AuthenticateOAuthVariables {\n input: OAuthInput;\n}\n\n/**\n * Supported OAuth providers\n */\nexport type OAuthProvider = \"google\" | \"github\" | \"microsoft\";\n\n/**\n * OAuth configuration for each provider\n */\ninterface OAuthConfig {\n authorizationUrl: string;\n scopes: string[];\n clientIdEnvKey: string;\n}\n\nconst OAUTH_CONFIGS: Record<OAuthProvider, OAuthConfig> = {\n google: {\n authorizationUrl: \"https://accounts.google.com/o/oauth2/v2/auth\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_GOOGLE_CLIENT_ID\",\n },\n github: {\n authorizationUrl: \"https://github.com/login/oauth/authorize\",\n scopes: [\"user:email\", \"read:user\"],\n clientIdEnvKey: \"VITE_GITHUB_CLIENT_ID\",\n },\n microsoft: {\n authorizationUrl:\n \"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\",\n scopes: [\"openid\", \"email\", \"profile\"],\n clientIdEnvKey: \"VITE_MICROSOFT_CLIENT_ID\",\n },\n};\n\n/**\n * Generate a random state parameter for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return Array.from(array, (byte) => byte.toString(16).padStart(2, \"0\")).join(\n \"\",\n );\n}\n\n/**\n * Read a Vite env var by key via a dynamic (non-statically-analyzable) lookup.\n *\n * Build-time composition compiles THIS package into its own npm dist first —\n * where `VITE_GOOGLE_CLIENT_ID` / `VITE_MICROSOFT_CLIENT_ID` are unset — and\n * only later bundles that dist into a host app shell. A direct\n * `import.meta.env.VITE_GOOGLE_CLIENT_ID` member read is statically replaced\n * with `undefined` during this package's build, so it ships dead and the host\n * shell's build value can never reach it (this is exactly why the SSO buttons\n * rendered permanently disabled).\n *\n * Reading the whole `import.meta.env` object and indexing it dynamically leaves\n * the lookup to runtime, so the value baked into `import.meta.env` by the HOST\n * app shell's build wins. Same pattern fe-libs uses for `VITE_GATEWAY_TARGET`.\n */\nfunction readViteEnv(key: string): string | undefined {\n try {\n const env = (import.meta as { env?: Record<string, string | undefined> })\n .env;\n return env?.[key];\n } catch {\n return undefined;\n }\n}\n\n/**\n * Detect whether the bundle is running inside a native shell (Capacitor on\n * iOS/Android, or Electron). Mirrors the helper in LoginPage so OAuth\n * initiation can swap to a custom-scheme redirect_uri + system-browser\n * hand-off — the only flow OAuth providers will accept from a single-device\n * install.\n */\nfunction isNativeShell(): boolean {\n if (typeof window === \"undefined\") return false;\n const w = window as typeof window & {\n Capacitor?: { isNativePlatform?: () => boolean };\n };\n if (typeof w.Capacitor?.isNativePlatform === \"function\") {\n try {\n if (w.Capacitor.isNativePlatform()) return true;\n } catch {\n /* fall through */\n }\n }\n if (\n typeof navigator !== \"undefined\" &&\n /Electron/i.test(navigator.userAgent || \"\")\n ) {\n return true;\n }\n return false;\n}\n\n/**\n * Custom-scheme redirect URI used in native shells. The scheme is registered\n * by the host shell (Electron `setAsDefaultProtocolClient`, iOS Info.plist\n * CFBundleURLTypes, Android intent-filter). The OS routes the provider's\n * callback back to the running app, which forwards it to React Router via\n * the host's deep-link handler.\n *\n * IMPORTANT: This redirect URI MUST be registered for the OAuth client in\n * each provider's console:\n * - Google Cloud Console → Credentials → OAuth client → add\n * `vibecontrols://app/auth/oauth/callback` (Installed App).\n * - Microsoft Entra App registration → Authentication → register the\n * same custom-scheme redirect URI as a mobile/desktop redirect.\n * The backend `authenticateOAuth` mutation must also accept this redirectUri\n * verbatim during the code exchange.\n */\nconst NATIVE_OAUTH_REDIRECT_URI = \"vibecontrols://app/auth/oauth/callback\";\n\nasync function openOAuthAuthorizationUrl(authUrl: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n const opener = (\n window as typeof window & {\n __burdenoffOpenExternal?: (url: string) => Promise<boolean>;\n }\n ).__burdenoffOpenExternal;\n if (typeof opener === \"function\") {\n try {\n const ok: unknown = await opener(authUrl);\n if (ok === true) return;\n } catch {\n /* fall through to legacy hand-off */\n }\n }\n // Browser fallback: same-window navigation (current production behavior).\n window.location.href = authUrl;\n}\n\n/**\n * Hook for OAuth authentication operations\n */\nexport function useOAuthOperations() {\n const context = useAuth();\n const [authenticateOAuthMutation, { loading }] = useMutation<\n AuthenticateOAuthResponse,\n AuthenticateOAuthVariables\n >(AuthenticateOAuthDocument);\n\n /**\n * Resolve a provider's OAuth client ID.\n *\n * Priority:\n * 1. `oauthClientIds` injected by the host app shell — the only source that\n * works in published builds (see AuthRootProps.oauthClientIds for why a\n * direct `import.meta.env` read is baked dead at this package's build).\n * 2. `import.meta.env.VITE_*_CLIENT_ID` — fallback for when this package is\n * consumed from source in local dev (where the host's Vite build inlines\n * it into the same bundle).\n */\n const resolveClientId = (provider: OAuthProvider): string | undefined => {\n const fromHost = context.oauthClientIds?.[provider];\n if (fromHost) return fromHost;\n return readViteEnv(OAUTH_CONFIGS[provider].clientIdEnvKey);\n };\n\n /**\n * Initiate OAuth flow by redirecting to the provider\n * @param provider - The OAuth provider to use\n */\n const initiateOAuth = (provider: OAuthProvider) => {\n const config = OAUTH_CONFIGS[provider];\n\n // Client ID comes from the host app shell (see resolveClientId).\n const clientId = resolveClientId(provider);\n\n if (!clientId) {\n console.error(`OAuth client ID not configured for ${provider}`);\n throw new Error(\n `${provider} OAuth is not configured. Please set ${config.clientIdEnvKey} environment variable.`,\n );\n }\n\n // Generate state for CSRF protection and store it\n // Use localStorage instead of sessionStorage for better persistence across redirects\n const state = generateState();\n localStorage.setItem(\"oauth_state\", state);\n localStorage.setItem(\"oauth_provider\", provider);\n\n // Native shells (Electron, Capacitor) can't use the current origin as the\n // redirect_uri — `vibecontrols://app` / `https://localhost` aren't valid\n // registered URIs at OAuth providers. Use a custom-scheme URL that the\n // host shell has registered as a deep-link target; the OS will route the\n // provider's callback back into the running app.\n const redirectUri = isNativeShell()\n ? NATIVE_OAUTH_REDIRECT_URI\n : `${window.location.origin}/auth/oauth/callback`;\n localStorage.setItem(\"oauth_redirect_uri\", redirectUri);\n\n // Build authorization URL\n const params = new URLSearchParams({\n client_id: clientId,\n redirect_uri: redirectUri,\n response_type: \"code\",\n scope: config.scopes.join(\" \"),\n state: state,\n // Google-specific: force account selection\n ...(provider === \"google\" && { prompt: \"select_account\" }),\n // Google-specific: enable offline access for refresh token\n ...(provider === \"google\" && { access_type: \"offline\" }),\n });\n\n const authUrl = `${config.authorizationUrl}?${params.toString()}`;\n\n // Hand off to the system browser in native shells (keeps the embedded\n // webview in place so the deep-link callback can return cleanly). On the\n // web this falls back to same-window navigation.\n void openOAuthAuthorizationUrl(authUrl);\n };\n\n /**\n * Complete OAuth flow by exchanging authorization code for tokens\n * Called from the callback page after provider redirects back\n * @param code - Authorization code from OAuth provider\n * @param state - State parameter for CSRF validation\n */\n const completeOAuth = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n // Validate state parameter\n const storedState = localStorage.getItem(\"oauth_state\");\n if (!storedState || storedState !== state) {\n console.error(\"[OAuth Complete] State mismatch!\", {\n storedState,\n urlState: state,\n });\n throw new Error(\"Invalid OAuth state. Please try again.\");\n }\n\n // Get provider and redirect URI from localStorage\n const provider = localStorage.getItem(\"oauth_provider\") as OAuthProvider;\n const redirectUri = localStorage.getItem(\"oauth_redirect_uri\");\n\n if (!provider || !redirectUri) {\n console.error(\"[OAuth Complete] Missing provider or redirectUri\");\n throw new Error(\"OAuth session data missing. Please try again.\");\n }\n\n // Clear localStorage\n localStorage.removeItem(\"oauth_state\");\n localStorage.removeItem(\"oauth_provider\");\n localStorage.removeItem(\"oauth_redirect_uri\");\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Call backend mutation to exchange code for tokens\n const { data, error } = await authenticateOAuthMutation({\n variables: {\n input: {\n provider: provider.toUpperCase(),\n code,\n redirectUri,\n },\n },\n });\n\n if (!data?.authenticateOAuth) {\n const errMsg =\n error?.message ?? \"OAuth authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateOAuth;\n\n if (result.requiresMFA) {\n store.setMfaRequired(true, result.mfaChallengeId || undefined);\n return result;\n }\n\n if (result.success && result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n store.setMfaRequired(false);\n\n context.onAuthSuccess?.(\n result.user,\n result.token,\n result.refreshToken || undefined,\n );\n } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (err) {\n const message =\n err instanceof Error ? err.message : \"OAuth authentication failed\";\n store.setError(message);\n throw err;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Convenience method to initiate Google OAuth\n */\n const initiateGoogleOAuth = () => initiateOAuth(\"google\");\n\n /**\n * Convenience method to initiate GitHub OAuth\n */\n const initiateGitHubOAuth = () => initiateOAuth(\"github\");\n\n /**\n * Convenience method to initiate Microsoft OAuth\n */\n const initiateMicrosoftOAuth = () => initiateOAuth(\"microsoft\");\n\n /**\n * Check if a provider is configured (has client ID)\n */\n const isProviderConfigured = (provider: OAuthProvider): boolean => {\n return !!resolveClientId(provider);\n };\n\n return {\n initiateOAuth,\n completeOAuth,\n initiateGoogleOAuth,\n initiateGitHubOAuth,\n initiateMicrosoftOAuth,\n isProviderConfigured,\n isLoading: loading,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useAuthStore as e } from "../store/authStore.js";
|
|
2
2
|
import { ChangePasswordDocument as t, RequestPasswordResetDocument as n, ResetPasswordDocument as r } from "../../generated/global-operations.js";
|
|
3
3
|
import { useMutation as i } from "@apollo/client/react";
|
|
4
|
-
|
|
4
|
+
// src/auth/hooks/usePasswordOperations.ts
|
|
5
5
|
function a() {
|
|
6
6
|
let a = e((e) => e.isLoading), [o] = i(t), [s] = i(n), [c] = i(r);
|
|
7
7
|
return {
|
|
@@ -57,7 +57,6 @@ function a() {
|
|
|
57
57
|
isLoading: a
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
//#endregion
|
|
61
60
|
export { a as usePasswordOperations };
|
|
62
61
|
|
|
63
62
|
//# sourceMappingURL=usePasswordOperations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePasswordOperations.js","names":[],"sources":["../../../src/auth/hooks/usePasswordOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { ChangePasswordInput, PasswordResetRequestResult } from \"../types\";\n\nimport {\n ChangePasswordDocument,\n RequestPasswordResetDocument,\n ResetPasswordDocument,\n} from \"../../generated/global-operations\";\n// Apollo Client 4.x Response Interfaces\ninterface ChangePasswordResponse {\n changePassword: boolean;\n}\n\ninterface ChangePasswordVariables {\n input: ChangePasswordInput;\n}\n\ninterface RequestPasswordResetResponse {\n requestPasswordReset: PasswordResetRequestResult;\n}\n\ninterface RequestPasswordResetVariables {\n email: string;\n recaptchaToken?: string;\n}\n\ninterface ResetPasswordResponse {\n resetPassword: boolean;\n}\n\ninterface ResetPasswordVariables {\n token: string;\n newPassword: string;\n}\n\n// GraphQL Operations\n\n/**\n * Hook for password management operations\n * Uses Zustand v5 pattern: selectors for state, getState() for actions\n */\nexport function usePasswordOperations() {\n // Use selectors for reactive state\n const isLoading = useAuthStore((state) => state.isLoading);\n\n const [changePasswordMutation] = useMutation<\n ChangePasswordResponse,\n ChangePasswordVariables\n >(ChangePasswordDocument);\n const [requestPasswordResetMutation] = useMutation<\n RequestPasswordResetResponse,\n RequestPasswordResetVariables\n >(RequestPasswordResetDocument);\n const [resetPasswordMutation] = useMutation<\n ResetPasswordResponse,\n ResetPasswordVariables\n >(ResetPasswordDocument);\n\n /**\n * Change password for authenticated user\n */\n const changePassword = async (\n input: ChangePasswordInput,\n ): Promise<boolean> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n const { data } = await changePasswordMutation({\n variables: { input },\n });\n\n return data!.changePassword;\n } catch (error) {\n console.error(\"Failed to change password:\", error);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Request password reset email\n */\n const requestPasswordReset = async (\n email: string,\n recaptchaToken?: string,\n ): Promise<PasswordResetRequestResult> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n const { data } = await requestPasswordResetMutation({\n variables: { email, ...(recaptchaToken ? { recaptchaToken } : {}) },\n });\n\n return data!.requestPasswordReset;\n } catch (error) {\n console.error(\"Failed to request password reset:\", error);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Reset password with token\n */\n const resetPassword = async (\n token: string,\n newPassword: string,\n ): Promise<boolean> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n type GqlErrorLike = {\n extensions?: { code?: string };\n message: string;\n };\n\n // Apollo Client 4 resolves (rather than throws) when the server returns\n // GraphQL errors. The error lives in result.error (a CombinedGraphQLErrors\n // with an `.errors` array). We inspect it before touching `data` to avoid\n // a TypeError from `null.resetPassword`.\n const result = await resetPasswordMutation({\n variables: { token, newPassword },\n });\n\n // AC4: check result.error first\n const resultError = (\n result as unknown as { error?: { errors?: GqlErrorLike[] } }\n )?.error;\n if (resultError) {\n const code = resultError.errors?.[0]?.extensions?.code;\n if (code === \"TOKEN_EXPIRED\") throw new Error(\"TOKEN_EXPIRED\");\n if (code === \"INVALID_TOKEN\") throw new Error(\"INVALID_TOKEN\");\n throw new Error(resultError.errors?.[0]?.message ?? \"RESET_FAILED\");\n }\n\n return result.data!.resetPassword;\n } catch (error: unknown) {\n // Re-throw sentinel errors unchanged; for anything else apply a final\n // AC3-style graphQLErrors fallback before surfacing as generic.\n const sentinelMsg = (error as Error)?.message ?? \"\";\n if (sentinelMsg === \"TOKEN_EXPIRED\" || sentinelMsg === \"INVALID_TOKEN\") {\n throw error;\n }\n type GqlErrorLike = {\n extensions?: { code?: string };\n message: string;\n };\n const gqlError: GqlErrorLike | undefined =\n (error as { errors?: GqlErrorLike[] })?.errors?.[0] ??\n (error as { graphQLErrors?: GqlErrorLike[] })?.graphQLErrors?.[0];\n const code = gqlError?.extensions?.code;\n if (code === \"TOKEN_EXPIRED\") throw new Error(\"TOKEN_EXPIRED\");\n if (code === \"INVALID_TOKEN\") throw new Error(\"INVALID_TOKEN\");\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n return {\n changePassword,\n requestPasswordReset,\n resetPassword,\n isLoading,\n };\n}\n"],"mappings":";;;;AA0CA,
|
|
1
|
+
{"version":3,"file":"usePasswordOperations.js","names":["usePasswordOperations","isLoading","useAuthStore","state","changePasswordMutation","useMutation","ChangePasswordDocument","requestPasswordResetMutation","RequestPasswordResetDocument","resetPasswordMutation","ResetPasswordDocument","store","data","error","recaptchaToken","result","resultError","code","sentinelMsg"],"sources":["../../../src/auth/hooks/usePasswordOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { ChangePasswordInput, PasswordResetRequestResult } from \"../types\";\n\nimport {\n ChangePasswordDocument,\n RequestPasswordResetDocument,\n ResetPasswordDocument,\n} from \"../../generated/global-operations\";\n// Apollo Client 4.x Response Interfaces\ninterface ChangePasswordResponse {\n changePassword: boolean;\n}\n\ninterface ChangePasswordVariables {\n input: ChangePasswordInput;\n}\n\ninterface RequestPasswordResetResponse {\n requestPasswordReset: PasswordResetRequestResult;\n}\n\ninterface RequestPasswordResetVariables {\n email: string;\n recaptchaToken?: string;\n}\n\ninterface ResetPasswordResponse {\n resetPassword: boolean;\n}\n\ninterface ResetPasswordVariables {\n token: string;\n newPassword: string;\n}\n\n// GraphQL Operations\n\n/**\n * Hook for password management operations\n * Uses Zustand v5 pattern: selectors for state, getState() for actions\n */\nexport function usePasswordOperations() {\n // Use selectors for reactive state\n const isLoading = useAuthStore((state) => state.isLoading);\n\n const [changePasswordMutation] = useMutation<\n ChangePasswordResponse,\n ChangePasswordVariables\n >(ChangePasswordDocument);\n const [requestPasswordResetMutation] = useMutation<\n RequestPasswordResetResponse,\n RequestPasswordResetVariables\n >(RequestPasswordResetDocument);\n const [resetPasswordMutation] = useMutation<\n ResetPasswordResponse,\n ResetPasswordVariables\n >(ResetPasswordDocument);\n\n /**\n * Change password for authenticated user\n */\n const changePassword = async (\n input: ChangePasswordInput,\n ): Promise<boolean> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n const { data } = await changePasswordMutation({\n variables: { input },\n });\n\n return data!.changePassword;\n } catch (error) {\n console.error(\"Failed to change password:\", error);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Request password reset email\n */\n const requestPasswordReset = async (\n email: string,\n recaptchaToken?: string,\n ): Promise<PasswordResetRequestResult> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n const { data } = await requestPasswordResetMutation({\n variables: { email, ...(recaptchaToken ? { recaptchaToken } : {}) },\n });\n\n return data!.requestPasswordReset;\n } catch (error) {\n console.error(\"Failed to request password reset:\", error);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Reset password with token\n */\n const resetPassword = async (\n token: string,\n newPassword: string,\n ): Promise<boolean> => {\n const store = useAuthStore.getState();\n try {\n store.setLoading(true);\n\n type GqlErrorLike = {\n extensions?: { code?: string };\n message: string;\n };\n\n // Apollo Client 4 resolves (rather than throws) when the server returns\n // GraphQL errors. The error lives in result.error (a CombinedGraphQLErrors\n // with an `.errors` array). We inspect it before touching `data` to avoid\n // a TypeError from `null.resetPassword`.\n const result = await resetPasswordMutation({\n variables: { token, newPassword },\n });\n\n // AC4: check result.error first\n const resultError = (\n result as unknown as { error?: { errors?: GqlErrorLike[] } }\n )?.error;\n if (resultError) {\n const code = resultError.errors?.[0]?.extensions?.code;\n if (code === \"TOKEN_EXPIRED\") throw new Error(\"TOKEN_EXPIRED\");\n if (code === \"INVALID_TOKEN\") throw new Error(\"INVALID_TOKEN\");\n throw new Error(resultError.errors?.[0]?.message ?? \"RESET_FAILED\");\n }\n\n return result.data!.resetPassword;\n } catch (error: unknown) {\n // Re-throw sentinel errors unchanged; for anything else apply a final\n // AC3-style graphQLErrors fallback before surfacing as generic.\n const sentinelMsg = (error as Error)?.message ?? \"\";\n if (sentinelMsg === \"TOKEN_EXPIRED\" || sentinelMsg === \"INVALID_TOKEN\") {\n throw error;\n }\n type GqlErrorLike = {\n extensions?: { code?: string };\n message: string;\n };\n const gqlError: GqlErrorLike | undefined =\n (error as { errors?: GqlErrorLike[] })?.errors?.[0] ??\n (error as { graphQLErrors?: GqlErrorLike[] })?.graphQLErrors?.[0];\n const code = gqlError?.extensions?.code;\n if (code === \"TOKEN_EXPIRED\") throw new Error(\"TOKEN_EXPIRED\");\n if (code === \"INVALID_TOKEN\") throw new Error(\"INVALID_TOKEN\");\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n return {\n changePassword,\n requestPasswordReset,\n resetPassword,\n isLoading,\n };\n}\n"],"mappings":";;;;AA0CA,SAAgBA,IAAwB;CAEtC,IAAMC,IAAYC,GAAc,MAAUC,EAAM,SAAS,GAEnD,CAACC,KAA0BC,EAG/BC,CAAsB,GAClB,CAACC,KAAgCF,EAGrCG,CAA4B,GACxB,CAACC,KAAyBJ,EAG9BK,CAAqB;CA4GvB,OAAO;EACL,uBAvGA,MACqB;GACrB,IAAMC,IAAQT,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAErB,IAAM,EAAE,YAAS,MAAME,EAAuB,EAC5C,WAAW,EAAE,SAAM,EACrB,CAAC;IAED,OAAOQ,EAAM;GACf,SAASC,GAAO;IAEd,MADA,QAAQ,MAAM,8BAA8BA,CAAK,GAC3CA;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAuFE,6BAjFA,GACA,MACwC;GACxC,IAAMF,IAAQT,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAErB,IAAM,EAAE,YAAS,MAAMK,EAA6B,EAClD,WAAW;KAAE;KAAO,GAAIO,IAAiB,EAAE,kBAAe,IAAI,CAAC;IAAG,EACpE,CAAC;IAED,OAAOF,EAAM;GACf,SAASC,GAAO;IAEd,MADA,QAAQ,MAAM,qCAAqCA,CAAK,GAClDA;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAgEE,sBA1DA,GACA,MACqB;GACrB,IAAMF,IAAQT,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAWrB,IAAMa,IAAS,MAAMN,EAAsB,EACzC,WAAW;KAAE;KAAO;IAAY,EAClC,CAAC,GAGKO,IACJD,GACC;IACH,IAAIC,GAAa;KACf,IAAMC,IAAOD,EAAY,SAAS,EAAE,EAAE,YAAY;KAGlD,MAFwC,MAApCC,MAAS,kBAAiC,kBAC1CA,MAAS,kBAAiC,kBAC9BD,EAAY,SAAS,EAAE,EAAE,WAAW,cAFS;IAG/D;IAEA,OAAOD,EAAO,KAAM;GACtB,SAASF,GAAgB;IAGvB,IAAMK,IAAeL,GAAiB,WAAW;IACjD,IAAIK,MAAgB,mBAAmBA,MAAgB,iBACrD,MAAML;IASR,IAAMI,KAFHJ,GAAuC,SAAS,MAChDA,GAA8C,gBAAgB,GAAA,EAC1C,YAAY;IAGnC,MAFII,MAAS,kBAA2B,MAAM,eAAe,IACzDA,MAAS,kBAA2B,MAAM,eAAe,IACvDJ;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAME;CACF;AACF"}
|
|
@@ -2,12 +2,12 @@ import { useAuthStore as e } from "../store/authStore.js";
|
|
|
2
2
|
import { ActiveSessionsDocument as t, RevokeAllSessionsDocument as n, RevokeSessionDocument as r, TokenInfoDocument as i, ValidateTokensDocument as a } from "../../generated/global-operations.js";
|
|
3
3
|
import { useState as o } from "react";
|
|
4
4
|
import { useMutation as s, useQuery as c } from "@apollo/client/react";
|
|
5
|
-
|
|
5
|
+
// src/auth/hooks/useSessionOperations.ts
|
|
6
6
|
function l(l = {}) {
|
|
7
7
|
let u = e(), d = l.disableTokenInfoQuery ?? !1, f = l.pageSize ?? 10, [p, m] = o(0), [h] = s(a), { data: g, loading: _, refetch: v } = c(i, {
|
|
8
8
|
skip: d || !u.isAuthenticated,
|
|
9
9
|
fetchPolicy: "cache-and-network",
|
|
10
|
-
pollInterval:
|
|
10
|
+
pollInterval: 3e5
|
|
11
11
|
}), { data: y, loading: b, refetch: x } = c(t, {
|
|
12
12
|
skip: !u.isAuthenticated,
|
|
13
13
|
fetchPolicy: "cache-and-network",
|
|
@@ -26,7 +26,7 @@ function l(l = {}) {
|
|
|
26
26
|
} catch (e) {
|
|
27
27
|
throw console.error("Failed to validate tokens:", e), e;
|
|
28
28
|
}
|
|
29
|
-
}, T = () => g?.tokenInfo?.expiresAt
|
|
29
|
+
}, T = () => !g?.tokenInfo?.expiresAt || Date.now() / 1e3 > g.tokenInfo.expiresAt, E = () => {
|
|
30
30
|
if (!g?.tokenInfo?.expiresAt) return 0;
|
|
31
31
|
let e = g.tokenInfo.expiresAt - Date.now() / 1e3;
|
|
32
32
|
return Math.max(0, e);
|
|
@@ -73,7 +73,6 @@ function l(l = {}) {
|
|
|
73
73
|
refetch: v
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
-
//#endregion
|
|
77
76
|
export { l as useSessionOperations };
|
|
78
77
|
|
|
79
78
|
//# sourceMappingURL=useSessionOperations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSessionOperations.js","names":[],"sources":["../../../src/auth/hooks/useSessionOperations.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { useQuery, useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n TokenInfoDocument,\n ValidateTokensDocument,\n ActiveSessionsDocument,\n RevokeSessionDocument,\n RevokeAllSessionsDocument,\n} from \"../../generated/global-operations\";\nimport type {\n ActiveSessionsQuery,\n ActiveSessionsQueryVariables,\n RevokeSessionMutation,\n RevokeSessionMutationVariables,\n RevokeAllSessionsMutation,\n RevokeAllSessionsMutationVariables,\n} from \"../../generated/global-operations\";\nimport type { TokenValidationResponse, ValidateTokensInput } from \"../types\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface TokenInfoQueryResponse {\n tokenInfo: TokenValidationResponse;\n}\n\ninterface ValidateTokensMutationResponse {\n validateTokens: TokenValidationResponse;\n}\n\ninterface ValidateTokensMutationVariables {\n input: ValidateTokensInput;\n}\n\ninterface UseSessionOperationsOptions {\n disableTokenInfoQuery?: boolean;\n /** Number of sessions to load per page (default: 10) */\n pageSize?: number;\n}\n\n/**\n * Hook for session/token management operations\n */\nexport function useSessionOperations(\n options: UseSessionOperationsOptions = {},\n) {\n const store = useAuthStore();\n const disableTokenInfoQuery = options.disableTokenInfoQuery ?? false;\n const pageSize = options.pageSize ?? 10;\n\n // Pagination state: page is 0-based for offset calculation\n const [page, setPage] = useState(0);\n\n const [validateTokensMutation] = useMutation<\n ValidateTokensMutationResponse,\n ValidateTokensMutationVariables\n >(ValidateTokensDocument);\n\n const { data, loading, refetch } = useQuery<TokenInfoQueryResponse>(\n TokenInfoDocument,\n {\n skip: disableTokenInfoQuery || !store.isAuthenticated,\n fetchPolicy: \"cache-and-network\",\n pollInterval: 5 * 60 * 1000, // Poll every 5 minutes (was 1 min — too aggressive)\n },\n );\n\n // Active sessions query\n // Pass the access token explicitly in Apollo operation context so the query\n // works both via the shell's MultiGatewayProvider auth link and in standalone mode.\n const {\n data: sessionsData,\n loading: sessionsLoading,\n refetch: refetchSessions,\n } = useQuery<ActiveSessionsQuery, ActiveSessionsQueryVariables>(\n ActiveSessionsDocument,\n {\n skip: !store.isAuthenticated,\n fetchPolicy: \"cache-and-network\",\n variables: {\n pagination: {\n limit: pageSize,\n offset: page * pageSize,\n },\n },\n context: store.accessToken\n ? {\n headers: {\n authorization: `Bearer ${store.accessToken}`,\n // activeSessions is a global-layer query scoped to the current user\n // (scopeType: \"self\"). Explicitly clear the workspace-id header so\n // the gateway doesn't attempt workspace scoping for this query.\n \"x-workspace-id\": \"\",\n },\n }\n : undefined,\n },\n );\n\n // Session mutations\n const [revokeSessionMutation] = useMutation<\n RevokeSessionMutation,\n RevokeSessionMutationVariables\n >(RevokeSessionDocument);\n\n const [revokeAllSessionsMutation] = useMutation<\n RevokeAllSessionsMutation,\n RevokeAllSessionsMutationVariables\n >(RevokeAllSessionsDocument);\n\n /**\n * Validate tokens\n */\n const validateTokens = async (\n input: ValidateTokensInput,\n ): Promise<TokenValidationResponse> => {\n try {\n const { data } = await validateTokensMutation({ variables: { input } });\n return data!.validateTokens;\n } catch (error) {\n console.error(\"Failed to validate tokens:\", error);\n throw error;\n }\n };\n\n /**\n * Check if token is expired\n */\n const isTokenExpired = (): boolean => {\n if (!data?.tokenInfo?.expiresAt) return true;\n return Date.now() / 1000 > data.tokenInfo.expiresAt;\n };\n\n /**\n * Get remaining time until token expires (in seconds)\n */\n const getTokenRemainingTime = (): number => {\n if (!data?.tokenInfo?.expiresAt) return 0;\n const remaining = data.tokenInfo.expiresAt - Date.now() / 1000;\n return Math.max(0, remaining);\n };\n\n /**\n * Revoke a specific session by ID\n */\n const revokeSession = async (sessionId: string): Promise<boolean> => {\n try {\n const { data } = await revokeSessionMutation({\n variables: { sessionId },\n });\n if (data?.revokeSession) {\n // Refetch active sessions to reflect the change\n await refetchSessions();\n }\n return data?.revokeSession ?? false;\n } catch (error) {\n console.error(\"Failed to revoke session:\", error);\n throw error;\n }\n };\n\n /**\n * Revoke all sessions except the current one\n */\n const revokeAllSessions = async (): Promise<{\n success: boolean;\n revokedCount: number;\n }> => {\n try {\n const { data } = await revokeAllSessionsMutation();\n if (data?.revokeAllSessions?.success) {\n // Refetch active sessions to reflect the change\n await refetchSessions();\n }\n return data?.revokeAllSessions ?? { success: false, revokedCount: 0 };\n } catch (error) {\n console.error(\"Failed to revoke all sessions:\", error);\n throw error;\n }\n };\n\n const totalCount = sessionsData?.activeSessions?.totalCount ?? 0;\n const hasMore = sessionsData?.activeSessions?.hasMore ?? false;\n const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));\n\n const goToNextPage = () => {\n if (hasMore) {\n setPage((prev) => prev + 1);\n }\n };\n\n const goToPrevPage = () => {\n if (page > 0) {\n setPage((prev) => prev - 1);\n }\n };\n\n return {\n // Token operations\n validateTokens,\n isTokenExpired,\n getTokenRemainingTime,\n tokenInfo: data?.tokenInfo,\n // Session operations\n revokeSession,\n revokeAllSessions,\n activeSessions: sessionsData?.activeSessions?.sessions ?? [],\n sessionsLoading,\n refetchSessions,\n // Pagination\n page,\n pageSize,\n totalCount,\n totalPages,\n hasMore,\n goToNextPage,\n goToPrevPage,\n // General state\n isLoading: loading || sessionsLoading,\n refetch,\n };\n}\n"],"mappings":";;;;;AA6CA,
|
|
1
|
+
{"version":3,"file":"useSessionOperations.js","names":["useSessionOperations","store","useAuthStore","disableTokenInfoQuery","options","pageSize","page","setPage","useState","validateTokensMutation","useMutation","ValidateTokensDocument","useQuery","TokenInfoDocument","sessionsData","sessionsLoading","refetchSessions","ActiveSessionsDocument","revokeSessionMutation","RevokeSessionDocument","revokeAllSessionsMutation","RevokeAllSessionsDocument","validateTokens","data","error","isTokenExpired","getTokenRemainingTime","remaining","revokeSession","revokeAllSessions","totalCount","hasMore","totalPages","prev","loading"],"sources":["../../../src/auth/hooks/useSessionOperations.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { useQuery, useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n TokenInfoDocument,\n ValidateTokensDocument,\n ActiveSessionsDocument,\n RevokeSessionDocument,\n RevokeAllSessionsDocument,\n} from \"../../generated/global-operations\";\nimport type {\n ActiveSessionsQuery,\n ActiveSessionsQueryVariables,\n RevokeSessionMutation,\n RevokeSessionMutationVariables,\n RevokeAllSessionsMutation,\n RevokeAllSessionsMutationVariables,\n} from \"../../generated/global-operations\";\nimport type { TokenValidationResponse, ValidateTokensInput } from \"../types\";\n\n// =============================================================================\n// GRAPHQL RESPONSE INTERFACES\n// =============================================================================\n\ninterface TokenInfoQueryResponse {\n tokenInfo: TokenValidationResponse;\n}\n\ninterface ValidateTokensMutationResponse {\n validateTokens: TokenValidationResponse;\n}\n\ninterface ValidateTokensMutationVariables {\n input: ValidateTokensInput;\n}\n\ninterface UseSessionOperationsOptions {\n disableTokenInfoQuery?: boolean;\n /** Number of sessions to load per page (default: 10) */\n pageSize?: number;\n}\n\n/**\n * Hook for session/token management operations\n */\nexport function useSessionOperations(\n options: UseSessionOperationsOptions = {},\n) {\n const store = useAuthStore();\n const disableTokenInfoQuery = options.disableTokenInfoQuery ?? false;\n const pageSize = options.pageSize ?? 10;\n\n // Pagination state: page is 0-based for offset calculation\n const [page, setPage] = useState(0);\n\n const [validateTokensMutation] = useMutation<\n ValidateTokensMutationResponse,\n ValidateTokensMutationVariables\n >(ValidateTokensDocument);\n\n const { data, loading, refetch } = useQuery<TokenInfoQueryResponse>(\n TokenInfoDocument,\n {\n skip: disableTokenInfoQuery || !store.isAuthenticated,\n fetchPolicy: \"cache-and-network\",\n pollInterval: 5 * 60 * 1000, // Poll every 5 minutes (was 1 min — too aggressive)\n },\n );\n\n // Active sessions query\n // Pass the access token explicitly in Apollo operation context so the query\n // works both via the shell's MultiGatewayProvider auth link and in standalone mode.\n const {\n data: sessionsData,\n loading: sessionsLoading,\n refetch: refetchSessions,\n } = useQuery<ActiveSessionsQuery, ActiveSessionsQueryVariables>(\n ActiveSessionsDocument,\n {\n skip: !store.isAuthenticated,\n fetchPolicy: \"cache-and-network\",\n variables: {\n pagination: {\n limit: pageSize,\n offset: page * pageSize,\n },\n },\n context: store.accessToken\n ? {\n headers: {\n authorization: `Bearer ${store.accessToken}`,\n // activeSessions is a global-layer query scoped to the current user\n // (scopeType: \"self\"). Explicitly clear the workspace-id header so\n // the gateway doesn't attempt workspace scoping for this query.\n \"x-workspace-id\": \"\",\n },\n }\n : undefined,\n },\n );\n\n // Session mutations\n const [revokeSessionMutation] = useMutation<\n RevokeSessionMutation,\n RevokeSessionMutationVariables\n >(RevokeSessionDocument);\n\n const [revokeAllSessionsMutation] = useMutation<\n RevokeAllSessionsMutation,\n RevokeAllSessionsMutationVariables\n >(RevokeAllSessionsDocument);\n\n /**\n * Validate tokens\n */\n const validateTokens = async (\n input: ValidateTokensInput,\n ): Promise<TokenValidationResponse> => {\n try {\n const { data } = await validateTokensMutation({ variables: { input } });\n return data!.validateTokens;\n } catch (error) {\n console.error(\"Failed to validate tokens:\", error);\n throw error;\n }\n };\n\n /**\n * Check if token is expired\n */\n const isTokenExpired = (): boolean => {\n if (!data?.tokenInfo?.expiresAt) return true;\n return Date.now() / 1000 > data.tokenInfo.expiresAt;\n };\n\n /**\n * Get remaining time until token expires (in seconds)\n */\n const getTokenRemainingTime = (): number => {\n if (!data?.tokenInfo?.expiresAt) return 0;\n const remaining = data.tokenInfo.expiresAt - Date.now() / 1000;\n return Math.max(0, remaining);\n };\n\n /**\n * Revoke a specific session by ID\n */\n const revokeSession = async (sessionId: string): Promise<boolean> => {\n try {\n const { data } = await revokeSessionMutation({\n variables: { sessionId },\n });\n if (data?.revokeSession) {\n // Refetch active sessions to reflect the change\n await refetchSessions();\n }\n return data?.revokeSession ?? false;\n } catch (error) {\n console.error(\"Failed to revoke session:\", error);\n throw error;\n }\n };\n\n /**\n * Revoke all sessions except the current one\n */\n const revokeAllSessions = async (): Promise<{\n success: boolean;\n revokedCount: number;\n }> => {\n try {\n const { data } = await revokeAllSessionsMutation();\n if (data?.revokeAllSessions?.success) {\n // Refetch active sessions to reflect the change\n await refetchSessions();\n }\n return data?.revokeAllSessions ?? { success: false, revokedCount: 0 };\n } catch (error) {\n console.error(\"Failed to revoke all sessions:\", error);\n throw error;\n }\n };\n\n const totalCount = sessionsData?.activeSessions?.totalCount ?? 0;\n const hasMore = sessionsData?.activeSessions?.hasMore ?? false;\n const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));\n\n const goToNextPage = () => {\n if (hasMore) {\n setPage((prev) => prev + 1);\n }\n };\n\n const goToPrevPage = () => {\n if (page > 0) {\n setPage((prev) => prev - 1);\n }\n };\n\n return {\n // Token operations\n validateTokens,\n isTokenExpired,\n getTokenRemainingTime,\n tokenInfo: data?.tokenInfo,\n // Session operations\n revokeSession,\n revokeAllSessions,\n activeSessions: sessionsData?.activeSessions?.sessions ?? [],\n sessionsLoading,\n refetchSessions,\n // Pagination\n page,\n pageSize,\n totalCount,\n totalPages,\n hasMore,\n goToNextPage,\n goToPrevPage,\n // General state\n isLoading: loading || sessionsLoading,\n refetch,\n };\n}\n"],"mappings":";;;;;AA6CA,SAAgBA,EACd,IAAuC,CAAC,GACxC;CACA,IAAMC,IAAQC,EAAa,GACrBC,IAAwBC,EAAQ,yBAAyB,IACzDC,IAAWD,EAAQ,YAAY,IAG/B,CAACE,GAAMC,KAAWC,EAAS,CAAC,GAE5B,CAACC,KAA0BC,EAG/BC,CAAsB,GAElB,EAAE,SAAM,YAAS,eAAYC,EACjCC,GACA;EACE,MAAMV,KAAyB,CAACF,EAAM;EACtC,aAAa;EACb,cAAc;CAChB,CACF,GAKM,EACJ,MAAMa,GACN,SAASC,GACT,SAASC,MACPJ,EACFK,GACA;EACE,MAAM,CAAChB,EAAM;EACb,aAAa;EACb,WAAW,EACT,YAAY;GACV,OAAOI;GACP,QAAQC,IAAOD;EACjB,EACF;EACA,SAASJ,EAAM,cACX,EACE,SAAS;GACP,eAAe,UAAUA,EAAM;GAI/B,kBAAkB;EACpB,EACF,IACA,KAAA;CACN,CACF,GAGM,CAACiB,KAAyBR,EAG9BS,CAAqB,GAEjB,CAACC,KAA6BV,EAGlCW,CAAyB,GAKrBC,IAAiB,OACrB,MACqC;EACrC,IAAI;GACF,IAAM,EAAE,YAAS,MAAMb,EAAuB,EAAE,WAAW,EAAE,SAAM,EAAE,CAAC;GACtE,OAAOc,EAAM;EACf,SAASC,GAAO;GAEd,MADA,QAAQ,MAAM,8BAA8BA,CAAK,GAC3CA;EACR;CACF,GAKMC,UACJ,CAAKF,GAAM,WAAW,aACf,KAAK,IAAI,IAAI,MAAOA,EAAK,UAAU,WAMtCG,UAAsC;EAC1C,IAAI,CAACH,GAAM,WAAW,WAAW,OAAO;EACxC,IAAMI,IAAYJ,EAAK,UAAU,YAAY,KAAK,IAAI,IAAI;EAC1D,OAAO,KAAK,IAAI,GAAGI,CAAS;CAC9B,GAKMC,IAAgB,OAAO,MAAwC;EACnE,IAAI;GACF,IAAM,EAAE,YAAS,MAAMV,EAAsB,EAC3C,WAAW,EAAE,aAAU,EACzB,CAAC;GAKD,OAJIK,GAAM,iBAER,MAAMP,EAAgB,GAEjBO,GAAM,iBAAiB;EAChC,SAASC,GAAO;GAEd,MADA,QAAQ,MAAM,6BAA6BA,CAAK,GAC1CA;EACR;CACF,GAKMK,IAAoB,YAGpB;EACJ,IAAI;GACF,IAAM,EAAE,YAAS,MAAMT,EAA0B;GAKjD,OAJIG,GAAM,mBAAmB,WAE3B,MAAMP,EAAgB,GAEjBO,GAAM,qBAAqB;IAAE,SAAS;IAAO,cAAc;GAAE;EACtE,SAASC,GAAO;GAEd,MADA,QAAQ,MAAM,kCAAkCA,CAAK,GAC/CA;EACR;CACF,GAEMM,IAAahB,GAAc,gBAAgB,cAAc,GACzDiB,IAAUjB,GAAc,gBAAgB,WAAW,IACnDkB,IAAa,KAAK,IAAI,GAAG,KAAK,KAAKF,IAAazB,CAAQ,CAAC;CAc/D,OAAO;EAEL;EACA;EACA;EACA,WAAWkB,GAAM;EAEjB;EACA;EACA,gBAAgBT,GAAc,gBAAgB,YAAY,CAAC;EAC3D;EACA;EAEA;EACA;EACA;EACA;EACA;EACA,oBA9ByB;GACzB,AAAIiB,KACFxB,GAAS,MAAS0B,IAAO,CAAC;EAE9B;EA2BE,oBAzByB;GACzB,AAAI3B,IAAO,KACTC,GAAS,MAAS0B,IAAO,CAAC;EAE9B;EAuBE,WAAWC,KAAWnB;EACtB;CACF;AACF"}
|
|
@@ -2,7 +2,7 @@ import { useAuth as e } from "../providers/AuthProvider.js";
|
|
|
2
2
|
import { getRecaptchaSiteKey as t, isCaptchaEnabled as n } from "../utils/captchaConfig.js";
|
|
3
3
|
import { useQuery as r } from "@apollo/client/react";
|
|
4
4
|
import { gql as i } from "@apollo/client";
|
|
5
|
-
|
|
5
|
+
// src/auth/hooks/useSignupCaptcha.ts
|
|
6
6
|
var a = i`
|
|
7
7
|
query PublicProductCaptcha($slug: String!) {
|
|
8
8
|
publicProduct(slug: $slug) {
|
|
@@ -33,7 +33,6 @@ function o() {
|
|
|
33
33
|
loading: c
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
//#endregion
|
|
37
36
|
export { o as useSignupCaptcha };
|
|
38
37
|
|
|
39
38
|
//# sourceMappingURL=useSignupCaptcha.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSignupCaptcha.js","names":[],"sources":["../../../src/auth/hooks/useSignupCaptcha.ts"],"sourcesContent":["import { gql } from \"@apollo/client\";\nimport { useQuery } from \"@apollo/client/react\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { getRecaptchaSiteKey, isCaptchaEnabled } from \"../utils/captchaConfig\";\n\n/**\n * Public, unauthenticated lookup of a product's reCAPTCHA v2 site key.\n *\n * The site key is intentionally public (it's embedded in the rendered widget);\n * the matching secret key lives only in the platform product catalog and is\n * verified server-side. `publicProduct` is `@rbac(public: true)`, so this runs\n * before the user has signed in.\n */\nconst PUBLIC_PRODUCT_CAPTCHA_QUERY = gql`\n query PublicProductCaptcha($slug: String!) {\n publicProduct(slug: $slug) {\n slug\n recaptchaSiteKey\n }\n }\n`;\n\ninterface PublicProductCaptchaData {\n publicProduct: {\n slug: string;\n recaptchaSiteKey: string | null;\n } | null;\n}\n\nexport interface SignupCaptchaState {\n /** Whether the reCAPTCHA v2 checkbox should be rendered + required on signup. */\n enabled: boolean;\n /** The reCAPTCHA v2 site key to render the widget with (undefined when disabled). */\n siteKey: string | undefined;\n /** True while the per-product config is still being fetched. */\n loading: boolean;\n}\n\n/**\n * Resolve whether the signup page should show the reCAPTCHA v2 checkbox, and\n * with which site key, for the current host product.\n *\n * Source of truth is the platform product catalog: a product opts into signup\n * captcha simply by having a `recaptchaSiteKey` configured (looked up here via\n * the host-provided `productSlug`). This keeps enablement per-product and\n * data-driven — no rebuild needed to turn it on/off for a product.\n *\n * Falls back to the build-time env (`VITE_CAPTCHA_ENABLED` +\n * `VITE_RECAPTCHA_SITE_KEY`) only for local source-dev where no `productSlug`\n * is provided, preserving the previous behavior.\n */\nexport function useSignupCaptcha(): SignupCaptchaState {\n const { productSlug } = useAuth();\n const slug = productSlug?.trim();\n\n const { data, loading } = useQuery<PublicProductCaptchaData>(\n PUBLIC_PRODUCT_CAPTCHA_QUERY,\n {\n variables: { slug },\n skip: !slug,\n // Site keys change rarely; serve from cache and only block once.\n fetchPolicy: \"cache-first\",\n // A lookup failure must never break the signup form — degrade to \"off\".\n errorPolicy: \"all\",\n },\n );\n\n if (!slug) {\n // Source-dev / hosts that don't pass a slug: honor the legacy env switch.\n const envSiteKey = getRecaptchaSiteKey();\n const envEnabled = isCaptchaEnabled() && Boolean(envSiteKey);\n return {\n enabled: envEnabled,\n siteKey: envEnabled ? envSiteKey : undefined,\n loading: false,\n };\n }\n\n const siteKey = data?.publicProduct?.recaptchaSiteKey ?? undefined;\n const trimmedKey =\n siteKey && siteKey.trim().length > 0 ? siteKey.trim() : undefined;\n\n return {\n enabled: Boolean(trimmedKey),\n siteKey: trimmedKey,\n loading,\n };\n}\n"],"mappings":";;;;;AAaA,
|
|
1
|
+
{"version":3,"file":"useSignupCaptcha.js","names":["PUBLIC_PRODUCT_CAPTCHA_QUERY","useSignupCaptcha","useAuth","slug","productSlug","useQuery","envSiteKey","envEnabled","isCaptchaEnabled","siteKey","data","trimmedKey"],"sources":["../../../src/auth/hooks/useSignupCaptcha.ts"],"sourcesContent":["import { gql } from \"@apollo/client\";\nimport { useQuery } from \"@apollo/client/react\";\nimport { useAuth } from \"../providers/AuthProvider\";\nimport { getRecaptchaSiteKey, isCaptchaEnabled } from \"../utils/captchaConfig\";\n\n/**\n * Public, unauthenticated lookup of a product's reCAPTCHA v2 site key.\n *\n * The site key is intentionally public (it's embedded in the rendered widget);\n * the matching secret key lives only in the platform product catalog and is\n * verified server-side. `publicProduct` is `@rbac(public: true)`, so this runs\n * before the user has signed in.\n */\nconst PUBLIC_PRODUCT_CAPTCHA_QUERY = gql`\n query PublicProductCaptcha($slug: String!) {\n publicProduct(slug: $slug) {\n slug\n recaptchaSiteKey\n }\n }\n`;\n\ninterface PublicProductCaptchaData {\n publicProduct: {\n slug: string;\n recaptchaSiteKey: string | null;\n } | null;\n}\n\nexport interface SignupCaptchaState {\n /** Whether the reCAPTCHA v2 checkbox should be rendered + required on signup. */\n enabled: boolean;\n /** The reCAPTCHA v2 site key to render the widget with (undefined when disabled). */\n siteKey: string | undefined;\n /** True while the per-product config is still being fetched. */\n loading: boolean;\n}\n\n/**\n * Resolve whether the signup page should show the reCAPTCHA v2 checkbox, and\n * with which site key, for the current host product.\n *\n * Source of truth is the platform product catalog: a product opts into signup\n * captcha simply by having a `recaptchaSiteKey` configured (looked up here via\n * the host-provided `productSlug`). This keeps enablement per-product and\n * data-driven — no rebuild needed to turn it on/off for a product.\n *\n * Falls back to the build-time env (`VITE_CAPTCHA_ENABLED` +\n * `VITE_RECAPTCHA_SITE_KEY`) only for local source-dev where no `productSlug`\n * is provided, preserving the previous behavior.\n */\nexport function useSignupCaptcha(): SignupCaptchaState {\n const { productSlug } = useAuth();\n const slug = productSlug?.trim();\n\n const { data, loading } = useQuery<PublicProductCaptchaData>(\n PUBLIC_PRODUCT_CAPTCHA_QUERY,\n {\n variables: { slug },\n skip: !slug,\n // Site keys change rarely; serve from cache and only block once.\n fetchPolicy: \"cache-first\",\n // A lookup failure must never break the signup form — degrade to \"off\".\n errorPolicy: \"all\",\n },\n );\n\n if (!slug) {\n // Source-dev / hosts that don't pass a slug: honor the legacy env switch.\n const envSiteKey = getRecaptchaSiteKey();\n const envEnabled = isCaptchaEnabled() && Boolean(envSiteKey);\n return {\n enabled: envEnabled,\n siteKey: envEnabled ? envSiteKey : undefined,\n loading: false,\n };\n }\n\n const siteKey = data?.publicProduct?.recaptchaSiteKey ?? undefined;\n const trimmedKey =\n siteKey && siteKey.trim().length > 0 ? siteKey.trim() : undefined;\n\n return {\n enabled: Boolean(trimmedKey),\n siteKey: trimmedKey,\n loading,\n };\n}\n"],"mappings":";;;;;AAaA,IAAMA,IAA+B,CAAG;;;;;;;;AAsCxC,SAAgBC,IAAuC;CACrD,IAAM,EAAE,mBAAgBC,EAAQ,GAC1BC,IAAOC,GAAa,KAAK,GAEzB,EAAE,SAAM,eAAYC,EACxBL,GACA;EACE,WAAW,EAAE,QAAK;EAClB,MAAM,CAACG;EAEP,aAAa;EAEb,aAAa;CACf,CACF;CAEA,IAAI,CAACA,GAAM;EAET,IAAMG,IAAa,kBAAoB,GACjCC,IAAaC,EAAiB,KAAK,EAAQF;EACjD,OAAO;GACL,SAASC;GACT,SAASA,IAAaD,IAAa,KAAA;GACnC,SAAS;EACX;CACF;CAEA,IAAMG,IAAUC,GAAM,eAAe,oBAAoB,KAAA,GACnDC,IACJF,KAAWA,EAAQ,KAAK,CAAC,CAAC,SAAS,IAAIA,EAAQ,KAAK,IAAI,KAAA;CAE1D,OAAO;EACL,SAAS,EAAQE;EACjB,SAASA;EACT;CACF;AACF"}
|