@burdenoff/microfe-auth 2026.925.1 → 2026.1005.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.d.ts +0 -1
- package/dist/auth/AuthRoot.js.map +1 -1
- package/dist/auth/AuthRoutes.d.ts +1 -1
- package/dist/auth/AuthRoutes.js.map +1 -1
- package/dist/auth/components/AuthIcons.d.ts +12 -13
- package/dist/auth/components/AuthIcons.js.map +1 -1
- package/dist/auth/components/AuthLayout.d.ts +1 -2
- package/dist/auth/components/AuthLayout.js.map +1 -1
- package/dist/auth/components/CaptchaV2.d.ts +1 -1
- package/dist/auth/components/CaptchaV2.js +2 -2
- package/dist/auth/components/CaptchaV2.js.map +1 -1
- package/dist/auth/components/ConfirmActionDialog.d.ts +1 -1
- package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
- package/dist/auth/components/profile/UserProfileMenu.d.ts +1 -1
- package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
- package/dist/auth/components/workspace/WorkspaceSelector.d.ts +1 -2
- package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
- package/dist/auth/constants/permissions.js.map +1 -1
- package/dist/auth/hooks/useAccountOperations.d.ts +0 -1
- package/dist/auth/hooks/useAccountOperations.js.map +1 -1
- package/dist/auth/hooks/useActiveContextBootstrapMirror.js.map +1 -1
- package/dist/auth/hooks/useApiKeyOperations.d.ts +1 -2
- package/dist/auth/hooks/useAppOperations.d.ts +1 -2
- package/dist/auth/hooks/useAuthOperations.d.ts +0 -1
- package/dist/auth/hooks/useAuthOperations.js.map +1 -1
- package/dist/auth/hooks/useAuthPermissions.js.map +1 -1
- package/dist/auth/hooks/useCaptchaV3.js +1 -1
- package/dist/auth/hooks/useCaptchaV3.js.map +1 -1
- package/dist/auth/hooks/useContextSwitcherData.d.ts +1 -1
- package/dist/auth/hooks/useContextSwitcherData.js +356 -234
- package/dist/auth/hooks/useContextSwitcherData.js.map +1 -1
- package/dist/auth/hooks/useDelegationOperations.d.ts +0 -1
- package/dist/auth/hooks/useMfaOperations.d.ts +0 -1
- package/dist/auth/hooks/useMfaOperations.js.map +1 -1
- package/dist/auth/hooks/useMyWorkspaces.d.ts +0 -1
- package/dist/auth/hooks/useMyWorkspaces.js.map +1 -1
- package/dist/auth/hooks/useOAuthOperations.d.ts +0 -1
- package/dist/auth/hooks/useOAuthOperations.js +2 -2
- package/dist/auth/hooks/useOAuthOperations.js.map +1 -1
- package/dist/auth/hooks/usePasswordOperations.d.ts +0 -1
- package/dist/auth/hooks/usePasswordOperations.js.map +1 -1
- package/dist/auth/hooks/useSessionOperations.d.ts +0 -1
- package/dist/auth/hooks/useSessionOperations.js.map +1 -1
- package/dist/auth/hooks/useSignupCaptcha.js +1 -1
- package/dist/auth/hooks/useSignupCaptcha.js.map +1 -1
- package/dist/auth/hooks/useSsoOperations.d.ts +0 -1
- package/dist/auth/hooks/useSsoOperations.js.map +1 -1
- package/dist/auth/hooks/useTokenRefresh.js.map +1 -1
- package/dist/auth/hooks/useUserOperations.d.ts +0 -1
- package/dist/auth/hooks/useUserOperations.js.map +1 -1
- package/dist/auth/hooks/useWorkspaceOperations.d.ts +0 -1
- package/dist/auth/hooks/useWorkspaceOperations.js.map +1 -1
- package/dist/auth/hooks/useWorkspaceToken.js.map +1 -1
- package/dist/auth/pages/AuditLogPage.d.ts +1 -1
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.d.ts +1 -1
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.d.ts +1 -1
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.d.ts +1 -1
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.d.ts +1 -1
- package/dist/auth/pages/MfaSetupPage.js +88 -88
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.d.ts +1 -1
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/OAuthCallbackPage.d.ts +1 -1
- package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.d.ts +1 -1
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.d.ts +1 -1
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.d.ts +1 -1
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SSOClearPage.d.ts +1 -1
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.d.ts +1 -1
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.d.ts +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SSORelayPage.d.ts +1 -1
- package/dist/auth/pages/SSORelayPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.d.ts +1 -1
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionBridgePage.d.ts +1 -1
- package/dist/auth/pages/SessionBridgePage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.d.ts +1 -1
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.d.ts +1 -1
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.d.ts +1 -1
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.d.ts +1 -1
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.d.ts +1 -1
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.d.ts +1 -1
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.d.ts +1 -1
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/auth/providers/AuthProvider.d.ts +0 -1
- package/dist/auth/providers/AuthProvider.js.map +1 -1
- package/dist/auth/providers/CaptchaProvider.d.ts +1 -2
- package/dist/auth/providers/CaptchaProvider.js +2 -2
- package/dist/auth/providers/CaptchaProvider.js.map +1 -1
- package/dist/auth/store/authStore.d.ts +0 -1
- package/dist/auth/store/authStore.js.map +1 -1
- package/dist/auth/types/index.d.ts +0 -1
- package/dist/auth/utils/captchaConfig.js.map +1 -1
- package/dist/auth/utils/formatters.js.map +1 -1
- package/dist/auth/utils/gatewayBaseUrls.js.map +1 -1
- package/dist/auth/utils/legalUrls.js.map +1 -1
- package/dist/auth/utils/redirect.js.map +1 -1
- package/dist/auth/utils/ssoOrigins.js.map +1 -1
- package/dist/auth/utils/validation.d.ts +0 -1
- package/dist/auth/utils/validation.js.map +1 -1
- package/dist/dev/main.d.ts +0 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/shared/hooks/useTr.js.map +1 -1
- package/dist/shared/types/index.d.ts +0 -1
- package/dist/translations/en.js.map +1 -1
- package/package.json +27 -20
- package/dist/_virtual/_rolldown/runtime.js +0 -13
- package/dist/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js +0 -70
- package/dist/node_modules/hoist-non-react-statics/dist/hoist-non-react-statics.cjs.js.map +0 -1
- package/dist/node_modules/object-assign/index.js +0 -41
- package/dist/node_modules/object-assign/index.js.map +0 -1
- package/dist/node_modules/prop-types/checkPropTypes.js +0 -46
- package/dist/node_modules/prop-types/checkPropTypes.js.map +0 -1
- package/dist/node_modules/prop-types/factoryWithThrowingShims.js +0 -49
- package/dist/node_modules/prop-types/factoryWithThrowingShims.js.map +0 -1
- package/dist/node_modules/prop-types/factoryWithTypeCheckers.js +0 -272
- package/dist/node_modules/prop-types/factoryWithTypeCheckers.js.map +0 -1
- package/dist/node_modules/prop-types/index.js +0 -16
- package/dist/node_modules/prop-types/index.js.map +0 -1
- package/dist/node_modules/prop-types/lib/ReactPropTypesSecret.js +0 -10
- package/dist/node_modules/prop-types/lib/ReactPropTypesSecret.js.map +0 -1
- package/dist/node_modules/prop-types/lib/has.js +0 -10
- package/dist/node_modules/prop-types/lib/has.js.map +0 -1
- package/dist/node_modules/qrcode.react/lib/esm/index.js +0 -959
- package/dist/node_modules/qrcode.react/lib/esm/index.js.map +0 -1
- package/dist/node_modules/react-async-script/lib/esm/async-script-loader.js +0 -114
- package/dist/node_modules/react-async-script/lib/esm/async-script-loader.js.map +0 -1
- package/dist/node_modules/react-google-recaptcha/lib/esm/index.js +0 -8
- package/dist/node_modules/react-google-recaptcha/lib/esm/index.js.map +0 -1
- package/dist/node_modules/react-google-recaptcha/lib/esm/recaptcha-wrapper.js +0 -20
- package/dist/node_modules/react-google-recaptcha/lib/esm/recaptcha-wrapper.js.map +0 -1
- package/dist/node_modules/react-google-recaptcha/lib/esm/recaptcha.js +0 -148
- package/dist/node_modules/react-google-recaptcha/lib/esm/recaptcha.js.map +0 -1
- package/dist/node_modules/react-google-recaptcha-v3/dist/react-google-recaptcha-v3.esm.js +0 -445
- package/dist/node_modules/react-google-recaptcha-v3/dist/react-google-recaptcha-v3.esm.js.map +0 -1
- package/dist/node_modules/react-is/cjs/react-is.development.js +0 -84
- package/dist/node_modules/react-is/cjs/react-is.development.js.map +0 -1
- package/dist/node_modules/react-is/cjs/react-is.production.min.js +0 -64
- package/dist/node_modules/react-is/cjs/react-is.production.min.js.map +0 -1
- package/dist/node_modules/react-is/index.js +0 -12
- package/dist/node_modules/react-is/index.js.map +0 -1
|
@@ -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":";;;;;;;AAmCA,IAAM,IAA6B,qLAe7B,IAAwC;AA4B9C,SAAgB,IAAkB;CAChC,IAAM,IAAQ,GAAc,EACtB,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAmB,KAAwB,EAAS,GAAM,EAC3D,EAAE,kBAAe,wBAAqB,QACpC,GAAwB,EAC9B,EAAE,CACH,EAIK,IAAwB,EAAO,GAAM,EAIrC,IAAc,EAAM,aACpB,IAAe,QAEjB,IACI,EAAE,SAAS,EAAE,eAAe,UAAU,KAAe,EAAE,GACvD,KAAA,GACN,CAAC,EAAY,CACd,EAKK,EACJ,SACA,SAAS,GACT,OAAO,GACP,eACE,EAA+B,GAAsB;EACvD,MACE,CAAC,EAAM,mBACP,CAAC,EAAM,eACP,EAAsB;EACxB,aAAa;EACb,SAAS;EACV,CAAC;AAWF,CAPA,QAAgB;AACd,EAAI,GAAM,cAAc,SAAS,CAAC,EAAsB,WACtD,EAAa,UAAU,CAAC,cAAc,EAAK,aAAa,MAAM;IAE/D,CAAC,EAAK,CAAC,EAGV,QAAgB;AACd,EAAI,MAEA,EAAW,SAAS,aAAa,CAAC,SAAS,kBAAkB,IAC7D,EAAW,SAAS,aAAa,CAAC,SAAS,eAAe,IAC1D,EAAW,SAAS,aAAa,CAAC,SAAS,oBAAoB,IAE/D,QAAQ,MAAM,+BAA+B,EAAW;IAG3D,CAAC,EAAW,CAAC;CAOhB,IAAM,IAAkB,EACtB,OAAO,MAA+C;EACpD,IAAM,IAAQ,KAAe,EAAa,UAAU,CAAC;AAErD,MAAI,CAAC,EAEH,QADA,QAAQ,MAAM,qDAAqD,EAC5D,EAAE;AAIX,EADA,EAAM,qBAAqB,GAAK,EAChC,EAAqB,GAAK;AAC1B,MAAI;GACF,IAAM,IAAS,MAAM,EAElB;IACD,SAAS;IACT,OAAO;IACP,WAAW;IACX,SAAS;IACV,CAAC;AAEF,OAAI,EAAO,QAAQ,QAAQ;IACzB,IAAM,IAAe,EAAO,OAAO,IAAI,WAAW;AAElD,UADA,QAAQ,MAAM,+BAA+B,EAAO,OAAO,EACjD,MAAM,EAAa;;GAG/B,IAAM,IAA0B,EAAO,MAAM,cAAc,SAAS,EAAE;AAEtE,UADA,EAAM,cAAc,EAAW,EACxB;WACA,GAAO;AAEd,UADA,QAAQ,MAAM,+BAA+B,EAAM,EAC5C,EAAE;YACD;AAER,GADA,EAAM,qBAAqB,GAAM,EACjC,EAAqB,GAAM;;IAG/B,CAAC,GAAO,EAAc,CACvB,EAMK,IAAsB,EAC1B,OACE,GACA,GACA,MACkE;EAClE,IAAM,IACJ,KAAe,EAAa,UAAU,CAAC,eAAe,KAAA;AAExD,MAAI,CAAC,EACH,QAAO;GAAE,SAAS;GAAO,OAAO;GAA6B;AAG/D,IAAgB,GAAK;AACrB,MAAI;GACF,IAAM,IAAS,MAAM,EAElB;IACD,SAAS;IACT,OAAO;IACP,WAAW,EACT,OAAO;KACL;KACA;KACD,EACF;IACD;IACA,gBAAgB;IAChB,SAAS;IACT;IACA;IACD,CAAC;AAEF,OAAI,EAAO,QAAQ,QAAQ;IACzB,IAAM,IAAe,EAAO,OAAO,IAAI,WAAW;AAElD,WADA,QAAQ,MAAM,mBAAmB,EAAO,OAAO,EACxC;KAAE,SAAS;KAAO,OAAO;KAAc;;GAGhD,IAAM,IAAc,EAAO,MAAM;AAQjC,UANI,GAAa,WAAW,EAAY,SACtC,EAAM,sBAAsB,EAAY,EACxC,EAAM,kBAAkB,EAAY,MAAM,EACnC;IAAE,SAAS;IAAM,OAAO,EAAY;IAAO,IAG7C;IACL,SAAS;IACT,OACE,GAAa,OAAO,WAAW;IAClC;WACM,GAAO;AAEd,UADA,QAAQ,MAAM,oCAAoC,EAAM,EACjD;IACL,SAAS;IACT,OACE,aAAiB,QAAQ,EAAM,UAAU;IAC5C;YACO;AACR,KAAgB,GAAM;;IAG1B,CAAC,GAAO,EAAiB,CAC1B,EAKK,IAAkB,EACtB,OACE,MAEO,EAAoB,EAAU,IAAI,EAAU,eAAe,EAEpE,CAAC,EAAoB,CACtB,EAQK,IAAmB,EACvB,OACE,MAKI;AAIJ,EADA,EAAsB,UAAU,IAChC,EAAM,qBAAqB,GAAK;AAChC,MAAI;GACF,IAAM,IAAa,MAAM,EAAgB,EAAY;AAErD,OAAI,EAAW,WAAW,EACxB,QAAO;IACL,SAAS;IACT,OAAO;IACR;GAGH,IAAM,IAAO,EAAM,MACf;AAYJ,OAVI,GAAM,uBACR,IAAoB,EAAW,MAC5B,MAAO,EAAG,OAAO,EAAK,mBACxB,GAGH,AACE,MAAoB,EAAW,IAG7B,CAAC,EACH,QAAO;IAAE,SAAS;IAAO,OAAO;IAAgC;GAGlE,IAAM,IAAc,MAAM,EACxB,EAAkB,IAClB,EAAkB,gBAClB,EACD;AAmBD,UAjBK,EAAY,WAcjB,EAAM,sBAAsB,EAAkB,GAAG,EACjD,EAAM,yBAAyB,EAAkB,eAAe,EAEzD;IACL,SAAS;IACT,WAAW;IACZ,KAnBC,QAAQ,KACN,8CACA,EAAY,MACb,EACM;IACL,SAAS;IACT,WAAW;IACX,OAAO,8CAA8C,EAAY;IAClE;WAYI,GAAO;AAEd,UADA,QAAQ,MAAM,iCAAiC,EAAM,EAC9C;IACL,SAAS;IACT,OAAO,aAAiB,QAAQ,EAAM,UAAU;IACjD;YACO;AAKR,GAJA,EAAM,qBAAqB,GAAM,EAIjC,iBAAiB;AACf,MAAsB,UAAU;MAC/B,IAAI;;IAGX;EAAC;EAAO;EAAiB;EAAoB,CAC9C;AAED,QAAO;EAEL,YAAY,GAAM,cAAc,SAAS,EAAM;EAC/C,iBAAiB,GAAM,cAAc,SAAS,EAAM,WAAW;EAC/D,oBAAoB,EAAM;EAC1B,gBAAgB,EAAM;EAGtB,WACE,KACA,KACA,KACA,EAAM;EACR,OAAO,GAAY,WAAW;EAG9B;EACA;EACA;EACA;EACA;EACD"}
|
|
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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"}
|
|
@@ -40,7 +40,7 @@ function o(e) {
|
|
|
40
40
|
MODE: "production",
|
|
41
41
|
PROD: !0,
|
|
42
42
|
SSR: !1
|
|
43
|
-
}
|
|
43
|
+
}[e];
|
|
44
44
|
} catch {
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
@@ -58,7 +58,7 @@ async function l(e) {
|
|
|
58
58
|
if (typeof window > "u") return;
|
|
59
59
|
let t = window.__burdenoffOpenExternal;
|
|
60
60
|
if (typeof t == "function") try {
|
|
61
|
-
if (await t(e)) return;
|
|
61
|
+
if (await t(e) === !0) return;
|
|
62
62
|
} catch {}
|
|
63
63
|
window.location.href = e;
|
|
64
64
|
}
|
|
@@ -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 = await opener(authUrl);\n if (ok) 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":[],"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 +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,SAAgB,IAAwB;CAEtC,IAAM,IAAY,GAAc,MAAU,EAAM,
|
|
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,SAAgB,IAAwB;CAEtC,IAAM,IAAY,GAAc,MAAU,EAAM,SAAS,GAEnD,CAAC,KAA0B,EAG/B,CAAsB,GAClB,CAAC,KAAgC,EAGrC,CAA4B,GACxB,CAAC,KAAyB,EAG9B,CAAqB;CA4GvB,OAAO;EACL,uBAvGA,MACqB;GACrB,IAAM,IAAQ,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAErB,IAAM,EAAE,YAAS,MAAM,EAAuB,EAC5C,WAAW,EAAE,SAAM,EACrB,CAAC;IAED,OAAO,EAAM;GACf,SAAS,GAAO;IAEd,MADA,QAAQ,MAAM,8BAA8B,CAAK,GAC3C;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAuFE,6BAjFA,GACA,MACwC;GACxC,IAAM,IAAQ,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAErB,IAAM,EAAE,YAAS,MAAM,EAA6B,EAClD,WAAW;KAAE;KAAO,GAAI,IAAiB,EAAE,kBAAe,IAAI,CAAC;IAAG,EACpE,CAAC;IAED,OAAO,EAAM;GACf,SAAS,GAAO;IAEd,MADA,QAAQ,MAAM,qCAAqC,CAAK,GAClD;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAgEE,sBA1DA,GACA,MACqB;GACrB,IAAM,IAAQ,EAAa,SAAS;GACpC,IAAI;IACF,EAAM,WAAW,EAAI;IAWrB,IAAM,IAAS,MAAM,EAAsB,EACzC,WAAW;KAAE;KAAO;IAAY,EAClC,CAAC,GAGK,IACJ,GACC;IACH,IAAI,GAAa;KACf,IAAM,IAAO,EAAY,SAAS,IAAI,YAAY;KAGlD,MAFwC,MAApC,MAAS,kBAAiC,kBAC1C,MAAS,kBAAiC,kBAC9B,EAAY,SAAS,IAAI,WAAW,cAFS;IAG/D;IAEA,OAAO,EAAO,KAAM;GACtB,SAAS,GAAgB;IAGvB,IAAM,IAAe,GAAiB,WAAW;IACjD,IAAI,MAAgB,mBAAmB,MAAgB,iBACrD,MAAM;IASR,IAAM,KAFH,GAAuC,SAAS,MAChD,GAA8C,gBAAgB,KAC1C,YAAY;IAGnC,MAFI,MAAS,kBAA2B,MAAM,eAAe,IACzD,MAAS,kBAA2B,MAAM,eAAe,IACvD;GACR,UAAU;IACR,EAAM,WAAW,EAAK;GACxB;EACF;EAME;CACF;AACF"}
|
|
@@ -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,SAAgB,EACd,IAAuC,
|
|
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,SAAgB,EACd,IAAuC,CAAC,GACxC;CACA,IAAM,IAAQ,EAAa,GACrB,IAAwB,EAAQ,yBAAyB,IACzD,IAAW,EAAQ,YAAY,IAG/B,CAAC,GAAM,KAAW,EAAS,CAAC,GAE5B,CAAC,KAA0B,EAG/B,CAAsB,GAElB,EAAE,SAAM,YAAS,eAAY,EACjC,GACA;EACE,MAAM,KAAyB,CAAC,EAAM;EACtC,aAAa;EACb,cAAc,MAAS;CACzB,CACF,GAKM,EACJ,MAAM,GACN,SAAS,GACT,SAAS,MACP,EACF,GACA;EACE,MAAM,CAAC,EAAM;EACb,aAAa;EACb,WAAW,EACT,YAAY;GACV,OAAO;GACP,QAAQ,IAAO;EACjB,EACF;EACA,SAAS,EAAM,cACX,EACE,SAAS;GACP,eAAe,UAAU,EAAM;GAI/B,kBAAkB;EACpB,EACF,IACA,KAAA;CACN,CACF,GAGM,CAAC,KAAyB,EAG9B,CAAqB,GAEjB,CAAC,KAA6B,EAGlC,CAAyB,GAKrB,IAAiB,OACrB,MACqC;EACrC,IAAI;GACF,IAAM,EAAE,YAAS,MAAM,EAAuB,EAAE,WAAW,EAAE,SAAM,EAAE,CAAC;GACtE,OAAO,EAAM;EACf,SAAS,GAAO;GAEd,MADA,QAAQ,MAAM,8BAA8B,CAAK,GAC3C;EACR;CACF,GAKM,UACC,GAAM,WAAW,YACf,KAAK,IAAI,IAAI,MAAO,EAAK,UAAU,YADF,IAOpC,UAAsC;EAC1C,IAAI,CAAC,GAAM,WAAW,WAAW,OAAO;EACxC,IAAM,IAAY,EAAK,UAAU,YAAY,KAAK,IAAI,IAAI;EAC1D,OAAO,KAAK,IAAI,GAAG,CAAS;CAC9B,GAKM,IAAgB,OAAO,MAAwC;EACnE,IAAI;GACF,IAAM,EAAE,YAAS,MAAM,EAAsB,EAC3C,WAAW,EAAE,aAAU,EACzB,CAAC;GAKD,OAJI,GAAM,iBAER,MAAM,EAAgB,GAEjB,GAAM,iBAAiB;EAChC,SAAS,GAAO;GAEd,MADA,QAAQ,MAAM,6BAA6B,CAAK,GAC1C;EACR;CACF,GAKM,IAAoB,YAGpB;EACJ,IAAI;GACF,IAAM,EAAE,YAAS,MAAM,EAA0B;GAKjD,OAJI,GAAM,mBAAmB,WAE3B,MAAM,EAAgB,GAEjB,GAAM,qBAAqB;IAAE,SAAS;IAAO,cAAc;GAAE;EACtE,SAAS,GAAO;GAEd,MADA,QAAQ,MAAM,kCAAkC,CAAK,GAC/C;EACR;CACF,GAEM,IAAa,GAAc,gBAAgB,cAAc,GACzD,IAAU,GAAc,gBAAgB,WAAW,IACnD,IAAa,KAAK,IAAI,GAAG,KAAK,KAAK,IAAa,CAAQ,CAAC;CAc/D,OAAO;EAEL;EACA;EACA;EACA,WAAW,GAAM;EAEjB;EACA;EACA,gBAAgB,GAAc,gBAAgB,YAAY,CAAC;EAC3D;EACA;EAEA;EACA;EACA;EACA;EACA;EACA,oBA9ByB;GACzB,AAAI,KACF,GAAS,MAAS,IAAO,CAAC;EAE9B;EA2BE,oBAzByB;GACzB,AAAI,IAAO,KACT,GAAS,MAAS,IAAO,CAAC;EAE9B;EAuBE,WAAW,KAAW;EACtB;CACF;AACF"}
|
|
@@ -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,IAAM,IAA+B,CAAG;;;;;;;;AAsCxC,SAAgB,IAAuC;CACrD,IAAM,EAAE,mBAAgB,
|
|
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,IAAM,IAA+B,CAAG;;;;;;;;AAsCxC,SAAgB,IAAuC;CACrD,IAAM,EAAE,mBAAgB,EAAQ,GAC1B,IAAO,GAAa,KAAK,GAEzB,EAAE,SAAM,eAAY,EACxB,GACA;EACE,WAAW,EAAE,QAAK;EAClB,MAAM,CAAC;EAEP,aAAa;EAEb,aAAa;CACf,CACF;CAEA,IAAI,CAAC,GAAM;EAET,IAAM,IAAa,kBAAoB,GACjC,IAAa,EAAiB,KAAK,EAAQ;EACjD,OAAO;GACL,SAAS;GACT,SAAS,IAAa,IAAa,KAAA;GACnC,SAAS;EACX;CACF;CAEA,IAAM,IAAU,GAAM,eAAe,oBAAoB,KAAA,GACnD,IACJ,KAAW,EAAQ,KAAK,EAAE,SAAS,IAAI,EAAQ,KAAK,IAAI,KAAA;CAE1D,OAAO;EACL,SAAS,EAAQ;EACjB,SAAS;EACT;CACF;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSsoOperations.js","names":[],"sources":["../../../src/auth/hooks/useSsoOperations.ts"],"sourcesContent":["import { useMutation, useQuery } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { AuthResult, SSOConfig, User } from \"../types\";\nimport {\n OIDC_CODE_VERIFIER,\n OIDC_STATE,\n OIDC_NONCE,\n OIDC_TENANT_ID,\n OIDC_FLOW,\n OIDC_REDIRECT,\n} from \"../constants/sessionKeys\";\n\nimport {\n AuthenticateSamlDocument,\n GenerateSamlRequestDocument,\n AuthenticateOidcDocument,\n GenerateOidcAuthUrlDocument,\n GetSsoConfigDocument,\n GetOidcConfigDocument,\n SaveSsoConfigDocument,\n DeleteSsoConfigDocument,\n} from \"../../generated/global-operations\";\n// =============================================================================\n// GraphQL Response Types (Apollo Client 4.x)\n// =============================================================================\n\ninterface GetSSOConfigResponse {\n getSSOConfig: SSOConfig | null;\n}\n\ninterface GetSSOConfigVariables {\n tenantId: string;\n}\n\ninterface SaveSSOConfigResponse {\n saveSSOConfig: SSOConfig;\n}\n\ninterface SaveSSOConfigVariables {\n tenantId: string;\n input: SSOConfigInput;\n}\n\ninterface DeleteSSOConfigResponse {\n deleteSSOConfig: boolean;\n}\n\ninterface DeleteSSOConfigVariables {\n tenantId: string;\n}\n\ninterface GenerateSAMLRequestResponse {\n generateSAMLRequest: SAMLRequestResult;\n}\n\ninterface GenerateSAMLRequestVariables {\n tenantId: string;\n relayState?: string;\n}\n\ninterface AuthenticateSAMLResponse {\n authenticateSAML: AuthResult;\n}\n\ninterface AuthenticateSAMLVariables {\n input: {\n samlResponse: string;\n relayState?: string;\n tenantId?: string;\n };\n}\n\ninterface GetOIDCConfigResponse {\n getOIDCConfig: OIDCConfig | null;\n}\n\ninterface GenerateOIDCAuthUrlResponse {\n generateOIDCAuthUrl: ExternalOIDCAuthUrlResult;\n}\n\ninterface GenerateOIDCAuthUrlVariables {\n tenantId: string;\n state?: string;\n}\n\ninterface AuthenticateOIDCPayload {\n accessToken: string;\n refreshToken?: string;\n expiresIn: number;\n user: User;\n}\n\ninterface AuthenticateOIDCResponse {\n authenticateOIDC: AuthenticateOIDCPayload;\n}\n\ninterface AuthenticateOIDCVariables {\n input: {\n tenantId: string;\n code: string;\n state: string;\n codeVerifier: string;\n nonce?: string | null;\n };\n}\n\n// =============================================================================\n// GraphQL Operations\n// =============================================================================\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SSOConfigInput {\n entityId: string;\n ssoUrl: string;\n sloUrl?: string;\n certificate: string;\n enabled?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SAMLRequestResult {\n requestId: string;\n ssoUrl: string;\n samlRequest: string;\n}\n\nexport interface ExternalOIDCAuthUrlResult {\n authUrl: string;\n state: string;\n nonce: string;\n codeVerifier: string;\n relayState?: string;\n}\n\nexport interface OIDCConfig {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n userinfoEndpoint: string;\n jwksUri: string;\n registrationEndpoint?: string;\n scopesSupported: string[];\n responseTypesSupported: string[];\n grantTypesSupported: string[];\n}\n\ninterface GraphQLErrorLike {\n message?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getGraphQLErrors(error: unknown): GraphQLErrorLike[] {\n if (!isRecord(error)) return [];\n const graphQLErrors = error.graphQLErrors;\n if (!Array.isArray(graphQLErrors)) return [];\n return graphQLErrors.filter((item): item is GraphQLErrorLike =>\n isRecord(item),\n );\n}\n\nfunction getErrorMessage(error: unknown, fallback: string): string {\n const graphQLError = getGraphQLErrors(error)[0];\n if (graphQLError?.message) return graphQLError.message;\n if (error instanceof Error) return error.message;\n if (isRecord(error) && typeof error.message === \"string\") {\n return error.message;\n }\n return fallback;\n}\n\n// =============================================================================\n// Hook\n// =============================================================================\n\n/**\n * Hook for SSO/SAML operations\n */\nexport function useSsoOperations(tenantId?: string) {\n const [generateSamlRequestMutation, { loading: generatingRequest }] =\n useMutation<GenerateSAMLRequestResponse, GenerateSAMLRequestVariables>(\n GenerateSamlRequestDocument,\n );\n const [authenticateSamlMutation, { loading: authenticating }] = useMutation<\n AuthenticateSAMLResponse,\n AuthenticateSAMLVariables\n >(AuthenticateSamlDocument);\n const [saveSsoConfigMutation, { loading: saving }] = useMutation<\n SaveSSOConfigResponse,\n SaveSSOConfigVariables\n >(SaveSsoConfigDocument);\n const [deleteSsoConfigMutation, { loading: deleting }] = useMutation<\n DeleteSSOConfigResponse,\n DeleteSSOConfigVariables\n >(DeleteSsoConfigDocument);\n const [generateOidcAuthUrlMutation, { loading: generatingOidcUrl }] =\n useMutation<GenerateOIDCAuthUrlResponse, GenerateOIDCAuthUrlVariables>(\n GenerateOidcAuthUrlDocument,\n );\n const [authenticateOidcMutation, { loading: authenticatingOidc }] =\n useMutation<AuthenticateOIDCResponse, AuthenticateOIDCVariables>(\n AuthenticateOidcDocument,\n );\n\n // Get SSO config for tenant\n const {\n data: ssoConfigData,\n loading: loadingConfig,\n refetch: refetchConfig,\n } = useQuery<GetSSOConfigResponse, GetSSOConfigVariables>(\n GetSsoConfigDocument,\n {\n variables: { tenantId: tenantId || \"\" },\n skip: !tenantId,\n },\n );\n\n // Get OIDC discovery info (for our OIDC provider)\n const { data: oidcConfigData, loading: loadingOidc } =\n useQuery<GetOIDCConfigResponse>(GetOidcConfigDocument, {\n skip: false, // Always fetch OIDC config\n });\n\n /**\n * Initiate SP-initiated SAML login\n * Redirects user to IdP with SAML AuthnRequest\n */\n const initiateSsoLogin = async (\n targetTenantId: string,\n relayState?: string,\n ): Promise<void> => {\n try {\n const { data } = await generateSamlRequestMutation({\n variables: { tenantId: targetTenantId, relayState },\n });\n\n // Check if data exists\n if (!data || !data.generateSAMLRequest) {\n throw new Error(\n \"SAML SSO is not configured for this tenant. Please contact your administrator or use email/password login.\",\n );\n }\n\n const result = data.generateSAMLRequest;\n\n // Build the IdP redirect URL with SAML request\n const params = new URLSearchParams({\n SAMLRequest: result.samlRequest,\n });\n if (relayState) {\n params.append(\"RelayState\", relayState);\n }\n\n // Redirect to IdP SSO URL\n window.location.href = `${result.ssoUrl}?${params.toString()}`;\n } catch (error: unknown) {\n // Extract error message from Apollo error or regular error\n const message = getErrorMessage(\n error,\n \"Failed to initiate SAML SSO login\",\n );\n\n console.error(\"Failed to initiate SSO login:\", message);\n throw new Error(message);\n }\n };\n\n /**\n * Handle SAML callback from IdP\n * Called when IdP redirects back with SAML Response\n */\n const handleSamlCallback = async (\n samlResponse: string,\n relayState?: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n const targetTenantId =\n tenantId || sessionStorage.getItem(OIDC_TENANT_ID) || undefined;\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n const { data, error } = await authenticateSamlMutation({\n variables: {\n input: {\n samlResponse,\n relayState,\n tenantId: targetTenantId,\n },\n },\n });\n\n if (!data?.authenticateSAML) {\n const errMsg =\n error?.message ?? \"SAML authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateSAML;\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 } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"SAML authentication failed\";\n store.setError(message);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Initiate external OIDC login (Azure AD, Google, etc.)\n * Redirects user to external IdP for authentication\n */\n const initiateExternalOidcLogin = async (\n targetTenantId: string,\n redirectPath?: string,\n ): Promise<void> => {\n try {\n const { data } = await generateOidcAuthUrlMutation({\n variables: { tenantId: targetTenantId, state: redirectPath },\n });\n\n // Check if data exists\n if (!data || !data.generateOIDCAuthUrl) {\n throw new Error(\n \"SSO is not configured for this tenant. Please contact your administrator or use email/password login.\",\n );\n }\n\n const result = data.generateOIDCAuthUrl;\n\n // Store PKCE values for token exchange\n sessionStorage.setItem(OIDC_CODE_VERIFIER, result.codeVerifier);\n sessionStorage.setItem(OIDC_STATE, result.state); // Secure random state for CSRF protection\n sessionStorage.setItem(OIDC_NONCE, result.nonce);\n sessionStorage.setItem(OIDC_TENANT_ID, targetTenantId);\n sessionStorage.setItem(OIDC_FLOW, \"external\"); // Mark as external OIDC flow\n // Store relay state (redirect path) separately from security state\n sessionStorage.setItem(\n \"oidc_redirect\",\n result.relayState || redirectPath || \"/dashboard\",\n );\n\n // Redirect to external IdP\n window.location.href = result.authUrl;\n } catch (error: unknown) {\n // Extract error message from Apollo error or regular error\n const message = getErrorMessage(\n error,\n \"Failed to initiate OIDC SSO login\",\n );\n\n console.error(\"Failed to initiate external OIDC login:\", message);\n throw new Error(message);\n }\n };\n\n /**\n * Handle external OIDC callback\n * Called when external IdP redirects back with authorization code\n */\n const handleExternalOidcCallback = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Verify state\n const storedState = sessionStorage.getItem(OIDC_STATE);\n\n if (state !== storedState) {\n throw new Error(\n `Invalid state parameter. Possible CSRF attack. Expected: ${storedState?.substring(0, 10)}..., Got: ${state.substring(0, 10)}...`,\n );\n }\n\n const codeVerifier = sessionStorage.getItem(OIDC_CODE_VERIFIER);\n const nonce = sessionStorage.getItem(OIDC_NONCE);\n const targetTenantId =\n sessionStorage.getItem(OIDC_TENANT_ID) || \"default\";\n\n if (!codeVerifier) {\n throw new Error(\n \"Code verifier not found. Please restart the login flow.\",\n );\n }\n\n const result = await authenticateOidcMutation({\n variables: {\n input: {\n tenantId: targetTenantId,\n code,\n state,\n codeVerifier,\n nonce,\n },\n },\n });\n\n const { data } = result;\n if (!data) {\n throw new Error(\"No data returned from authenticateOIDC mutation\");\n }\n\n const authPayload = data.authenticateOIDC;\n\n // Get redirect path before cleanup\n const redirectPath =\n sessionStorage.getItem(OIDC_REDIRECT) || \"/dashboard\";\n\n // Clean up session storage\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n sessionStorage.removeItem(OIDC_NONCE);\n sessionStorage.removeItem(OIDC_TENANT_ID);\n sessionStorage.removeItem(OIDC_FLOW);\n sessionStorage.removeItem(OIDC_REDIRECT);\n\n if (authPayload.user && authPayload.accessToken) {\n store.setUser(authPayload.user);\n store.setAuthTokens(\n authPayload.accessToken,\n authPayload.refreshToken || \"\",\n );\n store.setAuthenticated(true);\n\n return {\n success: true,\n user: authPayload.user,\n token: authPayload.accessToken,\n refreshToken: authPayload.refreshToken,\n expiresIn: authPayload.expiresIn,\n redirectPath,\n };\n }\n\n return { success: false, error: \"Authentication failed\", redirectPath };\n } catch (error) {\n console.error(\"[OIDC Callback] Error:\", error);\n\n const message =\n error instanceof Error ? error.message : \"OIDC authentication failed\";\n store.setError(message);\n\n // Clean up on error\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n sessionStorage.removeItem(OIDC_NONCE);\n sessionStorage.removeItem(OIDC_TENANT_ID);\n sessionStorage.removeItem(OIDC_FLOW);\n sessionStorage.removeItem(OIDC_REDIRECT);\n\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Check if the current callback is from external OIDC flow\n * Checks for multiple indicators to be robust\n */\n const isExternalOidcCallback = (): boolean => {\n const hasFlowFlag = sessionStorage.getItem(OIDC_FLOW) === \"external\";\n const hasTenantId = !!sessionStorage.getItem(OIDC_TENANT_ID);\n const hasCodeVerifier = !!sessionStorage.getItem(OIDC_CODE_VERIFIER);\n\n // Return true if we have the flow flag OR if we have both tenant ID and code verifier\n return hasFlowFlag || (hasTenantId && hasCodeVerifier);\n };\n\n /**\n * Save SSO configuration for tenant\n */\n const saveSsoConfig = async (\n configOrTenantId: SSOConfigInput | string,\n config?: SSOConfigInput,\n ): Promise<SSOConfig> => {\n // Support both old (tenantId, config) and new (config) signatures\n const targetTenantId =\n typeof configOrTenantId === \"string\" ? configOrTenantId : tenantId || \"\";\n const finalConfig =\n typeof configOrTenantId === \"string\" ? config! : configOrTenantId;\n\n try {\n const { data, error } = await saveSsoConfigMutation({\n variables: { tenantId: targetTenantId, input: finalConfig },\n });\n await refetchConfig();\n if (!data?.saveSSOConfig) {\n throw new Error(error?.message ?? \"Failed to save SSO config\");\n }\n return data.saveSSOConfig;\n } catch (error) {\n console.error(\"Failed to save SSO config:\", error);\n throw error;\n }\n };\n\n /**\n * Delete SSO configuration for tenant\n */\n const deleteSsoConfig = async (targetTenantId?: string): Promise<boolean> => {\n const finalTenantId = targetTenantId || tenantId || \"\";\n\n try {\n const { data, error } = await deleteSsoConfigMutation({\n variables: { tenantId: finalTenantId },\n });\n await refetchConfig();\n if (data?.deleteSSOConfig == null) {\n throw new Error(error?.message ?? \"Failed to delete SSO config\");\n }\n return data.deleteSSOConfig;\n } catch (error) {\n console.error(\"Failed to delete SSO config:\", error);\n throw error;\n }\n };\n\n /**\n * Get OIDC authorization URL for OAuth login (async for PKCE SHA-256)\n */\n const getOidcAuthUrl = async (\n clientId: string,\n redirectUri: string,\n scopes: string[] = [\"openid\", \"profile\", \"email\"],\n ): Promise<string> => {\n const config = oidcConfigData?.getOIDCConfig;\n if (!config) {\n throw new Error(\"OIDC configuration not available\");\n }\n\n // Generate PKCE code verifier and challenge (async for SHA-256)\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Store verifier in sessionStorage for token exchange\n sessionStorage.setItem(OIDC_CODE_VERIFIER, codeVerifier);\n sessionStorage.setItem(OIDC_STATE, state);\n\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: scopes.join(\" \"),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n });\n\n return `${config.authorizationEndpoint}?${params.toString()}`;\n };\n\n /**\n * Exchange OIDC authorization code for tokens\n */\n const exchangeOidcCode = async (\n code: string,\n clientId: string,\n redirectUri: string,\n ): Promise<AuthResult> => {\n const config = oidcConfigData?.getOIDCConfig;\n if (!config) {\n throw new Error(\"OIDC configuration not available\");\n }\n\n const store = useAuthStore.getState();\n const codeVerifier = sessionStorage.getItem(OIDC_CODE_VERIFIER);\n\n if (!codeVerifier) {\n throw new Error(\n \"Code verifier not found. Please restart the login flow.\",\n );\n }\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Exchange code for tokens at the OIDC token endpoint\n const response = await fetch(config.tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n const tokenResponse = await response.json();\n\n if (!response.ok) {\n throw new Error(\n tokenResponse.error_description ||\n tokenResponse.error ||\n \"Token exchange failed\",\n );\n }\n\n // Clear PKCE data from session storage\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n\n // Get user info from userinfo endpoint\n const userinfoResponse = await fetch(config.userinfoEndpoint, {\n headers: {\n Authorization: `Bearer ${tokenResponse.access_token}`,\n },\n });\n\n const userinfo = await userinfoResponse.json();\n\n // Build AuthResult from OIDC response\n const now = new Date().toISOString();\n const result: AuthResult = {\n success: true,\n token: tokenResponse.access_token,\n refreshToken: tokenResponse.refresh_token,\n expiresIn: tokenResponse.expires_in,\n user: {\n id: userinfo.sub,\n email: userinfo.email,\n username: userinfo.preferred_username || userinfo.email,\n emailVerified: userinfo.email_verified ?? true,\n firstName: userinfo.given_name,\n lastName: userinfo.family_name,\n name: userinfo.name,\n role: \"MEMBER\",\n status: \"ACTIVE\",\n mfaEnabled: false,\n createdAt: now,\n updatedAt: now,\n },\n };\n\n if (result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n }\n\n return result;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"OIDC authentication failed\";\n store.setError(message);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Validate OIDC state parameter\n */\n const validateOidcState = (state: string): boolean => {\n const storedState = sessionStorage.getItem(OIDC_STATE);\n return storedState === state;\n };\n\n return {\n // SSO/SAML operations\n initiateSsoLogin,\n handleSamlCallback,\n saveSsoConfig,\n deleteSsoConfig,\n\n // OIDC operations (our OIDC provider)\n getOidcAuthUrl,\n exchangeOidcCode,\n validateOidcState,\n\n // External OIDC operations (Azure AD, Google, etc.)\n initiateExternalOidcLogin,\n handleExternalOidcCallback,\n isExternalOidcCallback,\n\n // Data\n ssoConfig: ssoConfigData?.getSSOConfig ?? undefined,\n oidcConfig: oidcConfigData?.getOIDCConfig ?? undefined,\n\n // Loading states\n isLoading:\n loadingConfig ||\n loadingOidc ||\n generatingRequest ||\n authenticating ||\n saving ||\n deleting ||\n generatingOidcUrl ||\n authenticatingOidc,\n isGeneratingRequest: generatingRequest,\n isAuthenticating: authenticating || authenticatingOidc,\n isSaving: saving,\n isDeleting: deleting,\n isGeneratingOidcUrl: generatingOidcUrl,\n\n // Refetch\n refetchConfig,\n };\n}\n\n// =============================================================================\n// PKCE Helpers\n// =============================================================================\n\n/**\n * Generate random code verifier for PKCE\n */\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Generate code challenge from verifier using SHA-256 (S256 method)\n * Uses Web Crypto API for secure hashing\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n\n // Use SubtleCrypto for SHA-256 hash\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n\n // Base64 URL encode the hash\n return base64UrlEncode(hashArray);\n}\n\n/**\n * Generate random state for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Base64 URL encode\n */\nfunction base64UrlEncode(array: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...array));\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useSsoOperations.js","names":[],"sources":["../../../src/auth/hooks/useSsoOperations.ts"],"sourcesContent":["import { useMutation, useQuery } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport type { AuthResult, SSOConfig, User } from \"../types\";\nimport {\n OIDC_CODE_VERIFIER,\n OIDC_STATE,\n OIDC_NONCE,\n OIDC_TENANT_ID,\n OIDC_FLOW,\n OIDC_REDIRECT,\n} from \"../constants/sessionKeys\";\n\nimport {\n AuthenticateSamlDocument,\n GenerateSamlRequestDocument,\n AuthenticateOidcDocument,\n GenerateOidcAuthUrlDocument,\n GetSsoConfigDocument,\n GetOidcConfigDocument,\n SaveSsoConfigDocument,\n DeleteSsoConfigDocument,\n} from \"../../generated/global-operations\";\n// =============================================================================\n// GraphQL Response Types (Apollo Client 4.x)\n// =============================================================================\n\ninterface GetSSOConfigResponse {\n getSSOConfig: SSOConfig | null;\n}\n\ninterface GetSSOConfigVariables {\n tenantId: string;\n}\n\ninterface SaveSSOConfigResponse {\n saveSSOConfig: SSOConfig;\n}\n\ninterface SaveSSOConfigVariables {\n tenantId: string;\n input: SSOConfigInput;\n}\n\ninterface DeleteSSOConfigResponse {\n deleteSSOConfig: boolean;\n}\n\ninterface DeleteSSOConfigVariables {\n tenantId: string;\n}\n\ninterface GenerateSAMLRequestResponse {\n generateSAMLRequest: SAMLRequestResult;\n}\n\ninterface GenerateSAMLRequestVariables {\n tenantId: string;\n relayState?: string;\n}\n\ninterface AuthenticateSAMLResponse {\n authenticateSAML: AuthResult;\n}\n\ninterface AuthenticateSAMLVariables {\n input: {\n samlResponse: string;\n relayState?: string;\n tenantId?: string;\n };\n}\n\ninterface GetOIDCConfigResponse {\n getOIDCConfig: OIDCConfig | null;\n}\n\ninterface GenerateOIDCAuthUrlResponse {\n generateOIDCAuthUrl: ExternalOIDCAuthUrlResult;\n}\n\ninterface GenerateOIDCAuthUrlVariables {\n tenantId: string;\n state?: string;\n}\n\ninterface AuthenticateOIDCPayload {\n accessToken: string;\n refreshToken?: string;\n expiresIn: number;\n user: User;\n}\n\ninterface AuthenticateOIDCResponse {\n authenticateOIDC: AuthenticateOIDCPayload;\n}\n\ninterface AuthenticateOIDCVariables {\n input: {\n tenantId: string;\n code: string;\n state: string;\n codeVerifier: string;\n nonce?: string | null;\n };\n}\n\n// =============================================================================\n// GraphQL Operations\n// =============================================================================\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface SSOConfigInput {\n entityId: string;\n ssoUrl: string;\n sloUrl?: string;\n certificate: string;\n enabled?: boolean;\n metadata?: Record<string, unknown>;\n}\n\nexport interface SAMLRequestResult {\n requestId: string;\n ssoUrl: string;\n samlRequest: string;\n}\n\nexport interface ExternalOIDCAuthUrlResult {\n authUrl: string;\n state: string;\n nonce: string;\n codeVerifier: string;\n relayState?: string;\n}\n\nexport interface OIDCConfig {\n issuer: string;\n authorizationEndpoint: string;\n tokenEndpoint: string;\n userinfoEndpoint: string;\n jwksUri: string;\n registrationEndpoint?: string;\n scopesSupported: string[];\n responseTypesSupported: string[];\n grantTypesSupported: string[];\n}\n\ninterface GraphQLErrorLike {\n message?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction getGraphQLErrors(error: unknown): GraphQLErrorLike[] {\n if (!isRecord(error)) return [];\n const graphQLErrors = error.graphQLErrors;\n if (!Array.isArray(graphQLErrors)) return [];\n return graphQLErrors.filter((item): item is GraphQLErrorLike =>\n isRecord(item),\n );\n}\n\nfunction getErrorMessage(error: unknown, fallback: string): string {\n const graphQLError = getGraphQLErrors(error)[0];\n if (graphQLError?.message) return graphQLError.message;\n if (error instanceof Error) return error.message;\n if (isRecord(error) && typeof error.message === \"string\") {\n return error.message;\n }\n return fallback;\n}\n\n// =============================================================================\n// Hook\n// =============================================================================\n\n/**\n * Hook for SSO/SAML operations\n */\nexport function useSsoOperations(tenantId?: string) {\n const [generateSamlRequestMutation, { loading: generatingRequest }] =\n useMutation<GenerateSAMLRequestResponse, GenerateSAMLRequestVariables>(\n GenerateSamlRequestDocument,\n );\n const [authenticateSamlMutation, { loading: authenticating }] = useMutation<\n AuthenticateSAMLResponse,\n AuthenticateSAMLVariables\n >(AuthenticateSamlDocument);\n const [saveSsoConfigMutation, { loading: saving }] = useMutation<\n SaveSSOConfigResponse,\n SaveSSOConfigVariables\n >(SaveSsoConfigDocument);\n const [deleteSsoConfigMutation, { loading: deleting }] = useMutation<\n DeleteSSOConfigResponse,\n DeleteSSOConfigVariables\n >(DeleteSsoConfigDocument);\n const [generateOidcAuthUrlMutation, { loading: generatingOidcUrl }] =\n useMutation<GenerateOIDCAuthUrlResponse, GenerateOIDCAuthUrlVariables>(\n GenerateOidcAuthUrlDocument,\n );\n const [authenticateOidcMutation, { loading: authenticatingOidc }] =\n useMutation<AuthenticateOIDCResponse, AuthenticateOIDCVariables>(\n AuthenticateOidcDocument,\n );\n\n // Get SSO config for tenant\n const {\n data: ssoConfigData,\n loading: loadingConfig,\n refetch: refetchConfig,\n } = useQuery<GetSSOConfigResponse, GetSSOConfigVariables>(\n GetSsoConfigDocument,\n {\n variables: { tenantId: tenantId || \"\" },\n skip: !tenantId,\n },\n );\n\n // Get OIDC discovery info (for our OIDC provider)\n const { data: oidcConfigData, loading: loadingOidc } =\n useQuery<GetOIDCConfigResponse>(GetOidcConfigDocument, {\n skip: false, // Always fetch OIDC config\n });\n\n /**\n * Initiate SP-initiated SAML login\n * Redirects user to IdP with SAML AuthnRequest\n */\n const initiateSsoLogin = async (\n targetTenantId: string,\n relayState?: string,\n ): Promise<void> => {\n try {\n const { data } = await generateSamlRequestMutation({\n variables: { tenantId: targetTenantId, relayState },\n });\n\n // Check if data exists\n if (!data || !data.generateSAMLRequest) {\n throw new Error(\n \"SAML SSO is not configured for this tenant. Please contact your administrator or use email/password login.\",\n );\n }\n\n const result = data.generateSAMLRequest;\n\n // Build the IdP redirect URL with SAML request\n const params = new URLSearchParams({\n SAMLRequest: result.samlRequest,\n });\n if (relayState) {\n params.append(\"RelayState\", relayState);\n }\n\n // Redirect to IdP SSO URL\n window.location.href = `${result.ssoUrl}?${params.toString()}`;\n } catch (error: unknown) {\n // Extract error message from Apollo error or regular error\n const message = getErrorMessage(\n error,\n \"Failed to initiate SAML SSO login\",\n );\n\n console.error(\"Failed to initiate SSO login:\", message);\n throw new Error(message);\n }\n };\n\n /**\n * Handle SAML callback from IdP\n * Called when IdP redirects back with SAML Response\n */\n const handleSamlCallback = async (\n samlResponse: string,\n relayState?: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n const targetTenantId =\n tenantId || sessionStorage.getItem(OIDC_TENANT_ID) || undefined;\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n const { data, error } = await authenticateSamlMutation({\n variables: {\n input: {\n samlResponse,\n relayState,\n tenantId: targetTenantId,\n },\n },\n });\n\n if (!data?.authenticateSAML) {\n const errMsg =\n error?.message ?? \"SAML authentication failed. Please try again.\";\n throw new Error(errMsg);\n }\n\n const result = data.authenticateSAML;\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 } else if (result.error) {\n store.setError(result.error);\n }\n\n return result;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"SAML authentication failed\";\n store.setError(message);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Initiate external OIDC login (Azure AD, Google, etc.)\n * Redirects user to external IdP for authentication\n */\n const initiateExternalOidcLogin = async (\n targetTenantId: string,\n redirectPath?: string,\n ): Promise<void> => {\n try {\n const { data } = await generateOidcAuthUrlMutation({\n variables: { tenantId: targetTenantId, state: redirectPath },\n });\n\n // Check if data exists\n if (!data || !data.generateOIDCAuthUrl) {\n throw new Error(\n \"SSO is not configured for this tenant. Please contact your administrator or use email/password login.\",\n );\n }\n\n const result = data.generateOIDCAuthUrl;\n\n // Store PKCE values for token exchange\n sessionStorage.setItem(OIDC_CODE_VERIFIER, result.codeVerifier);\n sessionStorage.setItem(OIDC_STATE, result.state); // Secure random state for CSRF protection\n sessionStorage.setItem(OIDC_NONCE, result.nonce);\n sessionStorage.setItem(OIDC_TENANT_ID, targetTenantId);\n sessionStorage.setItem(OIDC_FLOW, \"external\"); // Mark as external OIDC flow\n // Store relay state (redirect path) separately from security state\n sessionStorage.setItem(\n \"oidc_redirect\",\n result.relayState || redirectPath || \"/dashboard\",\n );\n\n // Redirect to external IdP\n window.location.href = result.authUrl;\n } catch (error: unknown) {\n // Extract error message from Apollo error or regular error\n const message = getErrorMessage(\n error,\n \"Failed to initiate OIDC SSO login\",\n );\n\n console.error(\"Failed to initiate external OIDC login:\", message);\n throw new Error(message);\n }\n };\n\n /**\n * Handle external OIDC callback\n * Called when external IdP redirects back with authorization code\n */\n const handleExternalOidcCallback = async (\n code: string,\n state: string,\n ): Promise<AuthResult> => {\n const store = useAuthStore.getState();\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Verify state\n const storedState = sessionStorage.getItem(OIDC_STATE);\n\n if (state !== storedState) {\n throw new Error(\n `Invalid state parameter. Possible CSRF attack. Expected: ${storedState?.substring(0, 10)}..., Got: ${state.substring(0, 10)}...`,\n );\n }\n\n const codeVerifier = sessionStorage.getItem(OIDC_CODE_VERIFIER);\n const nonce = sessionStorage.getItem(OIDC_NONCE);\n const targetTenantId =\n sessionStorage.getItem(OIDC_TENANT_ID) || \"default\";\n\n if (!codeVerifier) {\n throw new Error(\n \"Code verifier not found. Please restart the login flow.\",\n );\n }\n\n const result = await authenticateOidcMutation({\n variables: {\n input: {\n tenantId: targetTenantId,\n code,\n state,\n codeVerifier,\n nonce,\n },\n },\n });\n\n const { data } = result;\n if (!data) {\n throw new Error(\"No data returned from authenticateOIDC mutation\");\n }\n\n const authPayload = data.authenticateOIDC;\n\n // Get redirect path before cleanup\n const redirectPath =\n sessionStorage.getItem(OIDC_REDIRECT) || \"/dashboard\";\n\n // Clean up session storage\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n sessionStorage.removeItem(OIDC_NONCE);\n sessionStorage.removeItem(OIDC_TENANT_ID);\n sessionStorage.removeItem(OIDC_FLOW);\n sessionStorage.removeItem(OIDC_REDIRECT);\n\n if (authPayload.user && authPayload.accessToken) {\n store.setUser(authPayload.user);\n store.setAuthTokens(\n authPayload.accessToken,\n authPayload.refreshToken || \"\",\n );\n store.setAuthenticated(true);\n\n return {\n success: true,\n user: authPayload.user,\n token: authPayload.accessToken,\n refreshToken: authPayload.refreshToken,\n expiresIn: authPayload.expiresIn,\n redirectPath,\n };\n }\n\n return { success: false, error: \"Authentication failed\", redirectPath };\n } catch (error) {\n console.error(\"[OIDC Callback] Error:\", error);\n\n const message =\n error instanceof Error ? error.message : \"OIDC authentication failed\";\n store.setError(message);\n\n // Clean up on error\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n sessionStorage.removeItem(OIDC_NONCE);\n sessionStorage.removeItem(OIDC_TENANT_ID);\n sessionStorage.removeItem(OIDC_FLOW);\n sessionStorage.removeItem(OIDC_REDIRECT);\n\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Check if the current callback is from external OIDC flow\n * Checks for multiple indicators to be robust\n */\n const isExternalOidcCallback = (): boolean => {\n const hasFlowFlag = sessionStorage.getItem(OIDC_FLOW) === \"external\";\n const hasTenantId = !!sessionStorage.getItem(OIDC_TENANT_ID);\n const hasCodeVerifier = !!sessionStorage.getItem(OIDC_CODE_VERIFIER);\n\n // Return true if we have the flow flag OR if we have both tenant ID and code verifier\n return hasFlowFlag || (hasTenantId && hasCodeVerifier);\n };\n\n /**\n * Save SSO configuration for tenant\n */\n const saveSsoConfig = async (\n configOrTenantId: SSOConfigInput | string,\n config?: SSOConfigInput,\n ): Promise<SSOConfig> => {\n // Support both old (tenantId, config) and new (config) signatures\n const targetTenantId =\n typeof configOrTenantId === \"string\" ? configOrTenantId : tenantId || \"\";\n const finalConfig =\n typeof configOrTenantId === \"string\" ? config! : configOrTenantId;\n\n try {\n const { data, error } = await saveSsoConfigMutation({\n variables: { tenantId: targetTenantId, input: finalConfig },\n });\n await refetchConfig();\n if (!data?.saveSSOConfig) {\n throw new Error(error?.message ?? \"Failed to save SSO config\");\n }\n return data.saveSSOConfig;\n } catch (error) {\n console.error(\"Failed to save SSO config:\", error);\n throw error;\n }\n };\n\n /**\n * Delete SSO configuration for tenant\n */\n const deleteSsoConfig = async (targetTenantId?: string): Promise<boolean> => {\n const finalTenantId = targetTenantId || tenantId || \"\";\n\n try {\n const { data, error } = await deleteSsoConfigMutation({\n variables: { tenantId: finalTenantId },\n });\n await refetchConfig();\n if (data?.deleteSSOConfig == null) {\n throw new Error(error?.message ?? \"Failed to delete SSO config\");\n }\n return data.deleteSSOConfig;\n } catch (error) {\n console.error(\"Failed to delete SSO config:\", error);\n throw error;\n }\n };\n\n /**\n * Get OIDC authorization URL for OAuth login (async for PKCE SHA-256)\n */\n const getOidcAuthUrl = async (\n clientId: string,\n redirectUri: string,\n scopes: string[] = [\"openid\", \"profile\", \"email\"],\n ): Promise<string> => {\n const config = oidcConfigData?.getOIDCConfig;\n if (!config) {\n throw new Error(\"OIDC configuration not available\");\n }\n\n // Generate PKCE code verifier and challenge (async for SHA-256)\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n // Store verifier in sessionStorage for token exchange\n sessionStorage.setItem(OIDC_CODE_VERIFIER, codeVerifier);\n sessionStorage.setItem(OIDC_STATE, state);\n\n const params = new URLSearchParams({\n response_type: \"code\",\n client_id: clientId,\n redirect_uri: redirectUri,\n scope: scopes.join(\" \"),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: \"S256\",\n });\n\n return `${config.authorizationEndpoint}?${params.toString()}`;\n };\n\n /**\n * Exchange OIDC authorization code for tokens\n */\n const exchangeOidcCode = async (\n code: string,\n clientId: string,\n redirectUri: string,\n ): Promise<AuthResult> => {\n const config = oidcConfigData?.getOIDCConfig;\n if (!config) {\n throw new Error(\"OIDC configuration not available\");\n }\n\n const store = useAuthStore.getState();\n const codeVerifier = sessionStorage.getItem(OIDC_CODE_VERIFIER);\n\n if (!codeVerifier) {\n throw new Error(\n \"Code verifier not found. Please restart the login flow.\",\n );\n }\n\n try {\n store.setLoading(true);\n store.setError(null);\n\n // Exchange code for tokens at the OIDC token endpoint\n const response = await fetch(config.tokenEndpoint, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n },\n body: new URLSearchParams({\n grant_type: \"authorization_code\",\n code,\n redirect_uri: redirectUri,\n client_id: clientId,\n code_verifier: codeVerifier,\n }),\n });\n\n const tokenResponse = await response.json();\n\n if (!response.ok) {\n throw new Error(\n tokenResponse.error_description ||\n tokenResponse.error ||\n \"Token exchange failed\",\n );\n }\n\n // Clear PKCE data from session storage\n sessionStorage.removeItem(OIDC_CODE_VERIFIER);\n sessionStorage.removeItem(OIDC_STATE);\n\n // Get user info from userinfo endpoint\n const userinfoResponse = await fetch(config.userinfoEndpoint, {\n headers: {\n Authorization: `Bearer ${tokenResponse.access_token}`,\n },\n });\n\n const userinfo = await userinfoResponse.json();\n\n // Build AuthResult from OIDC response\n const now = new Date().toISOString();\n const result: AuthResult = {\n success: true,\n token: tokenResponse.access_token,\n refreshToken: tokenResponse.refresh_token,\n expiresIn: tokenResponse.expires_in,\n user: {\n id: userinfo.sub,\n email: userinfo.email,\n username: userinfo.preferred_username || userinfo.email,\n emailVerified: userinfo.email_verified ?? true,\n firstName: userinfo.given_name,\n lastName: userinfo.family_name,\n name: userinfo.name,\n role: \"MEMBER\",\n status: \"ACTIVE\",\n mfaEnabled: false,\n createdAt: now,\n updatedAt: now,\n },\n };\n\n if (result.user && result.token) {\n store.setUser(result.user);\n store.setAuthTokens(result.token, result.refreshToken || \"\");\n store.setAuthenticated(true);\n }\n\n return result;\n } catch (error) {\n const message =\n error instanceof Error ? error.message : \"OIDC authentication failed\";\n store.setError(message);\n throw error;\n } finally {\n store.setLoading(false);\n }\n };\n\n /**\n * Validate OIDC state parameter\n */\n const validateOidcState = (state: string): boolean => {\n const storedState = sessionStorage.getItem(OIDC_STATE);\n return storedState === state;\n };\n\n return {\n // SSO/SAML operations\n initiateSsoLogin,\n handleSamlCallback,\n saveSsoConfig,\n deleteSsoConfig,\n\n // OIDC operations (our OIDC provider)\n getOidcAuthUrl,\n exchangeOidcCode,\n validateOidcState,\n\n // External OIDC operations (Azure AD, Google, etc.)\n initiateExternalOidcLogin,\n handleExternalOidcCallback,\n isExternalOidcCallback,\n\n // Data\n ssoConfig: ssoConfigData?.getSSOConfig ?? undefined,\n oidcConfig: oidcConfigData?.getOIDCConfig ?? undefined,\n\n // Loading states\n isLoading:\n loadingConfig ||\n loadingOidc ||\n generatingRequest ||\n authenticating ||\n saving ||\n deleting ||\n generatingOidcUrl ||\n authenticatingOidc,\n isGeneratingRequest: generatingRequest,\n isAuthenticating: authenticating || authenticatingOidc,\n isSaving: saving,\n isDeleting: deleting,\n isGeneratingOidcUrl: generatingOidcUrl,\n\n // Refetch\n refetchConfig,\n };\n}\n\n// =============================================================================\n// PKCE Helpers\n// =============================================================================\n\n/**\n * Generate random code verifier for PKCE\n */\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Generate code challenge from verifier using SHA-256 (S256 method)\n * Uses Web Crypto API for secure hashing\n */\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n\n // Use SubtleCrypto for SHA-256 hash\n const hashBuffer = await crypto.subtle.digest(\"SHA-256\", data);\n const hashArray = new Uint8Array(hashBuffer);\n\n // Base64 URL encode the hash\n return base64UrlEncode(hashArray);\n}\n\n/**\n * Generate random state for CSRF protection\n */\nfunction generateState(): string {\n const array = new Uint8Array(16);\n crypto.getRandomValues(array);\n return base64UrlEncode(array);\n}\n\n/**\n * Base64 URL encode\n */\nfunction base64UrlEncode(array: Uint8Array): string {\n const base64 = btoa(String.fromCharCode(...array));\n return base64.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTokenRefresh.js","names":[],"sources":["../../../src/auth/hooks/useTokenRefresh.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useWorkspaceToken } from \"./useWorkspaceToken\";\n\n/**\n * Automatic workspace token refresh hook\n *\n * Monitors the workspace token and automatically refreshes it\n * 5 minutes before expiry. This ensures uninterrupted access to\n * workspace resources.\n *\n * Usage:\n * ```tsx\n * // In your root App component\n * function App() {\n * useTokenRefresh({\n * workspaceAuthServiceUrl: 'http://localhost:3730/graphql',\n * refreshBeforeExpiry: 5 * 60 * 1000, // 5 minutes\n * });\n *\n * return <AppContent />;\n * }\n * ```\n */\nexport interface UseTokenRefreshOptions {\n /**\n * URL of workspace auth service GraphQL endpoint\n * Default: http://localhost:3730/graphql\n */\n workspaceAuthServiceUrl?: string;\n\n /**\n * Time before expiry to refresh token (in milliseconds)\n * Default: 5 minutes (300000ms)\n */\n refreshBeforeExpiry?: number;\n\n /**\n * Enable/disable auto-refresh\n * Default: true\n */\n enabled?: boolean;\n\n /**\n * Callback when token refresh succeeds\n */\n onRefreshSuccess?: (token: string) => void;\n\n /**\n * Callback when token refresh fails\n */\n onRefreshError?: (error: Error) => void;\n}\n\nexport function useTokenRefresh(options: UseTokenRefreshOptions = {}) {\n const {\n workspaceAuthServiceUrl = \"http://localhost:3730/graphql\",\n refreshBeforeExpiry = 5 * 60 * 1000, // 5 minutes\n enabled = true,\n onRefreshSuccess,\n onRefreshError,\n } = options;\n\n const { workspaceToken } = useAuthStore();\n const { refreshToken, getTimeUntilExpiry, isTokenExpired } =\n useWorkspaceToken({\n workspaceGatewayUrl: workspaceAuthServiceUrl,\n });\n\n const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n // Clear any existing timer\n if (refreshTimerRef.current) {\n clearTimeout(refreshTimerRef.current);\n refreshTimerRef.current = null;\n }\n\n // Exit if disabled or no token\n if (!enabled || !workspaceToken) {\n return;\n }\n\n // Check if token is already expired\n if (isTokenExpired(workspaceToken)) {\n console.warn(\n \"[useTokenRefresh] Token is already expired, triggering immediate refresh\",\n );\n\n refreshToken()\n .then((newToken) => {\n onRefreshSuccess?.(newToken);\n })\n .catch((error) => {\n console.error(\n \"[useTokenRefresh] Failed to refresh expired token:\",\n error,\n );\n onRefreshError?.(error);\n });\n\n return;\n }\n\n // Calculate time until refresh is needed\n const timeUntilExpiry = getTimeUntilExpiry(workspaceToken);\n const timeUntilRefresh = timeUntilExpiry - refreshBeforeExpiry;\n\n if (timeUntilRefresh <= 0) {\n // Token expires in less than refresh window, refresh immediately\n refreshToken()\n .then((newToken) => {\n onRefreshSuccess?.(newToken);\n })\n .catch((error) => {\n console.error(\n \"[useTokenRefresh] Failed to refresh token (immediate):\",\n error,\n );\n onRefreshError?.(error);\n });\n\n return;\n }\n\n // Schedule refresh\n refreshTimerRef.current = setTimeout(async () => {\n try {\n const newToken = await refreshToken();\n onRefreshSuccess?.(newToken);\n } catch (error) {\n console.error(\"[useTokenRefresh] Token refresh failed:\", error);\n onRefreshError?.(error as Error);\n\n // Optionally: Implement retry logic here\n // For now, we'll let the error propagate to the callback\n }\n }, timeUntilRefresh);\n\n // Cleanup on unmount or when dependencies change\n return () => {\n if (refreshTimerRef.current) {\n clearTimeout(refreshTimerRef.current);\n refreshTimerRef.current = null;\n }\n };\n }, [\n workspaceToken,\n enabled,\n refreshBeforeExpiry,\n refreshToken,\n getTimeUntilExpiry,\n isTokenExpired,\n onRefreshSuccess,\n onRefreshError,\n ]);\n\n // Return utility for manual refresh\n return {\n /**\n * Manually trigger a token refresh\n */\n manualRefresh: refreshToken,\n\n /**\n * Check if auto-refresh is currently scheduled\n */\n isScheduled: refreshTimerRef.current !== null,\n };\n}\n"],"mappings":";;;;AAsDA,SAAgB,EAAgB,IAAkC,
|
|
1
|
+
{"version":3,"file":"useTokenRefresh.js","names":[],"sources":["../../../src/auth/hooks/useTokenRefresh.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useWorkspaceToken } from \"./useWorkspaceToken\";\n\n/**\n * Automatic workspace token refresh hook\n *\n * Monitors the workspace token and automatically refreshes it\n * 5 minutes before expiry. This ensures uninterrupted access to\n * workspace resources.\n *\n * Usage:\n * ```tsx\n * // In your root App component\n * function App() {\n * useTokenRefresh({\n * workspaceAuthServiceUrl: 'http://localhost:3730/graphql',\n * refreshBeforeExpiry: 5 * 60 * 1000, // 5 minutes\n * });\n *\n * return <AppContent />;\n * }\n * ```\n */\nexport interface UseTokenRefreshOptions {\n /**\n * URL of workspace auth service GraphQL endpoint\n * Default: http://localhost:3730/graphql\n */\n workspaceAuthServiceUrl?: string;\n\n /**\n * Time before expiry to refresh token (in milliseconds)\n * Default: 5 minutes (300000ms)\n */\n refreshBeforeExpiry?: number;\n\n /**\n * Enable/disable auto-refresh\n * Default: true\n */\n enabled?: boolean;\n\n /**\n * Callback when token refresh succeeds\n */\n onRefreshSuccess?: (token: string) => void;\n\n /**\n * Callback when token refresh fails\n */\n onRefreshError?: (error: Error) => void;\n}\n\nexport function useTokenRefresh(options: UseTokenRefreshOptions = {}) {\n const {\n workspaceAuthServiceUrl = \"http://localhost:3730/graphql\",\n refreshBeforeExpiry = 5 * 60 * 1000, // 5 minutes\n enabled = true,\n onRefreshSuccess,\n onRefreshError,\n } = options;\n\n const { workspaceToken } = useAuthStore();\n const { refreshToken, getTimeUntilExpiry, isTokenExpired } =\n useWorkspaceToken({\n workspaceGatewayUrl: workspaceAuthServiceUrl,\n });\n\n const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n // Clear any existing timer\n if (refreshTimerRef.current) {\n clearTimeout(refreshTimerRef.current);\n refreshTimerRef.current = null;\n }\n\n // Exit if disabled or no token\n if (!enabled || !workspaceToken) {\n return;\n }\n\n // Check if token is already expired\n if (isTokenExpired(workspaceToken)) {\n console.warn(\n \"[useTokenRefresh] Token is already expired, triggering immediate refresh\",\n );\n\n refreshToken()\n .then((newToken) => {\n onRefreshSuccess?.(newToken);\n })\n .catch((error) => {\n console.error(\n \"[useTokenRefresh] Failed to refresh expired token:\",\n error,\n );\n onRefreshError?.(error);\n });\n\n return;\n }\n\n // Calculate time until refresh is needed\n const timeUntilExpiry = getTimeUntilExpiry(workspaceToken);\n const timeUntilRefresh = timeUntilExpiry - refreshBeforeExpiry;\n\n if (timeUntilRefresh <= 0) {\n // Token expires in less than refresh window, refresh immediately\n refreshToken()\n .then((newToken) => {\n onRefreshSuccess?.(newToken);\n })\n .catch((error) => {\n console.error(\n \"[useTokenRefresh] Failed to refresh token (immediate):\",\n error,\n );\n onRefreshError?.(error);\n });\n\n return;\n }\n\n // Schedule refresh\n refreshTimerRef.current = setTimeout(async () => {\n try {\n const newToken = await refreshToken();\n onRefreshSuccess?.(newToken);\n } catch (error) {\n console.error(\"[useTokenRefresh] Token refresh failed:\", error);\n onRefreshError?.(error as Error);\n\n // Optionally: Implement retry logic here\n // For now, we'll let the error propagate to the callback\n }\n }, timeUntilRefresh);\n\n // Cleanup on unmount or when dependencies change\n return () => {\n if (refreshTimerRef.current) {\n clearTimeout(refreshTimerRef.current);\n refreshTimerRef.current = null;\n }\n };\n }, [\n workspaceToken,\n enabled,\n refreshBeforeExpiry,\n refreshToken,\n getTimeUntilExpiry,\n isTokenExpired,\n onRefreshSuccess,\n onRefreshError,\n ]);\n\n // Return utility for manual refresh\n return {\n /**\n * Manually trigger a token refresh\n */\n manualRefresh: refreshToken,\n\n /**\n * Check if auto-refresh is currently scheduled\n */\n isScheduled: refreshTimerRef.current !== null,\n };\n}\n"],"mappings":";;;;AAsDA,SAAgB,EAAgB,IAAkC,CAAC,GAAG;CACpE,IAAM,EACJ,6BAA0B,iCAC1B,yBAAsB,MAAS,KAC/B,aAAU,IACV,qBACA,sBACE,GAEE,EAAE,sBAAmB,EAAa,GAClC,EAAE,iBAAc,uBAAoB,sBACxC,EAAkB,EAChB,qBAAqB,EACvB,CAAC,GAEG,IAAkB,EAA6C,IAAI;CAyFzE,OAvFA,QAAgB;EAQd,IANA,AAEE,EAAgB,aADhB,aAAa,EAAgB,OAAO,GACV,OAIxB,CAAC,KAAW,CAAC,GACf;EAIF,IAAI,EAAe,CAAc,GAAG;GAKlC,AAJA,QAAQ,KACN,0EACF,GAEA,EAAa,EACV,MAAM,MAAa;IAClB,IAAmB,CAAQ;GAC7B,CAAC,EACA,OAAO,MAAU;IAKhB,AAJA,QAAQ,MACN,sDACA,CACF,GACA,IAAiB,CAAK;GACxB,CAAC;GAEH;EACF;EAIA,IAAM,IADkB,EAAmB,CAClB,IAAkB;EAE3C,IAAI,KAAoB,GAAG;GAEzB,EAAa,EACV,MAAM,MAAa;IAClB,IAAmB,CAAQ;GAC7B,CAAC,EACA,OAAO,MAAU;IAKhB,AAJA,QAAQ,MACN,0DACA,CACF,GACA,IAAiB,CAAK;GACxB,CAAC;GAEH;EACF;EAiBA,OAdA,EAAgB,UAAU,WAAW,YAAY;GAC/C,IAAI;IACF,IAAM,IAAW,MAAM,EAAa;IACpC,IAAmB,CAAQ;GAC7B,SAAS,GAAO;IAEd,AADA,QAAQ,MAAM,2CAA2C,CAAK,GAC9D,IAAiB,CAAc;GAIjC;EACF,GAAG,CAAgB,SAGN;GACX,AAEE,EAAgB,aADhB,aAAa,EAAgB,OAAO,GACV;EAE9B;CACF,GAAG;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC,GAGM;EAIL,eAAe;EAKf,aAAa,EAAgB,YAAY;CAC3C;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUserOperations.js","names":[],"sources":["../../../src/auth/hooks/useUserOperations.ts"],"sourcesContent":["import { useMutation, useLazyQuery } from \"@apollo/client/react\";\nimport { useCallback } from \"react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n GetUserDocument,\n ListUsersDocument,\n UpdateCurrentUserProfileDocument,\n UpdateUserDocument,\n DeleteUserDocument,\n HardDeleteUserDocument,\n} from \"../../generated/global-operations\";\nimport type {\n User,\n UserConnection,\n UserFilterInput,\n PaginationInput,\n DeleteUserResult,\n} from \"../types\";\n\n// =============================================================================\n// GRAPHQL RESPONSE TYPES\n// =============================================================================\n\ninterface GetUserQueryVariables {\n id: string;\n}\n\ninterface GetUserQueryResponse {\n user: User | null;\n}\n\ninterface ListUsersQueryVariables {\n filter?: UserFilterInput;\n pagination?: PaginationInput;\n}\n\ninterface ListUsersQueryResponse {\n users: UserConnection;\n}\n\ninterface UpdateCurrentUserProfileVariables {\n input: UpdateUserProfileInput;\n}\n\ninterface UpdateCurrentUserProfileResponse {\n updateCurrentUserProfile: User;\n}\n\ninterface UpdateUserVariables {\n id: string;\n input: AdminUpdateUserInput;\n}\n\ninterface UpdateUserResponse {\n updateUser: User;\n}\n\ninterface DeleteUserVariables {\n id: string;\n}\n\ninterface DeleteUserResponse {\n deleteUser: DeleteUserResult;\n}\n\ninterface HardDeleteUserVariables {\n id: string;\n}\n\ninterface HardDeleteUserResponse {\n hardDeleteUser: DeleteUserResult;\n}\n\n// GraphQL Operations\n\n// Input types for update\ninterface UpdateUserProfileInput {\n avatar?: string;\n username?: string;\n firstName?: string;\n lastName?: string;\n name?: string;\n phoneNumber?: string;\n defaultWorkspaceId?: string;\n language?: string;\n timezone?: string;\n}\n\ninterface AdminUpdateUserInput extends UpdateUserProfileInput {\n role?: string;\n status?: string;\n emailVerified?: boolean;\n}\n\n/**\n * Hook for user management operations\n */\nexport function useUserOperations() {\n const user = useAuthStore((state) => state.user);\n const setUser = useAuthStore((state) => state.setUser);\n const setLoading = useAuthStore((state) => state.setLoading);\n const isLoading = useAuthStore((state) => state.isLoading);\n\n const [getUserQuery, { loading: getUserLoading }] = useLazyQuery<\n GetUserQueryResponse,\n GetUserQueryVariables\n >(GetUserDocument);\n const [listUsersQuery, { loading: listUsersLoading }] = useLazyQuery<\n ListUsersQueryResponse,\n ListUsersQueryVariables\n >(ListUsersDocument, {\n fetchPolicy: \"network-only\",\n });\n const [updateCurrentUserProfileMutation] = useMutation<\n UpdateCurrentUserProfileResponse,\n UpdateCurrentUserProfileVariables\n >(UpdateCurrentUserProfileDocument);\n const [updateUserMutation] = useMutation<\n UpdateUserResponse,\n UpdateUserVariables\n >(UpdateUserDocument);\n const [deleteUserMutation] = useMutation<\n DeleteUserResponse,\n DeleteUserVariables\n >(DeleteUserDocument);\n const [hardDeleteUserMutation] = useMutation<\n HardDeleteUserResponse,\n HardDeleteUserVariables\n >(HardDeleteUserDocument);\n\n /**\n * Get user by ID (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const getUser = useCallback(\n async (id: string): Promise<User | null> => {\n try {\n const { data } = await getUserQuery({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n return data?.user || null;\n } catch (error) {\n console.error(\"Failed to get user:\", error);\n throw error;\n }\n },\n [getUserQuery],\n );\n\n /**\n * List users with filtering and pagination (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const listUsers = useCallback(\n async (\n filter?: UserFilterInput,\n pagination?: PaginationInput,\n ): Promise<UserConnection> => {\n try {\n const { data, error } = await listUsersQuery({\n variables: { filter, pagination },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n if (error) throw error;\n return data?.users || { users: [], totalCount: 0, hasMore: false };\n } catch (error) {\n // Ignore abort errors - they happen during component unmount/re-render\n if (error instanceof Error && error.name === \"AbortError\") {\n return { users: [], totalCount: 0, hasMore: false };\n }\n console.error(\"Failed to list users:\", error);\n throw error;\n }\n },\n [listUsersQuery],\n );\n\n /**\n * Update current user's profile\n */\n const updateCurrentUserProfile = useCallback(\n async (input: UpdateUserProfileInput): Promise<User> => {\n try {\n setLoading(true);\n\n const { data } = await updateCurrentUserProfileMutation({\n variables: { input },\n });\n\n const updatedUser = data!.updateCurrentUserProfile;\n\n // Update user in store\n if (user) {\n setUser({ ...user, ...updatedUser });\n }\n\n return updatedUser;\n } catch (error) {\n console.error(\"Failed to update profile:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [updateCurrentUserProfileMutation, setLoading, setUser, user],\n );\n\n /**\n * Update user by ID (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const updateUser = useCallback(\n async (id: string, input: AdminUpdateUserInput): Promise<User> => {\n try {\n setLoading(true);\n\n const { data } = await updateUserMutation({\n variables: { id, input },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.updateUser;\n } catch (error) {\n console.error(\"Failed to update user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [updateUserMutation, setLoading],\n );\n\n /**\n * Delete user (soft delete, admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const deleteUser = useCallback(\n async (id: string): Promise<DeleteUserResult> => {\n try {\n setLoading(true);\n\n const { data } = await deleteUserMutation({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.deleteUser;\n } catch (error) {\n console.error(\"Failed to delete user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [deleteUserMutation, setLoading],\n );\n\n /**\n * Hard delete user (permanent, super admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const hardDeleteUser = useCallback(\n async (id: string): Promise<DeleteUserResult> => {\n try {\n setLoading(true);\n\n const { data } = await hardDeleteUserMutation({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.hardDeleteUser;\n } catch (error) {\n console.error(\"Failed to hard delete user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [hardDeleteUserMutation, setLoading],\n );\n\n return {\n // Queries\n getUser,\n listUsers,\n // Mutations\n updateCurrentUserProfile,\n updateUser,\n deleteUser,\n hardDeleteUser,\n // State\n currentUser: user,\n isLoading: getUserLoading || listUsersLoading || isLoading,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useUserOperations.js","names":[],"sources":["../../../src/auth/hooks/useUserOperations.ts"],"sourcesContent":["import { useMutation, useLazyQuery } from \"@apollo/client/react\";\nimport { useCallback } from \"react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n GetUserDocument,\n ListUsersDocument,\n UpdateCurrentUserProfileDocument,\n UpdateUserDocument,\n DeleteUserDocument,\n HardDeleteUserDocument,\n} from \"../../generated/global-operations\";\nimport type {\n User,\n UserConnection,\n UserFilterInput,\n PaginationInput,\n DeleteUserResult,\n} from \"../types\";\n\n// =============================================================================\n// GRAPHQL RESPONSE TYPES\n// =============================================================================\n\ninterface GetUserQueryVariables {\n id: string;\n}\n\ninterface GetUserQueryResponse {\n user: User | null;\n}\n\ninterface ListUsersQueryVariables {\n filter?: UserFilterInput;\n pagination?: PaginationInput;\n}\n\ninterface ListUsersQueryResponse {\n users: UserConnection;\n}\n\ninterface UpdateCurrentUserProfileVariables {\n input: UpdateUserProfileInput;\n}\n\ninterface UpdateCurrentUserProfileResponse {\n updateCurrentUserProfile: User;\n}\n\ninterface UpdateUserVariables {\n id: string;\n input: AdminUpdateUserInput;\n}\n\ninterface UpdateUserResponse {\n updateUser: User;\n}\n\ninterface DeleteUserVariables {\n id: string;\n}\n\ninterface DeleteUserResponse {\n deleteUser: DeleteUserResult;\n}\n\ninterface HardDeleteUserVariables {\n id: string;\n}\n\ninterface HardDeleteUserResponse {\n hardDeleteUser: DeleteUserResult;\n}\n\n// GraphQL Operations\n\n// Input types for update\ninterface UpdateUserProfileInput {\n avatar?: string;\n username?: string;\n firstName?: string;\n lastName?: string;\n name?: string;\n phoneNumber?: string;\n defaultWorkspaceId?: string;\n language?: string;\n timezone?: string;\n}\n\ninterface AdminUpdateUserInput extends UpdateUserProfileInput {\n role?: string;\n status?: string;\n emailVerified?: boolean;\n}\n\n/**\n * Hook for user management operations\n */\nexport function useUserOperations() {\n const user = useAuthStore((state) => state.user);\n const setUser = useAuthStore((state) => state.setUser);\n const setLoading = useAuthStore((state) => state.setLoading);\n const isLoading = useAuthStore((state) => state.isLoading);\n\n const [getUserQuery, { loading: getUserLoading }] = useLazyQuery<\n GetUserQueryResponse,\n GetUserQueryVariables\n >(GetUserDocument);\n const [listUsersQuery, { loading: listUsersLoading }] = useLazyQuery<\n ListUsersQueryResponse,\n ListUsersQueryVariables\n >(ListUsersDocument, {\n fetchPolicy: \"network-only\",\n });\n const [updateCurrentUserProfileMutation] = useMutation<\n UpdateCurrentUserProfileResponse,\n UpdateCurrentUserProfileVariables\n >(UpdateCurrentUserProfileDocument);\n const [updateUserMutation] = useMutation<\n UpdateUserResponse,\n UpdateUserVariables\n >(UpdateUserDocument);\n const [deleteUserMutation] = useMutation<\n DeleteUserResponse,\n DeleteUserVariables\n >(DeleteUserDocument);\n const [hardDeleteUserMutation] = useMutation<\n HardDeleteUserResponse,\n HardDeleteUserVariables\n >(HardDeleteUserDocument);\n\n /**\n * Get user by ID (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const getUser = useCallback(\n async (id: string): Promise<User | null> => {\n try {\n const { data } = await getUserQuery({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n return data?.user || null;\n } catch (error) {\n console.error(\"Failed to get user:\", error);\n throw error;\n }\n },\n [getUserQuery],\n );\n\n /**\n * List users with filtering and pagination (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const listUsers = useCallback(\n async (\n filter?: UserFilterInput,\n pagination?: PaginationInput,\n ): Promise<UserConnection> => {\n try {\n const { data, error } = await listUsersQuery({\n variables: { filter, pagination },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n if (error) throw error;\n return data?.users || { users: [], totalCount: 0, hasMore: false };\n } catch (error) {\n // Ignore abort errors - they happen during component unmount/re-render\n if (error instanceof Error && error.name === \"AbortError\") {\n return { users: [], totalCount: 0, hasMore: false };\n }\n console.error(\"Failed to list users:\", error);\n throw error;\n }\n },\n [listUsersQuery],\n );\n\n /**\n * Update current user's profile\n */\n const updateCurrentUserProfile = useCallback(\n async (input: UpdateUserProfileInput): Promise<User> => {\n try {\n setLoading(true);\n\n const { data } = await updateCurrentUserProfileMutation({\n variables: { input },\n });\n\n const updatedUser = data!.updateCurrentUserProfile;\n\n // Update user in store\n if (user) {\n setUser({ ...user, ...updatedUser });\n }\n\n return updatedUser;\n } catch (error) {\n console.error(\"Failed to update profile:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [updateCurrentUserProfileMutation, setLoading, setUser, user],\n );\n\n /**\n * Update user by ID (admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const updateUser = useCallback(\n async (id: string, input: AdminUpdateUserInput): Promise<User> => {\n try {\n setLoading(true);\n\n const { data } = await updateUserMutation({\n variables: { id, input },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.updateUser;\n } catch (error) {\n console.error(\"Failed to update user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [updateUserMutation, setLoading],\n );\n\n /**\n * Delete user (soft delete, admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const deleteUser = useCallback(\n async (id: string): Promise<DeleteUserResult> => {\n try {\n setLoading(true);\n\n const { data } = await deleteUserMutation({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.deleteUser;\n } catch (error) {\n console.error(\"Failed to delete user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [deleteUserMutation, setLoading],\n );\n\n /**\n * Hard delete user (permanent, super admin only)\n * Uses global-layer context (no x-workspace-id) since user management is\n * a global/tenant operation, not workspace-scoped.\n */\n const hardDeleteUser = useCallback(\n async (id: string): Promise<DeleteUserResult> => {\n try {\n setLoading(true);\n\n const { data } = await hardDeleteUserMutation({\n variables: { id },\n context: { headers: { \"x-workspace-id\": \"\" } },\n });\n\n return data!.hardDeleteUser;\n } catch (error) {\n console.error(\"Failed to hard delete user:\", error);\n throw error;\n } finally {\n setLoading(false);\n }\n },\n [hardDeleteUserMutation, setLoading],\n );\n\n return {\n // Queries\n getUser,\n listUsers,\n // Mutations\n updateCurrentUserProfile,\n updateUser,\n deleteUser,\n hardDeleteUser,\n // State\n currentUser: user,\n isLoading: getUserLoading || listUsersLoading || isLoading,\n };\n}\n"],"mappings":"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"}
|