mirror of https://github.com/coder/coder.git
fix: prevent email from being altered (#1863)
This commit is contained in:
parent
cfa316be89
commit
5598ac05dc
|
@ -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",
|
||||
|
@ -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(),
|
||||
})
|
||||
|
|
|
@ -259,7 +259,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)
|
||||
|
@ -268,25 +267,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)
|
||||
|
@ -300,38 +280,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.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
coderdtest.CreateFirstUser(t, client)
|
||||
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)
|
||||
_, _ = client.User(context.Background(), codersdk.Me)
|
||||
userProfile, err := client.UpdateUserProfile(context.Background(), codersdk.Me, codersdk.UpdateUserProfileRequest{
|
||||
Username: me.Username,
|
||||
Email: "newemail@coder.com",
|
||||
Username: "newusername",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, userProfile.Username, me.Username)
|
||||
require.Equal(t, userProfile.Email, "newemail@coder.com")
|
||||
require.Equal(t, userProfile.Username, "newusername")
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ type CreateUserRequest struct {
|
|||
}
|
||||
|
||||
type UpdateUserProfileRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Username string `json:"username" validate:"required,username"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -276,12 +276,12 @@ 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
|
||||
|
@ -289,7 +289,6 @@ export interface UpdateUserPasswordRequest {
|
|||
|
||||
// From codersdk/users.go:62:6
|
||||
export interface UpdateUserProfileRequest {
|
||||
readonly email: string
|
||||
readonly username: string
|
||||
}
|
||||
|
||||
|
@ -320,13 +319,13 @@ 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
|
||||
|
@ -334,15 +333,15 @@ export interface UserAuthorizationObject {
|
|||
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[]>
|
||||
|
|
|
@ -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
|
||||
|
@ -35,6 +33,7 @@ export interface AccountFormProps {
|
|||
}
|
||||
|
||||
export const AccountForm: React.FC<AccountFormProps> = ({
|
||||
email,
|
||||
isLoading,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
|
@ -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)}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
@ -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"],
|
||||
|
@ -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({
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue