diff --git a/clients/web/src/App.css b/clients/web/src/App.css index 111d4015..fc431065 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -412,3 +412,13 @@ .server-drag-handle:active { cursor: grabbing; } + +/* + * Sticky re-auth banner bar. Positioning (pos/top) and background are set via + * Mantine props on the Box; z-index and box-shadow have no Mantine style-prop + * shorthand, so they live here. + */ +.reauth-banner-bar { + z-index: 200; + box-shadow: var(--mantine-shadow-sm); +} diff --git a/clients/web/src/App.tsx b/clients/web/src/App.tsx index 96fac79e..1999cd1c 100644 --- a/clients/web/src/App.tsx +++ b/clients/web/src/App.tsx @@ -3434,20 +3434,154 @@ function App() { [pendingElicitations], ); + const handleStepUpAuthorize = async () => { + if (!pendingStepUp || stepUpAuthorizeInProgressRef.current) { + return; + } + const stepUp = pendingStepUp; + const client = inspectorClient; + if (!client) { + return; + } + + if (stepUp.enterpriseManaged) { + stepUpAuthorizeInProgressRef.current = true; + setPendingStepUp(null); + notifications.show({ + title: "Organization permissions", + message: emaStepUpInProgressMessage(), + color: "blue", + autoClose: 4000, + }); + try { + const outcome = await client.handleAuthChallenge(stepUp.challenge, { + confirmedStepUp: true, + }); + if (outcome.kind === "satisfied") { + await client.pushRemoteAuthState(); + notifications.show({ + title: "Permissions updated", + message: emaStepUpSuccessMessage({ + recoverySource: stepUp.source, + }), + color: "green", + autoClose: 5000, + }); + const retry = pendingStepUpRetryRef.current; + pendingStepUpRetryRef.current = null; + if (retry) { + await retry(); + } + return; + } + if (outcome.kind === "interactive") { + prepareOAuthRedirect({ + serverId: stepUp.serverId, + authKind: "step_up", + authorizationUrl: outcome.authorizationUrl, + authChallenge: outcome.challenge, + recoverySource: stepUp.source, + }); + return; + } + if (outcome.kind === "failed") { + const failureMessage = emaStepUpFailureMessage(outcome.error.message); + notifications.show({ + title: "Organization permissions", + message: failureMessage, + color: "red", + autoClose: 6000, + }); + switch (stepUp.source) { + case "tool": + setToolCallState({ + status: "error", + error: failureMessage, + }); + break; + case "prompt": + setGetPromptState((prev) => + prev + ? { ...prev, status: "error", error: failureMessage } + : prev, + ); + break; + case "resource": + setReadResourceState((prev) => + prev + ? { ...prev, status: "error", error: failureMessage } + : prev, + ); + break; + default: + break; + } + } + } finally { + stepUpAuthorizeInProgressRef.current = false; + } + return; + } + + stepUpAuthorizeInProgressRef.current = true; + prepareOAuthRedirect({ + serverId: stepUp.serverId, + authKind: "step_up", + authorizationUrl: stepUp.authorizationUrl, + authChallenge: stepUp.challenge, + recoverySource: stepUp.source, + }); + setPendingStepUp(null); + pendingStepUpRetryRef.current = null; + stepUpAuthorizeInProgressRef.current = false; + }; + + const handleStepUpCancel = () => { + const stepUp = pendingStepUpRef.current; + setPendingStepUp(null); + pendingStepUpRetryRef.current = null; + if (!stepUp) { + return; + } + const cancelled = "Authorization cancelled."; + switch (stepUp.source) { + case "tool": + setToolCallState({ status: "error", error: cancelled }); + break; + case "prompt": + setGetPromptState((prev) => + prev ? { ...prev, status: "error", error: cancelled } : prev, + ); + break; + case "resource": + setReadResourceState((prev) => + prev ? { ...prev, status: "error", error: cancelled } : prev, + ); + break; + case "app": + notifications.show({ + title: "Authorization cancelled", + message: cancelled, + color: "gray", + autoClose: 4000, + }); + break; + case "ambient": + break; + } + }; + return ( <> {reAuthBanner ? ( { - if (!pendingStepUp || stepUpAuthorizeInProgressRef.current) { - return; - } - const stepUp = pendingStepUp; - const client = inspectorClient; - if (!client) { - return; - } - - if (stepUp.enterpriseManaged) { - stepUpAuthorizeInProgressRef.current = true; - setPendingStepUp(null); - notifications.show({ - title: "Organization permissions", - message: emaStepUpInProgressMessage(), - color: "blue", - autoClose: 4000, - }); - try { - const outcome = await client.handleAuthChallenge( - stepUp.challenge, - { confirmedStepUp: true }, - ); - if (outcome.kind === "satisfied") { - await client.pushRemoteAuthState(); - notifications.show({ - title: "Permissions updated", - message: emaStepUpSuccessMessage({ - recoverySource: stepUp.source, - }), - color: "green", - autoClose: 5000, - }); - const retry = pendingStepUpRetryRef.current; - pendingStepUpRetryRef.current = null; - if (retry) { - await retry(); - } - return; - } - if (outcome.kind === "interactive") { - prepareOAuthRedirect({ - serverId: stepUp.serverId, - authKind: "step_up", - authorizationUrl: outcome.authorizationUrl, - authChallenge: outcome.challenge, - recoverySource: stepUp.source, - }); - return; - } - if (outcome.kind === "failed") { - const failureMessage = emaStepUpFailureMessage( - outcome.error.message, - ); - notifications.show({ - title: "Organization permissions", - message: failureMessage, - color: "red", - autoClose: 6000, - }); - switch (stepUp.source) { - case "tool": - setToolCallState({ - status: "error", - error: failureMessage, - }); - break; - case "prompt": - setGetPromptState((prev) => - prev - ? { ...prev, status: "error", error: failureMessage } - : prev, - ); - break; - case "resource": - setReadResourceState((prev) => - prev - ? { ...prev, status: "error", error: failureMessage } - : prev, - ); - break; - default: - break; - } - } - } finally { - stepUpAuthorizeInProgressRef.current = false; - } - return; - } - - stepUpAuthorizeInProgressRef.current = true; - prepareOAuthRedirect({ - serverId: stepUp.serverId, - authKind: "step_up", - authorizationUrl: stepUp.authorizationUrl, - authChallenge: stepUp.challenge, - recoverySource: stepUp.source, - }); - setPendingStepUp(null); - pendingStepUpRetryRef.current = null; - stepUpAuthorizeInProgressRef.current = false; - }} - onCancel={() => { - const stepUp = pendingStepUpRef.current; - setPendingStepUp(null); - pendingStepUpRetryRef.current = null; - if (!stepUp) { - return; - } - const cancelled = "Authorization cancelled."; - switch (stepUp.source) { - case "tool": - setToolCallState({ status: "error", error: cancelled }); - break; - case "prompt": - setGetPromptState((prev) => - prev ? { ...prev, status: "error", error: cancelled } : prev, - ); - break; - case "resource": - setReadResourceState((prev) => - prev ? { ...prev, status: "error", error: cancelled } : prev, - ); - break; - case "app": - notifications.show({ - title: "Authorization cancelled", - message: cancelled, - color: "gray", - autoClose: 4000, - }); - break; - case "ambient": - break; - } - }} + onAuthorize={handleStepUpAuthorize} + onCancel={handleStepUpCancel} /> ); diff --git a/clients/web/src/components/groups/ReAuthBanner/ReAuthBanner.tsx b/clients/web/src/components/groups/ReAuthBanner/ReAuthBanner.tsx index 99b8830e..a314b3e3 100644 --- a/clients/web/src/components/groups/ReAuthBanner/ReAuthBanner.tsx +++ b/clients/web/src/components/groups/ReAuthBanner/ReAuthBanner.tsx @@ -1,4 +1,4 @@ -import { Alert, Button, Group } from "@mantine/core"; +import { Alert, Button, Group, Text } from "@mantine/core"; export interface ReAuthBannerProps { message: string; @@ -14,19 +14,15 @@ export function ReAuthBanner({ return ( - {message} + + {message} + diff --git a/clients/web/src/components/groups/StepUpAuthModal/StepUpAuthModal.tsx b/clients/web/src/components/groups/StepUpAuthModal/StepUpAuthModal.tsx index c69c5423..e3e264e4 100644 --- a/clients/web/src/components/groups/StepUpAuthModal/StepUpAuthModal.tsx +++ b/clients/web/src/components/groups/StepUpAuthModal/StepUpAuthModal.tsx @@ -30,6 +30,7 @@ export function StepUpAuthModal({ }: StepUpAuthModalProps) { const additionalScopes = challenge ? stepUpAdditionalScopes(challenge) : []; const ema = enterpriseManaged === true; + const handleAuthorize = () => void onAuthorize(); return ( Cancel - + diff --git a/clients/web/src/theme/Alert.ts b/clients/web/src/theme/Alert.ts index 19148629..d2cad987 100644 --- a/clients/web/src/theme/Alert.ts +++ b/clients/web/src/theme/Alert.ts @@ -4,4 +4,17 @@ export const ThemeAlert = Alert.extend({ defaultProps: { radius: "md", }, + styles: (_theme, props) => { + // Re-auth banner: body-colored surface with a red hairline border, used + // for the persistent mid-session re-authentication banner. + if (props.variant === "reauth") { + return { + root: { + backgroundColor: "var(--mantine-color-body)", + border: "1px solid var(--mantine-color-red-3)", + }, + }; + } + return { root: {} }; + }, });