@burdenoff/microfe-auth 2026.828.2 → 2026.830.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/AuthRoot.js +43 -41
- package/dist/auth/AuthRoot.js.map +1 -1
- package/dist/auth/components/AuthLayout.js +65 -67
- package/dist/auth/components/AuthLayout.js.map +1 -1
- package/dist/auth/components/CaptchaV2.js +21 -24
- package/dist/auth/components/CaptchaV2.js.map +1 -1
- package/dist/auth/components/ConfirmActionDialog.js +33 -32
- package/dist/auth/components/ConfirmActionDialog.js.map +1 -1
- package/dist/auth/components/profile/UserProfileMenu.js +44 -46
- package/dist/auth/components/profile/UserProfileMenu.js.map +1 -1
- package/dist/auth/components/workspace/WorkspaceSelector.js +62 -68
- package/dist/auth/components/workspace/WorkspaceSelector.js.map +1 -1
- package/dist/auth/pages/AuditLogPage.js +65 -68
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.js +51 -54
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.js +223 -225
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.js +67 -69
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.js +85 -88
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.js +60 -63
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/OAuthCallbackPage.js +37 -40
- package/dist/auth/pages/OAuthCallbackPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.js +102 -105
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.js +54 -57
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.js +171 -174
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SSOClearPage.js +21 -20
- package/dist/auth/pages/SSOClearPage.js.map +1 -1
- package/dist/auth/pages/SSODepositPage.js +27 -26
- package/dist/auth/pages/SSODepositPage.js.map +1 -1
- package/dist/auth/pages/SSORelayCallbackPage.js +19 -18
- package/dist/auth/pages/SSORelayCallbackPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.js +247 -250
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.js +95 -98
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.js +53 -56
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.js +296 -299
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.js +65 -68
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.js +124 -127
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.js +171 -174
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +322 -325
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/shared/hooks/index.d.ts +1 -6
- package/dist/shared/hooks/useTr.d.ts +16 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/dist/translations/en.d.ts +806 -248
- package/dist/translations/en.js +870 -0
- package/dist/translations/en.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceSelector.js","names":[],"sources":["../../../../src/auth/components/workspace/WorkspaceSelector.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useId } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport type { Workspace } from \"../../types\";\n\n/**\n * Organization interface (grouped from workspaces)\n */\nexport interface Organization {\n id: string;\n name: string;\n workspaceCount: number;\n}\n\n/**\n * Project interface (optional, for future use)\n */\nexport interface Project {\n id: string;\n name: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSelectorProps {\n /**\n * Control modal visibility\n */\n isOpen: boolean;\n\n /**\n * Callback when modal is closed without selection\n */\n onClose: () => void;\n\n /**\n * List of workspaces to choose from\n */\n workspaces: Workspace[];\n\n /**\n * Optional list of projects (filtered by workspace)\n */\n projects?: Project[];\n\n /**\n * Callback when workspace is selected and confirmed\n */\n onSelect: (workspace: Workspace, project?: Project) => void;\n\n /**\n * Loading state while issuing workspace token\n */\n isLoading?: boolean;\n\n /**\n * Error message to display\n */\n error?: string | null;\n}\n\n/**\n * WorkspaceSelector Component\n *\n * Multi-level workspace selection modal for use across all Burdenoff products.\n *\n * Features:\n * - Three-tier selection: Organization → Workspace → Project (optional)\n * - Auto-select if only one option at each level\n * - Loading and error states\n * - Clean, accessible UI\n *\n * Usage:\n * ```tsx\n * import { WorkspaceSelector } from '@burdenoff/microfe-auth';\n *\n * <WorkspaceSelector\n * isOpen={showSelector}\n * onClose={() => setShowSelector(false)}\n * workspaces={workspaces}\n * onSelect={async (workspace) => {\n * await issueWorkspaceToken(workspace.id);\n * navigate('/');\n * }}\n * />\n * ```\n */\nconst EMPTY_PROJECTS: readonly Project[] = [];\n\nexport function WorkspaceSelector({\n isOpen,\n onClose,\n workspaces,\n projects = EMPTY_PROJECTS as Project[],\n onSelect,\n isLoading = false,\n error = null,\n}: WorkspaceSelectorProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n // Generated ids for label-control pairing\n const orgSelectId = useId();\n const workspaceSelectId = useId();\n const projectSelectId = useId();\n // Selection state\n const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);\n const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(\n null,\n );\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(\n null,\n );\n\n // Group workspaces by organization\n const organizations = useMemo<Organization[]>(() => {\n const orgMap = new Map<string, { name: string; count: number }>();\n\n workspaces.forEach((workspace) => {\n const orgId = workspace.organizationId || \"default\";\n if (!orgMap.has(orgId)) {\n // Use org ID for name since workspace doesn't have organizationName\n orgMap.set(orgId, {\n name:\n orgId === \"default\"\n ? \"Default\"\n : `Organization ${orgId.slice(0, 8)}`,\n count: 0,\n });\n }\n orgMap.get(orgId)!.count++;\n });\n\n return Array.from(orgMap.entries()).map(([id, { name, count }]) => ({\n id,\n name,\n workspaceCount: count,\n }));\n }, [workspaces]);\n\n // Filter workspaces by selected organization\n const filteredWorkspaces = useMemo(() => {\n if (!selectedOrgId) return workspaces;\n return workspaces.filter(\n (w) => (w.organizationId || \"default\") === selectedOrgId,\n );\n }, [workspaces, selectedOrgId]);\n\n // Filter projects by selected workspace\n const filteredProjects = useMemo(() => {\n if (!selectedWorkspaceId) return [];\n return projects.filter((p) => p.workspaceId === selectedWorkspaceId);\n }, [projects, selectedWorkspaceId]);\n\n // Get selected workspace object\n const selectedWorkspace = useMemo(() => {\n return workspaces.find((w) => w.id === selectedWorkspaceId);\n }, [workspaces, selectedWorkspaceId]);\n\n // Get selected project object\n const selectedProject = useMemo(() => {\n return projects.find((p) => p.id === selectedProjectId);\n }, [projects, selectedProjectId]);\n\n // Auto-select if only one organization\n useEffect(() => {\n if (organizations.length === 1 && !selectedOrgId) {\n setSelectedOrgId(organizations[0].id);\n }\n }, [organizations, selectedOrgId]);\n\n // Auto-select if only one workspace\n useEffect(() => {\n if (filteredWorkspaces.length === 1 && !selectedWorkspaceId) {\n setSelectedWorkspaceId(filteredWorkspaces[0].id);\n }\n }, [filteredWorkspaces, selectedWorkspaceId]);\n\n // Handle organization selection\n const handleOrgChange = (orgId: string) => {\n setSelectedOrgId(orgId);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n };\n\n // Handle workspace selection\n const handleWorkspaceChange = (workspaceId: string) => {\n setSelectedWorkspaceId(workspaceId);\n setSelectedProjectId(null);\n };\n\n // Handle project selection\n const handleProjectChange = (projectId: string) => {\n setSelectedProjectId(projectId);\n };\n\n // Handle continue button\n const handleContinue = () => {\n if (!selectedWorkspace) return;\n onSelect(selectedWorkspace, selectedProject);\n };\n\n // Handle cancel\n const handleCancel = () => {\n setSelectedOrgId(null);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n onClose();\n };\n\n // Check if continue button should be enabled\n const canContinue = selectedWorkspaceId !== null;\n\n if (!isOpen) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-black/50\"\n onClick={handleCancel}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div className=\"relative bg-bg-surface rounded-2xl shadow-xl max-w-2xl w-full mx-4 p-6 space-y-6\">\n {/* Header */}\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-semibold text-text-primary\">\n {tr(\"auth.workspaceSelector.title\", \"Select Your Workspace\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"auth.workspaceSelector.subtitle\",\n \"Choose the workspace you want to work in. You can switch workspaces later from the header.\",\n )}\n </p>\n </div>\n\n {/* Error display */}\n {error && (\n <div className=\"rounded-lg bg-status-error-bg-subtle p-4 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr(\"auth.workspaceSelector.error\", \"Error\")}\n </p>\n <p>{error}</p>\n </div>\n )}\n\n {/* Form */}\n <div className=\"space-y-4\">\n {/* Organization selection */}\n {organizations.length > 1 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={orgSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n Organization\n </span>\n </label>\n <select\n id={orgSelectId}\n value={selectedOrgId || \"\"}\n onChange={(e) => handleOrgChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectOrg\",\n \"Select organization...\",\n )}\n </option>\n {organizations.map((org) => (\n <option key={org.id} value={org.id}>\n {org.name} ({org.workspaceCount} workspace\n {org.workspaceCount !== 1 ? \"s\" : \"\"})\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Workspace selection */}\n <div className=\"space-y-2\">\n <label\n htmlFor={workspaceSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n Workspace\n </span>\n </label>\n <select\n id={workspaceSelectId}\n value={selectedWorkspaceId || \"\"}\n onChange={(e) => handleWorkspaceChange(e.target.value)}\n disabled={!selectedOrgId && organizations.length > 1}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectWorkspace\",\n \"Select workspace...\",\n )}\n </option>\n {filteredWorkspaces.map((workspace) => (\n <option key={workspace.id} value={workspace.id}>\n {workspace.name} ({workspace.slug})\n </option>\n ))}\n </select>\n </div>\n\n {/* Project selection (optional) */}\n {filteredProjects.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={projectSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n Project (Optional)\n </span>\n </label>\n <select\n id={projectSelectId}\n value={selectedProjectId || \"\"}\n onChange={(e) => handleProjectChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.noProject\",\n \"No project selected\",\n )}\n </option>\n {filteredProjects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Selection summary */}\n {selectedWorkspace && (\n <div className=\"rounded-lg bg-bg-sunken p-4 space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.workspaceSelector.selected\", \"Selected:\")}\n </p>\n <div className=\"text-sm text-text-secondary space-y-1\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n <span>{selectedWorkspace.name}</span>\n </div>\n {selectedProject && (\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n <span>{selectedProject.name}</span>\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-end gap-3 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-sunken rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!canContinue || isLoading}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isLoading\n ? tr(\"auth.workspaceSelector.loading\", \"Loading...\")\n : tr(\"auth.workspaceSelector.continue\", \"Continue\")}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkspaceSelector.js","names":[],"sources":["../../../../src/auth/components/workspace/WorkspaceSelector.tsx"],"sourcesContent":["import { useState, useMemo, useEffect, useId } from \"react\";\nimport { useTr } from \"../../../shared/hooks/useTr\";\nimport type { Workspace } from \"../../types\";\n\n/**\n * Organization interface (grouped from workspaces)\n */\nexport interface Organization {\n id: string;\n name: string;\n workspaceCount: number;\n}\n\n/**\n * Project interface (optional, for future use)\n */\nexport interface Project {\n id: string;\n name: string;\n workspaceId: string;\n}\n\nexport interface WorkspaceSelectorProps {\n /**\n * Control modal visibility\n */\n isOpen: boolean;\n\n /**\n * Callback when modal is closed without selection\n */\n onClose: () => void;\n\n /**\n * List of workspaces to choose from\n */\n workspaces: Workspace[];\n\n /**\n * Optional list of projects (filtered by workspace)\n */\n projects?: Project[];\n\n /**\n * Callback when workspace is selected and confirmed\n */\n onSelect: (workspace: Workspace, project?: Project) => void;\n\n /**\n * Loading state while issuing workspace token\n */\n isLoading?: boolean;\n\n /**\n * Error message to display\n */\n error?: string | null;\n}\n\n/**\n * WorkspaceSelector Component\n *\n * Multi-level workspace selection modal for use across all Burdenoff products.\n *\n * Features:\n * - Three-tier selection: Organization → Workspace → Project (optional)\n * - Auto-select if only one option at each level\n * - Loading and error states\n * - Clean, accessible UI\n *\n * Usage:\n * ```tsx\n * import { WorkspaceSelector } from '@burdenoff/microfe-auth';\n *\n * <WorkspaceSelector\n * isOpen={showSelector}\n * onClose={() => setShowSelector(false)}\n * workspaces={workspaces}\n * onSelect={async (workspace) => {\n * await issueWorkspaceToken(workspace.id);\n * navigate('/');\n * }}\n * />\n * ```\n */\nconst EMPTY_PROJECTS: readonly Project[] = [];\n\nexport function WorkspaceSelector({\n isOpen,\n onClose,\n workspaces,\n projects = EMPTY_PROJECTS as Project[],\n onSelect,\n isLoading = false,\n error = null,\n}: WorkspaceSelectorProps) {\n const tr = useTr();\n\n // Generated ids for label-control pairing\n const orgSelectId = useId();\n const workspaceSelectId = useId();\n const projectSelectId = useId();\n // Selection state\n const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);\n const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string | null>(\n null,\n );\n const [selectedProjectId, setSelectedProjectId] = useState<string | null>(\n null,\n );\n\n // Group workspaces by organization\n const organizations = useMemo<Organization[]>(() => {\n const orgMap = new Map<string, { name: string; count: number }>();\n\n workspaces.forEach((workspace) => {\n const orgId = workspace.organizationId || \"default\";\n if (!orgMap.has(orgId)) {\n // Use org ID for name since workspace doesn't have organizationName\n orgMap.set(orgId, {\n name:\n orgId === \"default\"\n ? tr(\"auth.workspaceSelector.defaultOrg\", \"Default\")\n : `${tr(\"auth.workspaceSelector.organizationPrefix\", \"Organization\")} ${orgId.slice(0, 8)}`,\n count: 0,\n });\n }\n orgMap.get(orgId)!.count++;\n });\n\n return Array.from(orgMap.entries()).map(([id, { name, count }]) => ({\n id,\n name,\n workspaceCount: count,\n }));\n }, [workspaces, tr]);\n\n // Filter workspaces by selected organization\n const filteredWorkspaces = useMemo(() => {\n if (!selectedOrgId) return workspaces;\n return workspaces.filter(\n (w) => (w.organizationId || \"default\") === selectedOrgId,\n );\n }, [workspaces, selectedOrgId]);\n\n // Filter projects by selected workspace\n const filteredProjects = useMemo(() => {\n if (!selectedWorkspaceId) return [];\n return projects.filter((p) => p.workspaceId === selectedWorkspaceId);\n }, [projects, selectedWorkspaceId]);\n\n // Get selected workspace object\n const selectedWorkspace = useMemo(() => {\n return workspaces.find((w) => w.id === selectedWorkspaceId);\n }, [workspaces, selectedWorkspaceId]);\n\n // Get selected project object\n const selectedProject = useMemo(() => {\n return projects.find((p) => p.id === selectedProjectId);\n }, [projects, selectedProjectId]);\n\n // Auto-select if only one organization\n useEffect(() => {\n if (organizations.length === 1 && !selectedOrgId) {\n setSelectedOrgId(organizations[0].id);\n }\n }, [organizations, selectedOrgId]);\n\n // Auto-select if only one workspace\n useEffect(() => {\n if (filteredWorkspaces.length === 1 && !selectedWorkspaceId) {\n setSelectedWorkspaceId(filteredWorkspaces[0].id);\n }\n }, [filteredWorkspaces, selectedWorkspaceId]);\n\n // Handle organization selection\n const handleOrgChange = (orgId: string) => {\n setSelectedOrgId(orgId);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n };\n\n // Handle workspace selection\n const handleWorkspaceChange = (workspaceId: string) => {\n setSelectedWorkspaceId(workspaceId);\n setSelectedProjectId(null);\n };\n\n // Handle project selection\n const handleProjectChange = (projectId: string) => {\n setSelectedProjectId(projectId);\n };\n\n // Handle continue button\n const handleContinue = () => {\n if (!selectedWorkspace) return;\n onSelect(selectedWorkspace, selectedProject);\n };\n\n // Handle cancel\n const handleCancel = () => {\n setSelectedOrgId(null);\n setSelectedWorkspaceId(null);\n setSelectedProjectId(null);\n onClose();\n };\n\n // Check if continue button should be enabled\n const canContinue = selectedWorkspaceId !== null;\n\n if (!isOpen) return null;\n\n return (\n <div className=\"fixed inset-0 z-50 flex items-center justify-center\">\n {/* Backdrop */}\n <div\n className=\"absolute inset-0 bg-black/50\"\n onClick={handleCancel}\n aria-hidden=\"true\"\n />\n\n {/* Modal */}\n <div className=\"relative bg-bg-surface rounded-2xl shadow-xl max-w-2xl w-full mx-4 p-6 space-y-6\">\n {/* Header */}\n <div className=\"space-y-2\">\n <h2 className=\"text-2xl font-semibold text-text-primary\">\n {tr(\"auth.workspaceSelector.title\", \"Select Your Workspace\")}\n </h2>\n <p className=\"text-sm text-text-secondary\">\n {tr(\n \"auth.workspaceSelector.subtitle\",\n \"Choose the workspace you want to work in. You can switch workspaces later from the header.\",\n )}\n </p>\n </div>\n\n {/* Error display */}\n {error && (\n <div className=\"rounded-lg bg-status-error-bg-subtle p-4 text-sm text-status-error-text\">\n <p className=\"font-medium\">\n {tr(\"auth.workspaceSelector.error\", \"Error\")}\n </p>\n <p>{error}</p>\n </div>\n )}\n\n {/* Form */}\n <div className=\"space-y-4\">\n {/* Organization selection */}\n {organizations.length > 1 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={orgSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4\"\n />\n </svg>\n {tr(\"auth.workspaceSelector.organization\", \"Organization\")}\n </span>\n </label>\n <select\n id={orgSelectId}\n value={selectedOrgId || \"\"}\n onChange={(e) => handleOrgChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectOrg\",\n \"Select organization...\",\n )}\n </option>\n {organizations.map((org) => (\n <option key={org.id} value={org.id}>\n {tr(\n \"auth.workspaceSelector.orgOption\",\n \"{{name}} ({{count}} workspace{{plural}})\",\n {\n name: org.name,\n count: org.workspaceCount,\n plural: org.workspaceCount !== 1 ? \"s\" : \"\",\n },\n )}\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Workspace selection */}\n <div className=\"space-y-2\">\n <label\n htmlFor={workspaceSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n {tr(\"auth.workspaceSelector.workspace\", \"Workspace\")}\n </span>\n </label>\n <select\n id={workspaceSelectId}\n value={selectedWorkspaceId || \"\"}\n onChange={(e) => handleWorkspaceChange(e.target.value)}\n disabled={!selectedOrgId && organizations.length > 1}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.selectWorkspace\",\n \"Select workspace...\",\n )}\n </option>\n {filteredWorkspaces.map((workspace) => (\n <option key={workspace.id} value={workspace.id}>\n {workspace.name} ({workspace.slug})\n </option>\n ))}\n </select>\n </div>\n\n {/* Project selection (optional) */}\n {filteredProjects.length > 0 && (\n <div className=\"space-y-2\">\n <label\n htmlFor={projectSelectId}\n className=\"block text-sm font-medium text-text-secondary\"\n >\n <span className=\"flex items-center gap-2\">\n <svg\n className=\"size-4\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n aria-hidden=\"true\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n {tr(\n \"auth.workspaceSelector.projectOptional\",\n \"Project (Optional)\",\n )}\n </span>\n </label>\n <select\n id={projectSelectId}\n value={selectedProjectId || \"\"}\n onChange={(e) => handleProjectChange(e.target.value)}\n className=\"w-full rounded-lg border border-border-default bg-bg-surface px-4 py-2.5 text-sm text-text-primary focus:border-primary focus:ring-2 focus:ring-primary/20\"\n >\n <option value=\"\">\n {tr(\n \"auth.workspaceSelector.noProject\",\n \"No project selected\",\n )}\n </option>\n {filteredProjects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n )}\n\n {/* Selection summary */}\n {selectedWorkspace && (\n <div className=\"rounded-lg bg-bg-sunken p-4 space-y-2\">\n <p className=\"text-sm font-medium text-text-primary\">\n {tr(\"auth.workspaceSelector.selected\", \"Selected:\")}\n </p>\n <div className=\"text-sm text-text-secondary space-y-1\">\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z\"\n />\n </svg>\n <span>{selectedWorkspace.name}</span>\n </div>\n {selectedProject && (\n <div className=\"flex items-center gap-2\">\n <svg\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z\"\n />\n </svg>\n <span>{selectedProject.name}</span>\n </div>\n )}\n </div>\n </div>\n )}\n </div>\n\n {/* Footer */}\n <div className=\"flex items-center justify-end gap-3 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={handleCancel}\n disabled={isLoading}\n className=\"px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-sunken rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {tr(\"auth.workspaceSelector.cancel\", \"Cancel\")}\n </button>\n <button\n type=\"button\"\n onClick={handleContinue}\n disabled={!canContinue || isLoading}\n className=\"px-4 py-2 text-sm font-medium text-action-primary-text bg-action-primary-bg hover:bg-action-primary-bg/90 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {isLoading\n ? tr(\"auth.workspaceSelector.loading\", \"Loading...\")\n : tr(\"auth.workspaceSelector.continue\", \"Continue\")}\n </button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
2
|
+
import { useAuthAuditLogsQuery as t } from "../../generated/global-operations.js";
|
|
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";
|
|
6
|
+
import { useCreateAuthExportJobMutation as o } from "../../generated/wspace-operations.js";
|
|
7
|
+
import { useState as s } from "react";
|
|
8
8
|
import { Fragment as c, jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
9
9
|
import { useFeatureFlagDetails as d } from "@burdenoff/fe-libs/shared/feature-flags";
|
|
10
10
|
import { isQuotaExhaustedError as f } from "@burdenoff/fe-libs/shared/utils";
|
|
@@ -13,20 +13,17 @@ import { useGatewayClient as g } from "@burdenoff/fe-libs/shared/graphql";
|
|
|
13
13
|
//#region src/auth/pages/AuditLogPage.tsx
|
|
14
14
|
var _ = 20;
|
|
15
15
|
function v() {
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
return n === e ? t : n;
|
|
19
|
-
}, [b, x] = o(1), S = (b - 1) * _, C = d("auth.audit-log")?.value === !0, { data: w, loading: T, error: E } = e({
|
|
20
|
-
skip: !C,
|
|
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({
|
|
17
|
+
skip: !S,
|
|
21
18
|
fetchPolicy: "cache-and-network",
|
|
22
19
|
variables: { pagination: {
|
|
23
20
|
limit: _,
|
|
24
|
-
offset:
|
|
21
|
+
offset: x
|
|
25
22
|
} }
|
|
26
|
-
}),
|
|
27
|
-
|
|
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);
|
|
28
25
|
try {
|
|
29
|
-
await
|
|
26
|
+
await O({ variables: { input: {
|
|
30
27
|
name: "Audit Log Export",
|
|
31
28
|
entityTypes: ["permission_audit_logs"],
|
|
32
29
|
format: "CSV",
|
|
@@ -35,12 +32,12 @@ function v() {
|
|
|
35
32
|
} } });
|
|
36
33
|
} catch (e) {
|
|
37
34
|
if (f(e)) {
|
|
38
|
-
|
|
35
|
+
N(!0), j(v("auth.auditLog.exportQuotaReached", "You have reached your export limit for this month."));
|
|
39
36
|
return;
|
|
40
37
|
}
|
|
41
|
-
|
|
38
|
+
j(e instanceof Error ? e.message : "Failed to start export");
|
|
42
39
|
}
|
|
43
|
-
},
|
|
40
|
+
}, F = (e) => {
|
|
44
41
|
switch (e) {
|
|
45
42
|
case "LOGIN_SUCCESS": return "OK";
|
|
46
43
|
case "LOGIN_FAILURE": return "ER";
|
|
@@ -52,24 +49,24 @@ function v() {
|
|
|
52
49
|
default: return "AU";
|
|
53
50
|
}
|
|
54
51
|
};
|
|
55
|
-
return
|
|
52
|
+
return S ? /* @__PURE__ */ u("div", {
|
|
56
53
|
className: "max-w-4xl mx-auto p-6",
|
|
57
54
|
children: [
|
|
58
55
|
/* @__PURE__ */ u("div", {
|
|
59
56
|
className: "flex items-center justify-between mb-8",
|
|
60
57
|
children: [/* @__PURE__ */ l("h1", {
|
|
61
58
|
className: "text-3xl font-semibold text-text-primary",
|
|
62
|
-
children:
|
|
59
|
+
children: v("auth.auditLog.title", "Security Audit Log")
|
|
63
60
|
}), /* @__PURE__ */ l("button", {
|
|
64
61
|
type: "button",
|
|
65
|
-
onClick: () => void
|
|
66
|
-
disabled:
|
|
67
|
-
"aria-label":
|
|
62
|
+
onClick: () => void P(),
|
|
63
|
+
disabled: k,
|
|
64
|
+
"aria-label": v("auth.auditLog.exportAriaLabel", "Export audit log"),
|
|
68
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",
|
|
69
|
-
children:
|
|
66
|
+
children: k ? /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l(r, {
|
|
70
67
|
className: "size-4 animate-spin",
|
|
71
68
|
"aria-hidden": "true"
|
|
72
|
-
}),
|
|
69
|
+
}), v("auth.auditLog.exporting", "Exporting...")] }) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("svg", {
|
|
73
70
|
className: "size-4",
|
|
74
71
|
fill: "none",
|
|
75
72
|
viewBox: "0 0 24 24",
|
|
@@ -81,74 +78,74 @@ function v() {
|
|
|
81
78
|
strokeWidth: 2,
|
|
82
79
|
d: "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
|
83
80
|
})
|
|
84
|
-
}),
|
|
81
|
+
}), v("auth.auditLog.export", "Export")] })
|
|
85
82
|
})]
|
|
86
83
|
}),
|
|
87
84
|
/* @__PURE__ */ l(h, {
|
|
88
85
|
className: "mb-6",
|
|
89
|
-
children:
|
|
86
|
+
children: v("auth.auditLog.purpose", "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.")
|
|
90
87
|
}),
|
|
91
88
|
/* @__PURE__ */ u(p, {
|
|
92
89
|
className: "mb-6",
|
|
93
90
|
children: [/* @__PURE__ */ l("p", {
|
|
94
91
|
className: "text-sm font-medium text-text-primary",
|
|
95
|
-
children:
|
|
92
|
+
children: v("auth.auditLog.emphasisTitle", "What this log captures")
|
|
96
93
|
}), /* @__PURE__ */ l("p", {
|
|
97
94
|
className: "mt-1 text-sm text-text-secondary",
|
|
98
|
-
children:
|
|
95
|
+
children: v("auth.auditLog.emphasisBody", "Successful and failed sign-ins, logouts, password and two-factor changes, and detected anomalies — newest first.")
|
|
99
96
|
})]
|
|
100
97
|
}),
|
|
101
|
-
|
|
98
|
+
A && /* @__PURE__ */ u("div", {
|
|
102
99
|
role: "alert",
|
|
103
100
|
className: "mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border",
|
|
104
|
-
children: [/* @__PURE__ */ l(
|
|
101
|
+
children: [/* @__PURE__ */ l(n, {
|
|
105
102
|
className: "size-5 text-status-error-text flex-shrink-0 mt-0.5",
|
|
106
103
|
"aria-hidden": "true"
|
|
107
104
|
}), /* @__PURE__ */ l("p", {
|
|
108
105
|
className: "text-sm text-status-error-text",
|
|
109
|
-
children:
|
|
106
|
+
children: A
|
|
110
107
|
})]
|
|
111
108
|
}),
|
|
112
|
-
/* @__PURE__ */ l(
|
|
113
|
-
open:
|
|
114
|
-
onOpenChange:
|
|
115
|
-
title:
|
|
116
|
-
description:
|
|
117
|
-
confirmText:
|
|
118
|
-
cancelText:
|
|
109
|
+
/* @__PURE__ */ l(a, {
|
|
110
|
+
open: M,
|
|
111
|
+
onOpenChange: N,
|
|
112
|
+
title: v("auth.auditLog.exportQuotaTitle", "Export Limit Reached"),
|
|
113
|
+
description: v("auth.auditLog.exportQuotaDescription", "You have reached your export limit for this month."),
|
|
114
|
+
confirmText: v("auth.auditLog.exportQuotaViewBilling", "View Billing"),
|
|
115
|
+
cancelText: v("auth.auditLog.exportQuotaDismiss", "Dismiss"),
|
|
119
116
|
variant: "warning",
|
|
120
117
|
onConfirm: () => window.location.assign("/billing")
|
|
121
118
|
}),
|
|
122
|
-
|
|
119
|
+
w ? /* @__PURE__ */ u("div", {
|
|
123
120
|
"aria-live": "polite",
|
|
124
121
|
"aria-busy": "true",
|
|
125
122
|
className: "text-center py-12",
|
|
126
|
-
children: [/* @__PURE__ */ l(
|
|
123
|
+
children: [/* @__PURE__ */ l(r, {
|
|
127
124
|
className: "size-8 mx-auto text-text-link animate-spin",
|
|
128
125
|
"aria-hidden": "true"
|
|
129
126
|
}), /* @__PURE__ */ l("p", {
|
|
130
127
|
className: "mt-3 text-text-secondary",
|
|
131
|
-
children:
|
|
128
|
+
children: v("auth.auditLog.loading", "Loading audit events...")
|
|
132
129
|
})]
|
|
133
|
-
}) :
|
|
130
|
+
}) : T ? /* @__PURE__ */ l("div", {
|
|
134
131
|
role: "alert",
|
|
135
132
|
children: /* @__PURE__ */ l(m, {
|
|
136
133
|
illustration: "empty-generic",
|
|
137
|
-
title:
|
|
138
|
-
description:
|
|
134
|
+
title: v("auth.auditLog.error", "Unable to load audit events"),
|
|
135
|
+
description: v("auth.auditLog.errorDesc", "Something went wrong fetching your security events. Please try again in a moment.")
|
|
139
136
|
})
|
|
140
|
-
}) :
|
|
137
|
+
}) : E.length === 0 ? /* @__PURE__ */ l("output", {
|
|
141
138
|
className: "block",
|
|
142
139
|
children: /* @__PURE__ */ l(m, {
|
|
143
140
|
illustration: "empty-notifications",
|
|
144
|
-
title:
|
|
145
|
-
description:
|
|
141
|
+
title: v("auth.auditLog.noEvents", "No security events recorded"),
|
|
142
|
+
description: v("auth.auditLog.noEventsDesc", "As you sign in and change security settings, those events will appear here.")
|
|
146
143
|
})
|
|
147
144
|
}) : /* @__PURE__ */ u(c, { children: [/* @__PURE__ */ l("div", {
|
|
148
145
|
className: "space-y-4",
|
|
149
146
|
"aria-live": "polite",
|
|
150
|
-
"aria-busy":
|
|
151
|
-
children:
|
|
147
|
+
"aria-busy": w,
|
|
148
|
+
children: E.map((e) => /* @__PURE__ */ l("div", {
|
|
152
149
|
className: "p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm",
|
|
153
150
|
children: /* @__PURE__ */ u("div", {
|
|
154
151
|
className: "flex items-start gap-4",
|
|
@@ -156,7 +153,7 @@ function v() {
|
|
|
156
153
|
/* @__PURE__ */ l("div", {
|
|
157
154
|
className: "flex size-10 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-xs font-semibold text-text-secondary",
|
|
158
155
|
"aria-hidden": "true",
|
|
159
|
-
children:
|
|
156
|
+
children: F(e.action)
|
|
160
157
|
}),
|
|
161
158
|
/* @__PURE__ */ u("div", {
|
|
162
159
|
className: "flex-1",
|
|
@@ -169,48 +166,48 @@ function v() {
|
|
|
169
166
|
className: "text-sm text-text-secondary mb-2",
|
|
170
167
|
children: /* @__PURE__ */ l("time", {
|
|
171
168
|
dateTime: e.createdAt,
|
|
172
|
-
children:
|
|
169
|
+
children: i(e.createdAt)
|
|
173
170
|
})
|
|
174
171
|
}),
|
|
175
172
|
/* @__PURE__ */ u("p", {
|
|
176
173
|
className: "text-sm text-text-secondary",
|
|
177
|
-
children: [e.description ??
|
|
174
|
+
children: [e.description ?? v("auth.auditLog.defaultDescription", "Authentication event"), e.ipAddress && ` • ${e.ipAddress}`]
|
|
178
175
|
})
|
|
179
176
|
]
|
|
180
177
|
}),
|
|
181
178
|
e.action.includes("FAILURE") && /* @__PURE__ */ l("output", {
|
|
182
179
|
className: "px-3 py-1 rounded-full bg-status-error-bg-subtle text-status-error-text text-sm font-medium",
|
|
183
|
-
children:
|
|
180
|
+
children: v("auth.auditLog.failed", "Failed")
|
|
184
181
|
})
|
|
185
182
|
]
|
|
186
183
|
})
|
|
187
184
|
}, e.id))
|
|
188
185
|
}), /* @__PURE__ */ u("nav", {
|
|
189
|
-
"aria-label":
|
|
186
|
+
"aria-label": v("auth.auditLog.paginationAriaLabel", "Audit log pagination"),
|
|
190
187
|
className: "mt-6 flex items-center justify-between",
|
|
191
188
|
children: [/* @__PURE__ */ u("p", {
|
|
192
189
|
className: "text-sm text-text-secondary",
|
|
193
190
|
children: [
|
|
194
|
-
|
|
191
|
+
v("auth.auditLog.page", "Page"),
|
|
195
192
|
" ",
|
|
196
|
-
|
|
193
|
+
y
|
|
197
194
|
]
|
|
198
195
|
}), /* @__PURE__ */ u("div", {
|
|
199
196
|
className: "flex gap-2",
|
|
200
197
|
children: [/* @__PURE__ */ l("button", {
|
|
201
198
|
type: "button",
|
|
202
|
-
onClick: () =>
|
|
203
|
-
disabled:
|
|
204
|
-
"aria-label":
|
|
199
|
+
onClick: () => b((e) => Math.max(1, e - 1)),
|
|
200
|
+
disabled: y === 1,
|
|
201
|
+
"aria-label": v("auth.auditLog.previousPageAriaLabel", "Go to previous page"),
|
|
205
202
|
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",
|
|
206
|
-
children:
|
|
203
|
+
children: v("auth.auditLog.previous", "Previous")
|
|
207
204
|
}), /* @__PURE__ */ l("button", {
|
|
208
205
|
type: "button",
|
|
209
|
-
onClick: () =>
|
|
210
|
-
disabled: !
|
|
211
|
-
"aria-label":
|
|
206
|
+
onClick: () => b((e) => e + 1),
|
|
207
|
+
disabled: !D,
|
|
208
|
+
"aria-label": v("auth.auditLog.nextPageAriaLabel", "Go to next page"),
|
|
212
209
|
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",
|
|
213
|
-
children:
|
|
210
|
+
children: v("auth.auditLog.next", "Next")
|
|
214
211
|
})]
|
|
215
212
|
})]
|
|
216
213
|
})] })
|
|
@@ -219,8 +216,8 @@ function v() {
|
|
|
219
216
|
className: "max-w-4xl mx-auto p-6",
|
|
220
217
|
children: /* @__PURE__ */ l(m, {
|
|
221
218
|
illustration: "empty-generic",
|
|
222
|
-
title:
|
|
223
|
-
description:
|
|
219
|
+
title: v("auth.auditLog.notAvailable", "Page Not Available"),
|
|
220
|
+
description: v("auth.auditLog.notEnabled", "The Security Audit Log is not enabled for your account.")
|
|
224
221
|
})
|
|
225
222
|
});
|
|
226
223
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogPage.js","names":[],"sources":["../../../src/auth/pages/AuditLogPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\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 { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\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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"}
|