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

Lines changed: 27 additions & 16 deletions
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

Lines changed: 25 additions & 12 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 7 additions & 2 deletions
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

Lines changed: 4 additions & 3 deletions
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

Lines changed: 17 additions & 28 deletions
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
Lines changed: 53 additions & 0 deletions
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)
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