mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: manage backend authXService errors (#3190)
This commit is contained in:
@@ -9,7 +9,7 @@ import { ApiError, getErrorDetail, getErrorMessage } from "api/errors"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FC, useState } from "react"
|
||||
|
||||
const Language = {
|
||||
export const Language = {
|
||||
retryMessage: "Retry",
|
||||
unknownErrorMessage: "An unknown error has occurred",
|
||||
moreDetails: "More",
|
||||
@@ -91,7 +91,6 @@ interface StyleProps {
|
||||
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
|
||||
root: {
|
||||
background: darken(theme.palette.error.main, 0.6),
|
||||
margin: `${theme.spacing(2)}px`,
|
||||
padding: `${theme.spacing(2)}px`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
gap: 0,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { AccountForm, AccountFormProps } from "./SettingsAccountForm"
|
||||
|
||||
export default {
|
||||
title: "components/SettingsAccountForm",
|
||||
component: AccountForm,
|
||||
argTypes: {
|
||||
onSubmit: { action: "Submit" },
|
||||
},
|
||||
}
|
||||
|
||||
const Template: Story<AccountFormProps> = (args: AccountFormProps) => <AccountForm {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
email: "test-user@org.com",
|
||||
isLoading: false,
|
||||
initialValues: {
|
||||
username: "test-user",
|
||||
},
|
||||
updateProfileError: undefined,
|
||||
onSubmit: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
Loading.args = {
|
||||
...Example.args,
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
export const WithError = Template.bind({})
|
||||
WithError.args = {
|
||||
...Example.args,
|
||||
updateProfileError: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Username is invalid",
|
||||
validations: [
|
||||
{
|
||||
field: "username",
|
||||
detail: "Username is too long.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
initialTouched: {
|
||||
username: true,
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { FormikContextType, FormikErrors, useFormik } from "formik"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { LoadingButton } from "../LoadingButton/LoadingButton"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
@@ -21,15 +21,14 @@ const validationSchema = Yup.object({
|
||||
username: nameValidator(Language.usernameLabel),
|
||||
})
|
||||
|
||||
export type AccountFormErrors = FormikErrors<AccountFormValues>
|
||||
|
||||
export interface AccountFormProps {
|
||||
email: string
|
||||
isLoading: boolean
|
||||
initialValues: AccountFormValues
|
||||
onSubmit: (values: AccountFormValues) => void
|
||||
formErrors?: AccountFormErrors
|
||||
error?: string
|
||||
updateProfileError?: Error | unknown
|
||||
// initialTouched is only used for testing the error state of the form.
|
||||
initialTouched?: FormikTouched<AccountFormValues>
|
||||
}
|
||||
|
||||
export const AccountForm: FC<AccountFormProps> = ({
|
||||
@@ -37,20 +36,22 @@ export const AccountForm: FC<AccountFormProps> = ({
|
||||
isLoading,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
formErrors = {},
|
||||
error,
|
||||
updateProfileError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const form: FormikContextType<AccountFormValues> = useFormik<AccountFormValues>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<AccountFormValues>(form, formErrors)
|
||||
const getFieldHelpers = getFormHelpersWithError<AccountFormValues>(form, updateProfileError)
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack>
|
||||
{updateProfileError && <ErrorSummary error={updateProfileError} />}
|
||||
<TextField
|
||||
disabled
|
||||
fullWidth
|
||||
@@ -67,8 +68,6 @@ export const AccountForm: FC<AccountFormProps> = ({
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||
|
||||
<div>
|
||||
<LoadingButton loading={isLoading} type="submit" variant="contained">
|
||||
{isLoading ? "" : Language.updateSettings}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm"
|
||||
|
||||
export default {
|
||||
title: "components/SettingsSecurityForm",
|
||||
component: SecurityForm,
|
||||
argTypes: {
|
||||
onSubmit: { action: "Submit" },
|
||||
},
|
||||
}
|
||||
|
||||
const Template: Story<SecurityFormProps> = (args: SecurityFormProps) => <SecurityForm {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
isLoading: false,
|
||||
initialValues: {
|
||||
old_password: "",
|
||||
password: "",
|
||||
confirm_password: "",
|
||||
},
|
||||
updateSecurityError: undefined,
|
||||
onSubmit: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading = Template.bind({})
|
||||
Loading.args = {
|
||||
...Example.args,
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
export const WithError = Template.bind({})
|
||||
WithError.args = {
|
||||
...Example.args,
|
||||
updateSecurityError: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Old password is incorrect",
|
||||
validations: [
|
||||
{
|
||||
field: "old_password",
|
||||
detail: "Old password is incorrect.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
initialTouched: {
|
||||
old_password: true,
|
||||
},
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { FormikContextType, FormikErrors, useFormik } from "formik"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import React from "react"
|
||||
import * as Yup from "yup"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { LoadingButton } from "../LoadingButton/LoadingButton"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
@@ -40,33 +40,35 @@ const validationSchema = Yup.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export type SecurityFormErrors = FormikErrors<SecurityFormValues>
|
||||
export interface SecurityFormProps {
|
||||
isLoading: boolean
|
||||
initialValues: SecurityFormValues
|
||||
onSubmit: (values: SecurityFormValues) => void
|
||||
formErrors?: SecurityFormErrors
|
||||
error?: string
|
||||
updateSecurityError?: Error | unknown
|
||||
// initialTouched is only used for testing the error state of the form.
|
||||
initialTouched?: FormikTouched<SecurityFormValues>
|
||||
}
|
||||
|
||||
export const SecurityForm: React.FC<SecurityFormProps> = ({
|
||||
isLoading,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
formErrors = {},
|
||||
error,
|
||||
updateSecurityError,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const form: FormikContextType<SecurityFormValues> = useFormik<SecurityFormValues>({
|
||||
initialValues,
|
||||
validationSchema,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<SecurityFormValues>(form, formErrors)
|
||||
const getFieldHelpers = getFormHelpersWithError<SecurityFormValues>(form, updateSecurityError)
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<Stack>
|
||||
{updateSecurityError && <ErrorSummary error={updateSecurityError} />}
|
||||
<TextField
|
||||
{...getFieldHelpers("old_password")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
@@ -95,8 +97,6 @@ export const SecurityForm: React.FC<SecurityFormProps> = ({
|
||||
type="password"
|
||||
/>
|
||||
|
||||
{error && <FormHelperText error>{error}</FormHelperText>}
|
||||
|
||||
<div>
|
||||
<LoadingButton loading={isLoading} type="submit" variant="contained">
|
||||
{isLoading ? "" : Language.updatePassword}
|
||||
|
||||
@@ -6,7 +6,6 @@ export default {
|
||||
component: SignInForm,
|
||||
argTypes: {
|
||||
isLoading: "boolean",
|
||||
authErrorMessage: "string",
|
||||
onSubmit: { action: "Submit" },
|
||||
},
|
||||
}
|
||||
@@ -16,7 +15,7 @@ const Template: Story<SignInFormProps> = (args: SignInFormProps) => <SignInForm
|
||||
export const SignedOut = Template.bind({})
|
||||
SignedOut.args = {
|
||||
isLoading: false,
|
||||
authErrorMessage: undefined,
|
||||
authError: undefined,
|
||||
onSubmit: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
@@ -33,12 +32,31 @@ Loading.args = {
|
||||
}
|
||||
|
||||
export const WithLoginError = Template.bind({})
|
||||
WithLoginError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" }
|
||||
WithLoginError.args = {
|
||||
...SignedOut.args,
|
||||
authError: {
|
||||
response: {
|
||||
data: {
|
||||
message: "Email or password was invalid",
|
||||
validations: [
|
||||
{
|
||||
field: "password",
|
||||
detail: "Password is invalid.",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
isAxiosError: true,
|
||||
},
|
||||
initialTouched: {
|
||||
password: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const WithAuthMethodsError = Template.bind({})
|
||||
WithAuthMethodsError.args = {
|
||||
...SignedOut.args,
|
||||
methodsErrorMessage: "Failed to fetch auth methods",
|
||||
methodsError: new Error("Failed to fetch auth methods"),
|
||||
}
|
||||
|
||||
export const WithGithub = Template.bind({})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Button from "@material-ui/core/Button"
|
||||
import FormHelperText from "@material-ui/core/FormHelperText"
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import GitHubIcon from "@material-ui/icons/GitHub"
|
||||
import { FormikContextType, useFormik } from "formik"
|
||||
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { FormikContextType, FormikTouched, useFormik } from "formik"
|
||||
import { FC } from "react"
|
||||
import * as Yup from "yup"
|
||||
import { AuthMethods } from "../../api/typesGenerated"
|
||||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils"
|
||||
import { Welcome } from "../Welcome/Welcome"
|
||||
import { LoadingButton } from "./../LoadingButton/LoadingButton"
|
||||
|
||||
@@ -39,17 +40,6 @@ const validationSchema = Yup.object({
|
||||
})
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
loginBtnWrapper: {
|
||||
marginTop: theme.spacing(6),
|
||||
borderTop: `1px solid ${theme.palette.action.disabled}`,
|
||||
paddingTop: theme.spacing(3),
|
||||
},
|
||||
loginTextField: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
submitBtn: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
buttonIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
@@ -78,19 +68,22 @@ const useStyles = makeStyles((theme) => ({
|
||||
export interface SignInFormProps {
|
||||
isLoading: boolean
|
||||
redirectTo: string
|
||||
authErrorMessage?: string
|
||||
methodsErrorMessage?: string
|
||||
authError?: Error | unknown
|
||||
methodsError?: Error | unknown
|
||||
authMethods?: AuthMethods
|
||||
onSubmit: ({ email, password }: { email: string; password: string }) => Promise<void>
|
||||
// initialTouched is only used for testing the error state of the form.
|
||||
initialTouched?: FormikTouched<BuiltInAuthFormValues>
|
||||
}
|
||||
|
||||
export const SignInForm: FC<SignInFormProps> = ({
|
||||
authMethods,
|
||||
redirectTo,
|
||||
isLoading,
|
||||
authErrorMessage,
|
||||
methodsErrorMessage,
|
||||
authError,
|
||||
methodsError,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
@@ -106,43 +99,46 @@ export const SignInForm: FC<SignInFormProps> = ({
|
||||
// field), or after a submission attempt.
|
||||
validateOnBlur: false,
|
||||
onSubmit,
|
||||
initialTouched,
|
||||
})
|
||||
const getFieldHelpers = getFormHelpers<BuiltInAuthFormValues>(form)
|
||||
const getFieldHelpers = getFormHelpersWithError<BuiltInAuthFormValues>(form, authError)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Welcome />
|
||||
<form onSubmit={form.handleSubmit}>
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
autoComplete="email"
|
||||
className={styles.loginTextField}
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
type="email"
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="current-password"
|
||||
className={styles.loginTextField}
|
||||
fullWidth
|
||||
id="password"
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
{authErrorMessage && <FormHelperText error>{authErrorMessage}</FormHelperText>}
|
||||
{methodsErrorMessage && (
|
||||
<FormHelperText error>{Language.methodsErrorMessage}</FormHelperText>
|
||||
)}
|
||||
<div className={styles.submitBtn}>
|
||||
<LoadingButton loading={isLoading} fullWidth type="submit" variant="contained">
|
||||
{isLoading ? "" : Language.passwordSignIn}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
<Stack>
|
||||
{authError && (
|
||||
<ErrorSummary error={authError} defaultMessage={Language.authErrorMessage} />
|
||||
)}
|
||||
{methodsError && (
|
||||
<ErrorSummary error={methodsError} defaultMessage={Language.methodsErrorMessage} />
|
||||
)}
|
||||
<TextField
|
||||
{...getFieldHelpers("email")}
|
||||
onChange={onChangeTrimmed(form)}
|
||||
autoFocus
|
||||
autoComplete="email"
|
||||
fullWidth
|
||||
label={Language.emailLabel}
|
||||
type="email"
|
||||
variant="outlined"
|
||||
/>
|
||||
<TextField
|
||||
{...getFieldHelpers("password")}
|
||||
autoComplete="current-password"
|
||||
fullWidth
|
||||
id="password"
|
||||
label={Language.passwordLabel}
|
||||
type="password"
|
||||
variant="outlined"
|
||||
/>
|
||||
<div>
|
||||
<LoadingButton loading={isLoading} fullWidth type="submit" variant="contained">
|
||||
{isLoading ? "" : Language.passwordSignIn}
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</Stack>
|
||||
</form>
|
||||
{authMethods?.github && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user