feat(auth): migrate auth plugin from Material-UI to Backstage UI (#33282)

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
Ben Lambert
2026-03-13 09:51:53 +01:00
committed by GitHub
parent 50c2aee83d
commit 06294aafba
5 changed files with 164 additions and 170 deletions
+2 -5
View File
@@ -47,13 +47,10 @@
"test": "backstage-cli package test"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@backstage/ui": "workspace:^",
"@remixicon/react": "^4.6.0",
"react-use": "^17.2.4"
},
"devDependencies": {
@@ -0,0 +1,44 @@
.card {
max-width: 600px;
margin: var(--bui-space-8) auto 0;
}
.appHeader {
display: flex;
align-items: center;
gap: var(--bui-space-4);
}
.appIcon {
color: var(--bui-fg-secondary);
}
.divider {
border: none;
border-top: 1px solid var(--bui-border-1);
margin: 0;
}
.callbackUrl {
font-family: var(--bui-font-monospace);
background: var(--bui-bg-neutral-2);
padding: var(--bui-space-2);
border-radius: var(--bui-radius-2);
word-break: break-all;
font-size: var(--bui-font-size-3);
margin-top: var(--bui-space-2);
}
.completedIcon {
margin-bottom: var(--bui-space-4);
}
.completedIconSuccess {
composes: completedIcon;
color: var(--bui-fg-success);
}
.completedIconDanger {
composes: completedIcon;
color: var(--bui-fg-danger);
}
@@ -16,89 +16,37 @@
import { useParams } from 'react-router-dom';
import {
Alert,
Box,
Button,
Card,
CardActions,
CardContent,
Divider,
makeStyles,
Typography,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
import CancelIcon from '@material-ui/icons/Cancel';
import AppsIcon from '@material-ui/icons/Apps';
import WarningIcon from '@material-ui/icons/Warning';
CardBody,
CardFooter,
Container,
Flex,
FullPage,
Text,
VisuallyHidden,
} from '@backstage/ui';
import {
Content,
EmptyState,
Header,
Page,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
RiAppsLine,
RiCheckboxCircleLine,
RiCloseCircleLine,
} from '@remixicon/react';
import { useConsentSession } from './useConsentSession';
import { configApiRef, useApi } from '@backstage/frontend-plugin-api';
import styles from './ConsentPage.module.css';
const useStyles = makeStyles(theme => ({
authCard: {
maxWidth: 600,
margin: '0 auto',
marginTop: theme.spacing(4),
},
appHeader: {
display: 'flex',
alignItems: 'center',
marginBottom: theme.spacing(2),
},
appIcon: {
marginRight: theme.spacing(2),
fontSize: 40,
},
appName: {
fontSize: '1.5rem',
fontWeight: 'bold',
},
securityWarning: {
margin: theme.spacing(2, 0),
},
buttonContainer: {
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(2),
padding: theme.spacing(2),
},
callbackUrl: {
fontFamily: 'monospace',
backgroundColor: theme.palette.background.default,
padding: theme.spacing(1),
borderRadius: theme.shape.borderRadius,
wordBreak: 'break-all',
fontSize: '0.875rem',
},
scopeList: {
backgroundColor: theme.palette.background.default,
borderRadius: theme.shape.borderRadius,
padding: theme.spacing(1),
},
}));
const ConsentPageLayout = ({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) => (
<Page themeId="tool">
<Header title={title} />
<Content>{children}</Content>
</Page>
const ConsentPageLayout = ({ children }: { children: React.ReactNode }) => (
<FullPage>
<VisuallyHidden>
<h1>Authorization</h1>
</VisuallyHidden>
<Container py="8">{children}</Container>
</FullPage>
);
export const ConsentPage = () => {
const classes = useStyles();
const { sessionId } = useParams<{ sessionId: string }>();
const { state, handleAction } = useConsentSession({ sessionId });
const configApi = useApi(configApiRef);
@@ -106,9 +54,10 @@ export const ConsentPage = () => {
if (!sessionId) {
return (
<ConsentPageLayout title="Authorization Error">
<EmptyState
missing="data"
<ConsentPageLayout>
<Alert
status="info"
icon
title="Invalid Request"
description="The consent request ID is missing or invalid."
/>
@@ -118,57 +67,62 @@ export const ConsentPage = () => {
if (state.status === 'loading') {
return (
<ConsentPageLayout title="Authorization Request">
<Box
display="flex"
justifyContent="center"
alignItems="center"
minHeight={300}
>
<Progress />
</Box>
<ConsentPageLayout>
<Alert loading title="Loading authorization request..." />
</ConsentPageLayout>
);
}
if (state.status === 'error') {
return (
<ConsentPageLayout title="Authorization Error">
<ResponseErrorPanel error={new Error(state.error)} />
<ConsentPageLayout>
<Alert
status="danger"
icon
title="Authorization Error"
description={state.error}
/>
</ConsentPageLayout>
);
}
if (state.status === 'completed') {
return (
<ConsentPageLayout title="Authorization Complete">
<Card className={classes.authCard}>
<CardContent>
<Box textAlign="center">
<ConsentPageLayout>
<Card className={styles.card}>
<CardBody>
<Flex
direction="column"
align="center"
gap="2"
style={{ textAlign: 'center' }}
>
{state.action === 'approve' ? (
<CheckCircleIcon
style={{ fontSize: 64, color: 'green', marginBottom: 16 }}
<RiCheckboxCircleLine
size={64}
className={styles.completedIconSuccess}
/>
) : (
<CancelIcon
style={{ fontSize: 64, color: 'red', marginBottom: 16 }}
<RiCloseCircleLine
size={64}
className={styles.completedIconDanger}
/>
)}
<Typography variant="h5" gutterBottom>
<Text as="h2" variant="title-small">
{state.action === 'approve'
? 'Authorization Approved'
: 'Authorization Denied'}
</Typography>
<Typography variant="body1" color="textSecondary" gutterBottom>
</Text>
<Text variant="body-medium" color="secondary">
{state.action === 'approve'
? `You have successfully authorized the application to access your ${appTitle} account.`
: `You have denied the application access to your ${appTitle} account.`}
</Typography>
<Typography variant="body2" color="textSecondary">
</Text>
<Text variant="body-small" color="secondary">
Redirecting to the application...
</Typography>
</Box>
</CardContent>
</Text>
</Flex>
</CardBody>
</Card>
</ConsentPageLayout>
);
@@ -179,70 +133,67 @@ export const ConsentPage = () => {
const appName = session.clientName ?? session.clientId;
return (
<ConsentPageLayout title="Authorization Request">
<Card className={classes.authCard}>
<CardContent>
<Box className={classes.appHeader}>
<AppsIcon className={classes.appIcon} />
<Box>
<Typography className={classes.appName}>{appName}</Typography>
<Typography variant="body2" color="textSecondary">
wants to access your {appTitle} account
</Typography>
<ConsentPageLayout>
<Card className={styles.card}>
<CardBody>
<Flex direction="column" gap="4">
<Box className={styles.appHeader}>
<RiAppsLine size={40} className={styles.appIcon} />
<Flex direction="column" gap="0.5">
<Text as="span" variant="title-small" weight="bold">
{appName}
</Text>
<Text variant="body-small" color="secondary">
wants to access your {appTitle} account
</Text>
</Flex>
</Box>
</Box>
<Divider />
<hr className={styles.divider} />
<Alert
severity="warning"
icon={<WarningIcon />}
className={classes.securityWarning}
>
<Typography variant="body2">
<strong>Security Notice:</strong> By authorizing this application,
you are granting it access to your {appTitle} account. The
application will receive an access token that allows it to act on
your behalf.
</Typography>
<Box mt={1}>
<Typography variant="body2">
<strong>Callback URL:</strong>
</Typography>
<Box className={classes.callbackUrl}>{session.redirectUri}</Box>
</Box>
</Alert>
<Alert
status="warning"
icon
title="Security Notice"
description={
<>
By authorizing this application, you are granting it access to
your {appTitle} account. The application will receive an
access token that allows it to act on your behalf.
<div className={styles.callbackUrl}>
{session.redirectUri}
</div>
</>
}
/>
<Box mt={2}>
<Typography variant="body2" color="textSecondary">
<Text variant="body-small" color="secondary">
Make sure you trust this application and recognize the callback
URL above. Only authorize applications you trust.
</Typography>
</Box>
</CardContent>
</Text>
</Flex>
</CardBody>
<CardActions className={classes.buttonContainer}>
<Button
variant="outlined"
color="secondary"
size="large"
disabled={isSubmitting}
onClick={() => handleAction('reject')}
startIcon={<CancelIcon />}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
size="large"
disabled={isSubmitting}
onClick={() => handleAction('approve')}
startIcon={<CheckCircleIcon />}
>
{isSubmitting ? 'Authorizing...' : 'Authorize'}
</Button>
</CardActions>
<CardFooter>
<Flex justify="between" gap="4">
<Button
variant="secondary"
isDisabled={isSubmitting}
onPress={() => handleAction('reject')}
iconStart={<RiCloseCircleLine />}
>
Cancel
</Button>
<Button
variant="primary"
isDisabled={isSubmitting}
onPress={() => handleAction('approve')}
iconStart={<RiCheckboxCircleLine />}
>
{isSubmitting ? 'Authorizing...' : 'Authorize'}
</Button>
</Flex>
</CardFooter>
</Card>
</ConsentPageLayout>
);