10000 Add user settings page for revoking OAuth2 apps · coder/coder@514d327 · GitHub
[go: up one dir, main page]

Skip to content

Commit 514d327

Browse files
committed
Add user settings page for revoking OAuth2 apps
1 parent 676ae49 commit 514d327

File tree

5 files changed

+203
-0
lines changed

5 files changed

+203
-0
lines changed 8000

site/src/AppRouter.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ const ExternalAuthPage = lazy(
156156
const UserExternalAuthSettingsPage = lazy(
157157
() => import("./pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPage"),
158158
);
159+
const UserOAuth2ProviderSettingsPage = lazy(
160+
() =>
161+
import("./pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage"),
162+
);
159163
const TemplateVersionPage = lazy(
160164
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
161165
);
@@ -362,6 +366,10 @@ export const AppRouter: FC = () => {
362366
path="external-auth"
363367
element={<UserExternalAuthSettingsPage />}
364368
/>
369+
<Route
370+
path="oauth2-provider"
371+
element={<UserOAuth2ProviderSettingsPage />}
372+
/>
365373
<Route path="tokens">
366374
<Route index element={<TokensPage />} />
367375
<Route path="new" element={<CreateTokenPage />} />
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { type FC, useState } from "react";
2+
import { useMutation, useQuery, useQueryClient } from "react-query";
3+
import { getErrorMessage } from "api/errors";
4+
import { getApps, revokeApp } from "api/queries/oauth2";
5+
import type * as TypesGen from "api/typesGenerated";
6+
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
7+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
8+
import { useMe } from "hooks";
9+
import { Section } from "../Section";
10+
import OAuth2ProviderPageView from "./OAuth2ProviderPageView";
11+
12+
const OAuth2ProviderPage: FC = () => {
13+
const me = useMe();
14+
const queryClient = useQueryClient();
15+
const userOAuth2AppsQuery = useQuery(getApps(me.id));
16+
const revokeAppMutation = useMutation(revokeApp(queryClient, me.id));
17+
const [appToRevoke, setAppToRevoke] = useState<TypesGen.OAuth2ProviderApp>();
18+
19+
return (
20+
<Section title="OAuth2 Applications" layout="fluid">
21+
<OAuth2ProviderPageView
22+
isLoading={userOAuth2AppsQuery.isLoading}
23+
error={userOAuth2AppsQuery.error}
24+
apps={userOAuth2AppsQuery.data}
25+
revoke={(app) => {
26+
setAppToRevoke(app);
27+
}}
28+
/>
29+
{appToRevoke !== undefined && (
30+
<DeleteDialog
31< 57A0 /td>+
title="Revoke Application"
32+
verb="Revoking"
33+
info={`This will invalidate any tokens created by the OAuth2 application "${appToRevoke.name}".`}
34+
label="Name of the application to revoke"
35+
isOpen
36+
confirmLoading={revokeAppMutation.isLoading}
37+
name={appToRevoke.name}
38+
entity="application"
39+
onCancel={() => setAppToRevoke(undefined)}
40+
onConfirm={async () => {
41+
try {
42+
await revokeAppMutation.mutateAsync(appToRevoke.id);
43+
displaySuccess(
44+
`You have successfully revoked the OAuth2 application "${appToRevoke.name}"`,
45+
);
46+
setAppToRevoke(undefined);
47+
} catch (error) {
48+
displayError(
49+
getErrorMessage(error, "Failed to revoke application."),
50+
);
51+
}
52+
}}
53+
/>
54+
)}
55+
</Section>
56+
);
57+
};
58+
59+
export default OAuth2ProviderPage;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MockOAuth2ProviderApps } from "testHelpers/entities";
3+
import OAuth2ProviderPageView from "./OAuth2ProviderPageView";
4+
5+
const meta: Meta<typeof OAuth2ProviderPageView> = {
6+
title: "pages/UserSettingsPage/OAuth2ProviderPageView",
7+
component: OAuth2ProviderPageView,
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof OAuth2ProviderPageView>;
12+
13+
export const Loading: Story = {
14+
args: {
15+
isLoading: true,
16+
revoke: () => undefined,
17+
},
18+
};
19+
20+
export const Error: Story = {
21+
args: {
22+
isLoading: false,
23+
error: "some error",
24+
revoke: () => undefined,
25+
},
26+
};
27+
28+
export const Apps: Story = {
29+
args: {
30+
isLoading: false,
31+
apps: MockOAuth2ProviderApps,
32+
revoke: () => undefined,
33+
},
34+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Button from "@mui/material/Button";
2+
import Table from "@mui/material/Table";
3+
import TableBody from "@mui/material/TableBody";
4+
import TableCell from "@mui/material/TableCell";
5+
import TableContainer from "@mui/material/TableContainer";
6+
import TableHead from "@mui/material/TableHead";
7+
import TableRow from "@mui/material/TableRow";
8+
import { type FC } from "react";
9+
import type * as TypesGen from "api/typesGenerated";
10+
import { AvatarData } from "components/AvatarData/AvatarData";
11+
import { Avatar } from "components/Avatar/Avatar";
12+
import { ErrorAlert } from "components/Alert/ErrorAlert";
13+
import { TableLoader } from "components/TableLoader/TableLoader";
14+
15+
export type OAuth2ProviderPageViewProps = {
16+
isLoading: boolean;
17+
error?: unknown;
18+
apps?: TypesGen.OAuth2ProviderApp[];
19+
revoke: (app: TypesGen.OAuth2ProviderApp) => void;
20+
};
21+
22+
const OAuth2ProviderPageView: FC<OAuth2ProviderPageViewProps> = ({
23+
isLoading,
24+
error,
25+
apps,
26+
revoke,
27+
}) => {
28+
return (
29+
<TableContainer>
30+
<Table>
31+
<TableHead>
32+
<TableRow>
33+
<TableCell width="100%">Name</TableCell>
34+
<TableCell width="1%" />
35+
</TableRow>
36+
</TableHead>
37+
<TableBody>
38+
{isLoading && <TableLoader />}
39+
{!isLoading &&
40+
apps?.map((app) => (
41+
<OAuth2AppRow key={app.id} app={app} revoke={revoke} />
42+
))}
43+
{!isLoading && (!apps || apps?.length === 0) && (
44+
<TableRow>
45+
<TableCell colSpan={999}>
46+
{error ? (
47+
<ErrorAlert error={error} />
48+
) : (
49+
<div css={{ textAlign: "center" }}>
50+
No OAuth2 applications have been authorized.
51+
</div>
52+
)}
53+
</TableCell>
54+
</TableRow>
55+
)}
56+
</TableBody>
57+
</Table>
58+
</TableContainer>
59+
);
60+
};
61+
62+
type OAuth2AppRowProps = {
63+
app: TypesGen.OAuth2ProviderApp;
64+
revoke: (app: TypesGen.OAuth2ProviderApp) => void;
65+
};
66+
67+
const OAuth2AppRow: FC<OAuth2AppRowProps> = ({ app, revoke }) => {
68+
return (
69+
<TableRow key={app.id} data-testid={`app-${app.id}`}>
70+
<TableCell>
71+
<AvatarData
72+
title={app.name}
73+
avatar={
74+
Boolean(app.icon) && (
75+
<Avatar src={app.icon} variant="square" fitImage />
76+
)
77+
}
78+
/>
79+
</TableCell>
80+
81+
<TableCell>
82+
<Button
83+
variant="contained"
84+
size="small"
85+
color="error"
86+
onClick={() => revoke(app)}
87+
>
88+
Revoke&hellip;
89+
</Button>
90+
</TableCell>
91+
</TableRow>
92+
);
93+
};
94+
95+
export default OAuth2ProviderPageView;

site/src/pages/UserSettingsPage/Sidebar.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import AccountIcon from "@mui/icons-material/Person";
55
import AppearanceIcon from "@mui/icons-material/Brush";
66
import ScheduleIcon from "@mui/icons-material/EditCalendarOutlined";
77
import SecurityIcon from "@mui/icons-material/LockOutlined";
8+
import Token from "@mui/icons-material/Token";
89
import type { User } from "api/typesGenerated";
910
import { UserAvatar } from "components/UserAvatar/UserAvatar";
1011
import {
@@ -23,6 +24,7 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
2324
const { entitlements } = useDashboard();
2425
const showSchedulePage =
2526
entitlements.features.advanced_template_scheduling.enabled;
27+
const showOAuth2Page = entitlements.features.oauth2_provider.enabled;
2628

2729
return (
2830
<BaseSidebar>
@@ -53,6 +55,11 @@ export const Sidebar: FC<SidebarProps> = ({ user }) => {
5355
<SidebarNavItem href="external-auth" icon={GitIcon}>
5456
External Authentication
5557
</SidebarNavItem>
58+
{showOAuth2Page && (
59+
<SidebarNavItem href="oauth2-provider" icon={Token}>
60+
OAuth2 Applications
61+
</SidebarNavItem>
62+
)}
5663
<SidebarNavItem href="tokens" icon={VpnKeyOutlined}>
5764
Tokens
5865
</SidebarNavItem>

0 commit comments

Comments
 (0)
0