@burdenoff/microfe-auth 2026.1031.1 → 2026.1101.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/dist/auth/AuthRoot.js +14 -11
  2. package/dist/auth/AuthRoot.js.map +1 -1
  3. package/dist/auth/AuthRoutes.js +1 -2
  4. package/dist/auth/AuthRoutes.js.map +1 -1
  5. package/dist/auth/components/AuthIcons.js +1 -2
  6. package/dist/auth/components/AuthIcons.js.map +1 -1
  7. package/dist/auth/components/AuthLayout.js +1 -2
  8. package/dist/auth/components/AuthLayout.js.map +1 -1
  9. package/dist/auth/components/CaptchaV2.js +9 -10
  10. package/dist/auth/components/CaptchaV2.js.map +1 -1
  11. package/dist/auth/components/ConfirmActionDialog.js +1 -2
  12. package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
  13. package/dist/auth/components/profile/UserProfileMenu.js +7 -6
  14. package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
  15. package/dist/auth/components/workspace/WorkspaceSelector.js +1 -2
  16. package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
  17. package/dist/auth/constants/permissions.js +1 -2
  18. package/dist/auth/constants/permissions.js.map +1 -1
  19. package/dist/auth/constants/sessionKeys.js +1 -2
  20. package/dist/auth/constants/sessionKeys.js.map +1 -1
  21. package/dist/auth/hooks/contextDeepLink.js +1 -2
  22. package/dist/auth/hooks/contextDeepLink.js.map +1 -1
  23. package/dist/auth/hooks/index.js +12 -11
  24. package/dist/auth/hooks/useAccountOperations.js +1 -2
  25. package/dist/auth/hooks/useAccountOperations.js.map +1 -1
  26. package/dist/auth/hooks/useActiveContextBootstrapMirror.js +1 -2
  27. package/dist/auth/hooks/useActiveContextBootstrapMirror.js.map +1 -1
  28. package/dist/auth/hooks/useAuthOperations.js +37 -37
  29. package/dist/auth/hooks/useAuthOperations.js.map +1 -1
  30. package/dist/auth/hooks/useAuthPermissions.js +1 -2
  31. package/dist/auth/hooks/useAuthPermissions.js.map +1 -1
  32. package/dist/auth/hooks/useCaptchaV3.js +1 -2
  33. package/dist/auth/hooks/useCaptchaV3.js.map +1 -1
  34. package/dist/auth/hooks/useContextSwitcherData.js +81 -78
  35. package/dist/auth/hooks/useContextSwitcherData.js.map +1 -1
  36. package/dist/auth/hooks/useMfaOperations.js +4 -5
  37. package/dist/auth/hooks/useMfaOperations.js.map +1 -1
  38. package/dist/auth/hooks/useMyWorkspaces.js +4 -5
  39. package/dist/auth/hooks/useMyWorkspaces.js.map +1 -1
  40. package/dist/auth/hooks/useOAuthOperations.js +11 -12
  41. package/dist/auth/hooks/useOAuthOperations.js.map +1 -1
  42. package/dist/auth/hooks/usePasswordOperations.js +1 -2
  43. package/dist/auth/hooks/usePasswordOperations.js.map +1 -1
  44. package/dist/auth/hooks/useSessionOperations.js +3 -4
  45. package/dist/auth/hooks/useSessionOperations.js.map +1 -1
  46. package/dist/auth/hooks/useSignupCaptcha.js +1 -2
  47. package/dist/auth/hooks/useSignupCaptcha.js.map +1 -1
  48. package/dist/auth/hooks/useSsoOperations.js +80 -81
  49. package/dist/auth/hooks/useSsoOperations.js.map +1 -1
  50. package/dist/auth/hooks/useTokenRefresh.js +8 -12
  51. package/dist/auth/hooks/useTokenRefresh.js.map +1 -1
  52. package/dist/auth/hooks/useUserOperations.js +1 -2
  53. package/dist/auth/hooks/useUserOperations.js.map +1 -1
  54. package/dist/auth/hooks/useWorkspaceOperations.js +1 -2
  55. package/dist/auth/hooks/useWorkspaceOperations.js.map +1 -1
  56. package/dist/auth/hooks/useWorkspaceToken.js +1 -2
  57. package/dist/auth/hooks/useWorkspaceToken.js.map +1 -1
  58. package/dist/auth/index.js +19 -18
  59. package/dist/auth/pages/AuditLogPage.js +21 -22
  60. package/dist/auth/pages/AuditLogPage.js.map +1 -1
  61. package/dist/auth/pages/EmailVerifyPage.js +15 -17
  62. package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
  63. package/dist/auth/pages/LoginPage.js +89 -105
  64. package/dist/auth/pages/LoginPage.js.map +1 -1
  65. package/dist/auth/pages/MagicLinkPage.js +30 -31
  66. package/dist/auth/pages/MagicLinkPage.js.map +1 -1
  67. package/dist/auth/pages/MfaSetupPage.js +18 -20
  68. package/dist/auth/pages/MfaSetupPage.js.map +1 -1
  69. package/dist/auth/pages/MfaVerifyPage.js +31 -35
  70. package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
  71. package/dist/auth/pages/OAuthCallbackPage.js +4 -11
  72. package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
  73. package/dist/auth/pages/PasswordResetPage.js +20 -23
  74. package/dist/auth/pages/PasswordResetPage.js.map +1 -1
  75. package/dist/auth/pages/ReactivatePage.js +14 -18
  76. package/dist/auth/pages/ReactivatePage.js.map +1 -1
  77. package/dist/auth/pages/RegisterPage.js +180 -181
  78. package/dist/auth/pages/RegisterPage.js.map +1 -1
  79. package/dist/auth/pages/SSOClearPage.js +9 -10
  80. package/dist/auth/pages/SSOClearPage.js.map +1 -1
  81. package/dist/auth/pages/SSODepositPage.js +9 -13
  82. package/dist/auth/pages/SSODepositPage.js.map +1 -1
  83. package/dist/auth/pages/SSORelayCallbackPage.js +3 -7
  84. package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
  85. package/dist/auth/pages/SSORelayPage.js +11 -10
  86. package/dist/auth/pages/SSORelayPage.js.map +1 -1
  87. package/dist/auth/pages/SecurityPage.js +19 -21
  88. package/dist/auth/pages/SecurityPage.js.map +1 -1
  89. package/dist/auth/pages/SessionBridgePage.js +9 -16
  90. package/dist/auth/pages/SessionBridgePage.js.map +1 -1
  91. package/dist/auth/pages/SessionsPage.js +17 -18
  92. package/dist/auth/pages/SessionsPage.js.map +1 -1
  93. package/dist/auth/pages/SsoCallbackPage.js +17 -22
  94. package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
  95. package/dist/auth/pages/SsoConfigPage.js +69 -69
  96. package/dist/auth/pages/SsoConfigPage.js.map +1 -1
  97. package/dist/auth/pages/SsoLoginPage.js +9 -10
  98. package/dist/auth/pages/SsoLoginPage.js.map +1 -1
  99. package/dist/auth/pages/UserDetailsPage.js +16 -23
  100. package/dist/auth/pages/UserDetailsPage.js.map +1 -1
  101. package/dist/auth/pages/UserProfilePage.js +140 -141
  102. package/dist/auth/pages/UserProfilePage.js.map +1 -1
  103. package/dist/auth/pages/UsersPage.js +446 -453
  104. package/dist/auth/pages/UsersPage.js.map +1 -1
  105. package/dist/auth/providers/AuthProvider.js +1 -2
  106. package/dist/auth/providers/AuthProvider.js.map +1 -1
  107. package/dist/auth/providers/CaptchaProvider.js +1 -2
  108. package/dist/auth/providers/CaptchaProvider.js.map +1 -1
  109. package/dist/auth/store/authStore.js +5 -12
  110. package/dist/auth/store/authStore.js.map +1 -1
  111. package/dist/auth/utils/captchaConfig.js +5 -8
  112. package/dist/auth/utils/captchaConfig.js.map +1 -1
  113. package/dist/auth/utils/formatters.js +1 -2
  114. package/dist/auth/utils/formatters.js.map +1 -1
  115. package/dist/auth/utils/gatewayBaseUrls.js +1 -2
  116. package/dist/auth/utils/gatewayBaseUrls.js.map +1 -1
  117. package/dist/auth/utils/legalUrls.js +2 -3
  118. package/dist/auth/utils/legalUrls.js.map +1 -1
  119. package/dist/auth/utils/redirect.js +1 -2
  120. package/dist/auth/utils/redirect.js.map +1 -1
  121. package/dist/auth/utils/ssoOrigins.js +2 -3
  122. package/dist/auth/utils/ssoOrigins.js.map +1 -1
  123. package/dist/auth/utils/validation.js +34 -36
  124. package/dist/auth/utils/validation.js.map +1 -1
  125. package/dist/generated/global-operations.js +2 -3
  126. package/dist/generated/global-operations.js.map +1 -1
  127. package/dist/generated/wspace-operations.js +2 -3
  128. package/dist/generated/wspace-operations.js.map +1 -1
  129. package/dist/microfe-auth.css +1 -1
  130. package/dist/shared/hooks/useTr.js +1 -2
  131. package/dist/shared/hooks/useTr.js.map +1 -1
  132. package/dist/translations/en.js +1 -2
  133. package/dist/translations/en.js.map +1 -1
  134. package/dist/utils/nativeBridge.js +1 -2
  135. package/package.json +7 -3
@@ -1,7 +1,7 @@
1
1
  import { useAuthStore as e } from "../store/authStore.js";
2
2
  import { useCallback as t, useState as n } from "react";
3
3
  import { graphqlFetch as r } from "@burdenoff/fe-libs/shared/graphql/fetch";
4
- //#region src/auth/hooks/useWorkspaceToken.ts
4
+ // src/auth/hooks/useWorkspaceToken.ts
5
5
  function i(i = {}) {
6
6
  let [a, o] = n(!1), [s, c] = n(null), l = t(async (t, n, i, a) => {
7
7
  o(!0), c(null);
@@ -88,7 +88,6 @@ function i(i = {}) {
88
88
  error: s
89
89
  };
90
90
  }
91
- //#endregion
92
91
  export { i as useWorkspaceToken };
93
92
 
94
93
  //# sourceMappingURL=useWorkspaceToken.js.map
@@ -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":["useWorkspaceToken","isLoading","setIsLoading","useState","error","setError","issueToken","useCallback","accessToken","useAuthStore","result","graphqlFetch","workspaceId","projectId","scopes","errorMsg","err","errorMessage","verifyToken","tokenToVerify","token","workspaceToken","refreshToken","currentOrganizationId","currentWorkspaceId","decodeToken","base64","jsonPayload","c","tokenToCheck","payload","now","timeUntilExpiry"],"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,19 +1,20 @@
1
- import "./providers/AuthProvider.js";
2
- import "./store/authStore.js";
3
- import "./hooks/useAuthOperations.js";
4
- import "./hooks/useMyWorkspaces.js";
5
- import "./pages/SSORelayPage.js";
6
- import "./pages/SSORelayCallbackPage.js";
7
- import "./AuthRoot.js";
8
- import "./hooks/useMfaOperations.js";
9
- import "./hooks/usePasswordOperations.js";
10
- import "./hooks/useSessionOperations.js";
11
- import "./hooks/useWorkspaceOperations.js";
12
- import "./hooks/useContextSwitcherData.js";
13
- import "./hooks/useActiveContextBootstrapMirror.js";
14
- import "./hooks/useUserOperations.js";
1
+ import { AuthRoot as e } from "./AuthRoot.js";
2
+ import { useAuthStore as t } from "./store/authStore.js";
3
+ import { useAuthOperations as n } from "./hooks/useAuthOperations.js";
4
+ import { useMyWorkspaces as r } from "./hooks/useMyWorkspaces.js";
5
+ import { useAuth as i } from "./providers/AuthProvider.js";
15
6
  import "./hooks/index.js";
16
- import "./hooks/useWorkspaceToken.js";
17
- import "./hooks/useTokenRefresh.js";
18
- import "./components/workspace/WorkspaceSelector.js";
19
- import "./components/profile/UserProfileMenu.js";
7
+ import { useWorkspaceToken as a } from "./hooks/useWorkspaceToken.js";
8
+ import { useMfaOperations as o } from "./hooks/useMfaOperations.js";
9
+ import { usePasswordOperations as s } from "./hooks/usePasswordOperations.js";
10
+ import { useSessionOperations as c } from "./hooks/useSessionOperations.js";
11
+ import { useWorkspaceOperations as l } from "./hooks/useWorkspaceOperations.js";
12
+ import { useContextSwitcherData as u, useProjectSyncEffect as d, useWorkspaceSyncEffect as f } from "./hooks/useContextSwitcherData.js";
13
+ import { useActiveContextBootstrapMirror as p } from "./hooks/useActiveContextBootstrapMirror.js";
14
+ import { useUserOperations as m } from "./hooks/useUserOperations.js";
15
+ import { useTokenRefresh as h } from "./hooks/useTokenRefresh.js";
16
+ import { WorkspaceSelector as g } from "./components/workspace/WorkspaceSelector.js";
17
+ import { UserProfileMenu as _ } from "./components/profile/UserProfileMenu.js";
18
+ import { SSORelayPage as v } from "./pages/SSORelayPage.js";
19
+ import { SSORelayCallbackPage as y } from "./pages/SSORelayCallbackPage.js";
20
+ export { e as AuthRoot, y as SSORelayCallbackPage, v as SSORelayPage, _ as UserProfileMenu, g as WorkspaceSelector, p as useActiveContextBootstrapMirror, i as useAuth, n as useAuthOperations, t as useAuthStore, u as useContextSwitcherData, o as useMfaOperations, r as useMyWorkspaces, s as usePasswordOperations, d as useProjectSyncEffect, c as useSessionOperations, h as useTokenRefresh, m as useUserOperations, l as useWorkspaceOperations, f as useWorkspaceSyncEffect, a as useWorkspaceToken };
@@ -1,8 +1,8 @@
1
1
  import { useTr as e } from "../../shared/hooks/useTr.js";
2
- import { useAuthAuditLogsQuery as t } from "../../generated/global-operations.js";
2
+ import { formatDateTime as t } from "../utils/formatters.js";
3
3
  import { AlertIcon as n, SpinnerIcon as r } from "../components/AuthIcons.js";
4
- import { formatDateTime as i } from "../utils/formatters.js";
5
- import { ConfirmActionDialog as a } from "../components/ConfirmActionDialog.js";
4
+ import { ConfirmActionDialog as i } from "../components/ConfirmActionDialog.js";
5
+ import { useAuthAuditLogsQuery as a } from "../../generated/global-operations.js";
6
6
  import { useCreateAuthExportJobMutation as o } from "../../generated/wspace-operations.js";
7
7
  import { useState as s } from "react";
8
8
  import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
@@ -10,20 +10,20 @@ import { useFeatureFlagDetails as d } from "@burdenoff/fe-libs/shared/feature-fl
10
10
  import { isQuotaExhaustedError as f } from "@burdenoff/fe-libs/shared/utils";
11
11
  import { EmphasisPanel as p, IllustratedEmptyState as m, PagePurpose as h } from "@burdenoff/fe-libs/ui";
12
12
  import { useGatewayClient as g } from "@burdenoff/fe-libs/shared/graphql";
13
- //#region src/auth/pages/AuditLogPage.tsx
13
+ // src/auth/pages/AuditLogPage.tsx
14
14
  var _ = 20;
15
15
  function v() {
16
- let v = e(), [y, b] = s(1), x = (y - 1) * _, S = d("auth.audit-log")?.value === !0, { data: C, loading: w, error: T } = t({
16
+ let v = e(), [y, b] = s(1), x = (y - 1) * _, S = d("auth.audit-log")?.value === !0, { data: C, loading: w, error: T } = a({
17
17
  skip: !S,
18
18
  fetchPolicy: "cache-and-network",
19
19
  variables: { pagination: {
20
20
  limit: _,
21
21
  offset: x
22
22
  } }
23
- }), E = C?.authAuditLogs ?? [], D = E.length === _, [O, { loading: k }] = o({ client: g("workspace") }), [A, j] = s(null), [M, N] = s(!1), P = async () => {
24
- j(null);
23
+ }), E = C?.authAuditLogs ?? [], D = E.length === _, O = g("workspace"), [k, { loading: A }] = o({ client: O }), [j, M] = s(null), [N, P] = s(!1), F = async () => {
24
+ M(null);
25
25
  try {
26
- await O({ variables: { input: {
26
+ await k({ variables: { input: {
27
27
  name: "Audit Log Export",
28
28
  entityTypes: ["permission_audit_logs"],
29
29
  format: "CSV",
@@ -32,12 +32,12 @@ function v() {
32
32
  } } });
33
33
  } catch (e) {
34
34
  if (f(e)) {
35
- N(!0), j(v("auth.auditLog.exportQuotaReached", "You have reached your export limit for this month."));
35
+ P(!0), M(v("auth.auditLog.exportQuotaReached", "You have reached your export limit for this month."));
36
36
  return;
37
37
  }
38
- j(e instanceof Error ? e.message : "Failed to start export");
38
+ M(e instanceof Error ? e.message : "Failed to start export");
39
39
  }
40
- }, F = (e) => {
40
+ }, I = (e) => {
41
41
  switch (e) {
42
42
  case "LOGIN_SUCCESS": return "OK";
43
43
  case "LOGIN_FAILURE": return "ER";
@@ -59,11 +59,11 @@ function v() {
59
59
  children: v("auth.auditLog.title", "Security Audit Log")
60
60
  }), /* @__PURE__ */ l("button", {
61
61
  type: "button",
62
- onClick: () => void P(),
63
- disabled: k,
62
+ onClick: () => void F(),
63
+ disabled: A,
64
64
  "aria-label": v("auth.auditLog.exportAriaLabel", "Export audit log"),
65
65
  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",
66
- children: k ? /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(r, {
66
+ children: A ? /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(r, {
67
67
  className: "size-4 animate-spin",
68
68
  "aria-hidden": "true"
69
69
  }), v("auth.auditLog.exporting", "Exporting...")] }) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("svg", {
@@ -95,7 +95,7 @@ function v() {
95
95
  children: v("auth.auditLog.emphasisBody", "Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.")
96
96
  })]
97
97
  }),
98
- A && /* @__PURE__ */ u("div", {
98
+ j && /* @__PURE__ */ u("div", {
99
99
  role: "alert",
100
100
  className: "mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border",
101
101
  children: [/* @__PURE__ */ l(n, {
@@ -103,12 +103,12 @@ function v() {
103
103
  "aria-hidden": "true"
104
104
  }), /* @__PURE__ */ l("p", {
105
105
  className: "text-sm text-status-error-text",
106
- children: A
106
+ children: j
107
107
  })]
108
108
  }),
109
- /* @__PURE__ */ l(a, {
110
- open: M,
111
- onOpenChange: N,
109
+ /* @__PURE__ */ l(i, {
110
+ open: N,
111
+ onOpenChange: P,
112
112
  title: v("auth.auditLog.exportQuotaTitle", "Export Limit Reached"),
113
113
  description: v("auth.auditLog.exportQuotaDescription", "You have reached your export limit for this month."),
114
114
  confirmText: v("auth.auditLog.exportQuotaViewBilling", "View Billing"),
@@ -153,7 +153,7 @@ function v() {
153
153
  /* @__PURE__ */ l("div", {
154
154
  className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary",
155
155
  "aria-hidden": "true",
156
- children: F(e.action)
156
+ children: I(e.action)
157
157
  }),
158
158
  /* @__PURE__ */ u("div", {
159
159
  className: "flex-1",
@@ -166,7 +166,7 @@ function v() {
166
166
  className: "text-sm text-text-secondary mb-2",
167
167
  children: /* @__PURE__ */ l("time", {
168
168
  dateTime: e.createdAt,
169
- children: i(e.createdAt)
169
+ children: t(e.createdAt)
170
170
  })
171
171
  }),
172
172
  /* @__PURE__ */ u("p", {
@@ -221,7 +221,6 @@ function v() {
221
221
  })
222
222
  });
223
223
  }
224
- //#endregion
225
224
  export { v as AuditLogPage };
226
225
 
227
226
  //# sourceMappingURL=AuditLogPage.js.map
@@ -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":["_Fragment","PAGE_SIZE","AuditLogPage","tr","useTr","page","setPage","useState","offset","showAuditLog","useFeatureFlagDetails","useAuthAuditLogsQuery","auditLogs","data","hasMore","wspaceClient","useGatewayClient","createExportJob","isExporting","useCreateAuthExportJobMutation","exportError","setExportError","showExportQuotaDialog","setShowExportQuotaDialog","handleExport","err","isQuotaExhaustedError","getEventMarker","eventType","Fragment","SpinnerIcon","PagePurpose","EmphasisPanel","AlertIcon","ConfirmActionDialog","loading","error","IllustratedEmptyState","event","formatDateTime","prev"],"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,20 +1,20 @@
1
- import { useAuthStore as e } from "../store/authStore.js";
2
- import { useTr as t } from "../../shared/hooks/useTr.js";
3
- import { useAuthOperations as n } from "../hooks/useAuthOperations.js";
1
+ import { useTr as e } from "../../shared/hooks/useTr.js";
2
+ import { useAuthOperations as t } from "../hooks/useAuthOperations.js";
3
+ import { useAuthStore as n } from "../store/authStore.js";
4
4
  import { AuthLayout as r } from "../components/AuthLayout.js";
5
5
  import { SpinnerIcon as i } from "../components/AuthIcons.js";
6
6
  import { useCallback as a, useEffect as o, useRef as s, useState as c } from "react";
7
7
  import { useSearchParams as l } from "react-router";
8
8
  import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
9
9
  import { Button as p } from "@burdenoff/fe-libs/button";
10
- //#region src/auth/pages/EmailVerifyPage.tsx
10
+ // src/auth/pages/EmailVerifyPage.tsx
11
11
  var m = 5, h = /* @__PURE__ */ new Map();
12
12
  function g(e) {
13
13
  let t = "/auth/login", n = e ? `${t}?email=${encodeURIComponent(e)}` : t;
14
14
  window.location.href = n;
15
15
  }
16
16
  function _() {
17
- let _ = t(), [v] = l(), y = v.get("token"), b = v.get("email"), { verifyEmail: x, resendVerification: S } = n(), C = e((e) => e.user), [w, T] = c("verifying"), E = s(b || void 0), [D, O] = c(!1), [k, A] = c(!1), [j, M] = c(m), N = s(null);
17
+ let _ = e(), [v] = l(), y = v.get("token"), b = v.get("email"), { verifyEmail: x, resendVerification: S } = t(), C = n((e) => e.user), [w, T] = c("verifying"), E = s(b || void 0), [D, O] = c(!1), [k, A] = c(!1), [j, M] = c(m), N = s(null);
18
18
  o(() => {
19
19
  if (!y) return;
20
20
  try {
@@ -44,17 +44,16 @@ function _() {
44
44
  }, [w]);
45
45
  let P = async () => {
46
46
  let e = b || C?.email;
47
- if (!e) {
48
- console.error("No email available for resend");
49
- return;
50
- }
51
- O(!0);
52
- try {
53
- await S(e), A(!0);
54
- } catch (e) {
55
- console.error("Failed to resend verification:", e);
56
- } finally {
57
- O(!1);
47
+ if (!e) console.error("No email available for resend");
48
+ else {
49
+ O(!0);
50
+ try {
51
+ await S(e), A(!0);
52
+ } catch (e) {
53
+ console.error("Failed to resend verification:", e);
54
+ } finally {
55
+ O(!1);
56
+ }
58
57
  }
59
58
  }, F = a(() => {
60
59
  g(E.current);
@@ -172,7 +171,6 @@ function _() {
172
171
  })
173
172
  });
174
173
  }
175
- //#endregion
176
174
  export { _ as EmailVerifyPage };
177
175
 
178
176
  //# sourceMappingURL=EmailVerifyPage.js.map
@@ -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\ntype VerificationRequest = ReturnType<\n ReturnType<typeof useAuthOperations>[\"verifyEmail\"]\n>;\n// Share only pending work across remounts; never persist bearer tokens.\nconst pendingVerifications = new Map<string, VerificationRequest>();\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 const verificationRef = useRef<{\n token: string;\n request: VerificationRequest;\n } | null>(null);\n\n useEffect(() => {\n if (!token) return;\n try {\n // Remove the marker left by older versions, without touching other data.\n window.sessionStorage.removeItem(`auth:verifyEmail:inflight:${token}`);\n } catch {\n // Verification must also work when browser storage is unavailable.\n }\n\n if (verificationRef.current?.token !== token) {\n let request = pendingVerifications.get(token);\n if (!request) {\n request = Promise.resolve()\n .then(() => verifyEmail(token))\n .finally(() => pendingVerifications.delete(token));\n pendingVerifications.set(token, request);\n }\n verificationRef.current = { token, request };\n setStatus(\"verifying\");\n }\n\n let active = true;\n verificationRef.current.request\n .then((result) => {\n if (!active) return;\n if (result.success) {\n setStatus(\"success\");\n if (result.email) verifiedEmailRef.current = result.email;\n } else {\n setStatus(\"error\");\n }\n })\n .catch(() => {\n if (active) setStatus(\"error\");\n });\n return () => {\n active = false;\n };\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":["_Fragment","REDIRECT_DELAY","pendingVerifications","navigateToLogin","baseUrl","url","email","EmailVerifyPage","tr","useTr","searchParams","useSearchParams","token","urlEmail","useAuthOperations","user","useAuthStore","state","status","setStatus","useState","verifiedEmailRef","useRef","isResending","setIsResending","resendSuccess","setResendSuccess","countdown","setCountdown","verificationRef","useEffect","request","verifyEmail","active","result","timer","prev","handleResend","resendEmail","resendVerification","err","handleContinueToLogin","useCallback","handleBackToLogin","AuthLayout","SpinnerIcon","Button","Fragment"],"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\ntype VerificationRequest = ReturnType<\n ReturnType<typeof useAuthOperations>[\"verifyEmail\"]\n>;\n// Share only pending work across remounts; never persist bearer tokens.\nconst pendingVerifications = new Map<string, VerificationRequest>();\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 const verificationRef = useRef<{\n token: string;\n request: VerificationRequest;\n } | null>(null);\n\n useEffect(() => {\n if (!token) return;\n try {\n // Remove the marker left by older versions, without touching other data.\n window.sessionStorage.removeItem(`auth:verifyEmail:inflight:${token}`);\n } catch {\n // Verification must also work when browser storage is unavailable.\n }\n\n if (verificationRef.current?.token !== token) {\n let request = pendingVerifications.get(token);\n if (!request) {\n request = Promise.resolve()\n .then(() => verifyEmail(token))\n .finally(() => pendingVerifications.delete(token));\n pendingVerifications.set(token, request);\n }\n verificationRef.current = { token, request };\n setStatus(\"verifying\");\n }\n\n let active = true;\n verificationRef.current.request\n .then((result) => {\n if (!active) return;\n if (result.success) {\n setStatus(\"success\");\n if (result.email) verifiedEmailRef.current = result.email;\n } else {\n setStatus(\"error\");\n }\n })\n .catch(() => {\n if (active) setStatus(\"error\");\n });\n return () => {\n active = false;\n };\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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"}