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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy