@burdenoff/microfe-auth 2026.520.2 → 2026.521.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.
@@ -1 +1 @@
1
- {"version":3,"file":"SessionsPage.js","names":[],"sources":["../../../src/auth/pages/SessionsPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useSessionOperations } from \"../hooks\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport type { SessionCard_SessionFragment } from \"../../generated/global-operations\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\nimport { useCreateAuthExportJobMutation } from \"../../generated/wspace-operations\";\n\n/**\n * Active sessions management page with professional UI\n *\n * Fragment: SessionCard_Session (src/operations/global/fragments/SessionCard.fragment.graphql)\n * Query: ActiveSessions (src/operations/global/queries/sessions.graphql)\n */\nexport function SessionsPage() {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const {\n activeSessions,\n revokeSession,\n revokeAllSessions,\n isLoading,\n page,\n totalPages,\n totalCount,\n hasMore,\n goToNextPage,\n goToPrevPage,\n } = useSessionOperations({ disableTokenInfoQuery: true });\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingSessionId, setPendingSessionId] = useState<string | null>(null);\n const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);\n\n const [createExportJob, { loading: isExporting }] =\n useCreateAuthExportJobMutation();\n const [exportError, setExportError] = useState<string | null>(null);\n\n const handleExport = async () => {\n setExportError(null);\n try {\n await createExportJob({\n variables: {\n input: {\n name: \"Sessions Export\",\n entityTypes: [\"auth_sessions\"],\n format: \"CSV\",\n exportScope: \"INDIVIDUAL\",\n includeMetadata: true,\n },\n },\n });\n } catch (err) {\n setExportError(\n err instanceof Error ? err.message : \"Failed to start export\",\n );\n }\n };\n\n // Deduplicate: keep newest session per device name\n const dedupedSessions = useMemo(() => {\n const byDevice = new Map<string, SessionCard_SessionFragment>();\n for (const s of activeSessions) {\n const device = parseDeviceName(s.userAgent ?? \"\");\n const existing = byDevice.get(device);\n if (\n !existing ||\n new Date(s.lastActivityAt) > new Date(existing.lastActivityAt)\n ) {\n byDevice.set(device, s);\n }\n }\n return [...byDevice.values()];\n }, [activeSessions]);\n\n const handleRevokeSession = async (sessionId: string) => {\n try {\n await revokeSession(sessionId);\n setSuccessMessage(\"Session revoked successfully\");\n setPendingSessionId(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to revoke session\");\n }\n };\n\n const handleRevokeAll = async () => {\n try {\n await revokeAllSessions();\n setSuccessMessage(\"All other sessions have been revoked\");\n setShowRevokeAllDialog(false);\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to revoke all sessions\",\n );\n }\n };\n\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\">\n <div className=\"w-16 h-16 mx-auto mb-4 rounded-full bg-status-warning-bg-subtle flex items-center justify-center\">\n <AlertIcon\n className=\"w-8 h-8 text-status-warning-text\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.sessions.authRequired\", \"Authentication Required\")}\n </h2>\n <p className=\"text-text-secondary\">\n {tr(\n \"auth.sessions.signInToView\",\n \"Please sign in to view your active sessions\",\n )}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingSessionId !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingSessionId(null);\n }\n }}\n title=\"Revoke this session?\"\n description=\"The selected device will be signed out immediately and will need to authenticate again.\"\n confirmText=\"Revoke session\"\n variant=\"danger\"\n isPending={isLoading}\n onConfirm={async () => {\n if (pendingSessionId) {\n await handleRevokeSession(pendingSessionId);\n }\n }}\n />\n <ConfirmActionDialog\n open={showRevokeAllDialog}\n onOpenChange={setShowRevokeAllDialog}\n title=\"Revoke every other session?\"\n description=\"All devices except your current one will be signed out immediately.\"\n confirmText=\"Revoke all other sessions\"\n variant=\"warning\"\n isPending={isLoading}\n onConfirm={handleRevokeAll}\n />\n\n {/* Live regions for screen reader 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 {/* 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.sessions.title\", \"Active Sessions\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.sessions.sessionCount\", \"You have\")} {totalCount}{\" \"}\n {tr(\n totalCount !== 1\n ? \"auth.sessions.sessionsPlural\"\n : \"auth.sessions.sessionsSingular\",\n totalCount !== 1 ? \"active sessions\" : \"active session\",\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={handleExport}\n disabled={isExporting}\n aria-label={tr(\n \"auth.sessions.exportAriaLabel\",\n \"Export sessions data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon\n className=\"w-4 h-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"w-4 h-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.sessions.export\", \"Export\")}\n </>\n )}\n </button>\n {totalCount > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowRevokeAllDialog(true)}\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 duration-200\"\n >\n {tr(\"auth.sessions.revokeAll\", \"Revoke All Sessions\")}\n </button>\n )}\n </div>\n </div>\n\n {/* Success Message */}\n {successMessage && (\n <div\n role=\"status\"\n className=\"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 transition-colors duration-200\"\n >\n Dismiss\n </button>\n </div>\n )}\n\n {/* Export Error */}\n {exportError && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"w-5 h-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\">{exportError}</p>\n <button\n type=\"button\"\n onClick={() => setExportError(null)}\n className=\"mt-2 text-sm text-status-error-text hover:underline transition-colors duration-200\"\n >\n {tr(\"auth.sessions.dismiss\", \"Dismiss\")}\n </button>\n </div>\n </div>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"w-5 h-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 transition-colors duration-200\"\n >\n Dismiss\n </button>\n </div>\n </div>\n )}\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=\"w-8 h-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.sessions.loading\", \"Loading sessions...\")}\n </span>\n </div>\n ) : dedupedSessions.length === 0 ? (\n <div\n className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\"\n role=\"status\"\n >\n <div className=\"w-16 h-16 mx-auto mb-4 rounded-full bg-bg-muted flex items-center justify-center\">\n <svg\n className=\"w-8 h-8 text-text-tertiary\"\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={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n <p className=\"text-text-secondary\">\n {tr(\"auth.sessions.noSessions\", \"No active sessions found\")}\n </p>\n </div>\n ) : (\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={isLoading}>\n {dedupedSessions.map((session) => (\n <div\n key={session.id}\n className=\"p-6 rounded-xl border border-border-default bg-bg-surface shadow-sm hover:shadow-md transition-shadow\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-4\">\n {/* Device Icon */}\n <div\n className=\"w-12 h-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"w-6 h-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center gap-3 mb-1\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {session.isCurrent &&\n parseDeviceName(session.userAgent) === \"Unknown Device\"\n ? tr(\"auth.sessions.thisDevice\", \"This Device\")\n : parseDeviceName(session.userAgent)}\n </h3>\n {session.isCurrent && (\n <span\n role=\"status\"\n className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\"\n >\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mb-2\">\n {parseBrowserName(session.userAgent) ===\n \"Unknown Browser\" && session.isCurrent\n ? tr(\"auth.sessions.currentBrowser\", \"Current Browser\")\n : parseBrowserName(session.userAgent)}\n </p>\n <dl className=\"flex flex-wrap gap-4 text-sm text-text-secondary\">\n {session.ipAddress && session.ipAddress !== \"unknown\" && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\"\n />\n </svg>\n <dt className=\"sr-only\">IP address</dt>\n <dd>{session.ipAddress}</dd>\n </div>\n )}\n {session.location && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Location</dt>\n <dd>{session.location}</dd>\n </div>\n )}\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Last active</dt>\n <dd>\n {tr(\"auth.sessions.lastActive\", \"Last active\")}{\" \"}\n {formatRelativeTime(session.lastActivityAt)}\n </dd>\n </div>\n </dl>\n </div>\n </div>\n\n {!session.isCurrent && (\n <button\n type=\"button\"\n onClick={() => setPendingSessionId(session.id)}\n aria-label={`${tr(\"auth.sessions.revoke\", \"Revoke\")} ${parseDeviceName(session.userAgent)} session`}\n className=\"ml-4 px-4 py-2 rounded-lg text-sm font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination controls */}\n {!isLoading && totalPages > 1 && (\n <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={goToPrevPage}\n disabled={page === 0}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.prevPage\", \"Previous page\")}\n >\n <svg\n className=\"w-4 h-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=\"M15 19l-7-7 7-7\"\n />\n </svg>\n {tr(\"auth.sessions.prev\", \"Previous\")}\n </button>\n\n <span className=\"text-sm text-text-secondary\">\n {tr(\"auth.sessions.pageOf\", \"Page\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{page + 1}</span>{\" \"}\n {tr(\"auth.sessions.of\", \"of\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{totalPages}</span>\n </span>\n\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!hasMore}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.nextPage\", \"Next page\")}\n >\n {tr(\"auth.sessions.next\", \"Next\")}\n <svg\n className=\"w-4 h-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=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"SessionsPage.js","names":[],"sources":["../../../src/auth/pages/SessionsPage.tsx"],"sourcesContent":["import { useState, useMemo } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useSessionOperations } from \"../hooks\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\n\n/**\n * Active sessions management page with professional UI\n *\n * Fragment: SessionCard_Session (src/operations/global/fragments/SessionCard.fragment.graphql)\n * Query: ActiveSessions (src/operations/global/queries/sessions.graphql)\n */\nexport function SessionsPage() {\n const { t } = useI18n();\n const tr = (key: string, fallback: string): string => {\n const translated = t(key);\n return translated === key ? fallback : translated;\n };\n\n const isAuthenticated = useAuthStore((state) => state.isAuthenticated);\n const {\n activeSessions,\n revokeSession,\n revokeAllSessions,\n isLoading,\n page,\n totalPages,\n totalCount,\n hasMore,\n goToNextPage,\n goToPrevPage,\n } = useSessionOperations({ disableTokenInfoQuery: true, pageSize: 20 });\n\n // One entry per device: keep the most-recently-active session for each device name\n const dedupedSessions = useMemo(() => {\n const byDevice = new Map<string, (typeof activeSessions)[number]>();\n for (const s of activeSessions) {\n const device = parseDeviceName(s.userAgent ?? \"\");\n const existing = byDevice.get(device);\n if (\n !existing ||\n new Date(s.lastActivityAt) > new Date(existing.lastActivityAt)\n ) {\n byDevice.set(device, s);\n }\n }\n return [...byDevice.values()];\n }, [activeSessions]);\n\n const [error, setError] = useState<string | null>(null);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n const [pendingSessionId, setPendingSessionId] = useState<string | null>(null);\n const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);\n\n const handleRevokeSession = async (sessionId: string) => {\n try {\n await revokeSession(sessionId);\n setSuccessMessage(\"Session revoked successfully\");\n setPendingSessionId(null);\n } catch (err) {\n setError(err instanceof Error ? err.message : \"Failed to revoke session\");\n }\n };\n\n const handleRevokeAll = async () => {\n try {\n await revokeAllSessions();\n setSuccessMessage(\"All other sessions have been revoked\");\n setShowRevokeAllDialog(false);\n } catch (err) {\n setError(\n err instanceof Error ? err.message : \"Failed to revoke all sessions\",\n );\n }\n };\n\n if (!isAuthenticated) {\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <div className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\">\n <div className=\"w-16 h-16 mx-auto mb-4 rounded-full bg-status-warning-bg-subtle flex items-center justify-center\">\n <AlertIcon\n className=\"w-8 h-8 text-status-warning-text\"\n aria-hidden=\"true\"\n />\n </div>\n <h2 className=\"text-xl font-semibold text-text-primary mb-2\">\n {tr(\"auth.sessions.authRequired\", \"Authentication Required\")}\n </h2>\n <p className=\"text-text-secondary\">\n {tr(\n \"auth.sessions.signInToView\",\n \"Please sign in to view your active sessions\",\n )}\n </p>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"max-w-4xl mx-auto p-6\">\n <ConfirmActionDialog\n open={pendingSessionId !== null}\n onOpenChange={(open) => {\n if (!open) {\n setPendingSessionId(null);\n }\n }}\n title=\"Revoke this session?\"\n description=\"The selected device will be signed out immediately and will need to authenticate again.\"\n confirmText=\"Revoke session\"\n variant=\"danger\"\n isPending={isLoading}\n onConfirm={async () => {\n if (pendingSessionId) {\n await handleRevokeSession(pendingSessionId);\n }\n }}\n />\n <ConfirmActionDialog\n open={showRevokeAllDialog}\n onOpenChange={setShowRevokeAllDialog}\n title=\"Revoke every other session?\"\n description=\"All devices except your current one will be signed out immediately.\"\n confirmText=\"Revoke all other sessions\"\n variant=\"warning\"\n isPending={isLoading}\n onConfirm={handleRevokeAll}\n />\n\n {/* Live regions for screen reader 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 {/* 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.sessions.title\", \"Active Sessions\")}\n </h1>\n <p className=\"text-text-secondary mt-2\">\n {tr(\"auth.sessions.sessionCount\", \"You have\")} {totalCount}{\" \"}\n {tr(\n totalCount !== 1\n ? \"auth.sessions.sessionsPlural\"\n : \"auth.sessions.sessionsSingular\",\n totalCount !== 1 ? \"active sessions\" : \"active session\",\n )}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n {/* Export button — commented out until wspace-export-svc is wired for auth_sessions\n <button\n type=\"button\"\n onClick={handleExport}\n disabled={isExporting}\n aria-label={tr(\n \"auth.sessions.exportAriaLabel\",\n \"Export sessions data\",\n )}\n className=\"flex items-center gap-2 px-4 py-2.5 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200\"\n >\n {isExporting ? (\n <>\n <SpinnerIcon\n className=\"w-4 h-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.exporting\", \"Exporting...\")}\n </>\n ) : (\n <>\n <svg\n className=\"w-4 h-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.sessions.export\", \"Export\")}\n </>\n )}\n </button>\n */}\n {totalCount > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowRevokeAllDialog(true)}\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 duration-200\"\n >\n {tr(\"auth.sessions.revokeAll\", \"Revoke All Sessions\")}\n </button>\n )}\n </div>\n </div>\n\n {/* Success Message */}\n {successMessage && (\n <div\n role=\"status\"\n className=\"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 transition-colors duration-200\"\n >\n Dismiss\n </button>\n </div>\n )}\n\n {/* Error Message */}\n {error && (\n <div\n role=\"alert\"\n className=\"mb-6 flex items-start gap-3 p-4 rounded-xl bg-status-error-bg-subtle border border-status-error-border\"\n >\n <AlertIcon\n className=\"w-5 h-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 transition-colors duration-200\"\n >\n Dismiss\n </button>\n </div>\n </div>\n )}\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=\"w-8 h-8 text-text-link animate-spin\"\n aria-hidden=\"true\"\n />\n <span className=\"ml-3 text-text-secondary\">\n {tr(\"auth.sessions.loading\", \"Loading sessions...\")}\n </span>\n </div>\n ) : dedupedSessions.length === 0 ? (\n <div\n className=\"text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\"\n role=\"status\"\n >\n <div className=\"w-16 h-16 mx-auto mb-4 rounded-full bg-bg-muted flex items-center justify-center\">\n <svg\n className=\"w-8 h-8 text-text-tertiary\"\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={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n <p className=\"text-text-secondary\">\n {tr(\"auth.sessions.noSessions\", \"No active sessions found\")}\n </p>\n </div>\n ) : (\n <div className=\"space-y-4\" aria-live=\"polite\" aria-busy={isLoading}>\n {dedupedSessions.map((session) => (\n <div\n key={session.id}\n className=\"p-6 rounded-xl border border-border-default bg-bg-surface shadow-sm hover:shadow-md transition-shadow\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-start gap-4\">\n {/* Device Icon */}\n <div\n className=\"w-12 h-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"w-6 h-6 text-text-link\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={1.5}\n d=\"M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z\"\n />\n </svg>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center gap-3 mb-1\">\n <h3 className=\"text-lg font-semibold text-text-primary\">\n {session.isCurrent &&\n parseDeviceName(session.userAgent) === \"Unknown Device\"\n ? tr(\"auth.sessions.thisDevice\", \"This Device\")\n : parseDeviceName(session.userAgent)}\n </h3>\n {session.isCurrent && (\n <span\n role=\"status\"\n className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\"\n >\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mb-2\">\n {parseBrowserName(session.userAgent) ===\n \"Unknown Browser\" && session.isCurrent\n ? tr(\"auth.sessions.currentBrowser\", \"Current Browser\")\n : parseBrowserName(session.userAgent)}\n </p>\n <dl className=\"flex flex-wrap gap-4 text-sm text-text-secondary\">\n {session.ipAddress && session.ipAddress !== \"unknown\" && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9\"\n />\n </svg>\n <dt className=\"sr-only\">IP address</dt>\n <dd>{session.ipAddress}</dd>\n </div>\n )}\n {session.location && (\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z\"\n />\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 11a3 3 0 11-6 0 3 3 0 016 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Location</dt>\n <dd>{session.location}</dd>\n </div>\n )}\n <div className=\"flex items-center gap-1.5\">\n <svg\n className=\"w-4 h-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=\"M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n <dt className=\"sr-only\">Last active</dt>\n <dd>\n {tr(\"auth.sessions.lastActive\", \"Last active\")}{\" \"}\n {formatRelativeTime(session.lastActivityAt)}\n </dd>\n </div>\n </dl>\n </div>\n </div>\n\n {!session.isCurrent && (\n <button\n type=\"button\"\n onClick={() => setPendingSessionId(session.id)}\n aria-label={`${tr(\"auth.sessions.revoke\", \"Revoke\")} ${parseDeviceName(session.userAgent)} session`}\n className=\"ml-4 px-4 py-2 rounded-lg text-sm font-medium border border-status-error-border text-status-error-text bg-bg-surface hover:bg-status-error-bg-subtle transition-colors duration-200\"\n >\n {tr(\"auth.sessions.revoke\", \"Revoke\")}\n </button>\n )}\n </div>\n </div>\n ))}\n </div>\n )}\n\n {/* Pagination controls */}\n {!isLoading && totalPages > 1 && (\n <div className=\"flex items-center justify-between mt-6 pt-4 border-t border-border-subtle\">\n <button\n type=\"button\"\n onClick={goToPrevPage}\n disabled={page === 0}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.prevPage\", \"Previous page\")}\n >\n <svg\n className=\"w-4 h-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=\"M15 19l-7-7 7-7\"\n />\n </svg>\n {tr(\"auth.sessions.prev\", \"Previous\")}\n </button>\n\n <span className=\"text-sm text-text-secondary\">\n {tr(\"auth.sessions.pageOf\", \"Page\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{page + 1}</span>{\" \"}\n {tr(\"auth.sessions.of\", \"of\")}{\" \"}\n <span className=\"font-medium text-text-primary\">{totalPages}</span>\n </span>\n\n <button\n type=\"button\"\n onClick={goToNextPage}\n disabled={!hasMore}\n className=\"flex items-center gap-2 px-4 py-2 rounded-lg border border-border-default text-text-primary bg-bg-surface hover:bg-bg-sunken disabled:opacity-40 disabled:cursor-not-allowed transition-colors font-medium text-sm\"\n aria-label={tr(\"auth.sessions.nextPage\", \"Next page\")}\n >\n {tr(\"auth.sessions.next\", \"Next\")}\n <svg\n className=\"w-4 h-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=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"UserProfilePage.d.ts","sourceRoot":"","sources":["../../../src/auth/pages/UserProfilePage.tsx"],"names":[],"mappings":"AA0BA;;GAEG;AACH,wBAAgB,eAAe,4CAmpB9B"}
1
+ {"version":3,"file":"UserProfilePage.d.ts","sourceRoot":"","sources":["../../../src/auth/pages/UserProfilePage.tsx"],"names":[],"mappings":"AA4BA;;GAEG;AACH,wBAAgB,eAAe,4CA4pB9B"}