refactor(web): resolve Mantine/CSS rule violations in mid-session auth UI
- App.tsx: replace the inline `style={{}}` on the re-auth banner wrapper with
Mantine style props (pos/top/bg) plus a `.reauth-banner-bar` class for the
z-index/box-shadow that have no Mantine shorthand.
- App.tsx: extract the ~100-line inline `onAuthorize` and the inline `onCancel`
step-up handlers into named `handleStepUpAuthorize`/`handleStepUpCancel`.
- ReAuthBanner: move the flat `styles={{ root }}` (background/border) into a
`reauth` Alert theme variant; replace the raw `<span>` with `<Text>`.
- StepUpAuthModal: extract the inline `onClick` wrapper into `handleAuthorize`.
No behavior change; validate + integration green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DZyeWManYTvcXxjcrhxSQc
This commit is contained in:
parent
f4b6095719
commit
74c6491b52
5 changed files with 173 additions and 155 deletions
|
|
@ -412,3 +412,13 @@
|
||||||
.server-drag-handle:active {
|
.server-drag-handle:active {
|
||||||
cursor: grabbing;
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3434,20 +3434,154 @@ function App() {
|
||||||
[pendingElicitations],
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box>
|
<Box>
|
||||||
{reAuthBanner ? (
|
{reAuthBanner ? (
|
||||||
<Box
|
<Box
|
||||||
|
className="reauth-banner-bar"
|
||||||
px="md"
|
px="md"
|
||||||
pt="xs"
|
pt="xs"
|
||||||
style={{
|
pos="sticky"
|
||||||
position: "sticky",
|
top={60}
|
||||||
top: 60,
|
bg="var(--mantine-color-body)"
|
||||||
zIndex: 200,
|
|
||||||
backgroundColor: "var(--mantine-color-body)",
|
|
||||||
boxShadow: "var(--mantine-shadow-sm)",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ReAuthBanner
|
<ReAuthBanner
|
||||||
message={reAuthBanner.message}
|
message={reAuthBanner.message}
|
||||||
|
|
@ -3693,144 +3827,8 @@ function App() {
|
||||||
challenge={pendingStepUp?.challenge ?? null}
|
challenge={pendingStepUp?.challenge ?? null}
|
||||||
authorizationScopes={pendingStepUp?.challenge?.authorizationScopes}
|
authorizationScopes={pendingStepUp?.challenge?.authorizationScopes}
|
||||||
enterpriseManaged={pendingStepUp?.enterpriseManaged}
|
enterpriseManaged={pendingStepUp?.enterpriseManaged}
|
||||||
onAuthorize={async () => {
|
onAuthorize={handleStepUpAuthorize}
|
||||||
if (!pendingStepUp || stepUpAuthorizeInProgressRef.current) {
|
onCancel={handleStepUpCancel}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Alert, Button, Group } from "@mantine/core";
|
import { Alert, Button, Group, Text } from "@mantine/core";
|
||||||
|
|
||||||
export interface ReAuthBannerProps {
|
export interface ReAuthBannerProps {
|
||||||
message: string;
|
message: string;
|
||||||
|
|
@ -14,19 +14,15 @@ export function ReAuthBanner({
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Alert
|
||||||
color="red"
|
color="red"
|
||||||
variant="light"
|
variant="reauth"
|
||||||
title="Re-authentication required"
|
title="Re-authentication required"
|
||||||
withCloseButton
|
withCloseButton
|
||||||
onClose={onDismiss}
|
onClose={onDismiss}
|
||||||
styles={{
|
|
||||||
root: {
|
|
||||||
backgroundColor: "var(--mantine-color-body)",
|
|
||||||
border: "1px solid var(--mantine-color-red-3)",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Group justify="space-between" align="center" wrap="nowrap" gap="md">
|
<Group justify="space-between" align="center" wrap="nowrap" gap="md">
|
||||||
<span>{message}</span>
|
<Text component="span" size="sm">
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
<Button size="xs" variant="filled" onClick={onReauthenticate}>
|
<Button size="xs" variant="filled" onClick={onReauthenticate}>
|
||||||
Re-authenticate
|
Re-authenticate
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export function StepUpAuthModal({
|
||||||
}: StepUpAuthModalProps) {
|
}: StepUpAuthModalProps) {
|
||||||
const additionalScopes = challenge ? stepUpAdditionalScopes(challenge) : [];
|
const additionalScopes = challenge ? stepUpAdditionalScopes(challenge) : [];
|
||||||
const ema = enterpriseManaged === true;
|
const ema = enterpriseManaged === true;
|
||||||
|
const handleAuthorize = () => void onAuthorize();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -79,7 +80,7 @@ export function StepUpAuthModal({
|
||||||
<Button variant="default" onClick={onCancel}>
|
<Button variant="default" onClick={onCancel}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => void onAuthorize()}>Authorize</Button>
|
<Button onClick={handleAuthorize}>Authorize</Button>
|
||||||
</Actions>
|
</Actions>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,17 @@ export const ThemeAlert = Alert.extend({
|
||||||
defaultProps: {
|
defaultProps: {
|
||||||
radius: "md",
|
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: {} };
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue