@burdenoff/microfe-auth 2026.925.1 → 2026.1009.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/README.md +14 -0
- 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 +346 -259
- 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":"useWorkspaceOperations.js","names":[],"sources":["../../../src/auth/hooks/useWorkspaceOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n CreateWorkspaceDocument,\n UpdateWorkspaceDocument,\n AddWorkspaceMemberDocument,\n UpdateWorkspaceMemberDocument,\n RemoveWorkspaceMemberDocument,\n} from \"../../generated/global-operations\";\nimport type {\n WorkspaceTokenResponse,\n SecretRotationResponse,\n RotateWorkspaceSecretInput,\n} from \"../types\";\n\n// =============================================================================\n// NOTE: Workspace token/secret operations (issueWorkspaceToken,\n// createWorkspaceSecret, rotateWorkspaceSecret, revokeWorkspaceSecret,\n// revokeWorkspaceToken, revokeAllMyWorkspaceTokens) are in the wspace-auth-svc\n// schema (workspace gateway), NOT the global gateway. These operations are\n// handled by useMyWorkspaces.ts (via graphqlFetch to workspace gateway).\n//\n// The workspace CRUD operations (createWorkspace, updateWorkspace, etc.) are\n// in the global gateway schema and use generated Document constants.\n// =============================================================================\n\n/**\n * Hook for workspace token and secret operations\n */\nexport function useWorkspaceOperations() {\n const store = useAuthStore();\n\n // Workspace CRUD mutations (available in global gateway schema)\n const [createWorkspaceMutation] = useMutation(CreateWorkspaceDocument);\n const [updateWorkspaceMutation] = useMutation(UpdateWorkspaceDocument);\n const [addWorkspaceMemberMutation] = useMutation(AddWorkspaceMemberDocument);\n const [updateWorkspaceMemberMutation] = useMutation(\n UpdateWorkspaceMemberDocument,\n );\n const [removeWorkspaceMemberMutation] = useMutation(\n RemoveWorkspaceMemberDocument,\n );\n\n /**\n * Generate workspace token for a workspace.\n * This hook intentionally delegates workspace-token issuance to\n * `useMyWorkspaces().issueWorkspaceToken()` because that flow requires the\n * workspaces gateway rather than the global Apollo client.\n */\n const generateWorkspaceToken = async (\n _workspaceId: string,\n ): Promise<WorkspaceTokenResponse> => {\n console.warn(\n \"generateWorkspaceToken: Use useMyWorkspaces().issueWorkspaceToken() instead. \" +\n \"This operation requires the workspace gateway.\",\n );\n throw new Error(\n \"Use useMyWorkspaces().issueWorkspaceToken() for workspace token issuance\",\n );\n };\n\n /**\n * Switch to a different workspace\n */\n const switchWorkspace = async (\n workspaceId: string,\n ): Promise<WorkspaceTokenResponse> => {\n // Clear current workspace token first\n store.clearWorkspace();\n\n // Generate new workspace token\n return generateWorkspaceToken(workspaceId);\n };\n\n /**\n * Create workspace secret (admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const createWorkspaceSecret = async (\n _workspaceId: string,\n ): Promise<boolean> => {\n console.warn(\n \"createWorkspaceSecret is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Rotate workspace secret (admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const rotateWorkspaceSecret = async (\n _input: RotateWorkspaceSecretInput,\n ): Promise<SecretRotationResponse> => {\n console.warn(\n \"rotateWorkspaceSecret is not available from the global auth client\",\n );\n throw new Error(\n \"rotateWorkspaceSecret is not available from the global auth client\",\n );\n };\n\n /**\n * Revoke workspace secret (emergency use, admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeWorkspaceSecret = async (\n _workspaceId: string,\n ): Promise<boolean> => {\n console.warn(\n \"revokeWorkspaceSecret is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Revoke a specific workspace token.\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeWorkspaceToken = async (\n _token: string,\n _reason?: string,\n ): Promise<boolean> => {\n console.warn(\n \"revokeWorkspaceToken is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Revoke all workspace tokens for current user.\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeAllMyWorkspaceTokens = async (\n _reason?: string,\n ): Promise<number> => {\n console.warn(\n \"revokeAllMyWorkspaceTokens is not available from the global auth client\",\n );\n return 0;\n };\n\n /**\n * Clear current workspace (logout from workspace)\n */\n const clearWorkspace = () => {\n store.clearWorkspace();\n };\n\n return {\n // Mutations\n generateWorkspaceToken,\n switchWorkspace,\n createWorkspaceSecret,\n rotateWorkspaceSecret,\n revokeWorkspaceSecret,\n revokeWorkspaceToken,\n revokeAllMyWorkspaceTokens,\n clearWorkspace,\n // Workspace CRUD mutations\n createWorkspaceMutation,\n updateWorkspaceMutation,\n addWorkspaceMemberMutation,\n updateWorkspaceMemberMutation,\n removeWorkspaceMemberMutation,\n // State\n currentWorkspaceId: store.currentWorkspaceId,\n workspaceToken: store.workspaceToken,\n isLoading: store.isLoading,\n };\n}\n"],"mappings":";;;;AA6BA,SAAgB,IAAyB;CACvC,IAAM,IAAQ,
|
|
1
|
+
{"version":3,"file":"useWorkspaceOperations.js","names":[],"sources":["../../../src/auth/hooks/useWorkspaceOperations.ts"],"sourcesContent":["import { useMutation } from \"@apollo/client/react\";\nimport { useAuthStore } from \"../store/authStore\";\nimport {\n CreateWorkspaceDocument,\n UpdateWorkspaceDocument,\n AddWorkspaceMemberDocument,\n UpdateWorkspaceMemberDocument,\n RemoveWorkspaceMemberDocument,\n} from \"../../generated/global-operations\";\nimport type {\n WorkspaceTokenResponse,\n SecretRotationResponse,\n RotateWorkspaceSecretInput,\n} from \"../types\";\n\n// =============================================================================\n// NOTE: Workspace token/secret operations (issueWorkspaceToken,\n// createWorkspaceSecret, rotateWorkspaceSecret, revokeWorkspaceSecret,\n// revokeWorkspaceToken, revokeAllMyWorkspaceTokens) are in the wspace-auth-svc\n// schema (workspace gateway), NOT the global gateway. These operations are\n// handled by useMyWorkspaces.ts (via graphqlFetch to workspace gateway).\n//\n// The workspace CRUD operations (createWorkspace, updateWorkspace, etc.) are\n// in the global gateway schema and use generated Document constants.\n// =============================================================================\n\n/**\n * Hook for workspace token and secret operations\n */\nexport function useWorkspaceOperations() {\n const store = useAuthStore();\n\n // Workspace CRUD mutations (available in global gateway schema)\n const [createWorkspaceMutation] = useMutation(CreateWorkspaceDocument);\n const [updateWorkspaceMutation] = useMutation(UpdateWorkspaceDocument);\n const [addWorkspaceMemberMutation] = useMutation(AddWorkspaceMemberDocument);\n const [updateWorkspaceMemberMutation] = useMutation(\n UpdateWorkspaceMemberDocument,\n );\n const [removeWorkspaceMemberMutation] = useMutation(\n RemoveWorkspaceMemberDocument,\n );\n\n /**\n * Generate workspace token for a workspace.\n * This hook intentionally delegates workspace-token issuance to\n * `useMyWorkspaces().issueWorkspaceToken()` because that flow requires the\n * workspaces gateway rather than the global Apollo client.\n */\n const generateWorkspaceToken = async (\n _workspaceId: string,\n ): Promise<WorkspaceTokenResponse> => {\n console.warn(\n \"generateWorkspaceToken: Use useMyWorkspaces().issueWorkspaceToken() instead. \" +\n \"This operation requires the workspace gateway.\",\n );\n throw new Error(\n \"Use useMyWorkspaces().issueWorkspaceToken() for workspace token issuance\",\n );\n };\n\n /**\n * Switch to a different workspace\n */\n const switchWorkspace = async (\n workspaceId: string,\n ): Promise<WorkspaceTokenResponse> => {\n // Clear current workspace token first\n store.clearWorkspace();\n\n // Generate new workspace token\n return generateWorkspaceToken(workspaceId);\n };\n\n /**\n * Create workspace secret (admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const createWorkspaceSecret = async (\n _workspaceId: string,\n ): Promise<boolean> => {\n console.warn(\n \"createWorkspaceSecret is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Rotate workspace secret (admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const rotateWorkspaceSecret = async (\n _input: RotateWorkspaceSecretInput,\n ): Promise<SecretRotationResponse> => {\n console.warn(\n \"rotateWorkspaceSecret is not available from the global auth client\",\n );\n throw new Error(\n \"rotateWorkspaceSecret is not available from the global auth client\",\n );\n };\n\n /**\n * Revoke workspace secret (emergency use, admin only).\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeWorkspaceSecret = async (\n _workspaceId: string,\n ): Promise<boolean> => {\n console.warn(\n \"revokeWorkspaceSecret is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Revoke a specific workspace token.\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeWorkspaceToken = async (\n _token: string,\n _reason?: string,\n ): Promise<boolean> => {\n console.warn(\n \"revokeWorkspaceToken is not available from the global auth client\",\n );\n return false;\n };\n\n /**\n * Revoke all workspace tokens for current user.\n * Unsupported from this hook because the operation lives on the workspaces gateway.\n */\n const revokeAllMyWorkspaceTokens = async (\n _reason?: string,\n ): Promise<number> => {\n console.warn(\n \"revokeAllMyWorkspaceTokens is not available from the global auth client\",\n );\n return 0;\n };\n\n /**\n * Clear current workspace (logout from workspace)\n */\n const clearWorkspace = () => {\n store.clearWorkspace();\n };\n\n return {\n // Mutations\n generateWorkspaceToken,\n switchWorkspace,\n createWorkspaceSecret,\n rotateWorkspaceSecret,\n revokeWorkspaceSecret,\n revokeWorkspaceToken,\n revokeAllMyWorkspaceTokens,\n clearWorkspace,\n // Workspace CRUD mutations\n createWorkspaceMutation,\n updateWorkspaceMutation,\n addWorkspaceMemberMutation,\n updateWorkspaceMemberMutation,\n removeWorkspaceMemberMutation,\n // State\n currentWorkspaceId: store.currentWorkspaceId,\n workspaceToken: store.workspaceToken,\n isLoading: store.isLoading,\n };\n}\n"],"mappings":";;;;AA6BA,SAAgB,IAAyB;CACvC,IAAM,IAAQ,EAAa,GAGrB,CAAC,KAA2B,EAAY,CAAuB,GAC/D,CAAC,KAA2B,EAAY,CAAuB,GAC/D,CAAC,KAA8B,EAAY,CAA0B,GACrE,CAAC,KAAiC,EACtC,CACF,GACM,CAAC,KAAiC,EACtC,CACF,GAQM,IAAyB,OAC7B,MACoC;EAKpC,MAJA,QAAQ,KACN,6HAEF,GACU,MACR,0EACF;CACF;CA0FA,OAAO;EAEL;EACA,wBAvFA,OAGA,EAAM,eAAe,GAGd,EAAuB,CAAW;EAkFzC,8BA1EA,OAEA,QAAQ,KACN,oEACF,GACO;EAsEP,8BA9DA,MACoC;GAIpC,MAHA,QAAQ,KACN,oEACF,GACU,MACR,oEACF;EACF;EAuDE,8BAhDA,OAEA,QAAQ,KACN,oEACF,GACO;EA4CP,6BApCA,GACA,OAEA,QAAQ,KACN,mEACF,GACO;EA+BP,mCAvBA,OAEA,QAAQ,KACN,yEACF,GACO;EAmBP,sBAb2B;GAC3B,EAAM,eAAe;EACvB;EAaE;EACA;EACA;EACA;EACA;EAEA,oBAAoB,EAAM;EAC1B,gBAAgB,EAAM;EACtB,WAAW,EAAM;CACnB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useWorkspaceToken.js","names":[],"sources":["../../../src/auth/hooks/useWorkspaceToken.ts"],"sourcesContent":["import { useState, useCallback } from \"react\";\nimport { graphqlFetch } from \"@burdenoff/fe-libs/shared/graphql/fetch\";\nimport { useAuthStore } from \"../store/authStore\";\n\n/**\n * Workspace token management hook\n *\n * Provides functions to issue, refresh, and verify workspace tokens.\n * Automatically updates the auth store with new tokens.\n *\n * API Flow:\n * - Calls workspace gateway (https://gw-wspace.local.burdenoff.com/workspaces/graphql)\n * - Gateway federates wspace-auth-svc which handles token operations\n * - Auth token (from global-auth-svc) goes in Authorization header\n *\n * Usage:\n * ```tsx\n * const { issueToken, refreshToken, verifyToken, isLoading, error } = useWorkspaceToken({\n * workspaceGatewayUrl: 'https://gw-wspace.local.burdenoff.com/workspaces/graphql'\n * });\n *\n * // After login and organization/workspace selection\n * await issueToken(organizationId, workspaceId);\n *\n * // Refresh token before expiry (uses stored org/workspace IDs)\n * await refreshToken();\n * ```\n */\nexport interface UseWorkspaceTokenOptions {\n /**\n * URL of workspace gateway GraphQL endpoint\n * Default: https://gw-wspace.local.burdenoff.com/workspaces/graphql\n *\n * Note: This should point to the workspace gateway, NOT directly to wspace-auth-svc.\n * The gateway federates wspace-auth-svc and handles routing.\n */\n workspaceGatewayUrl?: string;\n}\n\nexport interface WorkspaceTokenPayload {\n sub: string; // User ID\n workspaceId: string;\n tenantId?: string;\n email?: string;\n username?: string;\n role?: string;\n scopes: string[];\n iat: number;\n exp: number;\n}\n\nexport function useWorkspaceToken(_options: UseWorkspaceTokenOptions = {}) {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n /**\n * Issue a new workspace token for the given workspace\n *\n * @param organizationId - Organization ID (required)\n * @param workspaceId - Workspace ID (optional, auto-resolves to first accessible)\n * @param projectId - Project ID (optional, auto-resolves to first accessible)\n * @param scopes - Requested scopes (optional, defaults to full access)\n */\n const issueToken = useCallback(\n async (\n organizationId: string,\n workspaceId?: string,\n projectId?: string,\n scopes?: string[],\n ): Promise<string> => {\n setIsLoading(true);\n setError(null);\n\n try {\n const accessToken = useAuthStore.getState().accessToken;\n\n if (!accessToken) {\n throw new Error(\"No access token available. Please login first.\");\n }\n\n const response = await graphqlFetch<{\n issueWorkspaceToken: {\n success: boolean;\n token: string;\n expiresAt: string;\n organizationId: string;\n workspaceId: string;\n projectId?: string;\n scopes: string[];\n error?: { code?: string; message?: string };\n };\n }>({\n gateway: \"workspace\",\n query: `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n expiresAt\n organizationId\n workspaceId\n projectId\n scopes\n error {\n code\n message\n }\n }\n }\n `,\n variables: {\n input: {\n organizationId,\n ...(workspaceId && { workspaceId }),\n ...(projectId && { projectId }),\n ...(scopes && { scopes }),\n },\n },\n authToken: accessToken,\n });\n\n const result = response.data?.issueWorkspaceToken;\n\n if (!result?.success || !result?.token) {\n const errorMsg =\n typeof result?.error?.message === \"string\"\n ? result.error.message\n : typeof result?.error === \"string\"\n ? result.error\n : \"Failed to issue workspace token\";\n throw new Error(errorMsg);\n }\n\n useAuthStore.getState().setWorkspaceToken(result.token);\n useAuthStore.getState().setCurrentWorkspaceId(result.workspaceId);\n\n setIsLoading(false);\n return result.token;\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : \"Unknown error occurred\";\n console.error(\"[useWorkspaceToken] Error issuing token:\", errorMessage);\n setError(errorMessage);\n setIsLoading(false);\n throw err;\n }\n },\n [],\n );\n\n /**\n * Verify a workspace token\n *\n * @param token - Optional token to verify (uses stored token if not provided)\n */\n const verifyToken = useCallback(\n async (token?: string): Promise<WorkspaceTokenPayload> => {\n setIsLoading(true);\n setError(null);\n\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToVerify = token || workspaceToken;\n\n if (!tokenToVerify) {\n throw new Error(\"No workspace token available to verify\");\n }\n\n const response = await graphqlFetch<{\n verifyWorkspaceToken: {\n valid: boolean;\n payload?: WorkspaceTokenPayload;\n error?: { code?: string; message?: string };\n };\n }>({\n gateway: \"workspace\",\n query: `\n query VerifyWorkspaceToken($token: String!) {\n verifyWorkspaceToken(token: $token) {\n valid\n payload {\n sub\n workspaceId\n tenantId\n email\n username\n role\n scopes\n iat\n exp\n }\n error {\n code\n message\n }\n }\n }\n `,\n variables: { token: tokenToVerify },\n });\n\n const result = response.data?.verifyWorkspaceToken;\n\n if (!result?.valid) {\n const errorMsg =\n typeof result?.error?.message === \"string\"\n ? result.error.message\n : typeof result?.error === \"string\"\n ? result.error\n : \"Token verification failed\";\n throw new Error(errorMsg);\n }\n\n setIsLoading(false);\n return result.payload!;\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : \"Unknown error occurred\";\n console.error(\n \"[useWorkspaceToken] Error verifying token:\",\n errorMessage,\n );\n setError(errorMessage);\n setIsLoading(false);\n throw err;\n }\n },\n [],\n );\n\n /**\n * Refresh the workspace token\n */\n const refreshToken = useCallback(async (): Promise<string> => {\n const { currentOrganizationId, currentWorkspaceId } =\n useAuthStore.getState();\n if (!currentOrganizationId) {\n throw new Error(\"No organization ID in store. Please login first.\");\n }\n return issueToken(currentOrganizationId, currentWorkspaceId ?? undefined);\n }, [issueToken]);\n\n /**\n * Decode a JWT token to get its payload (client-side only, no verification)\n */\n const decodeToken = useCallback((token: string): WorkspaceTokenPayload => {\n try {\n const base64Url = token.split(\".\")[1];\n const base64 = base64Url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split(\"\")\n .map((c) => \"%\" + (\"00\" + c.charCodeAt(0).toString(16)).slice(-2))\n .join(\"\"),\n );\n return JSON.parse(jsonPayload);\n } catch {\n throw new Error(\"Failed to decode token\");\n }\n }, []);\n\n /**\n * Check if a token is expired (client-side check)\n */\n const isTokenExpired = useCallback(\n (token?: string): boolean => {\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToCheck = token || workspaceToken;\n\n if (!tokenToCheck) return true;\n\n const payload = decodeToken(tokenToCheck);\n const now = Math.floor(Date.now() / 1000);\n\n return payload.exp < now;\n } catch {\n return true;\n }\n },\n [decodeToken],\n );\n\n /**\n * Get time until token expiry in milliseconds\n */\n const getTimeUntilExpiry = useCallback(\n (token?: string): number => {\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToCheck = token || workspaceToken;\n\n if (!tokenToCheck) return 0;\n\n const payload = decodeToken(tokenToCheck);\n const now = Math.floor(Date.now() / 1000);\n const timeUntilExpiry = (payload.exp - now) * 1000; // Convert to ms\n\n return Math.max(0, timeUntilExpiry);\n } catch {\n return 0;\n }\n },\n [decodeToken],\n );\n\n return {\n issueToken,\n refreshToken,\n verifyToken,\n decodeToken,\n isTokenExpired,\n getTimeUntilExpiry,\n isLoading,\n error,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useWorkspaceToken.js","names":[],"sources":["../../../src/auth/hooks/useWorkspaceToken.ts"],"sourcesContent":["import { useState, useCallback } from \"react\";\nimport { graphqlFetch } from \"@burdenoff/fe-libs/shared/graphql/fetch\";\nimport { useAuthStore } from \"../store/authStore\";\n\n/**\n * Workspace token management hook\n *\n * Provides functions to issue, refresh, and verify workspace tokens.\n * Automatically updates the auth store with new tokens.\n *\n * API Flow:\n * - Calls workspace gateway (https://gw-wspace.local.burdenoff.com/workspaces/graphql)\n * - Gateway federates wspace-auth-svc which handles token operations\n * - Auth token (from global-auth-svc) goes in Authorization header\n *\n * Usage:\n * ```tsx\n * const { issueToken, refreshToken, verifyToken, isLoading, error } = useWorkspaceToken({\n * workspaceGatewayUrl: 'https://gw-wspace.local.burdenoff.com/workspaces/graphql'\n * });\n *\n * // After login and organization/workspace selection\n * await issueToken(organizationId, workspaceId);\n *\n * // Refresh token before expiry (uses stored org/workspace IDs)\n * await refreshToken();\n * ```\n */\nexport interface UseWorkspaceTokenOptions {\n /**\n * URL of workspace gateway GraphQL endpoint\n * Default: https://gw-wspace.local.burdenoff.com/workspaces/graphql\n *\n * Note: This should point to the workspace gateway, NOT directly to wspace-auth-svc.\n * The gateway federates wspace-auth-svc and handles routing.\n */\n workspaceGatewayUrl?: string;\n}\n\nexport interface WorkspaceTokenPayload {\n sub: string; // User ID\n workspaceId: string;\n tenantId?: string;\n email?: string;\n username?: string;\n role?: string;\n scopes: string[];\n iat: number;\n exp: number;\n}\n\nexport function useWorkspaceToken(_options: UseWorkspaceTokenOptions = {}) {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n /**\n * Issue a new workspace token for the given workspace\n *\n * @param organizationId - Organization ID (required)\n * @param workspaceId - Workspace ID (optional, auto-resolves to first accessible)\n * @param projectId - Project ID (optional, auto-resolves to first accessible)\n * @param scopes - Requested scopes (optional, defaults to full access)\n */\n const issueToken = useCallback(\n async (\n organizationId: string,\n workspaceId?: string,\n projectId?: string,\n scopes?: string[],\n ): Promise<string> => {\n setIsLoading(true);\n setError(null);\n\n try {\n const accessToken = useAuthStore.getState().accessToken;\n\n if (!accessToken) {\n throw new Error(\"No access token available. Please login first.\");\n }\n\n const response = await graphqlFetch<{\n issueWorkspaceToken: {\n success: boolean;\n token: string;\n expiresAt: string;\n organizationId: string;\n workspaceId: string;\n projectId?: string;\n scopes: string[];\n error?: { code?: string; message?: string };\n };\n }>({\n gateway: \"workspace\",\n query: `\n mutation IssueWorkspaceToken($input: IssueWorkspaceTokenInput!) {\n issueWorkspaceToken(input: $input) {\n success\n token\n expiresAt\n organizationId\n workspaceId\n projectId\n scopes\n error {\n code\n message\n }\n }\n }\n `,\n variables: {\n input: {\n organizationId,\n ...(workspaceId && { workspaceId }),\n ...(projectId && { projectId }),\n ...(scopes && { scopes }),\n },\n },\n authToken: accessToken,\n });\n\n const result = response.data?.issueWorkspaceToken;\n\n if (!result?.success || !result?.token) {\n const errorMsg =\n typeof result?.error?.message === \"string\"\n ? result.error.message\n : typeof result?.error === \"string\"\n ? result.error\n : \"Failed to issue workspace token\";\n throw new Error(errorMsg);\n }\n\n useAuthStore.getState().setWorkspaceToken(result.token);\n useAuthStore.getState().setCurrentWorkspaceId(result.workspaceId);\n\n setIsLoading(false);\n return result.token;\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : \"Unknown error occurred\";\n console.error(\"[useWorkspaceToken] Error issuing token:\", errorMessage);\n setError(errorMessage);\n setIsLoading(false);\n throw err;\n }\n },\n [],\n );\n\n /**\n * Verify a workspace token\n *\n * @param token - Optional token to verify (uses stored token if not provided)\n */\n const verifyToken = useCallback(\n async (token?: string): Promise<WorkspaceTokenPayload> => {\n setIsLoading(true);\n setError(null);\n\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToVerify = token || workspaceToken;\n\n if (!tokenToVerify) {\n throw new Error(\"No workspace token available to verify\");\n }\n\n const response = await graphqlFetch<{\n verifyWorkspaceToken: {\n valid: boolean;\n payload?: WorkspaceTokenPayload;\n error?: { code?: string; message?: string };\n };\n }>({\n gateway: \"workspace\",\n query: `\n query VerifyWorkspaceToken($token: String!) {\n verifyWorkspaceToken(token: $token) {\n valid\n payload {\n sub\n workspaceId\n tenantId\n email\n username\n role\n scopes\n iat\n exp\n }\n error {\n code\n message\n }\n }\n }\n `,\n variables: { token: tokenToVerify },\n });\n\n const result = response.data?.verifyWorkspaceToken;\n\n if (!result?.valid) {\n const errorMsg =\n typeof result?.error?.message === \"string\"\n ? result.error.message\n : typeof result?.error === \"string\"\n ? result.error\n : \"Token verification failed\";\n throw new Error(errorMsg);\n }\n\n setIsLoading(false);\n return result.payload!;\n } catch (err) {\n const errorMessage =\n err instanceof Error ? err.message : \"Unknown error occurred\";\n console.error(\n \"[useWorkspaceToken] Error verifying token:\",\n errorMessage,\n );\n setError(errorMessage);\n setIsLoading(false);\n throw err;\n }\n },\n [],\n );\n\n /**\n * Refresh the workspace token\n */\n const refreshToken = useCallback(async (): Promise<string> => {\n const { currentOrganizationId, currentWorkspaceId } =\n useAuthStore.getState();\n if (!currentOrganizationId) {\n throw new Error(\"No organization ID in store. Please login first.\");\n }\n return issueToken(currentOrganizationId, currentWorkspaceId ?? undefined);\n }, [issueToken]);\n\n /**\n * Decode a JWT token to get its payload (client-side only, no verification)\n */\n const decodeToken = useCallback((token: string): WorkspaceTokenPayload => {\n try {\n const base64Url = token.split(\".\")[1];\n const base64 = base64Url.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const jsonPayload = decodeURIComponent(\n atob(base64)\n .split(\"\")\n .map((c) => \"%\" + (\"00\" + c.charCodeAt(0).toString(16)).slice(-2))\n .join(\"\"),\n );\n return JSON.parse(jsonPayload);\n } catch {\n throw new Error(\"Failed to decode token\");\n }\n }, []);\n\n /**\n * Check if a token is expired (client-side check)\n */\n const isTokenExpired = useCallback(\n (token?: string): boolean => {\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToCheck = token || workspaceToken;\n\n if (!tokenToCheck) return true;\n\n const payload = decodeToken(tokenToCheck);\n const now = Math.floor(Date.now() / 1000);\n\n return payload.exp < now;\n } catch {\n return true;\n }\n },\n [decodeToken],\n );\n\n /**\n * Get time until token expiry in milliseconds\n */\n const getTimeUntilExpiry = useCallback(\n (token?: string): number => {\n try {\n const { workspaceToken } = useAuthStore.getState();\n const tokenToCheck = token || workspaceToken;\n\n if (!tokenToCheck) return 0;\n\n const payload = decodeToken(tokenToCheck);\n const now = Math.floor(Date.now() / 1000);\n const timeUntilExpiry = (payload.exp - now) * 1000; // Convert to ms\n\n return Math.max(0, timeUntilExpiry);\n } catch {\n return 0;\n }\n },\n [decodeToken],\n );\n\n return {\n issueToken,\n refreshToken,\n verifyToken,\n decodeToken,\n isTokenExpired,\n getTimeUntilExpiry,\n isLoading,\n error,\n };\n}\n"],"mappings":"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"}
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
* update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }
|
|
9
9
|
* and remove the client-side hasMore approximation below.
|
|
10
10
|
*/
|
|
11
|
-
export declare function AuditLogPage(): import(
|
|
11
|
+
export declare function AuditLogPage(): import('react').JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\n const tr = useTr();\n\n const [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n const [showExportQuotaDialog, setShowExportQuotaDialog] = useState(false);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowExportQuotaDialog(true);\n setExportError(\n tr(\n \"auth.auditLog.exportQuotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n description={tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.auditLog.purpose\",\n \"A running record of security-relevant events on your account — sign-ins, password and MFA changes, and anomalies. Scan it to spot anything you didn't do, and export it for your own records or compliance.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: what's tracked — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.auditLog.emphasisTitle\", \"What this log captures\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.auditLog.emphasisBody\",\n \"Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Export error */}\n {exportError && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n <ConfirmActionDialog\n open={showExportQuotaDialog}\n onOpenChange={setShowExportQuotaDialog}\n title={tr(\"auth.auditLog.exportQuotaTitle\", \"Export Limit Reached\")}\n description={tr(\n \"auth.auditLog.exportQuotaDescription\",\n \"You have reached your export limit for this month.\",\n )}\n confirmText={tr(\"auth.auditLog.exportQuotaViewBilling\", \"View Billing\")}\n cancelText={tr(\"auth.auditLog.exportQuotaDismiss\", \"Dismiss\")}\n variant=\"warning\"\n onConfirm={() => window.location.assign(\"/billing\")}\n />\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n description={tr(\n \"auth.auditLog.errorDesc\",\n \"Something went wrong fetching your security events. Please try again in a moment.\",\n )}\n />\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n description={tr(\n \"auth.auditLog.noEventsDesc\",\n \"As you sign in and change security settings, those events will appear here.\",\n )}\n />\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { isQuotaExhaustedError } from \"@burdenoff/fe-libs/shared/utils\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useGatewayClient } from \"@burdenoff/fe-libs/shared/graphql\";\nimport { useFeatureFlagDetails } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { formatDateTime } from \"../utils/formatters\";\nimport { AlertIcon, SpinnerIcon } from \"../components/AuthIcons\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n useAuthAuditLogsQuery,\n type AuditLogEntry_AuthAuditLogFragment,\n} from \"../../generated/global-operations\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\nconst PAGE_SIZE = 20;\n\n/**\n * Security audit log page with pagination and export\n *\n * Fragment: AuditLogEntry_AuthAuditLog (graphql/operations/global/fragments/AuditLogEntry.fragment.graphql)\n * Query: AuthAuditLogs (graphql/operations/global/queries/auditLog.graphql)\n *\n * TODO: When authAuditLogs backend migrates to AuthAuditLogConnection type,\n * update this query to use: authAuditLogs { auditLogs: [...], totalCount, hasMore }\n * and remove the client-side hasMore approximation below.\n */\nexport function AuditLogPage() {\n const tr = useTr();\n\n const [page, setPage] = useState(1);\n const offset = (page - 1) * PAGE_SIZE;\n\n // Default-deny: only show when the flag is explicitly enabled in the system.\n // useFeatureFlag uses default-allow (returns true when not found), so we use\n // useFeatureFlagDetails + explicit value check instead.\n const auditLogFlag = useFeatureFlagDetails(\"auth.audit-log\");\n const showAuditLog = auditLogFlag?.value === true;\n\n const { data, loading, error } = useAuthAuditLogsQuery({\n skip: !showAuditLog,\n fetchPolicy: \"cache-and-network\",\n variables: { pagination: { limit: PAGE_SIZE, offset } },\n });\n\n const auditLogs = data?.authAuditLogs ?? [];\n // Approximate hasMore: if we received a full page there may be more\n const hasMore = auditLogs.length === PAGE_SIZE;\n\n const wspaceClient = useGatewayClient(\"workspace\");\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation({ client: wspaceClient });\n const [exportError, setExportError] = useState<string | null>(null);\n const [showExportQuotaDialog, setShowExportQuotaDialog] = useState(false);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Audit Log Export\",\n // permission_audit_logs covers auth audit logs in the export system\n entityTypes: [\"permission_audit_logs\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setShowExportQuotaDialog(true);\n setExportError(\n tr(\n \"auth.auditLog.exportQuotaReached\",\n \"You have reached your export limit for this month.\",\n ),\n );\n return;\n }\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n const getEventMarker = (eventType: string) => {\n switch (eventType) {\n case \"LOGIN_SUCCESS\":\n return \"OK\";\n case \"LOGIN_FAILURE\":\n return \"ER\";\n case \"LOGOUT\":\n return \"LO\";\n case \"PASSWORD_CHANGE\":\n return \"PW\";\n case \"MFA_ENABLED\":\n case \"MFA_VERIFIED\":\n return \"MF\";\n case \"ANOMALY_DETECTED\":\n return \"AL\";\n default:\n return \"AU\";\n }\n };\n\n if (!showAuditLog) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.notAvailable\", \"Page Not Available\")}\n description={tr(\n \"auth.auditLog.notEnabled\",\n \"The Security Audit Log is not enabled for your account.\",\n )}\n />\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.auditLog.title\", \"Security Audit Log\")}\n </h1>\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.auditLog.exportAriaLabel\", \"Export audit log\")}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n {tr(\"auth.auditLog.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"size-4\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4\"\n />\n </svg>\n {tr(\"auth.auditLog.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.auditLog.purpose\",\n \"A running record of security-relevant events on your account — sign-ins, password and MFA changes, and anomalies. Scan it to spot anything you didn't do, and export it for your own records or compliance.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: what's tracked — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.auditLog.emphasisTitle\", \"What this log captures\")}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.auditLog.emphasisBody\",\n \"Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Export error */}\n {exportError && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\"\n aria-hidden=\"true\"\n />\n <p className=\"text-sm text-status-error-text\">{exportError}</p>\n </div>\n )}\n\n <ConfirmActionDialog\n open={showExportQuotaDialog}\n onOpenChange={setShowExportQuotaDialog}\n title={tr(\"auth.auditLog.exportQuotaTitle\", \"Export Limit Reached\")}\n description={tr(\n \"auth.auditLog.exportQuotaDescription\",\n \"You have reached your export limit for this month.\",\n )}\n confirmText={tr(\"auth.auditLog.exportQuotaViewBilling\", \"View Billing\")}\n cancelText={tr(\"auth.auditLog.exportQuotaDismiss\", \"Dismiss\")}\n variant=\"warning\"\n onConfirm={() => window.location.assign(\"/billing\")}\n />\n\n {loading ? (\n <div aria-live=\"polite\" aria-busy=\"true\" className=\"text-center py-12\">\n <SpinnerIcon\n className=\"size-8 mx-auto text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <p className=\"mt-3 text-text-secondary\">\n {tr(\"auth.auditLog.loading\", \"Loading audit events...\")}\n </p>\n </div>\n ) : error ? (\n <div role=\"alert\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.auditLog.error\", \"Unable to load audit events\")}\n description={tr(\n \"auth.auditLog.errorDesc\",\n \"Something went wrong fetching your security events. Please try again in a moment.\",\n )}\n />\n </div>\n ) : auditLogs.length === 0 ? (\n <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title={tr(\"auth.auditLog.noEvents\", \"No security events recorded\")}\n description={tr(\n \"auth.auditLog.noEventsDesc\",\n \"As you sign in and change security settings, those events will appear here.\",\n )}\n />\n </output>\n ) : (\n <>\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={loading}>\n {auditLogs.map((event: AuditLogEntry_AuthAuditLogFragment) => (\n <div\n key={event.id}\n className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\"\n >\n <div className=\"flex items-start gap-4\">\n <div\n className=\"flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary\"\n aria-hidden=\"true\"\n >\n {getEventMarker(event.action)}\n </div>\n <div className=\"flex-1\">\n <h3 className=\"text-lg font-medium text-text-primary mb-1\">\n {event.action.replace(/_/g, \" \")}\n </h3>\n <p className=\"text-sm text-text-secondary mb-2\">\n <time dateTime={event.createdAt}>\n {formatDateTime(event.createdAt)}\n </time>\n </p>\n <p className=\"text-sm text-text-secondary\">\n {event.description ??\n tr(\n \"auth.auditLog.defaultDescription\",\n \"Authentication event\",\n )}\n {event.ipAddress && ` • ${event.ipAddress}`}\n </p>\n </div>\n {event.action.includes(\"FAILURE\") && (\n <output className=\"px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium\">\n {tr(\"auth.auditLog.failed\", \"Failed\")}\n </output>\n )}\n </div>\n </div>\n ))}\n </div>\n\n {/* Pagination */}\n <nav\n aria-label={tr(\n \"auth.auditLog.paginationAriaLabel\",\n \"Audit log pagination\",\n )}\n className=\"mt-6 flex items-center justify-between\"\n >\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.auditLog.page\", \"Page\")} {page}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => setPage((prev) => Math.max(1, prev - 1))}\n disabled={page === 1}\n aria-label={tr(\n \"auth.auditLog.previousPageAriaLabel\",\n \"Go to previous page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.previous\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.auditLog.nextPageAriaLabel\",\n \"Go to next page\",\n )}\n className=\"px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface disabled:opacity-50 disabled:cursor-not-allowed hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.auditLog.next\", \"Next\")}\n </button>\n </div>\n </nav>\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EmailVerifyPage.js","names":[],"sources":["../../../src/auth/pages/EmailVerifyPage.tsx"],"sourcesContent":["import { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { useSearchParams } from \"react-router\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { SpinnerIcon } from \"../components/AuthIcons\";\n\nconst REDIRECT_DELAY = 5; // seconds\n\n/**\n * Navigate to login page with optional email pre-fill\n * Uses window.location.href for reliable navigation (works in new tabs)\n */\nfunction navigateToLogin(email?: string) {\n const baseUrl = \"/auth/login\";\n const url = email ? `${baseUrl}?email=${encodeURIComponent(email)}` : baseUrl;\n window.location.href = url;\n}\n\n/**\n * Email verification page with professional design\n */\nexport function EmailVerifyPage() {\n const tr = useTr();\n\n const [searchParams] = useSearchParams();\n const token = searchParams.get(\"token\");\n const urlEmail = searchParams.get(\"email\");\n const { verifyEmail, resendVerification } = useAuthOperations();\n const user = useAuthStore((state) => state.user);\n const [status, setStatus] = useState<\"verifying\" | \"success\" | \"error\">(\n \"verifying\",\n );\n const verifiedEmailRef = useRef<string | undefined>(urlEmail || undefined);\n const [isResending, setIsResending] = useState(false);\n const [resendSuccess, setResendSuccess] = useState(false);\n const [countdown, setCountdown] = useState(REDIRECT_DELAY);\n\n // Guard against duplicate calls. useRef alone resets across remounts\n // (StrictMode, Suspense resolution of the lazy Auth root, HMR), so we\n // additionally key on the token via sessionStorage — any remount for the\n // same token short-circuits instead of firing a second mutation.\n const verificationInitiatedRef = useRef(false);\n\n useEffect(() => {\n if (!token || verificationInitiatedRef.current) return;\n\n const storageKey = `auth:verifyEmail:inflight:${token}`;\n if (\n typeof window !== \"undefined\" &&\n window.sessionStorage.getItem(storageKey)\n ) {\n return;\n }\n verificationInitiatedRef.current = true;\n try {\n window.sessionStorage.setItem(storageKey, \"1\");\n } catch {\n // sessionStorage unavailable (private mode, SSR); ref guard still applies.\n }\n\n verifyEmail(token)\n .then((result) => {\n if (result.success) {\n setStatus(\"success\");\n if (result.email) verifiedEmailRef.current = result.email;\n } else {\n setStatus(\"error\");\n }\n })\n .catch(() => setStatus(\"error\"));\n }, [token, verifyEmail]);\n\n // Auto-redirect countdown after successful verification\n useEffect(() => {\n if (status !== \"success\") return;\n\n const timer = setInterval(() => {\n setCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n navigateToLogin(verifiedEmailRef.current);\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [status]);\n\n const handleResend = async () => {\n const resendEmail = urlEmail || user?.email;\n if (!resendEmail) {\n console.error(\"No email available for resend\");\n return;\n }\n setIsResending(true);\n try {\n await resendVerification(resendEmail);\n setResendSuccess(true);\n } catch (err) {\n console.error(\"Failed to resend verification:\", err);\n } finally {\n setIsResending(false);\n }\n };\n\n const handleContinueToLogin = useCallback(() => {\n navigateToLogin(verifiedEmailRef.current);\n }, []);\n\n const handleBackToLogin = useCallback(() => {\n navigateToLogin();\n }, []);\n\n if (status === \"verifying\") {\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.verifyingTitle\", \"Verifying your email\")}\n subtitle={tr(\n \"auth.emailVerify.verifyingSubtitle\",\n \"Please wait while we confirm your email address\",\n )}\n >\n {/* Loading Spinner */}\n <div className=\"flex flex-col items-center justify-center py-12\">\n <div className=\"size-16 rounded-full bg-[var(--color-bg-accent)] flex items-center justify-center mb-6\">\n <SpinnerIcon className=\"size-8 text-[var(--color-text-link)] animate-spin\" />\n </div>\n <p className=\"text-[var(--color-text-secondary)]\">\n {tr(\"auth.emailVerify.moment\", \"This will only take a moment...\")}\n </p>\n </div>\n </AuthLayout>\n );\n }\n\n if (status === \"success\") {\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.verifiedTitle\", \"Email verified!\")}\n subtitle={tr(\n \"auth.emailVerify.verifiedSubtitle\",\n \"Your email address has been confirmed\",\n )}\n >\n {/* Success Icon */}\n <div className=\"flex flex-col items-center justify-center py-6\">\n <div className=\"size-20 rounded-full bg-[var(--color-status-success-bg-subtle)] flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-[var(--color-status-success-text)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-6\">\n {tr(\n \"auth.emailVerify.successMessage\",\n \"Your email has been successfully verified. You can now sign in to access all features.\",\n )}\n </p>\n\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)] mb-6\">\n {tr(\"auth.emailVerify.redirecting\", \"Redirecting to sign in in\")}{\" \"}\n <span className=\"font-semibold text-[var(--color-text-primary)]\">\n {countdown}\n </span>{\" \"}\n {tr(\"auth.emailVerify.seconds\", \"seconds...\")}\n </p>\n\n <Button\n type=\"button\"\n onClick={handleContinueToLogin}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.emailVerify.continueSignIn\", \"Continue to sign in now\")}\n </Button>\n </div>\n </AuthLayout>\n );\n }\n\n // Error state\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.failedTitle\", \"Verification failed\")}\n subtitle={tr(\n \"auth.emailVerify.failedSubtitle\",\n \"We couldn't verify your email address\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Error Icon */}\n <div className=\"size-20 rounded-full bg-[var(--color-status-error-bg-subtle)] flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-[var(--color-status-error-text)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8\">\n {tr(\n \"auth.emailVerify.invalidLink\",\n \"The verification link is invalid or has expired. Please request a new verification email.\",\n )}\n </p>\n\n {resendSuccess ? (\n <div className=\"w-full p-4 rounded-xl bg-[var(--color-status-success-bg-subtle)] border border-[var(--color-status-success-border)] text-center mb-4\">\n <p className=\"text-sm text-[var(--color-status-success-text)]\">\n {tr(\n \"auth.emailVerify.emailSent\",\n \"Verification email sent! Please check your inbox.\",\n )}\n </p>\n </div>\n ) : (\n <Button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n variant=\"default\"\n size=\"lg\"\n className=\"mb-4 w-full font-semibold\"\n >\n {isResending ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>{tr(\"auth.emailVerify.sending\", \"Sending…\")}</span>\n </>\n ) : (\n tr(\"auth.emailVerify.resend\", \"Resend verification email\")\n )}\n </Button>\n )}\n\n <button\n type=\"button\"\n onClick={handleBackToLogin}\n className=\"w-full py-3 px-4 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] font-medium hover:bg-[var(--color-bg-sunken)] transition-all\"\n >\n {tr(\"auth.emailVerify.backToSignIn\", \"Back to sign in\")}\n </button>\n </div>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EmailVerifyPage.js","names":[],"sources":["../../../src/auth/pages/EmailVerifyPage.tsx"],"sourcesContent":["import { useEffect, useState, useCallback, useRef } from \"react\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { useSearchParams } from \"react-router\";\nimport { useAuthOperations } from \"../hooks/useAuthOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { AuthLayout } from \"../components/AuthLayout\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { SpinnerIcon } from \"../components/AuthIcons\";\n\nconst REDIRECT_DELAY = 5; // seconds\n\n/**\n * Navigate to login page with optional email pre-fill\n * Uses window.location.href for reliable navigation (works in new tabs)\n */\nfunction navigateToLogin(email?: string) {\n const baseUrl = \"/auth/login\";\n const url = email ? `${baseUrl}?email=${encodeURIComponent(email)}` : baseUrl;\n window.location.href = url;\n}\n\n/**\n * Email verification page with professional design\n */\nexport function EmailVerifyPage() {\n const tr = useTr();\n\n const [searchParams] = useSearchParams();\n const token = searchParams.get(\"token\");\n const urlEmail = searchParams.get(\"email\");\n const { verifyEmail, resendVerification } = useAuthOperations();\n const user = useAuthStore((state) => state.user);\n const [status, setStatus] = useState<\"verifying\" | \"success\" | \"error\">(\n \"verifying\",\n );\n const verifiedEmailRef = useRef<string | undefined>(urlEmail || undefined);\n const [isResending, setIsResending] = useState(false);\n const [resendSuccess, setResendSuccess] = useState(false);\n const [countdown, setCountdown] = useState(REDIRECT_DELAY);\n\n // Guard against duplicate calls. useRef alone resets across remounts\n // (StrictMode, Suspense resolution of the lazy Auth root, HMR), so we\n // additionally key on the token via sessionStorage — any remount for the\n // same token short-circuits instead of firing a second mutation.\n const verificationInitiatedRef = useRef(false);\n\n useEffect(() => {\n if (!token || verificationInitiatedRef.current) return;\n\n const storageKey = `auth:verifyEmail:inflight:${token}`;\n if (\n typeof window !== \"undefined\" &&\n window.sessionStorage.getItem(storageKey)\n ) {\n return;\n }\n verificationInitiatedRef.current = true;\n try {\n window.sessionStorage.setItem(storageKey, \"1\");\n } catch {\n // sessionStorage unavailable (private mode, SSR); ref guard still applies.\n }\n\n verifyEmail(token)\n .then((result) => {\n if (result.success) {\n setStatus(\"success\");\n if (result.email) verifiedEmailRef.current = result.email;\n } else {\n setStatus(\"error\");\n }\n })\n .catch(() => setStatus(\"error\"));\n }, [token, verifyEmail]);\n\n // Auto-redirect countdown after successful verification\n useEffect(() => {\n if (status !== \"success\") return;\n\n const timer = setInterval(() => {\n setCountdown((prev) => {\n if (prev <= 1) {\n clearInterval(timer);\n navigateToLogin(verifiedEmailRef.current);\n return 0;\n }\n return prev - 1;\n });\n }, 1000);\n\n return () => clearInterval(timer);\n }, [status]);\n\n const handleResend = async () => {\n const resendEmail = urlEmail || user?.email;\n if (!resendEmail) {\n console.error(\"No email available for resend\");\n return;\n }\n setIsResending(true);\n try {\n await resendVerification(resendEmail);\n setResendSuccess(true);\n } catch (err) {\n console.error(\"Failed to resend verification:\", err);\n } finally {\n setIsResending(false);\n }\n };\n\n const handleContinueToLogin = useCallback(() => {\n navigateToLogin(verifiedEmailRef.current);\n }, []);\n\n const handleBackToLogin = useCallback(() => {\n navigateToLogin();\n }, []);\n\n if (status === \"verifying\") {\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.verifyingTitle\", \"Verifying your email\")}\n subtitle={tr(\n \"auth.emailVerify.verifyingSubtitle\",\n \"Please wait while we confirm your email address\",\n )}\n >\n {/* Loading Spinner */}\n <div className=\"flex flex-col items-center justify-center py-12\">\n <div className=\"size-16 rounded-full bg-[var(--color-bg-accent)] flex items-center justify-center mb-6\">\n <SpinnerIcon className=\"size-8 text-[var(--color-text-link)] animate-spin\" />\n </div>\n <p className=\"text-[var(--color-text-secondary)]\">\n {tr(\"auth.emailVerify.moment\", \"This will only take a moment...\")}\n </p>\n </div>\n </AuthLayout>\n );\n }\n\n if (status === \"success\") {\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.verifiedTitle\", \"Email verified!\")}\n subtitle={tr(\n \"auth.emailVerify.verifiedSubtitle\",\n \"Your email address has been confirmed\",\n )}\n >\n {/* Success Icon */}\n <div className=\"flex flex-col items-center justify-center py-6\">\n <div className=\"size-20 rounded-full bg-[var(--color-status-success-bg-subtle)] flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-[var(--color-status-success-text)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-6\">\n {tr(\n \"auth.emailVerify.successMessage\",\n \"Your email has been successfully verified. You can now sign in to access all features.\",\n )}\n </p>\n\n <p className=\"text-center text-sm text-[var(--color-text-tertiary)] mb-6\">\n {tr(\"auth.emailVerify.redirecting\", \"Redirecting to sign in in\")}{\" \"}\n <span className=\"font-semibold text-[var(--color-text-primary)]\">\n {countdown}\n </span>{\" \"}\n {tr(\"auth.emailVerify.seconds\", \"seconds...\")}\n </p>\n\n <Button\n type=\"button\"\n onClick={handleContinueToLogin}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.emailVerify.continueSignIn\", \"Continue to sign in now\")}\n </Button>\n </div>\n </AuthLayout>\n );\n }\n\n // Error state\n return (\n <AuthLayout\n title={tr(\"auth.emailVerify.failedTitle\", \"Verification failed\")}\n subtitle={tr(\n \"auth.emailVerify.failedSubtitle\",\n \"We couldn't verify your email address\",\n )}\n >\n <div className=\"flex flex-col items-center justify-center py-6\">\n {/* Error Icon */}\n <div className=\"size-20 rounded-full bg-[var(--color-status-error-bg-subtle)] flex items-center justify-center mb-6\">\n <svg\n className=\"size-10 text-[var(--color-status-error-text)]\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z\"\n />\n </svg>\n </div>\n\n <p className=\"text-center text-[var(--color-text-secondary)] mb-8\">\n {tr(\n \"auth.emailVerify.invalidLink\",\n \"The verification link is invalid or has expired. Please request a new verification email.\",\n )}\n </p>\n\n {resendSuccess ? (\n <div className=\"w-full p-4 rounded-xl bg-[var(--color-status-success-bg-subtle)] border border-[var(--color-status-success-border)] text-center mb-4\">\n <p className=\"text-sm text-[var(--color-status-success-text)]\">\n {tr(\n \"auth.emailVerify.emailSent\",\n \"Verification email sent! Please check your inbox.\",\n )}\n </p>\n </div>\n ) : (\n <Button\n type=\"button\"\n onClick={handleResend}\n disabled={isResending}\n variant=\"default\"\n size=\"lg\"\n className=\"mb-4 w-full font-semibold\"\n >\n {isResending ? (\n <>\n <SpinnerIcon className=\"size-5 animate-spin\" />\n <span>{tr(\"auth.emailVerify.sending\", \"Sending…\")}</span>\n </>\n ) : (\n tr(\"auth.emailVerify.resend\", \"Resend verification email\")\n )}\n </Button>\n )}\n\n <button\n type=\"button\"\n onClick={handleBackToLogin}\n className=\"w-full py-3 px-4 rounded-xl border border-[var(--color-border-default)] bg-[var(--color-bg-surface)] text-[var(--color-text-primary)] font-medium hover:bg-[var(--color-bg-sunken)] transition-all\"\n >\n {tr(\"auth.emailVerify.backToSignIn\", \"Back to sign in\")}\n </button>\n </div>\n </AuthLayout>\n );\n}\n"],"mappings":"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"}
|