8000 Add OAuth2 provider app authorization page · coder/coder@4601a22 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4601a22

Browse files
committed
Add OAuth2 provider app authorization page
1 parent 304e9c9 commit 4601a22

File tree

4 files changed

+162
-0
lines changed

4 files changed

+162
-0
lines changed

site/src/AppRouter.tsx

Lines changed: 9 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 OAuth2ProviderAuthorizePage = lazy(
160+
() =>
161+
import("./pages/OAuth2ProviderAuthorizePage/OAuth2ProviderAuthorizePage"),
162+
);
159163
const UserOAuth2ProviderSettingsPage = lazy(
160164
() =>
161165
import("./pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPage"),
@@ -258,6 +262,11 @@ export const AppRouter: FC = () => {
258262
element={<ExternalAuthPage />}
259263
/>
260264

265+
<Route
266+
path="/oauth2-provider/authorize/:appId"
267+
element={<OAuth2ProviderAuthorizePage />}
268+
/>
269+
261270
<Route path="/workspaces" element={<WorkspacesPage />} />
262271

263272
<Route path="/starter-templates">
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { type FC } from "react";
2+
import { Helmet } from "react-helmet-async";
3+
import { useParams, useSearchParams } from "react-router-dom";
4+
import { useMutation, useQuery, useQueryClient } from "react-query";
5+
import { getErrorMessage } from "api/errors";
6+
import { authorizeApp, getApp } from "api/queries/oauth2";
7+
import { displayError } from "components/GlobalSnackbar/utils";
8+
import { useMe } from "hooks";
9+
import { OAuth2ProviderAuthorizePageView } from "./OAuth2ProviderAuthorizePageView";
10+
11+
export const OAuth2ProviderAuthorizePage: FC = () => {
12+
const me = useMe();
13+
const { appId } = useParams() as { appId: string };
14+
const [searchParams] = useSearchParams();
15+
const queryClient = useQueryClient();
16+
const appQuery = useQuery(getApp(appId));
17+
const authorizeAppMutation = useMutation(authorizeApp(queryClient, me.id));
18+
19+
return (
20+
<>
21+
<Helmet>
22+
<title>Authorize OAuth2 Application</title>
23+
</Helmet>
24+
<OAuth2ProviderAuthorizePageView
25+
app={appQuery.data}
26+
isLoading={appQuery.isLoading}
27+
isAuthorizing={authorizeAppMutation.isLoading}
28+
error={appQuery.error}
29+
cancel={(app) => {
30+
window.location.href =
31+
searchParams?.get("redirect_url") || app.callback_url;
32+
}}
33+
authorize={async (app) => {
34+
try {
35+
const auth = await authorizeAppMutation.mutateAsync({
36+
id: app.id,
37+
req: {
38+
redirect_url: searchParams?.get("redirect_url") ?? "",
39+
scope: searchParams?.get("scope") ?? "",
40+
state: searchParams?.get("state") ?? "",
41+
},
42+
});
43+
const url = new URL(auth.redirect_url);
44+
url.searchParams.set("state", auth.state);
45+
url.searchParams.set("code", auth.code);
46+
window.location.href = url.href;
47+
} catch (error) {
48+
displayError(
49+
getErrorMessage(error, "Failed to authorize application."),
50+
);
51+
}
52+
}}
53+
/>
54+
</>
55+
);
56+
};
57+
58+
export default OAuth2ProviderAuthorizePage;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MockOAuth2ProviderApps } from "testHelpers/entities";
3+
import { OAuth2ProviderAuthorizePageView } from "./OAuth2ProviderAuthorizePageView";
4+
5+
const meta: Meta<typeof OAuth2ProviderAuthorizePageView> = {
6+
title: "pages/OAuth2ProviderAuthorizePage",
7+
component: OAuth2ProviderAuthorizePageView,
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof OAuth2ProviderAuthorizePageView>;
12+
13+
export const Loading: Story = {
14+
args: {
15+
isLoading: true,
16+
isAuthorizing: false,
17+
authorize: () => undefined,
18+
cancel: () => undefined,
19+
},
20+
};
21+
22+
export const Error: Story = {
23+
args: {
24+
isLoading: false,
25+
isAuthorizing: false,
26+
error: "some error",
27+
authorize: () => undefined,
28+
cancel: () => undefined,
29+
},
30+
};
31+
32+
export const Loaded: Story = {
33+
args: {
34+
isLoading: false,
35+
isAuthorizing: false,
36+
app: MockOAuth2ProviderApps[0],
37+
authorize: () => undefined,
38+
cancel: () => undefined,
39+
},
40+
};
41+
42+
export const Authorizing: Story = {
43+
args: {
44+
isLoading: false,
45+
isAuthorizing: true,
46+
app: MockOAuth2ProviderApps[0],
47+
authorize: () => undefined,
48+
cancel: () => undefined,
49+
},
50+
};
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { type FC, type FormEvent } from "react";
2+
import type * as TypesGen from "api/typesGenerated";
3+
import { ErrorAlert } from "components/Alert/ErrorAlert";
4+
import { FormFooter } from "components/Form/Form";
5+
import { FullScreenLoader } from "components/Loader/FullScreenLoader";
6+
import { SignInLayout } from "components/SignInLayout/SignInLayout";
7+
import { Welcome } from "components/Welcome/Welcome";
8+
9+
export interface OAuth2ProviderAuthorizePageViewProps {
10+
app?: TypesGen.OAuth2ProviderApp;
11+
isLoading: boolean;
12+
isAuthorizing: boolean;
13+
error?: unknown;
14+
authorize: (app: TypesGen.OAuth2ProviderApp) => void;
15+
cancel: (app: TypesGen.OAuth2ProviderApp) => void;
16+
}
17+
18+
export const OAuth2ProviderAuthorizePageView: FC<
19+
OAuth2ProviderAuthorizePageViewProps
20+
> = ({ app, isLoading, isAuthorizing, error, authorize, cancel }) => {
21+
if (error) {
22+
return <ErrorAlert error={error} />;
23+
}
24+
if (isLoading || !app) {
25+
return <FullScreenLoader />;
26+
}
27+
// TODO: Scopes are ignored for now.
28+
return (
29+
<SignInLayout>
30+
<Welcome>Allow {app.name} full access to your account?</Welcome>
31+
<form
32+
onSubmit={(event: FormEvent) => {
33+
event.preventDefault();
34+
authorize(app);
35+
}}
36+
>
37+
<FormFooter
38+
onCancel={() => cancel(app)}
39+
isLoading={isAuthorizing}
40+
submitLabel="Authorize"
41+
/>
42+
</form>
43+
</SignInLayout>
44+
);
45+
};

0 commit comments

Comments
 (0)
0