@burdenoff/microfe-auth 2026.917.1 → 2026.925.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/pages/UserProfilePage.js +188 -171
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +199 -182
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/auth/utils/legalUrls.d.ts +1 -1
- package/dist/auth/utils/legalUrls.js.map +1 -1
- package/dist/auth/utils/ssoOrigins.js +1 -1
- package/dist/auth/utils/ssoOrigins.js.map +1 -1
- package/dist/auth/utils/validation.d.ts +19 -0
- package/dist/auth/utils/validation.js +20 -1
- package/dist/auth/utils/validation.js.map +1 -1
- package/dist/microfe-auth.css +1 -1
- package/dist/translations/en.d.ts +2 -0
- package/dist/translations/en.js +2 -0
- package/dist/translations/en.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsersPage.js","names":[],"sources":["../../../src/auth/pages/UsersPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useId } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useAuthPermissions } from \"../hooks/useAuthPermissions\";\nimport { useUserOperations } from \"../hooks/useUserOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { validateUserFields } from \"../utils/validation\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport type {\n User,\n UserRole,\n UserStatus,\n UserFilterInput,\n PaginationInput,\n} from \"../types\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\ntype ViewMode = \"list\" | \"edit\" | \"view\";\n\ninterface FormData {\n username: string;\n firstName: string;\n lastName: string;\n name: string;\n phoneNumber: string;\n role: UserRole;\n status: UserStatus;\n emailVerified: boolean;\n}\n\n// SUPER_ADMIN is determined by email allowlist at runtime, not stored as a\n// DB role — filtering by it always returns 0 rows, so it is excluded here.\nconst USER_ROLES: UserRole[] = [\n \"TENANT_ADMIN\",\n \"ORG_ADMIN\",\n \"WORKSPACE_ADMIN\",\n \"MEMBER\",\n \"GUEST\",\n];\n\nconst USER_STATUSES: UserStatus[] = [\n \"ACTIVE\",\n \"INACTIVE\",\n \"SUSPENDED\",\n \"DELETED\",\n];\n\n/**\n * Users CRUDL page for managing users (admin only)\n */\nexport function UsersPage() {\n const tr = useTr();\n const navigate = useNavigate();\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const currentUser = useAuthStore((state) => state.user);\n const {\n canViewUsers,\n canUpdateUsers,\n canDeleteUsers,\n isLoading: permissionsLoading,\n } = useAuthPermissions();\n const { listUsers, updateUser, deleteUser, hardDeleteUser, isLoading } =\n useUserOperations();\n\n // Stable IDs for ARIA associations (accessibility)\n const searchId = useId();\n const roleFilterId = useId();\n const statusFilterId = useId();\n const errorId = useId();\n const successId = useId();\n\n const [isExporting, setIsExporting] = useState(false);\n\n const handleExport = async () => {\n setIsExporting(true);\n try {\n const filter: UserFilterInput = {};\n if (searchQuery) filter.search = searchQuery;\n if (filterRole) filter.role = filterRole;\n if (filterStatus) filter.status = filterStatus;\n\n const result = await listUsers(filter, { offset: 0, limit: 10000 });\n const rows = result.users;\n\n const headers = [\n \"ID\",\n \"Email\",\n \"Username\",\n \"First Name\",\n \"Last Name\",\n \"Phone\",\n \"Role\",\n \"Status\",\n \"Email Verified\",\n \"MFA Enabled\",\n \"Last Login\",\n \"Created At\",\n ];\n\n const escape = (v: string | undefined | null) => {\n if (v == null) return \"\";\n const s = String(v);\n return s.includes(\",\") || s.includes('\"') || s.includes(\"\\n\")\n ? `\"${s.replace(/\"/g, '\"\"')}\"`\n : s;\n };\n\n const lines = [\n headers.join(\",\"),\n ...rows.map((u) =>\n [\n escape(u.id),\n escape(u.email),\n escape(u.username),\n escape(u.firstName),\n escape(u.lastName),\n escape(u.phoneNumber),\n escape(u.role),\n escape(u.status),\n escape(String(u.emailVerified)),\n escape(String(u.mfaEnabled)),\n escape(u.lastLoginAt),\n escape(u.createdAt),\n ].join(\",\"),\n ),\n ];\n\n const blob = new Blob([lines.join(\"\\n\")], {\n type: \"text/csv;charset=utf-8;\",\n });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `users-${new Date().toISOString().slice(0, 10)}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to export users\");\n } finally {\n setIsExporting(false);\n }\n };\n\n const [users, setUsers] = useState<User[]>([]);\n const [totalCount, setTotalCount] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [viewMode, setViewMode] = useState<ViewMode>(\"list\");\n const [selectedUser, setSelectedUser] = useState<User | null>(null);\n const [formData, setFormData] = useState<FormData>({\n username: \"\",\n firstName: \"\",\n lastName: \"\",\n name: \"\",\n phoneNumber: \"\",\n role: \"MEMBER\",\n status: \"ACTIVE\",\n emailVerified: false,\n });\n const [error, setError] = useState<string | null>(null);\n const [isUnauthorized, setIsUnauthorized] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingAction, setPendingAction] = useState<{\n type: \"deactivate\" | \"hard-delete\";\n user: User;\n } | null>(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [filterRole, setFilterRole] = useState<UserRole | \"\">(\"\");\n const [filterStatus, setFilterStatus] = useState<UserStatus | \"\">(\"\");\n const [currentPage, setCurrentPage] = useState(0);\n const pageSize = 10;\n\n const fetchUsers = useCallback(async () => {\n if (!isAuthenticated) return;\n\n try {\n setIsUnauthorized(false);\n const filter: UserFilterInput = {};\n if (searchQuery) filter.search = searchQuery;\n if (filterRole) filter.role = filterRole;\n if (filterStatus) filter.status = filterStatus;\n\n const pagination: PaginationInput = {\n offset: currentPage * pageSize,\n limit: pageSize,\n };\n\n const result = await listUsers(filter, pagination);\n setUsers(result.users);\n setTotalCount(result.totalCount);\n setHasMore(result.hasMore);\n setError(null);\n } catch (err) {\n // Ignore abort errors - they happen during component unmount/re-render\n if (\n err instanceof Error &&\n (err.name === \"AbortError\" || err.message.includes(\"aborted\"))\n ) {\n return;\n }\n const errorMessage =\n err instanceof Error ? err.message : \"Failed to fetch users\";\n // Check if it's an authorization error\n if (\n errorMessage.toLowerCase().includes(\"not authorized\") ||\n errorMessage.toLowerCase().includes(\"forbidden\")\n ) {\n setIsUnauthorized(true);\n setError(null);\n } else {\n setError(errorMessage);\n }\n }\n }, [\n listUsers,\n searchQuery,\n filterRole,\n filterStatus,\n currentPage,\n isAuthenticated,\n ]);\n\n useEffect(() => {\n fetchUsers();\n }, [fetchUsers]);\n\n const resetForm = () => {\n setFormData({\n username: \"\",\n firstName: \"\",\n lastName: \"\",\n name: \"\",\n phoneNumber: \"\",\n role: \"MEMBER\",\n status: \"ACTIVE\",\n emailVerified: false,\n });\n setSelectedUser(null);\n setError(null);\n setSuccessMessage(null);\n };\n\n const handleUpdate = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!selectedUser) return;\n setError(null);\n\n // BOFF-7214: same name/username invariants the signup form enforces\n // must hold on user edit too, otherwise an admin (or anyone with\n // users:update) can re-introduce a numbers-only account. Same\n // validator the register form uses, same canonicalised output.\n // Pass `requireAll: false` so an existing user whose optional\n // `firstName`/`lastName` is unset (e.g. provisioned via IdP) can\n // still update unrelated fields like phone or role (Codex P2 review).\n const validation = validateUserFields(formData, { requireAll: false });\n if (!validation.valid) {\n setError(\n validation.errors\n .map((e) => {\n const key = `auth.users.${e.field}_${e.code}`;\n return tr(key, e.message);\n })\n .join(\" · \"),\n );\n return;\n }\n\n try {\n await updateUser(selectedUser.id, {\n username: validation.values.username || undefined,\n firstName: validation.values.firstName || undefined,\n lastName: validation.values.lastName || undefined,\n name: formData.name || undefined,\n phoneNumber: formData.phoneNumber || undefined,\n role: formData.role,\n status: formData.status,\n emailVerified: formData.emailVerified,\n });\n setSuccessMessage(\n tr(\"auth.users.updateSuccess\", \"User updated successfully!\"),\n );\n setViewMode(\"list\");\n resetForm();\n fetchUsers();\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to update user\");\n }\n };\n\n const handleDelete = async (user: User) => {\n void nativeImpact(\"medium\");\n try {\n const result = await deleteUser(user.id);\n if (result.success) {\n void nativeNotify(\"success\");\n setSuccessMessage(\n tr(\n \"auth.users.userDeactivated\",\n 'User \"{email}\" has been deactivated.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n fetchUsers();\n } else {\n void nativeNotify(\"error\");\n setError(result.message);\n }\n } catch (err) {\n void nativeNotify(\"error\");\n setError(err instanceof Error ? err.message : \"Failed to delete user\");\n }\n };\n\n const handleHardDelete = async (user: User) => {\n try {\n const result = await hardDeleteUser(user.id);\n if (result.success) {\n setSuccessMessage(\n tr(\n \"auth.users.userPermanentlyDeleted\",\n 'User \"{email}\" has been permanently deleted.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n fetchUsers();\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to hard delete user\",\n );\n }\n };\n\n const startEdit = (user: User) => {\n setSelectedUser(user);\n setFormData({\n username: user.username || \"\",\n firstName: user.firstName || \"\",\n lastName: user.lastName || \"\",\n name: user.name || \"\",\n phoneNumber: user.phoneNumber || \"\",\n role: user.role,\n status: user.status,\n emailVerified: user.emailVerified,\n });\n setViewMode(\"edit\");\n setError(null);\n };\n\n const pendingActionDialog = (\n <ConfirmActionDialog\n open={pendingAction !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingAction(null);\n }\n }}\n title={\n pendingAction?.type === \"hard-delete\"\n ? tr(\"auth.users.hardDeleteTitle\", \"Permanently delete user?\")\n : tr(\"auth.users.deactivateTitle\", \"Deactivate user?\")\n }\n description={\n pendingAction?.type === \"hard-delete\"\n ? pendingAction\n ? tr(\n \"auth.users.hardDeleteDesc\",\n 'This permanently deletes \"{email}\" and cannot be undone.',\n ).replace(\"{email}\", pendingAction.user.email)\n : \"\"\n : pendingAction\n ? tr(\n \"auth.users.deactivateDesc\",\n '\"{email}\" will be prevented from signing in until reactivated.',\n ).replace(\"{email}\", pendingAction.user.email)\n : \"\"\n }\n confirmText={\n pendingAction?.type === \"hard-delete\"\n ? tr(\"auth.users.hardDeleteConfirm\", \"Delete permanently\")\n : tr(\"auth.users.deactivateConfirm\", \"Deactivate user\")\n }\n variant={pendingAction?.type === \"hard-delete\" ? \"danger\" : \"warning\"}\n onConfirm={async () => {\n if (!pendingAction) return;\n if (pendingAction.type === \"hard-delete\") {\n await handleHardDelete(pendingAction.user);\n return;\n }\n await handleDelete(pendingAction.user);\n }}\n />\n );\n\n const getRoleColor = (role: UserRole) => {\n switch (role) {\n case \"SUPER_ADMIN\":\n return \"bg-accent-purple-subtle text-accent-purple\";\n case \"TENANT_ADMIN\":\n return \"bg-status-info-bg-subtle text-status-info-text\";\n case \"ORG_ADMIN\":\n return \"bg-accent-indigo-subtle text-accent-indigo\";\n case \"WORKSPACE_ADMIN\":\n return \"bg-accent-cyan-subtle text-accent-cyan\";\n case \"MEMBER\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"GUEST\":\n return \"bg-bg-sunken text-text-primary\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n };\n\n const getStatusColor = (status: UserStatus) => {\n switch (status) {\n case \"ACTIVE\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"INACTIVE\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"SUSPENDED\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"DELETED\":\n return \"bg-status-error-bg-subtle text-status-error-text\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n };\n\n // Authentication check\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.users.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.users.signInToManage\",\n \"Please sign in to manage users\",\n )}\n />\n </div>\n );\n }\n\n // Loading permissions\n if (permissionsLoading) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <div className=\"text-center py-16\">\n <SpinnerIcon className=\"size-8 mx-auto text-text-secondary animate-spin\" />\n <p className=\"mt-4 text-text-secondary\">\n {tr(\"auth.users.loadingPermissions\", \"Loading permissions...\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Authorization check - user needs user:read permission\n if (!canViewUsers || isUnauthorized) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.users.adminAccessRequired\", \"Admin Access Required\")}\n description={\n <>\n {tr(\n \"auth.users.adminAccessDesc\",\n \"You need administrator privileges to view and manage users.\",\n )}{\" \"}\n {tr(\n \"auth.users.requiredRoles\",\n \"Required roles: Super Admin, Tenant Admin, or Org Admin\",\n )}\n </>\n }\n />\n </div>\n );\n }\n\n // List view\n if (viewMode === \"list\") {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n {pendingActionDialog}\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <div>\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.users.title\", \"Users\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.users.subtitle\", \"Manage users in your organization\")} (\n {totalCount} {tr(\"auth.users.paginationUsers\", \"total\")})\n </p>\n </div>\n {/* Export button */}\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.users.exportAriaLabel\", \"Export user list\")}\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\n className=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.users.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.users.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.users.purpose\",\n \"Manage everyone with access to your organization. Search and filter accounts, review their role and status, edit details, or deactivate people who should no longer have access.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: directory summary — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-secondary\">\n {tr(\"auth.users.summaryLabel\", \"People in this organization\")}\n </p>\n <p className=\"mt-1 text-3xl font-semibold text-text-primary\">\n {totalCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.users.summaryHint\",\n \"Use the filters below to find a specific person or role.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Live region for status announcements */}\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {successMessage}\n </div>\n <div\n role=\"alert\"\n aria-live=\"assertive\"\n aria-atomic=\"true\"\n className=\"sr-only\"\n >\n {error}\n </div>\n\n {/* Success Message */}\n {successMessage && (\n <output\n id={successId}\n className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\"\n >\n <p className=\"text-status-success-text\">{successMessage}</p>\n <button\n type=\"button\"\n onClick={() => setSuccessMessage(null)}\n className=\"mt-2 text-sm text-status-success-text hover:underline\"\n >\n {tr(\"auth.users.dismiss\", \"Dismiss\")}\n </button>\n </output>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n id={errorId}\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 <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n <button\n type=\"button\"\n onClick={() => setError(null)}\n className=\"mt-2 text-sm text-status-error-text hover:underline\"\n >\n {tr(\"auth.users.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"mb-6 p-4 rounded-xl border border-border-subtle bg-bg-surface\">\n <div className=\"flex flex-wrap gap-4 items-end\">\n {/* Visually hidden labels satisfy WCAG — placeholders are not labels */}\n <div className=\"flex-1 min-w-[200px]\">\n <label htmlFor={searchId} className=\"sr-only\">\n {tr(\"auth.users.searchLabel\", \"Search users\")}\n </label>\n <input\n id={searchId}\n type=\"search\"\n aria-label={tr(\"auth.users.searchLabel\", \"Search users\")}\n placeholder={tr(\n \"auth.users.searchPlaceholder\",\n \"Search by email, username, or name...\",\n )}\n value={searchQuery}\n onChange={(e) => {\n setSearchQuery(e.target.value);\n setCurrentPage(0);\n }}\n className=\"w-full px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n <div>\n <label htmlFor={roleFilterId} className=\"sr-only\">\n {tr(\"auth.users.filterRoleLabel\", \"Filter by role\")}\n </label>\n <select\n id={roleFilterId}\n value={filterRole}\n onChange={(e) => {\n setFilterRole(e.target.value as UserRole | \"\");\n setCurrentPage(0);\n }}\n className=\"px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n <option value=\"\">\n {tr(\"auth.users.allRoles\", \"All Roles\")}\n </option>\n {USER_ROLES.map((role) => (\n <option key={role} value={role}>\n {role.replace(\"_\", \" \")}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={statusFilterId} className=\"sr-only\">\n {tr(\"auth.users.filterStatusLabel\", \"Filter by status\")}\n </label>\n <select\n id={statusFilterId}\n value={filterStatus}\n onChange={(e) => {\n setFilterStatus(e.target.value as UserStatus | \"\");\n setCurrentPage(0);\n }}\n className=\"px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n <option value=\"\">\n {tr(\"auth.users.allStatuses\", \"All Statuses\")}\n </option>\n {USER_STATUSES.map((status) => (\n <option key={status} value={status}>\n {status}\n </option>\n ))}\n </select>\n </div>\n </div>\n </div>\n\n {isLoading ? (\n <div\n aria-live=\"polite\"\n aria-busy=\"true\"\n className=\"flex items-center justify-center py-12\"\n >\n <SpinnerIcon\n className=\"size-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.users.loadingUsers\", \"Loading users...\")}\n </span>\n </div>\n ) : users.length === 0 ? (\n <IllustratedEmptyState\n illustration={\n searchQuery || filterRole || filterStatus\n ? \"empty-search\"\n : \"empty-people\"\n }\n title={tr(\"auth.users.noUsersFound\", \"No users found\")}\n description={\n searchQuery || filterRole || filterStatus\n ? tr(\n \"auth.users.noUsersFilteredDesc\",\n \"No users match your current search or filters. Try clearing them to see everyone.\",\n )\n : tr(\n \"auth.users.noUsersDesc\",\n \"Once people join your organization they'll show up here for you to manage.\",\n )\n }\n />\n ) : (\n <>\n {/* Users Table */}\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface overflow-hidden shadow-sm\">\n <div className=\"overflow-x-auto\">\n <table\n className=\"w-full min-w-[800px]\"\n aria-label={tr(\"auth.users.tableAriaLabel\", \"Users table\")}\n >\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-canvas\">\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[280px]\"\n >\n {tr(\"auth.users.columnUser\", \"User\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[130px]\"\n >\n {tr(\"auth.users.columnRole\", \"Role\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[100px]\"\n >\n {tr(\"auth.users.columnStatus\", \"Status\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[80px]\"\n >\n {tr(\"auth.users.columnVerified\", \"Verified\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[100px]\"\n >\n {tr(\"auth.users.columnLastLogin\", \"Last Login\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-right p-4 text-sm font-medium text-text-secondary w-[180px]\"\n >\n {tr(\"auth.users.columnActions\", \"Actions\")}\n </th>\n </tr>\n </thead>\n <tbody>\n {users.map((user) => (\n <tr\n key={user.id}\n className=\"border-b border-border-subtle last:border-b-0 hover:bg-bg-sunken/50 transition-colors\"\n >\n <td className=\"p-4\">\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"auth-brand-badge flex size-10 shrink-0 items-center justify-center rounded-full font-medium\">\n {(user.name || user.username || user.email)\n .charAt(0)\n .toUpperCase()}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-text-primary truncate\">\n {user.name || user.username}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">\n {user.email}\n </p>\n </div>\n </div>\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-block px-2.5 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getRoleColor(user.role)}`}\n >\n {user.role.replace(\"_\", \" \")}\n </span>\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-block px-2.5 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getStatusColor(user.status)}`}\n >\n {user.status}\n </span>\n </td>\n <td className=\"p-4\">\n {user.emailVerified ? (\n <span className=\"inline-flex items-center gap-1 text-status-success-text whitespace-nowrap\">\n <svg\n className=\"size-4 flex-shrink-0\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"text-sm\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-status-warning-text whitespace-nowrap\">\n <svg\n className=\"size-4 flex-shrink-0\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"text-sm\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n </span>\n )}\n </td>\n <td className=\"p-4 text-sm text-text-secondary whitespace-nowrap\">\n {user.lastLoginAt\n ? new Date(user.lastLoginAt).toLocaleDateString()\n : tr(\"auth.users.lastLoginNever\", \"Never\")}\n </td>\n <td className=\"p-4\">\n <div className=\"flex justify-end gap-1.5 flex-nowrap\">\n <button\n type=\"button\"\n onClick={() => navigate(`/auth/users/${user.id}`)}\n aria-label={`${tr(\"auth.users.actionView\", \"View\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionView\", \"View\")}\n </button>\n {canUpdateUsers && (\n <button\n type=\"button\"\n onClick={() => startEdit(user)}\n aria-label={`${tr(\"auth.users.actionEdit\", \"Edit\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-status-info-border text-text-link bg-bg-surface hover:bg-status-info-bg-subtle transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionEdit\", \"Edit\")}\n </button>\n )}\n {canDeleteUsers && user.status !== \"DELETED\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({ type: \"deactivate\", user })\n }\n aria-label={`${tr(\"auth.users.actionDelete\", \"Delete\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionDelete\", \"Delete\")}\n </button>\n )}\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Pagination */}\n <div className=\"mt-6 flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.users.showing\", \"Showing\")}{\" \"}\n {currentPage * pageSize + 1} {tr(\"auth.users.to\", \"to\")}{\" \"}\n {Math.min((currentPage + 1) * pageSize, totalCount)}{\" \"}\n {tr(\"auth.users.of\", \"of\")} {totalCount}{\" \"}\n {tr(\"auth.users.paginationUsers\", \"users\")}\n </p>\n <nav\n aria-label={tr(\n \"auth.users.paginationAriaLabel\",\n \"User list pagination\",\n )}\n >\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() =>\n setCurrentPage((prev) => Math.max(0, prev - 1))\n }\n disabled={currentPage === 0}\n aria-label={tr(\n \"auth.users.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.users.previousPage\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.users.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.users.nextPage\", \"Next\")}\n </button>\n </div>\n </nav>\n </div>\n\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-users\"\n steps={[\n {\n id: \"search-user\",\n label: tr(\"auth.users.nextSearch\", \"Find a specific person\"),\n description: tr(\n \"auth.users.nextSearchDesc\",\n \"Use the search and role / status filters above to narrow the list.\",\n ),\n onClick: () => {\n const el = document.getElementById(searchId);\n if (el instanceof HTMLInputElement) el.focus();\n },\n },\n {\n id: \"export-users\",\n label: tr(\"auth.users.nextExport\", \"Export the user list\"),\n description: tr(\n \"auth.users.nextExportDesc\",\n \"Download a CSV of users matching your current filters.\",\n ),\n onClick: () => void handleExport(),\n },\n ]}\n />\n </>\n )}\n </div>\n );\n }\n\n // View details\n if (viewMode === \"view\" && selectedUser) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {pendingActionDialog}\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\n >\n <svg\n className=\"size-4\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.users.backToUsers\", \"Back to Users\")}\n </button>\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <div className=\"flex items-center gap-4 mb-8\">\n <div className=\"auth-brand-badge flex size-20 items-center justify-center rounded-full text-3xl font-semibold\">\n {(\n selectedUser.name ||\n selectedUser.username ||\n selectedUser.email\n )\n .charAt(0)\n .toUpperCase()}\n </div>\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {selectedUser.name || selectedUser.username}\n </h1>\n <p className=\"text-text-secondary\">{selectedUser.email}</p>\n <div className=\"flex items-center gap-2 mt-2\">\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getRoleColor(selectedUser.role)}`}\n >\n {selectedUser.role.replace(\"_\", \" \")}\n </span>\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getStatusColor(selectedUser.status)}`}\n >\n {selectedUser.status}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-6\">\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsUsername\", \"Username\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.username}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsEmail\", \"Email\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.email}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsEmailVerified\", \"Email Verified\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.emailVerified ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-status-warning-text\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsMfaEnabled\", \"MFA Enabled\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.mfaEnabled ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n )}\n </p>\n </div>\n\n {selectedUser.phoneNumber && (\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsPhoneNumber\", \"Phone Number\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.phoneNumber}\n </p>\n </div>\n )}\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsLastLogin\", \"Last Login\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.lastLoginAt\n ? new Date(selectedUser.lastLoginAt).toLocaleString()\n : tr(\"auth.users.lastLoginNever\", \"Never\")}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsCreated\", \"Created\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {new Date(selectedUser.createdAt).toLocaleString()}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsLastUpdated\", \"Last Updated\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {new Date(selectedUser.updatedAt).toLocaleString()}\n </p>\n </div>\n </div>\n\n <div className=\"mt-8 pt-6 border-t border-border-subtle flex gap-3\">\n <Button\n type=\"button\"\n onClick={() => startEdit(selectedUser)}\n variant=\"default\"\n className=\"font-semibold\"\n >\n {tr(\"auth.users.editUser\", \"Edit User\")}\n </Button>\n {selectedUser.status !== \"DELETED\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({ type: \"deactivate\", user: selectedUser })\n }\n className=\"px-4 py-2.5 rounded-xl border border-status-error-border text-status-error-text font-medium hover:bg-status-error-bg-subtle transition-colors\"\n >\n {tr(\"auth.users.deleteUser\", \"Delete User\")}\n </button>\n )}\n {canDeleteUsers &&\n selectedUser.status === \"DELETED\" &&\n currentUser?.role === \"SUPER_ADMIN\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({\n type: \"hard-delete\",\n user: selectedUser,\n })\n }\n className=\"px-4 py-2.5 rounded-xl bg-status-error-bg text-white font-medium hover:bg-status-error-bg-hover transition-colors\"\n >\n {tr(\"auth.users.permanentlyDelete\", \"Permanently Delete\")}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n }\n\n // Edit form\n return (\n <div className=\"max-w-2xl mx-auto p-6\">\n {pendingActionDialog}\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\n >\n <svg\n className=\"size-4\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.users.backToUsers\", \"Back to Users\")}\n </button>\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <h1 className=\"text-2xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.users.editUser\", \"Edit User\")}\n </h1>\n\n {selectedUser && (\n <p className=\"text-text-secondary mb-6\">\n {tr(\"auth.users.editingUser\", \"Editing:\")}{\" \"}\n <strong>{selectedUser.email}</strong>\n </p>\n )}\n\n {error && (\n <div className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n </div>\n )}\n\n <form\n onSubmit={handleUpdate}\n className=\"space-y-6\"\n aria-describedby={error ? errorId : undefined}\n noValidate\n >\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-username\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldUsername\", \"Username\")}\n </label>\n <input\n id=\"edit-username\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldUsername\", \"Username\")}\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"edit-name\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldName\", \"Name\")}\n </label>\n <input\n id=\"edit-name\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldName\", \"Name\")}\n value={formData.name}\n onChange={(e) =>\n setFormData({ ...formData, name: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-firstName\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldFirstName\", \"First Name\")}\n </label>\n <input\n id=\"edit-firstName\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldFirstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"edit-lastName\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldLastName\", \"Last Name\")}\n </label>\n <input\n id=\"edit-lastName\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldLastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n </div>\n\n <div>\n <label\n htmlFor=\"edit-phoneNumber\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldPhoneNumber\", \"Phone Number\")}\n </label>\n <input\n id=\"edit-phoneNumber\"\n type=\"tel\"\n aria-label={tr(\"auth.users.fieldPhoneNumber\", \"Phone Number\")}\n value={formData.phoneNumber}\n onChange={(e) =>\n setFormData({ ...formData, phoneNumber: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-role\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldRole\", \"Role\")}\n </label>\n <select\n id=\"edit-role\"\n aria-label={tr(\"auth.users.fieldRole\", \"Role\")}\n value={formData.role}\n onChange={(e) =>\n setFormData({ ...formData, role: e.target.value as UserRole })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n {USER_ROLES.map((role) => (\n <option key={role} value={role}>\n {role.replace(\"_\", \" \")}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label\n htmlFor=\"edit-status\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldStatus\", \"Status\")}\n </label>\n <select\n id=\"edit-status\"\n aria-label={tr(\"auth.users.fieldStatus\", \"Status\")}\n value={formData.status}\n onChange={(e) =>\n setFormData({\n ...formData,\n status: e.target.value as UserStatus,\n })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n {USER_STATUSES.map((status) => (\n <option key={status} value={status}>\n {status}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-bg-canvas/50\">\n <input\n type=\"checkbox\"\n id=\"emailVerified\"\n checked={formData.emailVerified}\n onChange={(e) =>\n setFormData({ ...formData, emailVerified: e.target.checked })\n }\n className=\"size-5 rounded border-border-default text-text-link focus:ring-action-primary-bg\"\n />\n <label\n htmlFor=\"emailVerified\"\n className=\"text-sm font-medium text-text-primary\"\n >\n {tr(\"auth.users.fieldEmailVerified\", \"Email Verified\")}\n </label>\n </div>\n\n <div className=\"flex gap-3 pt-4\">\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" />\n {tr(\"auth.users.saving\", \"Saving...\")}\n </>\n ) : (\n tr(\"auth.users.saveChanges\", \"Save Changes\")\n )}\n </Button>\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"px-6 py-3 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.users.cancel\", \"Cancel\")}\n </button>\n </div>\n </form>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UsersPage.js","names":[],"sources":["../../../src/auth/pages/UsersPage.tsx"],"sourcesContent":["import { useState, useEffect, useCallback, useId } from \"react\";\nimport { useNavigate } from \"react-router\";\nimport { useTr } from \"../../shared/hooks/useTr\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useAuthPermissions } from \"../hooks/useAuthPermissions\";\nimport { useUserOperations } from \"../hooks/useUserOperations\";\nimport { validatePhoneNumber } from \"../utils/validation\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { validateUserFields } from \"../utils/validation\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport type {\n User,\n UserRole,\n UserStatus,\n UserFilterInput,\n PaginationInput,\n} from \"../types\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\nimport { nativeImpact, nativeNotify } from \"../../utils/nativeBridge\";\n\ntype ViewMode = \"list\" | \"edit\" | \"view\";\n\ninterface FormData {\n username: string;\n firstName: string;\n lastName: string;\n name: string;\n phoneNumber: string;\n role: UserRole;\n status: UserStatus;\n emailVerified: boolean;\n}\n\n// SUPER_ADMIN is determined by email allowlist at runtime, not stored as a\n// DB role — filtering by it always returns 0 rows, so it is excluded here.\nconst USER_ROLES: UserRole[] = [\n \"TENANT_ADMIN\",\n \"ORG_ADMIN\",\n \"WORKSPACE_ADMIN\",\n \"MEMBER\",\n \"GUEST\",\n];\n\nconst USER_STATUSES: UserStatus[] = [\n \"ACTIVE\",\n \"INACTIVE\",\n \"SUSPENDED\",\n \"DELETED\",\n];\n\n/**\n * Users CRUDL page for managing users (admin only)\n */\nexport function UsersPage() {\n const tr = useTr();\n const navigate = useNavigate();\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const currentUser = useAuthStore((state) => state.user);\n const {\n canViewUsers,\n canUpdateUsers,\n canDeleteUsers,\n isLoading: permissionsLoading,\n } = useAuthPermissions();\n const { listUsers, updateUser, deleteUser, hardDeleteUser, isLoading } =\n useUserOperations();\n\n // Stable IDs for ARIA associations (accessibility)\n const searchId = useId();\n const roleFilterId = useId();\n const statusFilterId = useId();\n const errorId = useId();\n const successId = useId();\n\n const [isExporting, setIsExporting] = useState(false);\n\n const handleExport = async () => {\n setIsExporting(true);\n try {\n const filter: UserFilterInput = {};\n if (searchQuery) filter.search = searchQuery;\n if (filterRole) filter.role = filterRole;\n if (filterStatus) filter.status = filterStatus;\n\n const result = await listUsers(filter, { offset: 0, limit: 10000 });\n const rows = result.users;\n\n const headers = [\n \"ID\",\n \"Email\",\n \"Username\",\n \"First Name\",\n \"Last Name\",\n \"Phone\",\n \"Role\",\n \"Status\",\n \"Email Verified\",\n \"MFA Enabled\",\n \"Last Login\",\n \"Created At\",\n ];\n\n const escape = (v: string | undefined | null) => {\n if (v == null) return \"\";\n const s = String(v);\n return s.includes(\",\") || s.includes('\"') || s.includes(\"\\n\")\n ? `\"${s.replace(/\"/g, '\"\"')}\"`\n : s;\n };\n\n const lines = [\n headers.join(\",\"),\n ...rows.map((u) =>\n [\n escape(u.id),\n escape(u.email),\n escape(u.username),\n escape(u.firstName),\n escape(u.lastName),\n escape(u.phoneNumber),\n escape(u.role),\n escape(u.status),\n escape(String(u.emailVerified)),\n escape(String(u.mfaEnabled)),\n escape(u.lastLoginAt),\n escape(u.createdAt),\n ].join(\",\"),\n ),\n ];\n\n const blob = new Blob([lines.join(\"\\n\")], {\n type: \"text/csv;charset=utf-8;\",\n });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `users-${new Date().toISOString().slice(0, 10)}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to export users\");\n } finally {\n setIsExporting(false);\n }\n };\n\n const [users, setUsers] = useState<User[]>([]);\n const [totalCount, setTotalCount] = useState(0);\n const [hasMore, setHasMore] = useState(false);\n const [viewMode, setViewMode] = useState<ViewMode>(\"list\");\n const [selectedUser, setSelectedUser] = useState<User | null>(null);\n const [formData, setFormData] = useState<FormData>({\n username: \"\",\n firstName: \"\",\n lastName: \"\",\n name: \"\",\n phoneNumber: \"\",\n role: \"MEMBER\",\n status: \"ACTIVE\",\n emailVerified: false,\n });\n const [error, setError] = useState<string | null>(null);\n const [phoneError, setPhoneError] = useState<string | null>(null);\n const [isUnauthorized, setIsUnauthorized] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingAction, setPendingAction] = useState<{\n type: \"deactivate\" | \"hard-delete\";\n user: User;\n } | null>(null);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [filterRole, setFilterRole] = useState<UserRole | \"\">(\"\");\n const [filterStatus, setFilterStatus] = useState<UserStatus | \"\">(\"\");\n const [currentPage, setCurrentPage] = useState(0);\n const pageSize = 10;\n\n const fetchUsers = useCallback(async () => {\n if (!isAuthenticated) return;\n\n try {\n setIsUnauthorized(false);\n const filter: UserFilterInput = {};\n if (searchQuery) filter.search = searchQuery;\n if (filterRole) filter.role = filterRole;\n if (filterStatus) filter.status = filterStatus;\n\n const pagination: PaginationInput = {\n offset: currentPage * pageSize,\n limit: pageSize,\n };\n\n const result = await listUsers(filter, pagination);\n setUsers(result.users);\n setTotalCount(result.totalCount);\n setHasMore(result.hasMore);\n setError(null);\n } catch (err) {\n // Ignore abort errors - they happen during component unmount/re-render\n if (\n err instanceof Error &&\n (err.name === \"AbortError\" || err.message.includes(\"aborted\"))\n ) {\n return;\n }\n const errorMessage =\n err instanceof Error ? err.message : \"Failed to fetch users\";\n // Check if it's an authorization error\n if (\n errorMessage.toLowerCase().includes(\"not authorized\") ||\n errorMessage.toLowerCase().includes(\"forbidden\")\n ) {\n setIsUnauthorized(true);\n setError(null);\n } else {\n setError(errorMessage);\n }\n }\n }, [\n listUsers,\n searchQuery,\n filterRole,\n filterStatus,\n currentPage,\n isAuthenticated,\n ]);\n\n useEffect(() => {\n fetchUsers();\n }, [fetchUsers]);\n\n const resetForm = () => {\n setFormData({\n username: \"\",\n firstName: \"\",\n lastName: \"\",\n name: \"\",\n phoneNumber: \"\",\n role: \"MEMBER\",\n status: \"ACTIVE\",\n emailVerified: false,\n });\n setSelectedUser(null);\n setError(null);\n setPhoneError(null);\n setSuccessMessage(null);\n };\n\n const handleUpdate = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!selectedUser) return;\n setError(null);\n setPhoneError(null);\n\n const phoneValidation = validatePhoneNumber(formData.phoneNumber);\n if (!phoneValidation.valid) {\n const phoneMsg = tr(\n \"auth.users.phoneNumber_invalid\",\n \"Invalid phone number\",\n );\n setPhoneError(phoneMsg);\n setError(phoneMsg);\n return;\n }\n\n // BOFF-7214: same name/username invariants the signup form enforces\n // must hold on user edit too, otherwise an admin (or anyone with\n // users:update) can re-introduce a numbers-only account. Same\n // validator the register form uses, same canonicalised output.\n // Pass `requireAll: false` so an existing user whose optional\n // `firstName`/`lastName` is unset (e.g. provisioned via IdP) can\n // still update unrelated fields like phone or role (Codex P2 review).\n const validation = validateUserFields(formData, { requireAll: false });\n if (!validation.valid) {\n setError(\n validation.errors\n .map((e) => {\n const key = `auth.users.${e.field}_${e.code}`;\n return tr(key, e.message);\n })\n .join(\" · \"),\n );\n return;\n }\n\n try {\n await updateUser(selectedUser.id, {\n username: validation.values.username || undefined,\n firstName: validation.values.firstName || undefined,\n lastName: validation.values.lastName || undefined,\n name: formData.name || undefined,\n phoneNumber: formData.phoneNumber || undefined,\n role: formData.role,\n status: formData.status,\n emailVerified: formData.emailVerified,\n });\n setSuccessMessage(\n tr(\"auth.users.updateSuccess\", \"User updated successfully!\"),\n );\n setViewMode(\"list\");\n resetForm();\n fetchUsers();\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to update user\");\n }\n };\n\n const handleDelete = async (user: User) => {\n void nativeImpact(\"medium\");\n try {\n const result = await deleteUser(user.id);\n if (result.success) {\n void nativeNotify(\"success\");\n setSuccessMessage(\n tr(\n \"auth.users.userDeactivated\",\n 'User \"{email}\" has been deactivated.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n fetchUsers();\n } else {\n void nativeNotify(\"error\");\n setError(result.message);\n }\n } catch (err) {\n void nativeNotify(\"error\");\n setError(err instanceof Error ? err.message : \"Failed to delete user\");\n }\n };\n\n const handleHardDelete = async (user: User) => {\n try {\n const result = await hardDeleteUser(user.id);\n if (result.success) {\n setSuccessMessage(\n tr(\n \"auth.users.userPermanentlyDeleted\",\n 'User \"{email}\" has been permanently deleted.',\n ).replace(\"{email}\", user.email),\n );\n setPendingAction(null);\n fetchUsers();\n } else {\n setError(result.message);\n }\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to hard delete user\",\n );\n }\n };\n\n const startEdit = (user: User) => {\n setSelectedUser(user);\n setFormData({\n username: user.username || \"\",\n firstName: user.firstName || \"\",\n lastName: user.lastName || \"\",\n name: user.name || \"\",\n phoneNumber: user.phoneNumber || \"\",\n role: user.role,\n status: user.status,\n emailVerified: user.emailVerified,\n });\n setViewMode(\"edit\");\n setError(null);\n };\n\n const pendingActionDialog = (\n <ConfirmActionDialog\n open={pendingAction !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingAction(null);\n }\n }}\n title={\n pendingAction?.type === \"hard-delete\"\n ? tr(\"auth.users.hardDeleteTitle\", \"Permanently delete user?\")\n : tr(\"auth.users.deactivateTitle\", \"Deactivate user?\")\n }\n description={\n pendingAction?.type === \"hard-delete\"\n ? pendingAction\n ? tr(\n \"auth.users.hardDeleteDesc\",\n 'This permanently deletes \"{email}\" and cannot be undone.',\n ).replace(\"{email}\", pendingAction.user.email)\n : \"\"\n : pendingAction\n ? tr(\n \"auth.users.deactivateDesc\",\n '\"{email}\" will be prevented from signing in until reactivated.',\n ).replace(\"{email}\", pendingAction.user.email)\n : \"\"\n }\n confirmText={\n pendingAction?.type === \"hard-delete\"\n ? tr(\"auth.users.hardDeleteConfirm\", \"Delete permanently\")\n : tr(\"auth.users.deactivateConfirm\", \"Deactivate user\")\n }\n variant={pendingAction?.type === \"hard-delete\" ? \"danger\" : \"warning\"}\n onConfirm={async () => {\n if (!pendingAction) return;\n if (pendingAction.type === \"hard-delete\") {\n await handleHardDelete(pendingAction.user);\n return;\n }\n await handleDelete(pendingAction.user);\n }}\n />\n );\n\n const getRoleColor = (role: UserRole) => {\n switch (role) {\n case \"SUPER_ADMIN\":\n return \"bg-accent-purple-subtle text-accent-purple\";\n case \"TENANT_ADMIN\":\n return \"bg-status-info-bg-subtle text-status-info-text\";\n case \"ORG_ADMIN\":\n return \"bg-accent-indigo-subtle text-accent-indigo\";\n case \"WORKSPACE_ADMIN\":\n return \"bg-accent-cyan-subtle text-accent-cyan\";\n case \"MEMBER\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"GUEST\":\n return \"bg-bg-sunken text-text-primary\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n };\n\n const getStatusColor = (status: UserStatus) => {\n switch (status) {\n case \"ACTIVE\":\n return \"bg-status-success-bg-subtle text-status-success-text\";\n case \"INACTIVE\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"SUSPENDED\":\n return \"bg-status-warning-bg-subtle text-status-warning-text\";\n case \"DELETED\":\n return \"bg-status-error-bg-subtle text-status-error-text\";\n default:\n return \"bg-bg-sunken text-text-primary\";\n }\n };\n\n // Authentication check\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.users.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.users.signInToManage\",\n \"Please sign in to manage users\",\n )}\n />\n </div>\n );\n }\n\n // Loading permissions\n if (permissionsLoading) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <div className=\"text-center py-16\">\n <SpinnerIcon className=\"size-8 mx-auto text-text-secondary animate-spin\" />\n <p className=\"mt-4 text-text-secondary\">\n {tr(\"auth.users.loadingPermissions\", \"Loading permissions...\")}\n </p>\n </div>\n </div>\n );\n }\n\n // Authorization check - user needs user:read permission\n if (!canViewUsers || isUnauthorized) {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.users.adminAccessRequired\", \"Admin Access Required\")}\n description={\n <>\n {tr(\n \"auth.users.adminAccessDesc\",\n \"You need administrator privileges to view and manage users.\",\n )}{\" \"}\n {tr(\n \"auth.users.requiredRoles\",\n \"Required roles: Super Admin, Tenant Admin, or Org Admin\",\n )}\n </>\n }\n />\n </div>\n );\n }\n\n // List view\n if (viewMode === \"list\") {\n return (\n <div className=\"max-w-6xl mx-auto p-6\">\n {pendingActionDialog}\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <div>\n <h1 className=\"text-3xl font-semibold text-text-primary\">\n {tr(\"auth.users.title\", \"Users\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.users.subtitle\", \"Manage users in your organization\")} (\n {totalCount} {tr(\"auth.users.paginationUsers\", \"total\")})\n </p>\n </div>\n {/* Export button */}\n <button\n type=\"button\"\n onClick={() => void handleExport()}\n disabled={isExporting}\n aria-label={tr(\"auth.users.exportAriaLabel\", \"Export user list\")}\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\n className=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.users.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.users.export\", \"Export\")}\n </>\n )}\n </button>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.users.purpose\",\n \"Manage everyone with access to your organization. Search and filter accounts, review their role and status, edit details, or deactivate people who should no longer have access.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: directory summary — the one highlighted zone */}\n <EmphasisPanel className=\"mb-6\">\n <p className=\"text-sm font-medium text-text-secondary\">\n {tr(\"auth.users.summaryLabel\", \"People in this organization\")}\n </p>\n <p className=\"mt-1 text-3xl font-semibold text-text-primary\">\n {totalCount}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n \"auth.users.summaryHint\",\n \"Use the filters below to find a specific person or role.\",\n )}\n </p>\n </EmphasisPanel>\n\n {/* Live region for status announcements */}\n <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n {successMessage}\n </div>\n <div\n role=\"alert\"\n aria-live=\"assertive\"\n aria-atomic=\"true\"\n className=\"sr-only\"\n >\n {error}\n </div>\n\n {/* Success Message */}\n {successMessage && (\n <output\n id={successId}\n className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\"\n >\n <p className=\"text-status-success-text\">{successMessage}</p>\n <button\n type=\"button\"\n onClick={() => setSuccessMessage(null)}\n className=\"mt-2 text-sm text-status-success-text hover:underline\"\n >\n {tr(\"auth.users.dismiss\", \"Dismiss\")}\n </button>\n </output>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n id={errorId}\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 <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n <button\n type=\"button\"\n onClick={() => setError(null)}\n className=\"mt-2 text-sm text-status-error-text hover:underline\"\n >\n {tr(\"auth.users.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Filters */}\n <div className=\"mb-6 p-4 rounded-xl border border-border-subtle bg-bg-surface\">\n <div className=\"flex flex-wrap gap-4 items-end\">\n {/* Visually hidden labels satisfy WCAG — placeholders are not labels */}\n <div className=\"flex-1 min-w-[200px]\">\n <label htmlFor={searchId} className=\"sr-only\">\n {tr(\"auth.users.searchLabel\", \"Search users\")}\n </label>\n <input\n id={searchId}\n type=\"search\"\n aria-label={tr(\"auth.users.searchLabel\", \"Search users\")}\n placeholder={tr(\n \"auth.users.searchPlaceholder\",\n \"Search by email, username, or name...\",\n )}\n value={searchQuery}\n onChange={(e) => {\n setSearchQuery(e.target.value);\n setCurrentPage(0);\n }}\n className=\"w-full px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n <div>\n <label htmlFor={roleFilterId} className=\"sr-only\">\n {tr(\"auth.users.filterRoleLabel\", \"Filter by role\")}\n </label>\n <select\n id={roleFilterId}\n value={filterRole}\n onChange={(e) => {\n setFilterRole(e.target.value as UserRole | \"\");\n setCurrentPage(0);\n }}\n className=\"px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n <option value=\"\">\n {tr(\"auth.users.allRoles\", \"All Roles\")}\n </option>\n {USER_ROLES.map((role) => (\n <option key={role} value={role}>\n {role.replace(\"_\", \" \")}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor={statusFilterId} className=\"sr-only\">\n {tr(\"auth.users.filterStatusLabel\", \"Filter by status\")}\n </label>\n <select\n id={statusFilterId}\n value={filterStatus}\n onChange={(e) => {\n setFilterStatus(e.target.value as UserStatus | \"\");\n setCurrentPage(0);\n }}\n className=\"px-4 py-2.5 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n <option value=\"\">\n {tr(\"auth.users.allStatuses\", \"All Statuses\")}\n </option>\n {USER_STATUSES.map((status) => (\n <option key={status} value={status}>\n {status}\n </option>\n ))}\n </select>\n </div>\n </div>\n </div>\n\n {isLoading ? (\n <div\n aria-live=\"polite\"\n aria-busy=\"true\"\n className=\"flex items-center justify-center py-12\"\n >\n <SpinnerIcon\n className=\"size-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.users.loadingUsers\", \"Loading users...\")}\n </span>\n </div>\n ) : users.length === 0 ? (\n <IllustratedEmptyState\n illustration={\n searchQuery || filterRole || filterStatus\n ? \"empty-search\"\n : \"empty-people\"\n }\n title={tr(\"auth.users.noUsersFound\", \"No users found\")}\n description={\n searchQuery || filterRole || filterStatus\n ? tr(\n \"auth.users.noUsersFilteredDesc\",\n \"No users match your current search or filters. Try clearing them to see everyone.\",\n )\n : tr(\n \"auth.users.noUsersDesc\",\n \"Once people join your organization they'll show up here for you to manage.\",\n )\n }\n />\n ) : (\n <>\n {/* Users Table */}\n <div className=\"rounded-xl border border-border-subtle bg-bg-surface overflow-hidden shadow-sm\">\n <div className=\"overflow-x-auto\">\n <table\n className=\"w-full min-w-[800px]\"\n aria-label={tr(\"auth.users.tableAriaLabel\", \"Users table\")}\n >\n <thead>\n <tr className=\"border-b border-border-subtle bg-bg-canvas\">\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[280px]\"\n >\n {tr(\"auth.users.columnUser\", \"User\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[130px]\"\n >\n {tr(\"auth.users.columnRole\", \"Role\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[100px]\"\n >\n {tr(\"auth.users.columnStatus\", \"Status\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[80px]\"\n >\n {tr(\"auth.users.columnVerified\", \"Verified\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-left p-4 text-sm font-medium text-text-secondary w-[100px]\"\n >\n {tr(\"auth.users.columnLastLogin\", \"Last Login\")}\n </th>\n <th\n scope=\"col\"\n className=\"text-right p-4 text-sm font-medium text-text-secondary w-[180px]\"\n >\n {tr(\"auth.users.columnActions\", \"Actions\")}\n </th>\n </tr>\n </thead>\n <tbody>\n {users.map((user) => (\n <tr\n key={user.id}\n className=\"border-b border-border-subtle last:border-b-0 hover:bg-bg-sunken/50 transition-colors\"\n >\n <td className=\"p-4\">\n <div className=\"flex items-center gap-3 min-w-0\">\n <div className=\"auth-brand-badge flex size-10 shrink-0 items-center justify-center rounded-full font-medium\">\n {(user.name || user.username || user.email)\n .charAt(0)\n .toUpperCase()}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"font-medium text-text-primary truncate\">\n {user.name || user.username}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">\n {user.email}\n </p>\n </div>\n </div>\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-block px-2.5 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getRoleColor(user.role)}`}\n >\n {user.role.replace(\"_\", \" \")}\n </span>\n </td>\n <td className=\"p-4\">\n <span\n className={`inline-block px-2.5 py-1 rounded-full text-xs font-medium whitespace-nowrap ${getStatusColor(user.status)}`}\n >\n {user.status}\n </span>\n </td>\n <td className=\"p-4\">\n {user.emailVerified ? (\n <span className=\"inline-flex items-center gap-1 text-status-success-text whitespace-nowrap\">\n <svg\n className=\"size-4 flex-shrink-0\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"text-sm\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 text-status-warning-text whitespace-nowrap\">\n <svg\n className=\"size-4 flex-shrink-0\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"text-sm\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n </span>\n )}\n </td>\n <td className=\"p-4 text-sm text-text-secondary whitespace-nowrap\">\n {user.lastLoginAt\n ? new Date(user.lastLoginAt).toLocaleDateString()\n : tr(\"auth.users.lastLoginNever\", \"Never\")}\n </td>\n <td className=\"p-4\">\n <div className=\"flex justify-end gap-1.5 flex-nowrap\">\n <button\n type=\"button\"\n onClick={() => navigate(`/auth/users/${user.id}`)}\n aria-label={`${tr(\"auth.users.actionView\", \"View\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionView\", \"View\")}\n </button>\n {canUpdateUsers && (\n <button\n type=\"button\"\n onClick={() => startEdit(user)}\n aria-label={`${tr(\"auth.users.actionEdit\", \"Edit\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-status-info-border text-text-link bg-bg-surface hover:bg-status-info-bg-subtle transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionEdit\", \"Edit\")}\n </button>\n )}\n {canDeleteUsers && user.status !== \"DELETED\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({ type: \"deactivate\", user })\n }\n aria-label={`${tr(\"auth.users.actionDelete\", \"Delete\")} ${user.name || user.username || user.email}`}\n className=\"px-2.5 py-1.5 rounded-lg text-xs font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors whitespace-nowrap flex-shrink-0\"\n >\n {tr(\"auth.users.actionDelete\", \"Delete\")}\n </button>\n )}\n </div>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n </div>\n\n {/* Pagination */}\n <div className=\"mt-6 flex items-center justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {tr(\"auth.users.showing\", \"Showing\")}{\" \"}\n {currentPage * pageSize + 1} {tr(\"auth.users.to\", \"to\")}{\" \"}\n {Math.min((currentPage + 1) * pageSize, totalCount)}{\" \"}\n {tr(\"auth.users.of\", \"of\")} {totalCount}{\" \"}\n {tr(\"auth.users.paginationUsers\", \"users\")}\n </p>\n <nav\n aria-label={tr(\n \"auth.users.paginationAriaLabel\",\n \"User list pagination\",\n )}\n >\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() =>\n setCurrentPage((prev) => Math.max(0, prev - 1))\n }\n disabled={currentPage === 0}\n aria-label={tr(\n \"auth.users.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.users.previousPage\", \"Previous\")}\n </button>\n <button\n type=\"button\"\n onClick={() => setCurrentPage((prev) => prev + 1)}\n disabled={!hasMore}\n aria-label={tr(\n \"auth.users.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.users.nextPage\", \"Next\")}\n </button>\n </div>\n </nav>\n </div>\n\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-users\"\n steps={[\n {\n id: \"search-user\",\n label: tr(\"auth.users.nextSearch\", \"Find a specific person\"),\n description: tr(\n \"auth.users.nextSearchDesc\",\n \"Use the search and role / status filters above to narrow the list.\",\n ),\n onClick: () => {\n const el = document.getElementById(searchId);\n if (el instanceof HTMLInputElement) el.focus();\n },\n },\n {\n id: \"export-users\",\n label: tr(\"auth.users.nextExport\", \"Export the user list\"),\n description: tr(\n \"auth.users.nextExportDesc\",\n \"Download a CSV of users matching your current filters.\",\n ),\n onClick: () => void handleExport(),\n },\n ]}\n />\n </>\n )}\n </div>\n );\n }\n\n // View details\n if (viewMode === \"view\" && selectedUser) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n {pendingActionDialog}\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\n >\n <svg\n className=\"size-4\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.users.backToUsers\", \"Back to Users\")}\n </button>\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <div className=\"flex items-center gap-4 mb-8\">\n <div className=\"auth-brand-badge flex size-20 items-center justify-center rounded-full text-3xl font-semibold\">\n {(\n selectedUser.name ||\n selectedUser.username ||\n selectedUser.email\n )\n .charAt(0)\n .toUpperCase()}\n </div>\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">\n {selectedUser.name || selectedUser.username}\n </h1>\n <p className=\"text-text-secondary\">{selectedUser.email}</p>\n <div className=\"flex items-center gap-2 mt-2\">\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getRoleColor(selectedUser.role)}`}\n >\n {selectedUser.role.replace(\"_\", \" \")}\n </span>\n <span\n className={`px-2.5 py-1 rounded-full text-xs font-medium ${getStatusColor(selectedUser.status)}`}\n >\n {selectedUser.status}\n </span>\n </div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-6\">\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsUsername\", \"Username\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.username}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsEmail\", \"Email\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.email}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsEmailVerified\", \"Email Verified\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.emailVerified ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-status-warning-text\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n )}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsMfaEnabled\", \"MFA Enabled\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.mfaEnabled ? (\n <span className=\"text-status-success-text\">\n {tr(\"auth.users.verifiedYes\", \"Yes\")}\n </span>\n ) : (\n <span className=\"text-text-secondary\">\n {tr(\"auth.users.verifiedNo\", \"No\")}\n </span>\n )}\n </p>\n </div>\n\n {selectedUser.phoneNumber && (\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsPhoneNumber\", \"Phone Number\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.phoneNumber}\n </p>\n </div>\n )}\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsLastLogin\", \"Last Login\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {selectedUser.lastLoginAt\n ? new Date(selectedUser.lastLoginAt).toLocaleString()\n : tr(\"auth.users.lastLoginNever\", \"Never\")}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsCreated\", \"Created\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {new Date(selectedUser.createdAt).toLocaleString()}\n </p>\n </div>\n\n <div className=\"p-4 rounded-lg bg-bg-canvas/50\">\n <label className=\"block text-sm font-medium text-text-secondary mb-1\">\n {tr(\"auth.users.viewDetailsLastUpdated\", \"Last Updated\")}\n </label>\n <p className=\"text-text-primary font-medium\">\n {new Date(selectedUser.updatedAt).toLocaleString()}\n </p>\n </div>\n </div>\n\n <div className=\"mt-8 pt-6 border-t border-border-subtle flex gap-3\">\n <Button\n type=\"button\"\n onClick={() => startEdit(selectedUser)}\n variant=\"default\"\n className=\"font-semibold\"\n >\n {tr(\"auth.users.editUser\", \"Edit User\")}\n </Button>\n {selectedUser.status !== \"DELETED\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({ type: \"deactivate\", user: selectedUser })\n }\n className=\"px-4 py-2.5 rounded-xl border border-status-error-border text-status-error-text font-medium hover:bg-status-error-bg-subtle transition-colors\"\n >\n {tr(\"auth.users.deleteUser\", \"Delete User\")}\n </button>\n )}\n {canDeleteUsers &&\n selectedUser.status === \"DELETED\" &&\n currentUser?.role === \"SUPER_ADMIN\" && (\n <button\n type=\"button\"\n onClick={() =>\n setPendingAction({\n type: \"hard-delete\",\n user: selectedUser,\n })\n }\n className=\"px-4 py-2.5 rounded-xl bg-status-error-bg text-white font-medium hover:bg-status-error-bg-hover transition-colors\"\n >\n {tr(\"auth.users.permanentlyDelete\", \"Permanently Delete\")}\n </button>\n )}\n </div>\n </div>\n </div>\n );\n }\n\n // Edit form\n return (\n <div className=\"max-w-2xl mx-auto p-6\">\n {pendingActionDialog}\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"mb-6 flex items-center gap-2 text-text-link hover:underline font-medium\"\n >\n <svg\n className=\"size-4\"\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=\"M10 19l-7-7m0 0l7-7m-7 7h18\"\n />\n </svg>\n {tr(\"auth.users.backToUsers\", \"Back to Users\")}\n </button>\n\n <div className=\"p-6 rounded-xl border border-border-subtle bg-bg-surface shadow-sm\">\n <h1 className=\"text-2xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.users.editUser\", \"Edit User\")}\n </h1>\n\n {selectedUser && (\n <p className=\"text-text-secondary mb-6\">\n {tr(\"auth.users.editingUser\", \"Editing:\")}{\" \"}\n <strong>{selectedUser.email}</strong>\n </p>\n )}\n\n {error && (\n <div className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\">\n <AlertIcon className=\"size-5 text-status-error-text flex-shrink-0 mt-0.5\" />\n <div className=\"flex-1\">\n <p className=\"text-sm text-status-error-text\">{error}</p>\n </div>\n </div>\n )}\n\n <form\n onSubmit={handleUpdate}\n className=\"space-y-6\"\n aria-describedby={error ? errorId : undefined}\n noValidate\n >\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-username\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldUsername\", \"Username\")}\n </label>\n <input\n id=\"edit-username\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldUsername\", \"Username\")}\n value={formData.username}\n onChange={(e) =>\n setFormData({ ...formData, username: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"edit-name\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldName\", \"Name\")}\n </label>\n <input\n id=\"edit-name\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldName\", \"Name\")}\n value={formData.name}\n onChange={(e) =>\n setFormData({ ...formData, name: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-firstName\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldFirstName\", \"First Name\")}\n </label>\n <input\n id=\"edit-firstName\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldFirstName\", \"First Name\")}\n value={formData.firstName}\n onChange={(e) =>\n setFormData({ ...formData, firstName: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n\n <div>\n <label\n htmlFor=\"edit-lastName\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldLastName\", \"Last Name\")}\n </label>\n <input\n id=\"edit-lastName\"\n type=\"text\"\n aria-label={tr(\"auth.users.fieldLastName\", \"Last Name\")}\n value={formData.lastName}\n onChange={(e) =>\n setFormData({ ...formData, lastName: e.target.value })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n </div>\n </div>\n\n <div>\n <label\n htmlFor=\"edit-phoneNumber\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldPhoneNumber\", \"Phone Number\")}\n </label>\n <input\n id=\"edit-phoneNumber\"\n type=\"tel\"\n aria-label={tr(\"auth.users.fieldPhoneNumber\", \"Phone Number\")}\n aria-invalid={phoneError ? true : undefined}\n aria-describedby={\n phoneError ? \"edit-phoneNumber-error\" : undefined\n }\n value={formData.phoneNumber}\n onChange={(e) => {\n setFormData({ ...formData, phoneNumber: e.target.value });\n if (phoneError) {\n setPhoneError(null);\n setError(null);\n }\n }}\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n />\n {phoneError && (\n <p\n id=\"edit-phoneNumber-error\"\n role=\"alert\"\n className=\"mt-2 text-sm text-[var(--color-status-error-text)]\"\n >\n {phoneError}\n </p>\n )}\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div>\n <label\n htmlFor=\"edit-role\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldRole\", \"Role\")}\n </label>\n <select\n id=\"edit-role\"\n aria-label={tr(\"auth.users.fieldRole\", \"Role\")}\n value={formData.role}\n onChange={(e) =>\n setFormData({ ...formData, role: e.target.value as UserRole })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n {USER_ROLES.map((role) => (\n <option key={role} value={role}>\n {role.replace(\"_\", \" \")}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <label\n htmlFor=\"edit-status\"\n className=\"block text-sm font-medium text-text-primary mb-2\"\n >\n {tr(\"auth.users.fieldStatus\", \"Status\")}\n </label>\n <select\n id=\"edit-status\"\n aria-label={tr(\"auth.users.fieldStatus\", \"Status\")}\n value={formData.status}\n onChange={(e) =>\n setFormData({\n ...formData,\n status: e.target.value as UserStatus,\n })\n }\n className=\"block w-full px-4 py-3 border border-border-default rounded-xl bg-bg-surface text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/20 focus:border-action-primary-bg transition-all\"\n >\n {USER_STATUSES.map((status) => (\n <option key={status} value={status}>\n {status}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-bg-canvas/50\">\n <input\n type=\"checkbox\"\n id=\"emailVerified\"\n checked={formData.emailVerified}\n onChange={(e) =>\n setFormData({ ...formData, emailVerified: e.target.checked })\n }\n className=\"size-5 rounded border-border-default text-text-link focus:ring-action-primary-bg\"\n />\n <label\n htmlFor=\"emailVerified\"\n className=\"text-sm font-medium text-text-primary\"\n >\n {tr(\"auth.users.fieldEmailVerified\", \"Email Verified\")}\n </label>\n </div>\n\n <div className=\"flex gap-3 pt-6 mt-6 border-t border-border-default\">\n <Button\n type=\"submit\"\n disabled={isLoading}\n variant=\"default\"\n size=\"lg\"\n className=\"font-semibold\"\n >\n {isLoading ? (\n <>\n <SpinnerIcon className=\"size-4 animate-spin\" />\n {tr(\"auth.users.saving\", \"Saving...\")}\n </>\n ) : (\n tr(\"auth.users.saveChanges\", \"Save Changes\")\n )}\n </Button>\n <button\n type=\"button\"\n onClick={() => {\n setViewMode(\"list\");\n resetForm();\n }}\n className=\"inline-flex items-center h-10 px-8 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken transition-colors\"\n >\n {tr(\"auth.users.cancel\", \"Cancel\")}\n </button>\n </div>\n </form>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* domain would send every other product's users to the wrong company's terms.
|
|
24
24
|
* Every product serves its marketing site at `www.<product-domain>` using the
|
|
25
25
|
* same `/terms` and `/privacy` route names (verified against fluidgrids.ai,
|
|
26
|
-
* burdenoff.com, bigconsole.com, planmagnet.com, adaptercloud.com,
|
|
26
|
+
* burdenoff.com, bigconsole.com, planmagnet.com, adaptercloud.com, botmanor.com and
|
|
27
27
|
* timecampus.com).
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legalUrls.js","names":[],"sources":["../../../src/auth/utils/legalUrls.ts"],"sourcesContent":["/**\n * Public legal-document URLs for the signup consent copy.\n *\n * The signup form is shown to SIGNED-OUT visitors, so its \"Terms of Service\" and\n * \"Privacy Policy\" links must resolve to the public marketing site. They used to\n * be written as the app-relative paths `/legal/terms` and `/legal/privacy`, which\n * resolved against the app origin (e.g. `https://app.fluidgrids.ai/legal/terms`).\n * That route lives *inside* the shell's authenticated block, so a visitor who\n * clicked it was bounced to `/auth/login?returnTo=%2Flegal%2Fterms` and the shell\n * footer fired two FORBIDDEN queries on the way out:\n *\n * [Burdenoff backend error] You do not have permission to do that.\n * {code: FORBIDDEN, operationName: GetFooterContentPages}\n * {code: FORBIDDEN, operationName: GetFooterProduct}\n *\n * i.e. the consent links were unreadable for exactly the people being asked to\n * consent. The marketing site is also the authoritative home of the policy text\n * (rendered from the dated drafts in `fluidgrids-website/src/content/legal/`),\n * so linking there avoids maintaining a second copy that can drift.\n *\n * microfe-auth is mounted by every Burdenoff product shell, so the origin is\n * DERIVED from the current host rather than hardcoded — hardcoding one product's\n * domain would send every other product's users to the wrong company's terms.\n * Every product serves its marketing site at `www.<product-domain>` using the\n * same `/terms` and `/privacy` route names (verified against fluidgrids.ai,\n * burdenoff.com, bigconsole.com, planmagnet.com, adaptercloud.com,
|
|
1
|
+
{"version":3,"file":"legalUrls.js","names":[],"sources":["../../../src/auth/utils/legalUrls.ts"],"sourcesContent":["/**\n * Public legal-document URLs for the signup consent copy.\n *\n * The signup form is shown to SIGNED-OUT visitors, so its \"Terms of Service\" and\n * \"Privacy Policy\" links must resolve to the public marketing site. They used to\n * be written as the app-relative paths `/legal/terms` and `/legal/privacy`, which\n * resolved against the app origin (e.g. `https://app.fluidgrids.ai/legal/terms`).\n * That route lives *inside* the shell's authenticated block, so a visitor who\n * clicked it was bounced to `/auth/login?returnTo=%2Flegal%2Fterms` and the shell\n * footer fired two FORBIDDEN queries on the way out:\n *\n * [Burdenoff backend error] You do not have permission to do that.\n * {code: FORBIDDEN, operationName: GetFooterContentPages}\n * {code: FORBIDDEN, operationName: GetFooterProduct}\n *\n * i.e. the consent links were unreadable for exactly the people being asked to\n * consent. The marketing site is also the authoritative home of the policy text\n * (rendered from the dated drafts in `fluidgrids-website/src/content/legal/`),\n * so linking there avoids maintaining a second copy that can drift.\n *\n * microfe-auth is mounted by every Burdenoff product shell, so the origin is\n * DERIVED from the current host rather than hardcoded — hardcoding one product's\n * domain would send every other product's users to the wrong company's terms.\n * Every product serves its marketing site at `www.<product-domain>` using the\n * same `/terms` and `/privacy` route names (verified against fluidgrids.ai,\n * burdenoff.com, bigconsole.com, planmagnet.com, adaptercloud.com, botmanor.com and\n * timecampus.com).\n */\n\n/**\n * Shell subdomains that front a product's app surface. Stripping one of these\n * leaves the bare product domain whose `www.` host serves the marketing site.\n */\nconst APP_HOST_PREFIXES = [\n \"app.\",\n \"alphaapp.\",\n \"admin.\",\n \"alphaadmin.\",\n \"www.\",\n];\n\n/**\n * Resolve the public marketing origin for the product currently being served,\n * or `null` when it cannot be determined (local development, or a host with no\n * registrable domain). Exported for unit testing; prefer `resolveLegalUrl`.\n */\nexport function resolveMarketingOrigin(hostname?: string): string | null {\n const host = (\n hostname ?? (typeof window !== \"undefined\" ? window.location.hostname : \"\")\n )\n .toLowerCase()\n .trim();\n\n if (!host) return null;\n\n // Local development has no marketing site to link to. `0.0.0.0` and IPv6\n // literals (which `location.hostname` reports bracketed, e.g. \"[::1]\") are\n // included because a bare dotted-quad would otherwise survive the \"has a dot\"\n // check below and produce a nonsense origin like `https://www.0.0.0.0`.\n if (\n host === \"localhost\" ||\n host === \"127.0.0.1\" ||\n host === \"0.0.0.0\" ||\n host.startsWith(\"[\") ||\n host.endsWith(\".local.burdenoff.com\")\n ) {\n return null;\n }\n\n // Any other IPv4 literal is likewise not a registrable domain.\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host)) return null;\n\n let domain = host;\n for (const prefix of APP_HOST_PREFIXES) {\n if (domain.startsWith(prefix)) {\n domain = domain.slice(prefix.length);\n break;\n }\n }\n\n // A bare hostname with no dot is not a registrable domain we can build on.\n if (!domain.includes(\".\")) return null;\n\n return `https://www.${domain}`;\n}\n\n/** Public legal pages, by the route name every product marketing site uses. */\nexport type LegalDocumentPath = \"/terms\" | \"/privacy\";\n\n/**\n * Absolute URL of a public legal document for the current product.\n *\n * Falls back to the bare path when the marketing origin cannot be resolved\n * (local development), which keeps the link valid-looking in dev without\n * inventing a production domain.\n */\nexport function resolveLegalUrl(path: LegalDocumentPath): string {\n const origin = resolveMarketingOrigin();\n return origin ? `${origin}${path}` : path;\n}\n"],"mappings":";AAiCA,IAAM,IAAoB;CACxB;CACA;CACA;CACA;CACA;CACD;AAOD,SAAgB,EAAuB,GAAkC;CACvE,IAAM,KACJ,MAAa,OAAO,SAAW,MAAc,OAAO,SAAS,WAAW,KAEvE,aAAa,CACb,MAAM;AAmBT,KAjBI,CAAC,KAOH,MAAS,eACT,MAAS,eACT,MAAS,aACT,EAAK,WAAW,IAAI,IACpB,EAAK,SAAS,uBAAuB,IAMnC,uBAAuB,KAAK,EAAK,CAAE,QAAO;CAE9C,IAAI,IAAS;AACb,MAAK,IAAM,KAAU,EACnB,KAAI,EAAO,WAAW,EAAO,EAAE;AAC7B,MAAS,EAAO,MAAM,EAAO,OAAO;AACpC;;AAOJ,QAFK,EAAO,SAAS,IAAI,GAElB,eAAe,MAFY;;AAepC,SAAgB,EAAgB,GAAiC;CAC/D,IAAM,IAAS,GAAwB;AACvC,QAAO,IAAS,GAAG,IAAS,MAAS"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/auth/utils/ssoOrigins.ts
|
|
2
|
-
var e = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
|
|
2
|
+
var e = new Set(/* @__PURE__ */ "burdenoff.com,vibecontrols.com,fluidgrids.com,botlit.ai,botmanor.com,bigconsole.com,gethealthybowl.com,headshotmarketing.com,timecampus.com,ggnomad.com,movethewheels.com,crewfoundry.com,planmagnet.com,eventfullymanaged.com,kadaikodi.com,assethandler.com,technospam.com,semanticfed.com,manufacturedops.com,theglobalfuel.com,housingvista.com,proserviceworld.com,mybodyshield.com,artistrybase.com,collabkin.com,brainyrich.com,theculturepost.com,comradecircle.com,cosmicintersection.com,govcitizenhub.com,intelwatchtower.com,ecoimpacthub.com,buildmyiq.com,fluidgrids.ai,atheletebridge.com,adaptercloud.com,coolandlovely.com,subscriberbot.com,labsofscience.com,hookmovies.com".split(","));
|
|
3
3
|
function t(t) {
|
|
4
4
|
if (t.hostname === "localhost" || t.hostname === "127.0.0.1" || t.hostname.endsWith(".local.burdenoff.com")) return !0;
|
|
5
5
|
if (t.protocol !== "https:") return !1;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssoOrigins.js","names":[],"sources":["../../../src/auth/utils/ssoOrigins.ts"],"sourcesContent":["/**\n * Shared first-party SSO origin allow-list for the cross-domain auth pages\n * (SSOClearPage, SSODepositPage, SSORelayPage, SessionBridgePage).\n *\n * These four pages each used to hand-roll an identical copy of this domain set\n * plus its matching function, and they drifted independently — BOFF-4120 was\n * exactly that drift (SSOClearPage's copy was missing admin./alphaadmin., so an\n * admin logout returnTo was rejected on the bridge host and stranded the\n * operator on app.burdenoff.com). Keep the list + matching logic HERE only so\n * a new product domain or subdomain is added in one place. BOFF-4124.\n */\n\nconst ALLOWED_SSO_DOMAINS = new Set<string>([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n \"botlit.ai\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\n/**\n * True when `url` is a first-party Burdenoff shell origin permitted to take\n * part in the cross-domain SSO relay / clear / deposit / session-bridge flow:\n * - localhost / 127.0.0.1 (any port, local dev)\n * - *.local.burdenoff.com (local dev behind nginx)\n * - https://{\"\" | app. | alphaapp. | admin. | alphaadmin. | www.}<product-domain>\n * — note admin./alphaadmin. are included: the admin app shells\n * (admin.burdenoff.com / alphaadmin.burdenoff.com) are first-party origins\n * that relay through and return from the central bridge host.\n */\nexport function isAllowedSsoOrigin(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_SSO_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * String-origin variant for postMessage origin checks (SessionBridgePage),\n * which receives a raw origin string rather than a parsed URL. Returns false on\n * an unparseable origin.\n */\nexport function isAllowedSsoOriginString(origin: string): boolean {\n try {\n return isAllowedSsoOrigin(new URL(origin));\n } catch {\n return false;\n }\n}\n"],"mappings":";AAYA,IAAM,IAAsB,IAAI,IAAY,
|
|
1
|
+
{"version":3,"file":"ssoOrigins.js","names":[],"sources":["../../../src/auth/utils/ssoOrigins.ts"],"sourcesContent":["/**\n * Shared first-party SSO origin allow-list for the cross-domain auth pages\n * (SSOClearPage, SSODepositPage, SSORelayPage, SessionBridgePage).\n *\n * These four pages each used to hand-roll an identical copy of this domain set\n * plus its matching function, and they drifted independently — BOFF-4120 was\n * exactly that drift (SSOClearPage's copy was missing admin./alphaadmin., so an\n * admin logout returnTo was rejected on the bridge host and stranded the\n * operator on app.burdenoff.com). Keep the list + matching logic HERE only so\n * a new product domain or subdomain is added in one place. BOFF-4124.\n */\n\nconst ALLOWED_SSO_DOMAINS = new Set<string>([\n \"burdenoff.com\",\n \"vibecontrols.com\",\n \"fluidgrids.com\",\n // BotManor (formerly Botlit): botmanor.com is the primary domain; botlit.ai\n // stays trusted while it redirects there, so in-flight relays still return.\n \"botlit.ai\",\n \"botmanor.com\",\n \"bigconsole.com\",\n \"gethealthybowl.com\",\n \"headshotmarketing.com\",\n \"timecampus.com\",\n \"ggnomad.com\",\n \"movethewheels.com\",\n \"crewfoundry.com\",\n \"planmagnet.com\",\n \"eventfullymanaged.com\",\n \"kadaikodi.com\",\n \"assethandler.com\",\n \"technospam.com\",\n \"semanticfed.com\",\n \"manufacturedops.com\",\n \"theglobalfuel.com\",\n \"housingvista.com\",\n \"proserviceworld.com\",\n \"mybodyshield.com\",\n \"artistrybase.com\",\n \"collabkin.com\",\n \"brainyrich.com\",\n \"theculturepost.com\",\n \"comradecircle.com\",\n \"cosmicintersection.com\",\n \"govcitizenhub.com\",\n \"intelwatchtower.com\",\n \"ecoimpacthub.com\",\n \"buildmyiq.com\",\n \"fluidgrids.ai\",\n \"atheletebridge.com\",\n \"adaptercloud.com\",\n \"coolandlovely.com\",\n \"subscriberbot.com\",\n \"labsofscience.com\",\n \"hookmovies.com\",\n]);\n\n/**\n * True when `url` is a first-party Burdenoff shell origin permitted to take\n * part in the cross-domain SSO relay / clear / deposit / session-bridge flow:\n * - localhost / 127.0.0.1 (any port, local dev)\n * - *.local.burdenoff.com (local dev behind nginx)\n * - https://{\"\" | app. | alphaapp. | admin. | alphaadmin. | www.}<product-domain>\n * — note admin./alphaadmin. are included: the admin app shells\n * (admin.burdenoff.com / alphaadmin.burdenoff.com) are first-party origins\n * that relay through and return from the central bridge host.\n */\nexport function isAllowedSsoOrigin(url: URL): boolean {\n if (url.hostname === \"localhost\" || url.hostname === \"127.0.0.1\") return true;\n if (url.hostname.endsWith(\".local.burdenoff.com\")) return true;\n if (url.protocol !== \"https:\") return false;\n for (const domain of ALLOWED_SSO_DOMAINS) {\n if (\n url.hostname === domain ||\n url.hostname === `app.${domain}` ||\n url.hostname === `alphaapp.${domain}` ||\n url.hostname === `admin.${domain}` ||\n url.hostname === `alphaadmin.${domain}` ||\n url.hostname === `www.${domain}`\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * String-origin variant for postMessage origin checks (SessionBridgePage),\n * which receives a raw origin string rather than a parsed URL. Returns false on\n * an unparseable origin.\n */\nexport function isAllowedSsoOriginString(origin: string): boolean {\n try {\n return isAllowedSsoOrigin(new URL(origin));\n } catch {\n return false;\n }\n}\n"],"mappings":";AAYA,IAAM,IAAsB,IAAI,IAAY,2rBA2C3C,CAAC;AAYF,SAAgB,EAAmB,GAAmB;AAEpD,KADI,EAAI,aAAa,eAAe,EAAI,aAAa,eACjD,EAAI,SAAS,SAAS,uBAAuB,CAAE,QAAO;AAC1D,KAAI,EAAI,aAAa,SAAU,QAAO;AACtC,MAAK,IAAM,KAAU,EACnB,KACE,EAAI,aAAa,KACjB,EAAI,aAAa,OAAO,OACxB,EAAI,aAAa,YAAY,OAC7B,EAAI,aAAa,SAAS,OAC1B,EAAI,aAAa,cAAc,OAC/B,EAAI,aAAa,OAAO,IAExB,QAAO;AAGX,QAAO;;AAQT,SAAgB,EAAyB,GAAyB;AAChE,KAAI;AACF,SAAO,EAAmB,IAAI,IAAI,EAAO,CAAC;SACpC;AACN,SAAO"}
|
|
@@ -125,3 +125,22 @@ export declare function validateUserFields(input: {
|
|
|
125
125
|
}, options?: {
|
|
126
126
|
requireAll?: boolean;
|
|
127
127
|
}): UserFieldsValidationResult;
|
|
128
|
+
/**
|
|
129
|
+
* Validate a phone number (BOFF-7216).
|
|
130
|
+
*
|
|
131
|
+
* Phone numbers are optional on profile / admin edit forms — an empty input
|
|
132
|
+
* is accepted. When a value is supplied it must:
|
|
133
|
+
* - contain only digits, spaces, and the formatting characters + - ( )
|
|
134
|
+
* - include 7–15 digits (E.164-style length bounds; we don't strip "+"
|
|
135
|
+
* country prefix here, we just count digits)
|
|
136
|
+
* - start with +, a digit, or (
|
|
137
|
+
*
|
|
138
|
+
* Returns `{ valid: true }` on an empty / whitespace-only input.
|
|
139
|
+
* Returns `{ valid: false, error }` with a human-readable message on
|
|
140
|
+
* rejection. The message is intentionally plain English so the caller can
|
|
141
|
+
* either display it verbatim or pipe it through `tr()` with an i18n key.
|
|
142
|
+
*/
|
|
143
|
+
export declare function validatePhoneNumber(phone: string): {
|
|
144
|
+
valid: boolean;
|
|
145
|
+
error?: string;
|
|
146
|
+
};
|
|
@@ -96,7 +96,26 @@ function p(e, t = {}) {
|
|
|
96
96
|
}
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
+
function m(e) {
|
|
100
|
+
let t = e.trim();
|
|
101
|
+
if (t.length === 0) return { valid: !0 };
|
|
102
|
+
if (!/^[0-9+\-() ]+$/.test(t)) return {
|
|
103
|
+
valid: !1,
|
|
104
|
+
error: "Phone number can only contain digits, spaces, and the characters + - ( )"
|
|
105
|
+
};
|
|
106
|
+
let n = t.replace(/[^\d]/g, "");
|
|
107
|
+
return n.length < 7 ? {
|
|
108
|
+
valid: !1,
|
|
109
|
+
error: "Phone number is too short"
|
|
110
|
+
} : n.length > 15 ? {
|
|
111
|
+
valid: !1,
|
|
112
|
+
error: "Phone number is too long"
|
|
113
|
+
} : /^[+\d(]/.test(t) ? { valid: !0 } : {
|
|
114
|
+
valid: !1,
|
|
115
|
+
error: "Phone number must start with +, a digit, or ("
|
|
116
|
+
};
|
|
117
|
+
}
|
|
99
118
|
//#endregion
|
|
100
|
-
export { e as validateEmail, t as validatePassword, p as validateUserFields };
|
|
119
|
+
export { e as validateEmail, t as validatePassword, m as validatePhoneNumber, p as validateUserFields };
|
|
101
120
|
|
|
102
121
|
//# sourceMappingURL=validation.js.map
|