Skip to content

Commit 74c8766

Browse files
authored
fix: handle more auth API errors (#3241)
1 parent 6b82fdd commit 74c8766

File tree

9 files changed

+203
-70
lines changed

9 files changed

+203
-70
lines changed

site/src/components/SignInForm/SignInForm.stories.tsx

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Story } from "@storybook/react"
2-
import { SignInForm, SignInFormProps } from "./SignInForm"
2+
import { makeMockApiError } from "testHelpers/entities"
3+
import { LoginErrors, SignInForm, SignInFormProps } from "./SignInForm"
34

45
export default {
56
title: "components/SignInForm",
@@ -15,7 +16,7 @@ const Template: Story<SignInFormProps> = (args: SignInFormProps) => <SignInForm
1516
export const SignedOut = Template.bind({})
1617
SignedOut.args = {
1718
isLoading: false,
18-
authError: undefined,
19+
loginErrors: {},
1920
onSubmit: () => {
2021
return Promise.resolve()
2122
},
@@ -34,29 +35,39 @@ Loading.args = {
3435
export const WithLoginError = Template.bind({})
3536
WithLoginError.args = {
3637
...SignedOut.args,
37-
authError: {
38-
response: {
39-
data: {
40-
message: "Email or password was invalid",
41-
validations: [
42-
{
43-
field: "password",
44-
detail: "Password is invalid.",
45-
},
46-
],
47-
},
48-
},
49-
isAxiosError: true,
38+
loginErrors: {
39+
[LoginErrors.AUTH_ERROR]: makeMockApiError({
40+
message: "Email or password was invalid",
41+
validations: [
42+
{
43+
field: "password",
44+
detail: "Password is invalid.",
45+
},
46+
],
47+
}),
5048
},
5149
initialTouched: {
5250
password: true,
5351
},
5452
}
5553

54+
export const WithCheckPermissionsError = Template.bind({})
55+
WithCheckPermissionsError.args = {
56+
...SignedOut.args,
57+
loginErrors: {
58+
[LoginErrors.CHECK_PERMISSIONS_ERROR]: makeMockApiError({
59+
message: "Unable to fetch user permissions",
60+
detail: "Resource not found or you do not have access to this resource.",
61+
}),
62+
},
63+
}
64+
5665
export const WithAuthMethodsError = Template.bind({})
5766
WithAuthMethodsError.args = {
5867
...SignedOut.args,
59-
methodsError: new Error("Failed to fetch auth methods"),
68+
loginErrors: {
69+
[LoginErrors.GET_METHODS_ERROR]: new Error("Failed to fetch auth methods"),
70+
},
6071
}
6172

6273
export const WithGithub = Template.bind({})

site/src/components/SignInForm/SignInForm.tsx

+25-12
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ interface BuiltInAuthFormValues {
2323
password: string
2424
}
2525

26+
export enum LoginErrors {
27+
AUTH_ERROR = "authError",
28+
CHECK_PERMISSIONS_ERROR = "checkPermissionsError",
29+
GET_METHODS_ERROR = "getMethodsError",
30+
}
31+
2632
export const Language = {
2733
emailLabel: "Email",
2834
passwordLabel: "Password",
2935
emailInvalid: "Please enter a valid email address.",
3036
emailRequired: "Please enter an email address.",
31-
authErrorMessage: "Incorrect email or password.",
32-
methodsErrorMessage: "Unable to fetch auth methods.",
37+
errorMessages: {
38+
[LoginErrors.AUTH_ERROR]: "Incorrect email or password.",
39+
[LoginErrors.CHECK_PERMISSIONS_ERROR]: "Unable to fetch user permissions.",
40+
[LoginErrors.GET_METHODS_ERROR]: "Unable to fetch auth methods.",
41+
},
3342
passwordSignIn: "Sign In",
3443
githubSignIn: "GitHub",
3544
}
@@ -68,8 +77,7 @@ const useStyles = makeStyles((theme) => ({
6877
export interface SignInFormProps {
6978
isLoading: boolean
7079
redirectTo: string
71-
authError?: Error | unknown
72-
methodsError?: Error | unknown
80+
loginErrors: Partial<Record<LoginErrors, Error | unknown>>
7381
authMethods?: AuthMethods
7482
onSubmit: ({ email, password }: { email: string; password: string }) => Promise<void>
7583
// initialTouched is only used for testing the error state of the form.
@@ -80,8 +88,7 @@ export const SignInForm: FC<SignInFormProps> = ({
8088
authMethods,
8189
redirectTo,
8290
isLoading,
83-
authError,
84-
methodsError,
91+
loginErrors,
8592
onSubmit,
8693
initialTouched,
8794
}) => {
@@ -101,18 +108,24 @@ export const SignInForm: FC<SignInFormProps> = ({
101108
onSubmit,
102109
initialTouched,
103110
})
104-
const getFieldHelpers = getFormHelpersWithError<BuiltInAuthFormValues>(form, authError)
111+
const getFieldHelpers = getFormHelpersWithError<BuiltInAuthFormValues>(
112+
form,
113+
loginErrors.authError,
114+
)
105115

106116
return (
107117
<>
108118
<Welcome />
109119
<form onSubmit={form.handleSubmit}>
110120
<Stack>
111-
{authError && (
112-
<ErrorSummary error={authError} defaultMessage={Language.authErrorMessage} />
113-
)}
114-
{methodsError && (
115-
<ErrorSummary error={methodsError} defaultMessage={Language.methodsErrorMessage} />
121+
{Object.keys(loginErrors).map((errorKey: string) =>
122+
loginErrors[errorKey as LoginErrors] ? (
123+
<ErrorSummary
124+
key={errorKey}
125+
error={loginErrors[errorKey as LoginErrors]}
126+
defaultMessage={Language.errorMessages[errorKey as LoginErrors]}
127+
/>
128+
) : null,
116129
)}
117130
<TextField
118131
{...getFieldHelpers("email")}

site/src/pages/LoginPage/LoginPage.test.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe("LoginPage", () => {
3030
server.use(
3131
// Make login fail
3232
rest.post("/api/v2/users/login", async (req, res, ctx) => {
33-
return res(ctx.status(500), ctx.json({ message: Language.authErrorMessage }))
33+
return res(ctx.status(500), ctx.json({ message: Language.errorMessages.authError }))
3434
}),
3535
)
3636

@@ -45,7 +45,7 @@ describe("LoginPage", () => {
4545
act(() => signInButton.click())
4646

4747
// Then
48-
const errorMessage = await screen.findByText(Language.authErrorMessage)
48+
const errorMessage = await screen.findByText(Language.errorMessages.authError)
4949
expect(errorMessage).toBeDefined()
5050
expect(history.location.pathname).toEqual("/login")
5151
})

site/src/pages/LoginPage/LoginPage.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export const LoginPage: React.FC = () => {
4040
authSend({ type: "SIGN_IN", email, password })
4141
}
4242

43+
const { authError, checkPermissionsError, getMethodsError } = authState.context
44+
4345
if (authState.matches("signedIn")) {
4446
return <Navigate to={redirectTo} replace />
4547
} else {
@@ -54,8 +56,11 @@ export const LoginPage: React.FC = () => {
5456
authMethods={authState.context.methods}
5557
redirectTo={redirectTo}
5658
isLoading={isLoading}
57-
authError={authState.context.authError}
58-
methodsError={authState.context.getMethodsError as Error}
59+
loginErrors={{
60+
authError,
61+
checkPermissionsError,
62+
getMethodsError,
63+
}}
5964
onSubmit={onSubmit}
6065
/>
6166
</div>

site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.test.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackba
44
import { MockGitSSHKey, renderWithAuth } from "../../../testHelpers/renderHelpers"
55
import { Language as authXServiceLanguage } from "../../../xServices/auth/authXService"
66
import { Language as SSHKeysPageLanguage, SSHKeysPage } from "./SSHKeysPage"
7+
import { Language as SSHKeysPageViewLanguage } from "./SSHKeysPageView"
78

89
describe("SSH keys Page", () => {
910
it("shows the SSH key", async () => {
@@ -26,7 +27,7 @@ describe("SSH keys Page", () => {
2627

2728
// Click on the "Regenerate" button to display the confirm dialog
2829
const regenerateButton = screen.getByRole("button", {
29-
name: SSHKeysPageLanguage.regenerateLabel,
30+
name: SSHKeysPageViewLanguage.regenerateLabel,
3031
})
3132
fireEvent.click(regenerateButton)
3233
const confirmDialog = screen.getByRole("dialog")
@@ -72,7 +73,7 @@ describe("SSH keys Page", () => {
7273

7374
// Click on the "Regenerate" button to display the confirm dialog
7475
const regenerateButton = screen.getByRole("button", {
75-
name: SSHKeysPageLanguage.regenerateLabel,
76+
name: SSHKeysPageViewLanguage.regenerateLabel,
7677
})
7778
fireEvent.click(regenerateButton)
7879
const confirmDialog = screen.getByRole("dialog")
@@ -85,7 +86,7 @@ describe("SSH keys Page", () => {
8586
fireEvent.click(confirmButton)
8687

8788
// Check if the error message is displayed
88-
await screen.findByText(authXServiceLanguage.errorRegenerateSSHKey)
89+
await screen.findByText(SSHKeysPageViewLanguage.errorRegenerateSSHKey)
8990

9091
// Check if the API was called correctly
9192
expect(API.regenerateUserSSHKey).toBeCalledTimes(1)

site/src/pages/UserSettingsPage/SSHKeysPage/SSHKeysPage.tsx

+17-28
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
import Box from "@material-ui/core/Box"
2-
import Button from "@material-ui/core/Button"
3-
import CircularProgress from "@material-ui/core/CircularProgress"
41
import { useActor } from "@xstate/react"
52
import React, { useContext, useEffect } from "react"
6-
import { CodeExample } from "../../../components/CodeExample/CodeExample"
73
import { ConfirmDialog } from "../../../components/ConfirmDialog/ConfirmDialog"
84
import { Section } from "../../../components/Section/Section"
9-
import { Stack } from "../../../components/Stack/Stack"
105
import { XServiceContext } from "../../../xServices/StateContext"
6+
import { SSHKeysPageView } from "./SSHKeysPageView"
117

128
export const Language = {
139
title: "SSH keys",
1410
description:
1511
"Coder automatically inserts a private key into every workspace; you can add the corresponding public key to any services (such as Git) that you need access to from your workspace.",
16-
regenerateLabel: "Regenerate",
1712
regenerateDialogTitle: "Regenerate SSH key?",
1813
regenerateDialogMessage:
1914
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
@@ -24,36 +19,30 @@ export const Language = {
2419
export const SSHKeysPage: React.FC = () => {
2520
const xServices = useContext(XServiceContext)
2621
const [authState, authSend] = useActor(xServices.authXService)
27-
const { sshKey } = authState.context
22+
const { sshKey, getSSHKeyError, regenerateSSHKeyError } = authState.context
2823

2924
useEffect(() => {
3025
authSend({ type: "GET_SSH_KEY" })
3126
}, [authSend])
3227

28+
const isLoading = authState.matches("signedIn.ssh.gettingSSHKey")
29+
const hasLoaded = authState.matches("signedIn.ssh.loaded")
30+
31+
const onRegenerateClick = () => {
32+
authSend({ type: "REGENERATE_SSH_KEY" })
33+
}
34+
3335
return (
3436
<>
3537
<Section title={Language.title} description={Language.description}>
36-
{!sshKey && (
37-
<Box p={4}>
38-
<CircularProgress size={26} />
39-
</Box>
40-
)}
41-
42-
{sshKey && (
43-
<Stack>
44-
<CodeExample code={sshKey.public_key.trim()} />
45-
<div>
46-
<Button
47-
variant="outlined"
48-
onClick={() => {
49-
authSend({ type: "REGENERATE_SSH_KEY" })
50-
}}
51-
>
52-
{Language.regenerateLabel}
53-
</Button>
54-
</div>
55-
</Stack>
56-
)}
38+
<SSHKeysPageView
39+
isLoading={isLoading}
40+
hasLoaded={hasLoaded}
41+
getSSHKeyError={getSSHKeyError}
42+
regenerateSSHKeyError={regenerateSSHKeyError}
43+
sshKey={sshKey}
44+
onRegenerateClick={onRegenerateClick}
45+
/>
5746
</Section>
5847

5948
<ConfirmDialog
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Story } from "@storybook/react"
2+
import { makeMockApiError } from "testHelpers/entities"
3+
import { SSHKeysPageView, SSHKeysPageViewProps } from "./SSHKeysPageView"
4+
5+
export default {
6+
title: "components/SSHKeysPageView",
7+
component: SSHKeysPageView,
8+
argTypes: {
9+
onRegenerateClick: { action: "Submit" },
10+
},
11+
}
12+
13+
const Template: Story<SSHKeysPageViewProps> = (args: SSHKeysPageViewProps) => (
14+
<SSHKeysPageView {...args} />
15+
)
16+
17+
export const Example = Template.bind({})
18+
Example.args = {
19+
isLoading: false,
20+
hasLoaded: true,
21+
sshKey: {
22+
user_id: "test-user-id",
23+
created_at: "2022-07-28T07:45:50.795918897Z",
24+
updated_at: "2022-07-28T07:45:50.795919142Z",
25+
public_key: "SSH-Key",
26+
},
27+
onRegenerateClick: () => {
28+
return Promise.resolve()
29+
},
30+
}
31+
32+
export const Loading = Template.bind({})
33+
Loading.args = {
34+
...Example.args,
35+
isLoading: true,
36+
}
37+
38+
export const WithGetSSHKeyError = Template.bind({})
39+
WithGetSSHKeyError.args = {
40+
...Example.args,
41+
hasLoaded: false,
42+
getSSHKeyError: makeMockApiError({
43+
message: "Failed to get SSH key",
44+
}),
45+
}
46+
47+
export const WithRegenerateSSHKeyError = Template.bind({})
48+
WithRegenerateSSHKeyError.args = {
49+
...Example.args,
50+
regenerateSSHKeyError: makeMockApiError({
51+
message: "Failed to regenerate SSH key",
52+
}),
53+
}

0 commit comments

Comments
 (0)