Skip to content

Commit 14cdd85

Browse files
authored
fix(site): username validation in forms (#1851)
* refactor(site): move name validation to utils * fix(site): username validation in forms
1 parent 8a5277e commit 14cdd85

File tree

6 files changed

+72
-59
lines changed

6 files changed

+72
-59
lines changed

site/src/components/CreateUserForm/CreateUserForm.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FormikContextType, FormikErrors, useFormik } from "formik"
44
import React from "react"
55
import * as Yup from "yup"
66
import * as TypesGen from "../../api/typesGenerated"
7-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
7+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
88
import { FormFooter } from "../FormFooter/FormFooter"
99
import { FullPageForm } from "../FullPageForm/FullPageForm"
1010

@@ -15,7 +15,6 @@ export const Language = {
1515
emailInvalid: "Please enter a valid email address.",
1616
emailRequired: "Please enter an email address.",
1717
passwordRequired: "Please enter a password.",
18-
usernameRequired: "Please enter a username.",
1918
createUser: "Create",
2019
cancel: "Cancel",
2120
}
@@ -32,7 +31,7 @@ export interface CreateUserFormProps {
3231
const validationSchema = Yup.object({
3332
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
3433
password: Yup.string().required(Language.passwordRequired),
35-
username: Yup.string().required(Language.usernameRequired),
34+
username: nameValidator(Language.usernameLabel),
3635
})
3736

3837
export const CreateUserForm: React.FC<CreateUserFormProps> = ({

site/src/components/SettingsAccountForm/SettingsAccountForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField"
33
import { FormikContextType, FormikErrors, useFormik } from "formik"
44
import React from "react"
55
import * as Yup from "yup"
6-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
6+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
77
import { LoadingButton } from "../LoadingButton/LoadingButton"
88
import { Stack } from "../Stack/Stack"
99

@@ -22,7 +22,7 @@ export const Language = {
2222

2323
const validationSchema = Yup.object({
2424
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
25-
username: Yup.string().trim(),
25+
username: nameValidator(Language.usernameLabel),
2626
})
2727

2828
export type AccountFormErrors = FormikErrors<AccountFormValues>
Lines changed: 3 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { screen, waitFor } from "@testing-library/react"
22
import userEvent from "@testing-library/user-event"
33
import React from "react"
4-
import { reach, StringSchema } from "yup"
54
import * as API from "../../api/api"
65
import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter"
76
import { MockTemplate, MockWorkspace } from "../../testHelpers/entities"
87
import { renderWithAuth } from "../../testHelpers/renderHelpers"
8+
import { Language as FormLanguage } from "../../util/formUtils"
99
import CreateWorkspacePage from "./CreateWorkspacePage"
10-
import { Language, validationSchema } from "./CreateWorkspacePageView"
10+
import { Language } from "./CreateWorkspacePageView"
1111

1212
const renderCreateWorkspacePage = () => {
1313
return renderWithAuth(<CreateWorkspacePage />, {
@@ -23,8 +23,6 @@ const fillForm = async ({ name = "example" }: { name?: string }) => {
2323
await userEvent.click(submitButton)
2424
}
2525

26-
const nameSchema = reach(validationSchema, "name") as StringSchema
27-
2826
describe("CreateWorkspacePage", () => {
2927
it("renders", async () => {
3028
renderCreateWorkspacePage()
@@ -35,7 +33,7 @@ describe("CreateWorkspacePage", () => {
3533
it("shows validation error message", async () => {
3634
renderCreateWorkspacePage()
3735
await fillForm({ name: "$$$" })
38-
const errorMessage = await screen.findByText(Language.nameMatches)
36+
const errorMessage = await screen.findByText(FormLanguage.nameInvalidChars(Language.nameLabel))
3937
expect(errorMessage).toBeDefined()
4038
})
4139

@@ -47,38 +45,4 @@ describe("CreateWorkspacePage", () => {
4745
// Check if the request was made
4846
await waitFor(() => expect(API.createWorkspace).toBeCalledTimes(1))
4947
})
50-
51-
describe("validationSchema", () => {
52-
it("allows a 1-letter name", () => {
53-
const validate = () => nameSchema.validateSync("t")
54-
expect(validate).not.toThrow()
55-
})
56-
57-
it("allows a 32-letter name", () => {
58-
const input = Array(32).fill("a").join("")
59-
const validate = () => nameSchema.validateSync(input)
60-
expect(validate).not.toThrow()
61-
})
62-
63-
it("allows 'test-3' to be used as name", () => {
64-
const validate = () => nameSchema.validateSync("test-3")
65-
expect(validate).not.toThrow()
66-
})
67-
68-
it("allows '3-test' to be used as a name", () => {
69-
const validate = () => nameSchema.validateSync("3-test")
70-
expect(validate).not.toThrow()
71-
})
72-
73-
it("disallows a 33-letter name", () => {
74-
const input = Array(33).fill("a").join("")
75-
const validate = () => nameSchema.validateSync(input)
76-
expect(validate).toThrow()
77-
})
78-
79-
it("disallows a space", () => {
80-
const validate = () => nameSchema.validateSync("test 3")
81-
expect(validate).toThrow()
82-
})
83-
})
8448
})

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,13 @@ import { Loader } from "../../components/Loader/Loader"
1010
import { Margins } from "../../components/Margins/Margins"
1111
import { ParameterInput } from "../../components/ParameterInput/ParameterInput"
1212
import { Stack } from "../../components/Stack/Stack"
13-
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils"
13+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils"
1414

1515
export const Language = {
1616
templateLabel: "Template",
1717
nameLabel: "Name",
18-
nameRequired: "Please enter a name.",
19-
nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -",
20-
nameMax: "Name cannot be longer than 32 characters",
2118
}
2219

23-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
24-
const maxLenName = 32
25-
26-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
27-
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
28-
2920
export interface CreateWorkspacePageViewProps {
3021
loadingTemplates: boolean
3122
loadingTemplateSchema: boolean
@@ -39,10 +30,7 @@ export interface CreateWorkspacePageViewProps {
3930
}
4031

4132
export const validationSchema = Yup.object({
42-
name: Yup.string()
43-
.required(Language.nameRequired)
44-
.matches(usernameRE, Language.nameMatches)
45-
.max(maxLenName, Language.nameMax),
33+
name: nameValidator(Language.nameLabel),
4634
})
4735

4836
export const CreateWorkspacePageView: React.FC<CreateWorkspacePageViewProps> = (props) => {

site/src/util/formUtils.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FormikContextType } from "formik/dist/types"
2-
import { getFormHelpers, onChangeTrimmed } from "./formUtils"
2+
import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils"
33

44
interface TestType {
55
untouchedGoodField: string
@@ -35,6 +35,8 @@ const form = {
3535
},
3636
} as unknown as FormikContextType<TestType>
3737

38+
const nameSchema = nameValidator("name")
39+
3840
describe("form util functions", () => {
3941
describe("getFormHelpers", () => {
4042
describe("without API errors", () => {
@@ -94,4 +96,38 @@ describe("form util functions", () => {
9496
expect(mockHandleChange).toHaveBeenCalledWith({ target: { value: "hello" } })
9597
})
9698
})
99+
100+
describe("nameValidator", () => {
101+
it("allows a 1-letter name", () => {
102+
const validate = () => nameSchema.validateSync("a")
103+
expect(validate).not.toThrow()
104+
})
105+
106+
it("allows a 32-letter name", () => {
107+
const input = Array(32).fill("a").join("")
108+
const validate = () => nameSchema.validateSync(input)
109+
expect(validate).not.toThrow()
110+
})
111+
112+
it("allows 'test-3' to be used as name", () => {
113+
const validate = () => nameSchema.validateSync("test-3")
114+
expect(validate).not.toThrow()
115+
})
116+
117+
it("allows '3-test' to be used as a name", () => {
118+
const validate = () => nameSchema.validateSync("3-test")
119+
expect(validate).not.toThrow()
120+
})
121+
122+
it("disallows a 33-letter name", () => {
123+
const input = Array(33).fill("a").join("")
124+
const validate = () => nameSchema.validateSync(input)
125+
expect(validate).toThrow()
126+
})
127+
128+
it("disallows a space", () => {
129+
const validate = () => nameSchema.validateSync("test 3")
130+
expect(validate).toThrow()
131+
})
132+
})
97133
})

site/src/util/formUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
import { FormikContextType, FormikErrors, getIn } from "formik"
22
import { ChangeEvent, ChangeEventHandler, FocusEventHandler, ReactNode } from "react"
3+
import * as Yup from "yup"
4+
5+
export const Language = {
6+
nameRequired: (name: string): string => {
7+
return `Please enter a ${name.toLowerCase()}.`
8+
},
9+
nameInvalidChars: (name: string): string => {
10+
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
11+
},
12+
nameTooLong: (name: string): string => {
13+
return `${name} cannot be longer than 32 characters`
14+
},
15+
}
316

417
interface FormHelpers {
518
name: string
@@ -38,3 +51,16 @@ export const onChangeTrimmed =
3851
event.target.value = event.target.value.trim()
3952
form.handleChange(event)
4053
}
54+
55+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
56+
const maxLenName = 32
57+
58+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
59+
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
60+
61+
// REMARK: see #1756 for name/username semantics
62+
export const nameValidator = (name: string): Yup.StringSchema =>
63+
Yup.string()
64+
.required(Language.nameRequired(name))
65+
.matches(usernameRE, Language.nameInvalidChars(name))
66+
.max(maxLenName, Language.nameTooLong(name))

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