Skip to content

Commit a9c89e3

Browse files
committed
github oauth2 device flow frontend
1 parent 2896490 commit a9c89e3

File tree

7 files changed

+321
-105
lines changed

7 files changed

+321
-105
lines changed

site/src/api/api.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,6 +1605,29 @@ class ApiMethods {
16051605
return resp.data;
16061606
};
16071607

1608+
getOAuth2GitHubDeviceFlowCallback = async (
1609+
code: string,
1610+
state: string,
1611+
): Promise<TypesGen.OAuth2DeviceFlowCallbackResponse> => {
1612+
const resp = await this.axios.get(
1613+
`/api/v2/users/oauth2/github/callback?code=${code}&state=${state}`,
1614+
);
1615+
// sanity check
1616+
if (
1617+
typeof resp.data !== "object" ||
1618+
typeof resp.data.redirect_url !== "string"
1619+
) {
1620+
console.error("Invalid response from OAuth2 GitHub callback", resp);
1621+
throw new Error("Invalid response from OAuth2 GitHub callback");
1622+
}
1623+
return resp.data;
1624+
};
1625+
1626+
getOAuth2GitHubDevice = async (): Promise<TypesGen.ExternalAuthDevice> => {
1627+
const resp = await this.axios.get("/api/v2/users/oauth2/github/device");
1628+
return resp.data;
1629+
};
1630+
16081631
getOAuth2ProviderApps = async (
16091632
filter?: TypesGen.OAuth2ProviderAppFilter,
16101633
): Promise<TypesGen.OAuth2ProviderApp[]> => {

site/src/api/queries/oauth2.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,20 @@ const userAppsKey = (userId: string) => appsKey.concat(userId);
77
const appKey = (appId: string) => appsKey.concat(appId);
88
const appSecretsKey = (appId: string) => appKey(appId).concat("secrets");
99

10+
export const getGitHubDevice = () => {
11+
return {
12+
queryKey: ["oauth2-provider", "github", "device"],
13+
queryFn: () => API.getOAuth2GitHubDevice(),
14+
};
15+
};
16+
17+
export const getGitHubDeviceFlowCallback = (code: string, state: string) => {
18+
return {
19+
queryKey: ["oauth2-provider", "github", "callback", code, state],
20+
queryFn: () => API.getOAuth2GitHubDeviceFlowCallback(code, state),
21+
};
22+
};
23+
1024
export const getApps = (userId?: string) => {
1125
return {
1226
queryKey: userId ? appsKey.concat(userId) : appsKey,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
3+
import AlertTitle from "@mui/material/AlertTitle";
4+
import CircularProgress from "@mui/material/CircularProgress";
5+
import Link from "@mui/material/Link";
6+
import type { ApiErrorResponse } from "api/errors";
7+
import type { ExternalAuthDevice } from "api/typesGenerated";
8+
import { Alert, AlertDetail } from "components/Alert/Alert";
9+
import { CopyButton } from "components/CopyButton/CopyButton";
10+
import type { FC } from "react";
11+
12+
interface GitDeviceAuthProps {
13+
externalAuthDevice?: ExternalAuthDevice;
14+
deviceExchangeError?: ApiErrorResponse;
15+
}
16+
17+
export const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
18+
externalAuthDevice,
19+
deviceExchangeError,
20+
}) => {
21+
let status = (
22+
<p css={styles.status}>
23+
<CircularProgress size={16} color="secondary" data-chromatic="ignore" />
24+
Checking for authentication...
25+
</p>
26+
);
27+
if (deviceExchangeError) {
28+
// See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
29+
switch (deviceExchangeError.detail) {
30+
case "authorization_pending":
31+
break;
32+
case "expired_token":
33+
status = (
34+
<Alert severity="error">
35+
The one-time code has expired. Refresh to get a new one!
36+
</Alert>
37+
);
38+
break;
39+
case "access_denied":
40+
status = (
41+
<Alert severity="error">Access to the Git provider was denied.</Alert>
42+
);
43+
break;
44+
default:
45+
status = (
46+
<Alert severity="error">
47+
<AlertTitle>{deviceExchangeError.message}</AlertTitle>
48+
{deviceExchangeError.detail && (
49+
<AlertDetail>{deviceExchangeError.detail}</AlertDetail>
50+
)}
51+
</Alert>
52+
);
53+
break;
54+
}
55+
}
56+
57+
// If the error comes from the `externalAuthDevice` query,
58+
// we cannot even display the user_code.
59+
if (deviceExchangeError && !externalAuthDevice) {
60+
return <div>{status}</div>;
61+
}
62+
63+
if (!externalAuthDevice) {
64+
return <CircularProgress />;
65+
}
66+
67+
return (
68+
<div>
69+
<p css={styles.text}>
70+
Copy your one-time code:&nbsp;
71+
<div css={styles.copyCode}>
72+
<span css={styles.code}>{externalAuthDevice.user_code}</span>
73+
&nbsp; <CopyButton text={externalAuthDevice.user_code} />
74+
</div>
75+
<br />
76+
Then open the link below and paste it:
77+
</p>
78+
<div css={styles.links}>
79+
<Link
80+
css={styles.link}
81+
href={externalAuthDevice.verification_uri}
82+
target="_blank"
83+
rel="noreferrer"
84+
>
85+
<OpenInNewIcon fontSize="small" />
86+
Open and Paste
87+
</Link>
88+
</div>
89+
90+
{status}
91+
</div>
92+
);
93+
};
94+
95+
const styles = {
96+
text: (theme) => ({
97+
fontSize: 16,
98+
color: theme.palette.text.secondary,
99+
textAlign: "center",
100+
lineHeight: "160%",
101+
margin: 0,
102+
}),
103+
104+
copyCode: {
105+
display: "inline-flex",
106+
alignItems: "center",
107+
},
108+
109+
code: (theme) => ({
110+
fontWeight: "bold",
111+
color: theme.palette.text.primary,
112+
}),
113+
114+
links: {
115+
display: "flex",
116+
gap: 4,
117+
margin: 16,
118+
flexDirection: "column",
119+
},
120+
121+
link: {
122+
display: "flex",
123+
alignItems: "center",
124+
justifyContent: "center",
125+
fontSize: 16,
126+
gap: 8,
127+
},
128+
129+
status: (theme) => ({
130+
display: "flex",
131+
alignItems: "center",
132+
justifyContent: "center",
133+
gap: 8,
134+
color: theme.palette.text.disabled,
135+
}),
136+
} satisfies Record<string, Interpolation<Theme>>;

site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx

Lines changed: 2 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
33
import RefreshIcon from "@mui/icons-material/Refresh";
4-
import AlertTitle from "@mui/material/AlertTitle";
5-
import CircularProgress from "@mui/material/CircularProgress";
64
import Link from "@mui/material/Link";
75
import Tooltip from "@mui/material/Tooltip";
86
import type { ApiErrorResponse } from "api/errors";
97
import type { ExternalAuth, ExternalAuthDevice } from "api/typesGenerated";
10-
import { Alert, AlertDetail } from "components/Alert/Alert";
8+
import { Alert } from "components/Alert/Alert";
119
import { Avatar } from "components/Avatar/Avatar";
12-
import { CopyButton } from "components/CopyButton/CopyButton";
10+
import { GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth";
1311
import { SignInLayout } from "components/SignInLayout/SignInLayout";
1412
import { Welcome } from "components/Welcome/Welcome";
1513
import type { FC, ReactNode } from "react";
@@ -141,89 +139,6 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
141139
);
142140
};
143141

144-
interface GitDeviceAuthProps {
145-
externalAuthDevice?: ExternalAuthDevice;
146-
deviceExchangeError?: ApiErrorResponse;
147-
}
148-
149-
const GitDeviceAuth: FC<GitDeviceAuthProps> = ({
150-
externalAuthDevice,
151-
deviceExchangeError,
152-
}) => {
153-
let status = (
154-
<p css={styles.status}>
155-
<CircularProgress size={16} color="secondary" data-chromatic="ignore" />
156-
Checking for authentication...
157-
</p>
158-
);
159-
if (deviceExchangeError) {
160-
// See https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
161-
switch (deviceExchangeError.detail) {
162-
case "authorization_pending":
163-
break;
164-
case "expired_token":
165-
status = (
166-
<Alert severity="error">
167-
The one-time code has expired. Refresh to get a new one!
168-
</Alert>
169-
);
170-
break;
171-
case "access_denied":
172-
status = (
173-
<Alert severity="error">Access to the Git provider was denied.</Alert>
174-
);
175-
break;
176-
default:
177-
status = (
178-
<Alert severity="error">
179-
<AlertTitle>{deviceExchangeError.message}</AlertTitle>
180-
{deviceExchangeError.detail && (
181-
<AlertDetail>{deviceExchangeError.detail}</AlertDetail>
182-
)}
183-
</Alert>
184-
);
185-
break;
186-
}
187-
}
188-
189-
// If the error comes from the `externalAuthDevice` query,
190-
// we cannot even display the user_code.
191-
if (deviceExchangeError && !externalAuthDevice) {
192-
return <div>{status}</div>;
193-
}
194-
195-
if (!externalAuthDevice) {
196-
return <CircularProgress />;
197-
}
198-
199-
return (
200-
<div>
201-
<p css={styles.text}>
202-
Copy your one-time code:&nbsp;
203-
<div css={styles.copyCode}>
204-
<span css={styles.code}>{externalAuthDevice.user_code}</span>
205-
&nbsp; <CopyButton text={externalAuthDevice.user_code} />
206-
</div>
207-
<br />
208-
Then open the link below and paste it:
209-
</p>
210-
<div css={styles.links}>
211-
<Link
212-
css={styles.link}
213-
href={externalAuthDevice.verification_uri}
214-
target="_blank"
215-
rel="noreferrer"
216-
>
217-
<OpenInNewIcon fontSize="small" />
218-
Open and Paste
219-
</Link>
220-
</div>
221-
222-
{status}
223-
</div>
224-
);
225-
};
226-
227142
export default ExternalAuthPageView;
228143

229144
const styles = {
@@ -235,16 +150,6 @@ const styles = {
235150
margin: 0,
236151
}),
237152

238-
copyCode: {
239-
display: "inline-flex",
240-
alignItems: "center",
241-
},
242-
243-
code: (theme) => ({
244-
fontWeight: "bold",
245-
color: theme.palette.text.primary,
246-
}),
247-
248153
installAlert: {
249154
margin: 16,
250155
},
@@ -264,14 +169,6 @@ const styles = {
264169
gap: 8,
265170
},
266171

267-
status: (theme) => ({
268-
display: "flex",
269-
alignItems: "center",
270-
justifyContent: "center",
271-
gap: 8,
272-
color: theme.palette.text.disabled,
273-
}),
274-
275172
authorizedInstalls: (theme) => ({
276173
display: "flex",
277174
gap: 4,

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