Skip to content

fix: prevent email from being altered #1863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions coderd/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,19 +254,12 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
return
}
existentUser, err := api.Database.GetUserByEmailOrUsername(r.Context(), database.GetUserByEmailOrUsernameParams{
Email: params.Email,
Username: params.Username,
})
isDifferentUser := existentUser.ID != user.ID

if err == nil && isDifferentUser {
responseErrors := []httpapi.Error{}
if existentUser.Email == params.Email {
responseErrors = append(responseErrors, httpapi.Error{
Field: "email",
Detail: "this value is already in use and should be unique",
})
}
if existentUser.Username == params.Username {
responseErrors = append(responseErrors, httpapi.Error{
Field: "username",
Expand All @@ -288,7 +281,7 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {

updatedUserProfile, err := api.Database.UpdateUserProfile(r.Context(), database.UpdateUserProfileParams{
ID: user.ID,
Email: params.Email,
Email: user.Email,
Username: params.Username,
UpdatedAt: database.Now(),
})
Expand Down
40 changes: 2 additions & 38 deletions coderd/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ func TestUpdateUserProfile(t *testing.T) {
coderdtest.CreateFirstUser(t, client)
_, err := client.UpdateUserProfile(context.Background(), uuid.New().String(), codersdk.UpdateUserProfileRequest{
Username: "newusername",
Email: "newemail@coder.com",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
Expand All @@ -221,25 +220,6 @@ func TestUpdateUserProfile(t *testing.T) {
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
})

t.Run("ConflictingEmail", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
user := coderdtest.CreateFirstUser(t, client)
existentUser, _ := client.CreateUser(context.Background(), codersdk.CreateUserRequest{
Email: "bruno@coder.com",
Username: "bruno",
Password: "password",
OrganizationID: user.OrganizationID,
})
_, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: "newusername",
Email: existentUser.Email,
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})

t.Run("ConflictingUsername", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
Expand All @@ -253,38 +233,22 @@ func TestUpdateUserProfile(t *testing.T) {
require.NoError(t, err)
_, err = client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: existentUser.Username,
Email: "newemail@coder.com",
})
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
})

t.Run("UpdateUsernameAndEmail", func(t *testing.T) {
t.Run("UpdateUsername", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
_, _ = client.User(context.Background(), codersdk.Me)
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: "newusername",
Email: "newemail@coder.com",
})
require.NoError(t, err)
require.Equal(t, userProfile.Username, "newusername")
require.Equal(t, userProfile.Email, "newemail@coder.com")
})

t.Run("UpdateUsername", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
coderdtest.CreateFirstUser(t, client)
me, _ := client.User(context.Background(), codersdk.Me)
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
Username: me.Username,
Email: "newemail@coder.com",
})
require.NoError(t, err)
require.Equal(t, userProfile.Username, me.Username)
require.Equal(t, userProfile.Email, "newemail@coder.com")
})
}

Expand Down
1 change: 0 additions & 1 deletion codersdk/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ type CreateUserRequest struct {
}

type UpdateUserProfileRequest struct {
Email string `json:"email" validate:"required,email"`
Username string `json:"username" validate:"required,username"`
}

Expand Down
25 changes: 12 additions & 13 deletions site/src/api/typesGenerated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface AgentGitSSHKey {
readonly private_key: string
}

// From codersdk/users.go:152:6
// From codersdk/users.go:151:6
export interface AuthMethods {
readonly password: boolean
readonly github: boolean
Expand Down Expand Up @@ -44,7 +44,7 @@ export interface CreateFirstUserResponse {
readonly organization_id: string
}

// From codersdk/users.go:147:6
// From codersdk/users.go:146:6
export interface CreateOrganizationRequest {
readonly name: string
}
Expand Down Expand Up @@ -100,7 +100,7 @@ export interface CreateWorkspaceRequest {
readonly parameter_values?: CreateParameterRequest[]
}

// From codersdk/users.go:143:6
// From codersdk/users.go:142:6
export interface GenerateAPIKeyResponse {
readonly key: string
}
Expand All @@ -118,13 +118,13 @@ export interface GoogleInstanceIdentityToken {
readonly json_web_token: string
}

// From codersdk/users.go:132:6
// From codersdk/users.go:131:6
export interface LoginWithPasswordRequest {
readonly email: string
readonly password: string
}

// From codersdk/users.go:138:6
// From codersdk/users.go:137:6
export interface LoginWithPasswordResponse {
readonly session_token: string
}
Expand Down Expand Up @@ -276,20 +276,19 @@ export interface UpdateActiveTemplateVersion {
readonly id: string
}

// From codersdk/users.go:72:6
// From codersdk/users.go:71:6
export interface UpdateRoles {
readonly roles: string[]
}

// From codersdk/users.go:67:6
// From codersdk/users.go:66:6
export interface UpdateUserPasswordRequest {
readonly old_password: string
readonly password: string
}

// From codersdk/users.go:62:6
export interface UpdateUserProfileRequest {
readonly email: string
readonly username: string
}

Expand Down Expand Up @@ -320,29 +319,29 @@ export interface User {
readonly roles: Role[]
}

// From codersdk/users.go:97:6
// From codersdk/users.go:96:6
export interface UserAuthorization {
readonly object: UserAuthorizationObject
readonly action: string
}

// From codersdk/users.go:113:6
// From codersdk/users.go:112:6
export interface UserAuthorizationObject {
readonly resource_type: string
readonly owner_id?: string
readonly organization_id?: string
readonly resource_id?: string
}

// From codersdk/users.go:86:6
// From codersdk/users.go:85:6
export interface UserAuthorizationRequest {
readonly checks: Record<string, UserAuthorization>
}

// From codersdk/users.go:81:6
// From codersdk/users.go:80:6
export type UserAuthorizationResponse = Record<string, boolean>

// From codersdk/users.go:76:6
// From codersdk/users.go:75:6
export interface UserRoles {
readonly roles: string[]
readonly organization_roles: Record<string, string[]>
Expand Down
16 changes: 4 additions & 12 deletions site/src/components/SettingsAccountForm/SettingsAccountForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,23 @@ import { LoadingButton } from "../LoadingButton/LoadingButton"
import { Stack } from "../Stack/Stack"

interface AccountFormValues {
email: string
username: string
}

export const Language = {
usernameLabel: "Username",
emailLabel: "Email",
emailInvalid: "Please enter a valid email address.",
emailRequired: "Please enter an email address.",
updateSettings: "Update settings",
}

const validationSchema = Yup.object({
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired),
username: nameValidator(Language.usernameLabel),
})

export type AccountFormErrors = FormikErrors<AccountFormValues>

export interface AccountFormProps {
email: string
isLoading: boolean
initialValues: AccountFormValues
onSubmit: (values: AccountFormValues) => void
Expand All @@ -35,6 +33,7 @@ export interface AccountFormProps {
}

export const AccountForm: React.FC<AccountFormProps> = ({
email,
isLoading,
onSubmit,
initialValues,
Expand All @@ -52,14 +51,7 @@ export const AccountForm: React.FC<AccountFormProps> = ({
<>
<form onSubmit={form.handleSubmit}>
<Stack>
<TextField
{...getFieldHelpers("email")}
onChange={onChangeTrimmed(form)}
autoComplete="email"
fullWidth
label={Language.emailLabel}
variant="outlined"
/>
<TextField disabled fullWidth label={Language.emailLabel} value={email} variant="outlined" />
<TextField
{...getFieldHelpers("username")}
onChange={onChangeTrimmed(form)}
Expand Down
24 changes: 2 additions & 22 deletions site/src/pages/SettingsPages/AccountPage/AccountPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@ const renderPage = () => {
}

const newData = {
email: "user@coder.com",
username: "user",
}

const fillAndSubmitForm = async () => {
await waitFor(() => screen.findByLabelText("Email"))
fireEvent.change(screen.getByLabelText("Email"), { target: { value: newData.email } })
await waitFor(() => screen.findByLabelText("Username"))
fireEvent.change(screen.getByLabelText("Username"), { target: { value: newData.username } })
fireEvent.click(screen.getByText(AccountForm.Language.updateSettings))
}
Expand All @@ -34,6 +32,7 @@ describe("AccountPage", () => {
jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) =>
Promise.resolve({
id: userId,
email: "user@coder.com",
created_at: new Date().toString(),
status: "active",
organization_ids: ["123"],
Expand All @@ -51,25 +50,6 @@ describe("AccountPage", () => {
})
})

describe("when the email is already taken", () => {
it("shows an error", async () => {
jest.spyOn(API, "updateProfile").mockRejectedValueOnce({
isAxiosError: true,
response: {
data: { message: "Invalid profile", errors: [{ detail: "Email is already in use", field: "email" }] },
},
})

const { user } = renderPage()
await fillAndSubmitForm()

const errorMessage = await screen.findByText("Email is already in use")
expect(errorMessage).toBeDefined()
expect(API.updateProfile).toBeCalledTimes(1)
expect(API.updateProfile).toBeCalledWith(user.id, newData)
})
})

describe("when the username is already taken", () => {
it("shows an error", async () => {
jest.spyOn(API, "updateProfile").mockRejectedValueOnce({
Expand Down
3 changes: 2 additions & 1 deletion site/src/pages/SettingsPages/AccountPage/AccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ export const AccountPage: React.FC = () => {
return (
<Section title={Language.title}>
<AccountForm
email={me.email}
error={hasUnknownError ? Language.unknownError : undefined}
formErrors={formErrors}
isLoading={authState.matches("signedIn.profile.updatingProfile")}
initialValues={{ username: me.username, email: me.email }}
initialValues={{ username: me.username }}
onSubmit={(data) => {
authSend({
type: "UPDATE_PROFILE",
Expand Down