@burdenoff/microfe-auth 2026.624.3 → 2026.625.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/AuditLogPage.js +73 -72
- package/dist/auth/pages/AuditLogPage.js.map +1 -1
- package/dist/auth/pages/EmailVerifyPage.js +58 -53
- package/dist/auth/pages/EmailVerifyPage.js.map +1 -1
- package/dist/auth/pages/LoginPage.js +222 -214
- package/dist/auth/pages/LoginPage.js.map +1 -1
- package/dist/auth/pages/MagicLinkPage.js +75 -68
- package/dist/auth/pages/MagicLinkPage.js.map +1 -1
- package/dist/auth/pages/MfaSetupPage.js +176 -138
- package/dist/auth/pages/MfaSetupPage.js.map +1 -1
- package/dist/auth/pages/MfaVerifyPage.js +60 -52
- package/dist/auth/pages/MfaVerifyPage.js.map +1 -1
- package/dist/auth/pages/PasswordResetPage.js +277 -262
- package/dist/auth/pages/PasswordResetPage.js.map +1 -1
- package/dist/auth/pages/ReactivatePage.js +54 -46
- package/dist/auth/pages/ReactivatePage.js.map +1 -1
- package/dist/auth/pages/RegisterPage.js +223 -213
- package/dist/auth/pages/RegisterPage.js.map +1 -1
- package/dist/auth/pages/SecurityPage.js +298 -267
- package/dist/auth/pages/SecurityPage.js.map +1 -1
- package/dist/auth/pages/SessionsPage.js +169 -160
- package/dist/auth/pages/SessionsPage.js.map +1 -1
- package/dist/auth/pages/SsoCallbackPage.js +53 -50
- package/dist/auth/pages/SsoCallbackPage.js.map +1 -1
- package/dist/auth/pages/SsoConfigPage.js +466 -444
- package/dist/auth/pages/SsoConfigPage.js.map +1 -1
- package/dist/auth/pages/SsoLoginPage.js +67 -59
- package/dist/auth/pages/SsoLoginPage.js.map +1 -1
- package/dist/auth/pages/UserDetailsPage.js +154 -144
- package/dist/auth/pages/UserDetailsPage.js.map +1 -1
- package/dist/auth/pages/UserProfilePage.js +211 -170
- package/dist/auth/pages/UserProfilePage.js.map +1 -1
- package/dist/auth/pages/UsersPage.js +572 -576
- package/dist/auth/pages/UsersPage.js.map +1 -1
- package/dist/microfe-auth.css +1 -1
- package/package.json +2 -2
|
@@ -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/useSessionOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils/formatters\";\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=\"size-16 mx-auto mb-4 rounded-full bg-status-warning-bg-subtle flex items-center justify-center\">\n <AlertIcon\n className=\"size-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=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.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.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 <output className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\">\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 </output>\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=\"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 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=\"size-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 <output className=\"block text-center py-16 border-2 border-dashed border-border-subtle rounded-xl\">\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-bg-sunken flex items-center justify-center\">\n <svg\n className=\"size-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 </output>\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=\"size-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"size-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 <output className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\">\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </output>\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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"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 { useNavigate } from \"react-router-dom\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport {\n EmphasisPanel,\n IllustratedEmptyState,\n PagePurpose,\n NextSteps,\n} from \"@burdenoff/fe-libs/ui\";\nimport { useSessionOperations } from \"../hooks/useSessionOperations\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { ConfirmActionDialog } from \"../components/ConfirmActionDialog\";\nimport {\n formatRelativeTime,\n parseDeviceName,\n parseBrowserName,\n} from \"../utils/formatters\";\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 navigate = useNavigate();\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 <IllustratedEmptyState\n illustration=\"empty-generic\"\n title={tr(\"auth.sessions.authRequired\", \"Authentication Required\")}\n description={tr(\n \"auth.sessions.signInToView\",\n \"Please sign in to view your active sessions\",\n )}\n />\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-4\">\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=\"size-4 animate-spin\"\n aria-hidden=\"true\"\n />\n {tr(\"auth.sessions.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.sessions.export\", \"Export\")}\n </>\n )}\n </button>\n */}\n </div>\n </div>\n\n <PagePurpose className=\"mb-6\">\n {tr(\n \"auth.sessions.purpose\",\n \"This is every device currently signed in to your account. Recognise them all? If a device looks unfamiliar or you've lost it, revoke its session to sign it out immediately.\",\n )}\n </PagePurpose>\n\n {/* Emphasis: account-access summary — the one highlighted zone */}\n <EmphasisPanel className=\"mb-8\">\n <div className=\"flex items-center justify-between gap-4\">\n <div>\n <p className=\"text-sm font-medium text-text-secondary\">\n {tr(\"auth.sessions.summaryLabel\", \"Devices signed in\")}\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.sessions.summaryHint\",\n \"Keep this list trimmed to devices you trust.\",\n )}\n </p>\n </div>\n {totalCount > 1 && (\n <button\n type=\"button\"\n onClick={() => setShowRevokeAllDialog(true)}\n className=\"shrink-0 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.revokeAllOthers\", \"Revoke all other sessions\")}\n </button>\n )}\n </div>\n </EmphasisPanel>\n\n {/* Success Message */}\n {successMessage && (\n <output className=\"block mb-6 p-4 rounded-xl bg-status-success-bg-subtle border border-status-success-border\">\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 </output>\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=\"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 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=\"size-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 <output className=\"block\">\n <IllustratedEmptyState\n illustration=\"empty-sessions\"\n title={tr(\"auth.sessions.noSessions\", \"No active sessions found\")}\n description={tr(\n \"auth.sessions.noSessionsDesc\",\n \"When you sign in on a device it will appear here so you can review and manage it.\",\n )}\n />\n </output>\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=\"size-12 rounded-xl bg-bg-accent flex items-center justify-center flex-shrink-0\"\n aria-hidden=\"true\"\n >\n <svg\n className=\"size-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 <output className=\"px-2.5 py-1 rounded-full bg-status-success-bg-subtle text-status-success-text text-xs font-medium\">\n {tr(\n \"auth.sessions.currentSession\",\n \"Current Session\",\n )}\n </output>\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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"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=\"M9 5l7 7-7 7\"\n />\n </svg>\n </button>\n </div>\n )}\n\n {!isLoading && dedupedSessions.length > 0 && (\n <NextSteps\n className=\"mt-8\"\n storageKey=\"auth-sessions\"\n steps={[\n {\n id: \"enable-mfa\",\n label: tr(\n \"auth.sessions.nextEnableMfa\",\n \"Turn on two-factor authentication\",\n ),\n description: tr(\n \"auth.sessions.nextEnableMfaDesc\",\n \"Add a second sign-in step so a stolen password isn't enough.\",\n ),\n onClick: () => navigate(\"/auth/mfa\"),\n },\n {\n id: \"security-center\",\n label: tr(\n \"auth.sessions.nextSecurityCenter\",\n \"Open the Security Center\",\n ),\n description: tr(\n \"auth.sessions.nextSecurityCenterDesc\",\n \"Review your password and overall account protection.\",\n ),\n onClick: () => navigate(\"/auth/security\"),\n },\n ]}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -5,75 +5,76 @@ import { useEffect as i, useRef as a, useState as o } from "react";
|
|
|
5
5
|
import { useNavigate as s, useSearchParams as c } from "react-router-dom";
|
|
6
6
|
import { useI18n as l } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
7
7
|
import { Fragment as u, jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
8
|
+
import { Button as p } from "@burdenoff/fe-libs/button";
|
|
8
9
|
//#region src/auth/pages/SsoCallbackPage.tsx
|
|
9
|
-
function
|
|
10
|
-
let { t:
|
|
11
|
-
let n =
|
|
10
|
+
function m() {
|
|
11
|
+
let { t: m } = l(), h = (e, t) => {
|
|
12
|
+
let n = m(e);
|
|
12
13
|
return n === e ? t : n;
|
|
13
|
-
},
|
|
14
|
+
}, g = s(), [_] = c(), v = e((e) => e.setAuthTokens), y = e((e) => e.setAuthenticated), { exchangeOidcCode: b, validateOidcState: x, isExternalOidcCallback: S, handleExternalOidcCallback: C } = r(), [w, T] = o(null), [E, D] = o("processing"), [O, k] = o("unknown"), A = a(!1), j = a(null);
|
|
14
15
|
return i(() => {
|
|
15
|
-
if (
|
|
16
|
-
|
|
16
|
+
if (A.current) return;
|
|
17
|
+
A.current = !0;
|
|
17
18
|
let e = async () => {
|
|
18
19
|
try {
|
|
19
|
-
let e =
|
|
20
|
+
let e = _.get("error"), r = _.get("message") || _.get("error_description");
|
|
20
21
|
if (e) throw Error(r || `SSO authentication failed: ${e}`);
|
|
21
|
-
let i =
|
|
22
|
-
if (i)
|
|
23
|
-
else if (a)
|
|
22
|
+
let i = _.get("token"), a = _.get("code"), o = _.get("state");
|
|
23
|
+
if (i) k("saml"), await t(i);
|
|
24
|
+
else if (a) k("oidc"), await n(a, o);
|
|
24
25
|
else throw Error("Invalid SSO callback. No token or authorization code received.");
|
|
25
26
|
} catch (e) {
|
|
26
|
-
console.error("[SSO Callback] Error:", e),
|
|
27
|
+
console.error("[SSO Callback] Error:", e), T(e instanceof Error ? e.message : "Failed to process SSO response"), D("error");
|
|
27
28
|
}
|
|
28
29
|
}, t = async (e) => {
|
|
29
|
-
let t =
|
|
30
|
+
let t = _.get("refreshToken"), n = _.get("redirect"), r = _.get("requiresMFA") === "true", i = _.get("mfaChallengeId");
|
|
30
31
|
if (r && i) {
|
|
31
|
-
sessionStorage.setItem("mfa_challenge_id", i), sessionStorage.setItem("mfa_pending_token", e), sessionStorage.setItem("mfa_pending_refresh", t || ""), sessionStorage.setItem("mfa_redirect", n || "/dashboard"),
|
|
32
|
+
sessionStorage.setItem("mfa_challenge_id", i), sessionStorage.setItem("mfa_pending_token", e), sessionStorage.setItem("mfa_pending_refresh", t || ""), sessionStorage.setItem("mfa_redirect", n || "/dashboard"), g("/auth/mfa-verify", { replace: !0 });
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
v(e, t || ""), y(!0), D("success"), j.current = setTimeout(() => {
|
|
36
|
+
g(n || "/dashboard", { replace: !0 });
|
|
36
37
|
}, 1500);
|
|
37
38
|
}, n = async (e, t) => {
|
|
38
39
|
let n = sessionStorage.getItem("oidc_redirect");
|
|
39
|
-
if (
|
|
40
|
+
if (S()) {
|
|
40
41
|
if (!t) throw Error("State parameter is required for external OIDC flow");
|
|
41
|
-
let n = await
|
|
42
|
+
let n = await C(e, t);
|
|
42
43
|
if (!n.success) throw Error(n.error || "Failed to authenticate with external OIDC provider");
|
|
43
|
-
|
|
44
|
+
D("success");
|
|
44
45
|
let r = n.redirectPath || "/dashboard";
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
j.current = setTimeout(() => {
|
|
47
|
+
g(r, { replace: !0 });
|
|
47
48
|
}, 1500);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
if (t && !
|
|
51
|
-
let r = await
|
|
51
|
+
if (t && !x(t)) throw Error("Invalid state parameter. This may be a CSRF attack. Please try logging in again.");
|
|
52
|
+
let r = await b(e, sessionStorage.getItem("oidc_client_id") || "default-client", window.location.origin + "/auth/sso/callback");
|
|
52
53
|
if (!r.success) throw Error(r.error || "Failed to exchange authorization code");
|
|
53
54
|
if (r.requiresMFA && r.mfaChallengeId) {
|
|
54
|
-
sessionStorage.setItem("mfa_challenge_id", r.mfaChallengeId), sessionStorage.setItem("mfa_redirect", n || "/dashboard"), sessionStorage.removeItem("oidc_redirect"), sessionStorage.removeItem("oidc_client_id"),
|
|
55
|
+
sessionStorage.setItem("mfa_challenge_id", r.mfaChallengeId), sessionStorage.setItem("mfa_redirect", n || "/dashboard"), sessionStorage.removeItem("oidc_redirect"), sessionStorage.removeItem("oidc_client_id"), g("/auth/mfa-verify", { replace: !0 });
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
|
-
|
|
58
|
+
D("success");
|
|
58
59
|
let i = n || "/dashboard";
|
|
59
|
-
sessionStorage.removeItem("oidc_redirect"), sessionStorage.removeItem("oidc_client_id"),
|
|
60
|
-
|
|
60
|
+
sessionStorage.removeItem("oidc_redirect"), sessionStorage.removeItem("oidc_client_id"), j.current = setTimeout(() => {
|
|
61
|
+
g(i, { replace: !0 });
|
|
61
62
|
}, 1500);
|
|
62
63
|
};
|
|
63
64
|
e();
|
|
64
|
-
let r =
|
|
65
|
+
let r = j;
|
|
65
66
|
return () => {
|
|
66
67
|
r.current &&= (clearTimeout(r.current), null);
|
|
67
68
|
};
|
|
68
69
|
}, [
|
|
69
|
-
g,
|
|
70
|
-
h,
|
|
71
70
|
_,
|
|
71
|
+
g,
|
|
72
72
|
v,
|
|
73
73
|
y,
|
|
74
74
|
b,
|
|
75
75
|
x,
|
|
76
|
-
S
|
|
76
|
+
S,
|
|
77
|
+
C
|
|
77
78
|
]), /* @__PURE__ */ d("div", {
|
|
78
79
|
className: "min-h-screen flex items-center justify-center bg-bg-canvas p-4",
|
|
79
80
|
children: /* @__PURE__ */ d("div", {
|
|
@@ -99,21 +100,21 @@ function p() {
|
|
|
99
100
|
})
|
|
100
101
|
})
|
|
101
102
|
}),
|
|
102
|
-
|
|
103
|
+
E === "processing" && /* @__PURE__ */ f(u, { children: [
|
|
103
104
|
/* @__PURE__ */ d("h1", {
|
|
104
105
|
className: "text-2xl font-semibold text-text-primary mb-4",
|
|
105
|
-
children:
|
|
106
|
+
children: h("auth.ssoCallback.completing", "Completing Sign In")
|
|
106
107
|
}),
|
|
107
108
|
/* @__PURE__ */ d("p", {
|
|
108
109
|
className: "text-text-secondary mb-6",
|
|
109
|
-
children:
|
|
110
|
+
children: O === "oidc" ? h("auth.ssoCallback.exchangingCode", "Exchanging authorization code for access tokens...") : O === "saml" ? h("auth.ssoCallback.validatingSaml", "Validating your SAML credentials...") : h("auth.ssoCallback.validatingCreds", "Validating your credentials from your identity provider...")
|
|
110
111
|
}),
|
|
111
112
|
/* @__PURE__ */ d("div", {
|
|
112
113
|
className: "flex justify-center",
|
|
113
114
|
children: /* @__PURE__ */ d(n, { className: "size-10 text-text-link animate-spin" })
|
|
114
115
|
})
|
|
115
116
|
] }),
|
|
116
|
-
|
|
117
|
+
E === "success" && /* @__PURE__ */ f(u, { children: [
|
|
117
118
|
/* @__PURE__ */ d("div", {
|
|
118
119
|
className: "size-16 mx-auto mb-4 rounded-full bg-status-success-bg-subtle flex items-center justify-center",
|
|
119
120
|
children: /* @__PURE__ */ d("svg", {
|
|
@@ -131,57 +132,59 @@ function p() {
|
|
|
131
132
|
}),
|
|
132
133
|
/* @__PURE__ */ d("h1", {
|
|
133
134
|
className: "text-2xl font-semibold text-text-primary mb-4",
|
|
134
|
-
children:
|
|
135
|
+
children: h("auth.ssoCallback.success", "Sign In Successful")
|
|
135
136
|
}),
|
|
136
137
|
/* @__PURE__ */ d("p", {
|
|
137
138
|
className: "text-text-secondary mb-6",
|
|
138
|
-
children:
|
|
139
|
+
children: O === "oidc" ? h("auth.ssoCallback.oidcComplete", "OIDC authentication complete. Redirecting...") : O === "saml" ? h("auth.ssoCallback.samlComplete", "SAML authentication complete. Redirecting...") : h("auth.ssoCallback.authSuccess", "You have been authenticated successfully. Redirecting...")
|
|
139
140
|
}),
|
|
140
141
|
/* @__PURE__ */ d("div", {
|
|
141
142
|
className: "flex justify-center",
|
|
142
143
|
children: /* @__PURE__ */ d(n, { className: "size-6 text-status-success-text animate-spin" })
|
|
143
144
|
})
|
|
144
145
|
] }),
|
|
145
|
-
|
|
146
|
+
E === "error" && /* @__PURE__ */ f(u, { children: [
|
|
146
147
|
/* @__PURE__ */ d("div", {
|
|
147
148
|
className: "size-16 mx-auto mb-4 rounded-full bg-status-error-bg-subtle flex items-center justify-center",
|
|
148
149
|
children: /* @__PURE__ */ d(t, { className: "size-8 text-status-error-text" })
|
|
149
150
|
}),
|
|
150
151
|
/* @__PURE__ */ d("h1", {
|
|
151
152
|
className: "text-2xl font-semibold text-text-primary mb-4",
|
|
152
|
-
children:
|
|
153
|
+
children: h("auth.ssoCallback.failed", "Sign In Failed")
|
|
153
154
|
}),
|
|
154
155
|
/* @__PURE__ */ d("p", {
|
|
155
156
|
className: "text-status-error-text mb-6 text-sm",
|
|
156
|
-
children:
|
|
157
|
+
children: w
|
|
157
158
|
}),
|
|
158
159
|
/* @__PURE__ */ f("div", {
|
|
159
160
|
className: "space-y-3",
|
|
160
|
-
children: [/* @__PURE__ */ d(
|
|
161
|
+
children: [/* @__PURE__ */ d(p, {
|
|
161
162
|
type: "button",
|
|
162
|
-
onClick: () =>
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
onClick: () => g("/auth/sso/login", { replace: !0 }),
|
|
164
|
+
variant: "default",
|
|
165
|
+
size: "lg",
|
|
166
|
+
className: "w-full font-semibold",
|
|
167
|
+
children: h("auth.ssoCallback.tryAgain", "Try Again")
|
|
165
168
|
}), /* @__PURE__ */ d("button", {
|
|
166
169
|
type: "button",
|
|
167
|
-
onClick: () =>
|
|
170
|
+
onClick: () => g("/auth/login", { replace: !0 }),
|
|
168
171
|
className: "w-full px-6 py-3 rounded-xl border border-border-default text-text-primary font-medium hover:bg-bg-sunken transition-colors",
|
|
169
|
-
children:
|
|
172
|
+
children: h("auth.ssoCallback.backToLogin", "Back to Login")
|
|
170
173
|
})]
|
|
171
174
|
})
|
|
172
175
|
] }),
|
|
173
|
-
|
|
176
|
+
O !== "unknown" && /* @__PURE__ */ d("div", {
|
|
174
177
|
className: "mt-6",
|
|
175
178
|
children: /* @__PURE__ */ d("span", {
|
|
176
|
-
className: `inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${
|
|
177
|
-
children:
|
|
179
|
+
className: `inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${O === "oidc" ? "bg-accent-purple-subtle text-accent-purple" : "bg-status-info-bg-subtle text-text-linkHover"}`,
|
|
180
|
+
children: O === "oidc" ? "OpenID Connect" : "SAML 2.0"
|
|
178
181
|
})
|
|
179
182
|
}),
|
|
180
183
|
/* @__PURE__ */ d("div", {
|
|
181
184
|
className: "mt-8 pt-6 border-t border-border-subtle",
|
|
182
185
|
children: /* @__PURE__ */ d("p", {
|
|
183
186
|
className: "text-xs text-text-secondary",
|
|
184
|
-
children:
|
|
187
|
+
children: h("auth.ssoCallback.helpText", "Having trouble? Contact your organization's IT administrator for assistance with SSO login.")
|
|
185
188
|
})
|
|
186
189
|
})
|
|
187
190
|
]
|
|
@@ -190,6 +193,6 @@ function p() {
|
|
|
190
193
|
});
|
|
191
194
|
}
|
|
192
195
|
//#endregion
|
|
193
|
-
export {
|
|
196
|
+
export { m as SsoCallbackPage };
|
|
194
197
|
|
|
195
198
|
//# sourceMappingURL=SsoCallbackPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SsoCallbackPage.js","names":[],"sources":["../../../src/auth/pages/SsoCallbackPage.tsx"],"sourcesContent":["import { useEffect, useState, useRef } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useSsoOperations } from \"../hooks/useSsoOperations\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\n\n/**\n * SSO Callback Page\n * Handles redirects from:\n * - SAML ACS server (with token/refreshToken in query params)\n * - OIDC Authorization Server (with code/state in query params)\n */\nexport function SsoCallbackPage() {\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 navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const setAuthTokens = useAuthStore((state) => state.setAuthTokens);\n const setAuthenticated = useAuthStore((state) => state.setAuthenticated);\n const {\n exchangeOidcCode,\n validateOidcState,\n isExternalOidcCallback,\n handleExternalOidcCallback,\n } = useSsoOperations();\n\n const [error, setError] = useState<string | null>(null);\n const [status, setStatus] = useState<\"processing\" | \"success\" | \"error\">(\n \"processing\",\n );\n const [callbackType, setCallbackType] = useState<\"saml\" | \"oidc\" | \"unknown\">(\n \"unknown\",\n );\n const processedRef = useRef(false);\n const redirectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n // Prevent double execution in React StrictMode\n if (processedRef.current) {\n return;\n }\n processedRef.current = true;\n\n const processCallback = async () => {\n try {\n // Check for error from either SSO provider\n const errorParam = searchParams.get(\"error\");\n const errorMessage =\n searchParams.get(\"message\") || searchParams.get(\"error_description\");\n\n if (errorParam) {\n throw new Error(\n errorMessage || `SSO authentication failed: ${errorParam}`,\n );\n }\n\n // Detect callback type based on query parameters\n const token = searchParams.get(\"token\");\n const code = searchParams.get(\"code\");\n const state = searchParams.get(\"state\");\n\n if (token) {\n // SAML callback - token already exchanged by ACS server\n setCallbackType(\"saml\");\n await handleSamlCallback(token);\n } else if (code) {\n // OIDC callback - need to exchange code for tokens\n setCallbackType(\"oidc\");\n await handleOidcCallback(code, state);\n } else {\n throw new Error(\n \"Invalid SSO callback. No token or authorization code received.\",\n );\n }\n } catch (err) {\n console.error(\"[SSO Callback] Error:\", err);\n setError(\n err instanceof Error ? err.message : \"Failed to process SSO response\",\n );\n setStatus(\"error\");\n }\n };\n\n /**\n * Handle SAML callback from ACS server\n * Token is already exchanged, just store and redirect\n * Also handles MFA if required\n */\n const handleSamlCallback = async (token: string) => {\n const refreshToken = searchParams.get(\"refreshToken\");\n const redirect = searchParams.get(\"redirect\");\n const requiresMFA = searchParams.get(\"requiresMFA\") === \"true\";\n const mfaChallengeId = searchParams.get(\"mfaChallengeId\");\n\n // Check if MFA is required\n if (requiresMFA && mfaChallengeId) {\n // Store partial auth state and MFA info\n sessionStorage.setItem(\"mfa_challenge_id\", mfaChallengeId);\n sessionStorage.setItem(\"mfa_pending_token\", token);\n sessionStorage.setItem(\"mfa_pending_refresh\", refreshToken || \"\");\n sessionStorage.setItem(\"mfa_redirect\", redirect || \"/dashboard\");\n\n // Redirect to MFA verification\n navigate(\"/auth/mfa-verify\", { replace: true });\n return;\n }\n\n // Store tokens\n setAuthTokens(token, refreshToken || \"\");\n setAuthenticated(true);\n\n setStatus(\"success\");\n\n // Redirect to the specified URL or default to dashboard\n redirectTimerRef.current = setTimeout(() => {\n const redirectUrl = redirect || \"/dashboard\";\n navigate(redirectUrl, { replace: true });\n }, 1500);\n };\n\n /**\n * Handle OIDC callback - exchange code for tokens\n */\n const handleOidcCallback = async (code: string, state: string | null) => {\n // Cache the OIDC redirect target — used in both the MFA branch and the\n // success branch, plus removed once at the end of the flow.\n const oidcRedirect = sessionStorage.getItem(\"oidc_redirect\");\n const isExternal = isExternalOidcCallback();\n\n // Check if this is an external OIDC flow (Azure AD, Google, etc.)\n if (isExternal) {\n if (!state) {\n throw new Error(\"State parameter is required for external OIDC flow\");\n }\n\n // Use the external OIDC handler\n const result = await handleExternalOidcCallback(code, state);\n\n if (!result.success) {\n throw new Error(\n result.error ||\n \"Failed to authenticate with external OIDC provider\",\n );\n }\n\n setStatus(\"success\");\n\n // Redirect after brief success message using the redirect path from result\n const redirectUrl = result.redirectPath || \"/dashboard\";\n redirectTimerRef.current = setTimeout(() => {\n navigate(redirectUrl, { replace: true });\n }, 1500);\n return;\n }\n\n // Internal OIDC flow (our auth service as provider)\n // Validate state to prevent CSRF\n if (state && !validateOidcState(state)) {\n throw new Error(\n \"Invalid state parameter. This may be a CSRF attack. Please try logging in again.\",\n );\n }\n\n // Get client configuration from session storage\n const clientId =\n sessionStorage.getItem(\"oidc_client_id\") || \"default-client\";\n const redirectUri = window.location.origin + \"/auth/sso/callback\";\n\n // Exchange code for tokens\n const result = await exchangeOidcCode(code, clientId, redirectUri);\n\n if (!result.success) {\n throw new Error(\n result.error || \"Failed to exchange authorization code\",\n );\n }\n\n // Check if MFA is required after OIDC authentication\n if (result.requiresMFA && result.mfaChallengeId) {\n // Store MFA challenge info for the verification page\n sessionStorage.setItem(\"mfa_challenge_id\", result.mfaChallengeId);\n sessionStorage.setItem(\"mfa_redirect\", oidcRedirect || \"/dashboard\");\n\n // Clean up OIDC session data\n sessionStorage.removeItem(\"oidc_redirect\");\n sessionStorage.removeItem(\"oidc_client_id\");\n\n // Redirect to MFA verification\n navigate(\"/auth/mfa-verify\", { replace: true });\n return;\n }\n\n setStatus(\"success\");\n\n // Get redirect URL from state or session storage\n const redirect = oidcRedirect || \"/dashboard\";\n sessionStorage.removeItem(\"oidc_redirect\");\n sessionStorage.removeItem(\"oidc_client_id\");\n\n // Redirect after brief success message\n redirectTimerRef.current = setTimeout(() => {\n navigate(redirect, { replace: true });\n }, 1500);\n };\n\n processCallback();\n const timerRefCapture = redirectTimerRef;\n return () => {\n if (timerRefCapture.current) {\n clearTimeout(timerRefCapture.current);\n timerRefCapture.current = null;\n }\n };\n }, [\n searchParams,\n navigate,\n setAuthTokens,\n setAuthenticated,\n exchangeOidcCode,\n validateOidcState,\n isExternalOidcCallback,\n handleExternalOidcCallback,\n ]);\n\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-bg-canvas p-4\">\n <div className=\"w-full max-w-md\">\n <div className=\"bg-bg-surface rounded-2xl shadow-xl p-8 text-center\">\n {/* Logo */}\n <div className=\"mb-6\">\n <div className=\"auth-brand-badge mx-auto flex size-16 items-center justify-center rounded-xl\">\n <svg\n className=\"size-8 text-white\"\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=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n </div>\n </div>\n\n {/* Processing State */}\n {status === \"processing\" && (\n <>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.completing\", \"Completing Sign In\")}\n </h1>\n <p className=\"text-text-secondary mb-6\">\n {callbackType === \"oidc\"\n ? tr(\n \"auth.ssoCallback.exchangingCode\",\n \"Exchanging authorization code for access tokens...\",\n )\n : callbackType === \"saml\"\n ? tr(\n \"auth.ssoCallback.validatingSaml\",\n \"Validating your SAML credentials...\",\n )\n : tr(\n \"auth.ssoCallback.validatingCreds\",\n \"Validating your credentials from your identity provider...\",\n )}\n </p>\n <div className=\"flex justify-center\">\n <SpinnerIcon className=\"size-10 text-text-link animate-spin\" />\n </div>\n </>\n )}\n\n {/* Success State */}\n {status === \"success\" && (\n <>\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-success-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-8 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.success\", \"Sign In Successful\")}\n </h1>\n <p className=\"text-text-secondary mb-6\">\n {callbackType === \"oidc\"\n ? tr(\n \"auth.ssoCallback.oidcComplete\",\n \"OIDC authentication complete. Redirecting...\",\n )\n : callbackType === \"saml\"\n ? tr(\n \"auth.ssoCallback.samlComplete\",\n \"SAML authentication complete. Redirecting...\",\n )\n : tr(\n \"auth.ssoCallback.authSuccess\",\n \"You have been authenticated successfully. Redirecting...\",\n )}\n </p>\n <div className=\"flex justify-center\">\n <SpinnerIcon className=\"size-6 text-status-success-text animate-spin\" />\n </div>\n </>\n )}\n\n {/* Error State */}\n {status === \"error\" && (\n <>\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <AlertIcon className=\"size-8 text-status-error-text\" />\n </div>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.failed\", \"Sign In Failed\")}\n </h1>\n <p className=\"text-status-error-text mb-6 text-sm\">{error}</p>\n <div className=\"space-y-3\">\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/sso/login\", { replace: true })}\n className=\"auth-primary-button w-full px-6 py-3 font-medium\"\n >\n {tr(\"auth.ssoCallback.tryAgain\", \"Try Again\")}\n </button>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\", { replace: true })}\n className=\"w-full 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.ssoCallback.backToLogin\", \"Back to Login\")}\n </button>\n </div>\n </>\n )}\n\n {/* SSO Type Badge */}\n {callbackType !== \"unknown\" && (\n <div className=\"mt-6\">\n <span\n className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${\n callbackType === \"oidc\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : \"bg-status-info-bg-subtle text-text-linkHover\"\n }`}\n >\n {callbackType === \"oidc\" ? \"OpenID Connect\" : \"SAML 2.0\"}\n </span>\n </div>\n )}\n\n {/* Help Text */}\n <div className=\"mt-8 pt-6 border-t border-border-subtle\">\n <p className=\"text-xs text-text-secondary\">\n {tr(\n \"auth.ssoCallback.helpText\",\n \"Having trouble? Contact your organization's IT administrator for assistance with SSO login.\",\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"SsoCallbackPage.js","names":[],"sources":["../../../src/auth/pages/SsoCallbackPage.tsx"],"sourcesContent":["import { useEffect, useState, useRef } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell\";\nimport { useNavigate, useSearchParams } from \"react-router-dom\";\nimport { useAuthStore } from \"../store/authStore\";\nimport { useSsoOperations } from \"../hooks/useSsoOperations\";\nimport { Button } from \"@burdenoff/fe-libs/button\";\nimport { SpinnerIcon, AlertIcon } from \"../components/AuthIcons\";\n\n/**\n * SSO Callback Page\n * Handles redirects from:\n * - SAML ACS server (with token/refreshToken in query params)\n * - OIDC Authorization Server (with code/state in query params)\n */\nexport function SsoCallbackPage() {\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 navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const setAuthTokens = useAuthStore((state) => state.setAuthTokens);\n const setAuthenticated = useAuthStore((state) => state.setAuthenticated);\n const {\n exchangeOidcCode,\n validateOidcState,\n isExternalOidcCallback,\n handleExternalOidcCallback,\n } = useSsoOperations();\n\n const [error, setError] = useState<string | null>(null);\n const [status, setStatus] = useState<\"processing\" | \"success\" | \"error\">(\n \"processing\",\n );\n const [callbackType, setCallbackType] = useState<\"saml\" | \"oidc\" | \"unknown\">(\n \"unknown\",\n );\n const processedRef = useRef(false);\n const redirectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(() => {\n // Prevent double execution in React StrictMode\n if (processedRef.current) {\n return;\n }\n processedRef.current = true;\n\n const processCallback = async () => {\n try {\n // Check for error from either SSO provider\n const errorParam = searchParams.get(\"error\");\n const errorMessage =\n searchParams.get(\"message\") || searchParams.get(\"error_description\");\n\n if (errorParam) {\n throw new Error(\n errorMessage || `SSO authentication failed: ${errorParam}`,\n );\n }\n\n // Detect callback type based on query parameters\n const token = searchParams.get(\"token\");\n const code = searchParams.get(\"code\");\n const state = searchParams.get(\"state\");\n\n if (token) {\n // SAML callback - token already exchanged by ACS server\n setCallbackType(\"saml\");\n await handleSamlCallback(token);\n } else if (code) {\n // OIDC callback - need to exchange code for tokens\n setCallbackType(\"oidc\");\n await handleOidcCallback(code, state);\n } else {\n throw new Error(\n \"Invalid SSO callback. No token or authorization code received.\",\n );\n }\n } catch (err) {\n console.error(\"[SSO Callback] Error:\", err);\n setError(\n err instanceof Error ? err.message : \"Failed to process SSO response\",\n );\n setStatus(\"error\");\n }\n };\n\n /**\n * Handle SAML callback from ACS server\n * Token is already exchanged, just store and redirect\n * Also handles MFA if required\n */\n const handleSamlCallback = async (token: string) => {\n const refreshToken = searchParams.get(\"refreshToken\");\n const redirect = searchParams.get(\"redirect\");\n const requiresMFA = searchParams.get(\"requiresMFA\") === \"true\";\n const mfaChallengeId = searchParams.get(\"mfaChallengeId\");\n\n // Check if MFA is required\n if (requiresMFA && mfaChallengeId) {\n // Store partial auth state and MFA info\n sessionStorage.setItem(\"mfa_challenge_id\", mfaChallengeId);\n sessionStorage.setItem(\"mfa_pending_token\", token);\n sessionStorage.setItem(\"mfa_pending_refresh\", refreshToken || \"\");\n sessionStorage.setItem(\"mfa_redirect\", redirect || \"/dashboard\");\n\n // Redirect to MFA verification\n navigate(\"/auth/mfa-verify\", { replace: true });\n return;\n }\n\n // Store tokens\n setAuthTokens(token, refreshToken || \"\");\n setAuthenticated(true);\n\n setStatus(\"success\");\n\n // Redirect to the specified URL or default to dashboard\n redirectTimerRef.current = setTimeout(() => {\n const redirectUrl = redirect || \"/dashboard\";\n navigate(redirectUrl, { replace: true });\n }, 1500);\n };\n\n /**\n * Handle OIDC callback - exchange code for tokens\n */\n const handleOidcCallback = async (code: string, state: string | null) => {\n // Cache the OIDC redirect target — used in both the MFA branch and the\n // success branch, plus removed once at the end of the flow.\n const oidcRedirect = sessionStorage.getItem(\"oidc_redirect\");\n const isExternal = isExternalOidcCallback();\n\n // Check if this is an external OIDC flow (Azure AD, Google, etc.)\n if (isExternal) {\n if (!state) {\n throw new Error(\"State parameter is required for external OIDC flow\");\n }\n\n // Use the external OIDC handler\n const result = await handleExternalOidcCallback(code, state);\n\n if (!result.success) {\n throw new Error(\n result.error ||\n \"Failed to authenticate with external OIDC provider\",\n );\n }\n\n setStatus(\"success\");\n\n // Redirect after brief success message using the redirect path from result\n const redirectUrl = result.redirectPath || \"/dashboard\";\n redirectTimerRef.current = setTimeout(() => {\n navigate(redirectUrl, { replace: true });\n }, 1500);\n return;\n }\n\n // Internal OIDC flow (our auth service as provider)\n // Validate state to prevent CSRF\n if (state && !validateOidcState(state)) {\n throw new Error(\n \"Invalid state parameter. This may be a CSRF attack. Please try logging in again.\",\n );\n }\n\n // Get client configuration from session storage\n const clientId =\n sessionStorage.getItem(\"oidc_client_id\") || \"default-client\";\n const redirectUri = window.location.origin + \"/auth/sso/callback\";\n\n // Exchange code for tokens\n const result = await exchangeOidcCode(code, clientId, redirectUri);\n\n if (!result.success) {\n throw new Error(\n result.error || \"Failed to exchange authorization code\",\n );\n }\n\n // Check if MFA is required after OIDC authentication\n if (result.requiresMFA && result.mfaChallengeId) {\n // Store MFA challenge info for the verification page\n sessionStorage.setItem(\"mfa_challenge_id\", result.mfaChallengeId);\n sessionStorage.setItem(\"mfa_redirect\", oidcRedirect || \"/dashboard\");\n\n // Clean up OIDC session data\n sessionStorage.removeItem(\"oidc_redirect\");\n sessionStorage.removeItem(\"oidc_client_id\");\n\n // Redirect to MFA verification\n navigate(\"/auth/mfa-verify\", { replace: true });\n return;\n }\n\n setStatus(\"success\");\n\n // Get redirect URL from state or session storage\n const redirect = oidcRedirect || \"/dashboard\";\n sessionStorage.removeItem(\"oidc_redirect\");\n sessionStorage.removeItem(\"oidc_client_id\");\n\n // Redirect after brief success message\n redirectTimerRef.current = setTimeout(() => {\n navigate(redirect, { replace: true });\n }, 1500);\n };\n\n processCallback();\n const timerRefCapture = redirectTimerRef;\n return () => {\n if (timerRefCapture.current) {\n clearTimeout(timerRefCapture.current);\n timerRefCapture.current = null;\n }\n };\n }, [\n searchParams,\n navigate,\n setAuthTokens,\n setAuthenticated,\n exchangeOidcCode,\n validateOidcState,\n isExternalOidcCallback,\n handleExternalOidcCallback,\n ]);\n\n return (\n <div className=\"min-h-screen flex items-center justify-center bg-bg-canvas p-4\">\n <div className=\"w-full max-w-md\">\n <div className=\"bg-bg-surface rounded-2xl shadow-xl p-8 text-center\">\n {/* Logo */}\n <div className=\"mb-6\">\n <div className=\"auth-brand-badge mx-auto flex size-16 items-center justify-center rounded-xl\">\n <svg\n className=\"size-8 text-white\"\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=\"M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z\"\n />\n </svg>\n </div>\n </div>\n\n {/* Processing State */}\n {status === \"processing\" && (\n <>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.completing\", \"Completing Sign In\")}\n </h1>\n <p className=\"text-text-secondary mb-6\">\n {callbackType === \"oidc\"\n ? tr(\n \"auth.ssoCallback.exchangingCode\",\n \"Exchanging authorization code for access tokens...\",\n )\n : callbackType === \"saml\"\n ? tr(\n \"auth.ssoCallback.validatingSaml\",\n \"Validating your SAML credentials...\",\n )\n : tr(\n \"auth.ssoCallback.validatingCreds\",\n \"Validating your credentials from your identity provider...\",\n )}\n </p>\n <div className=\"flex justify-center\">\n <SpinnerIcon className=\"size-10 text-text-link animate-spin\" />\n </div>\n </>\n )}\n\n {/* Success State */}\n {status === \"success\" && (\n <>\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-success-bg-subtle flex items-center justify-center\">\n <svg\n className=\"size-8 text-status-success-text\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.success\", \"Sign In Successful\")}\n </h1>\n <p className=\"text-text-secondary mb-6\">\n {callbackType === \"oidc\"\n ? tr(\n \"auth.ssoCallback.oidcComplete\",\n \"OIDC authentication complete. Redirecting...\",\n )\n : callbackType === \"saml\"\n ? tr(\n \"auth.ssoCallback.samlComplete\",\n \"SAML authentication complete. Redirecting...\",\n )\n : tr(\n \"auth.ssoCallback.authSuccess\",\n \"You have been authenticated successfully. Redirecting...\",\n )}\n </p>\n <div className=\"flex justify-center\">\n <SpinnerIcon className=\"size-6 text-status-success-text animate-spin\" />\n </div>\n </>\n )}\n\n {/* Error State */}\n {status === \"error\" && (\n <>\n <div className=\"size-16 mx-auto mb-4 rounded-full bg-status-error-bg-subtle flex items-center justify-center\">\n <AlertIcon className=\"size-8 text-status-error-text\" />\n </div>\n <h1 className=\"text-2xl font-semibold text-text-primary mb-4\">\n {tr(\"auth.ssoCallback.failed\", \"Sign In Failed\")}\n </h1>\n <p className=\"text-status-error-text mb-6 text-sm\">{error}</p>\n <div className=\"space-y-3\">\n <Button\n type=\"button\"\n onClick={() => navigate(\"/auth/sso/login\", { replace: true })}\n variant=\"default\"\n size=\"lg\"\n className=\"w-full font-semibold\"\n >\n {tr(\"auth.ssoCallback.tryAgain\", \"Try Again\")}\n </Button>\n <button\n type=\"button\"\n onClick={() => navigate(\"/auth/login\", { replace: true })}\n className=\"w-full 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.ssoCallback.backToLogin\", \"Back to Login\")}\n </button>\n </div>\n </>\n )}\n\n {/* SSO Type Badge */}\n {callbackType !== \"unknown\" && (\n <div className=\"mt-6\">\n <span\n className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${\n callbackType === \"oidc\"\n ? \"bg-accent-purple-subtle text-accent-purple\"\n : \"bg-status-info-bg-subtle text-text-linkHover\"\n }`}\n >\n {callbackType === \"oidc\" ? \"OpenID Connect\" : \"SAML 2.0\"}\n </span>\n </div>\n )}\n\n {/* Help Text */}\n <div className=\"mt-8 pt-6 border-t border-border-subtle\">\n <p className=\"text-xs text-text-secondary\">\n {tr(\n \"auth.ssoCallback.helpText\",\n \"Having trouble? Contact your organization's IT administrator for assistance with SSO login.\",\n )}\n </p>\n </div>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|