From 3cd7b00b2098fffdcf9b9232cae9d214ff837720 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 19:56:32 +0000 Subject: [PATCH 1/3] feat: add user password change page --- site/src/api/api.ts | 4 +- .../SettingsSecurityForm.tsx | 100 ++++++++++++++++ .../AccountPage/LinkedAccountsPage.tsx | 12 -- .../SecurityPage/SecurityPage.test.tsx | 107 ++++++++++++++++++ .../SecurityPage/SecurityPage.tsx | 42 +++++++ site/src/pages/UsersPage/UsersPage.test.tsx | 2 +- site/src/xServices/auth/authXService.ts | 16 +++ site/src/xServices/users/usersXService.ts | 2 +- 8 files changed, 269 insertions(+), 16 deletions(-) create mode 100644 site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx delete mode 100644 site/src/pages/SettingsPages/AccountPage/LinkedAccountsPage.tsx create mode 100644 site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx create mode 100644 site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 0697200681513..315ad07ef46f0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -223,8 +223,8 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise => - axios.put(`/api/v2/users/${userId}/password`, { password }) +export const updateUserPassword = async (userId: TypesGen.User["id"], updatePassword: TypesGen.UpdateUserPasswordRequest): Promise => + axios.put(`/api/v2/users/${userId}/password`, updatePassword) export const getSiteRoles = async (): Promise> => { const response = await axios.get>(`/api/v2/users/roles`) diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx new file mode 100644 index 0000000000000..56902a6c223a9 --- /dev/null +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -0,0 +1,100 @@ +import FormHelperText from "@material-ui/core/FormHelperText" +import TextField from "@material-ui/core/TextField" +import { FormikContextType, FormikErrors, useFormik } from "formik" +import React from "react" +import * as Yup from "yup" +import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" +import { LoadingButton } from "../LoadingButton/LoadingButton" +import { Stack } from "../Stack/Stack" + +interface SecurityFormValues { + old_password: string + password: string + confirm_password: string +} + +export const Language = { + oldPasswordLabel: "Old Password", + newPasswordLabel: "New Password", + confirmPasswordLabel: "Confirm Password", + oldPasswordRequired: "Old password is required", + newPasswordRequired: "New password is required", + confirmPasswordRequired: "Password confirmation is required", + passwordMinLength: "Password must be at least 8 characters", + passwordMaxLength: "Password must be no more than 64 characters", + confirmPasswordMatch: "Password and confirmation must match", + updatePassword: "Update password", +} + +const validationSchema = Yup.object({ + old_password: Yup.string().trim().required(Language.oldPasswordRequired), + password: Yup.string().trim().min(8, Language.passwordMinLength).max(64, Language.passwordMaxLength).required(Language.newPasswordRequired), + confirm_password: Yup.string().trim().test("passwords-match", Language.confirmPasswordMatch, function (value) { + return (this.parent as SecurityFormValues).password === value + }) +}) + +export type SecurityFormErrors = FormikErrors +export interface SecurityFormProps { + isLoading: boolean + initialValues: SecurityFormValues + onSubmit: (values: SecurityFormValues) => void + formErrors?: SecurityFormErrors + error?: string +} + +export const SecurityForm: React.FC = ({ + isLoading, + onSubmit, + initialValues, + formErrors = {}, + error, +}) => { + const form: FormikContextType = useFormik({ + initialValues, + validationSchema, + onSubmit, + }) + const getFieldHelpers = getFormHelpers(form, formErrors) + + return ( + <> +
+ + + + + + {error && {error}} + +
+ + {isLoading ? "" : Language.updatePassword} + +
+
+
+ + ) +} diff --git a/site/src/pages/SettingsPages/AccountPage/LinkedAccountsPage.tsx b/site/src/pages/SettingsPages/AccountPage/LinkedAccountsPage.tsx deleted file mode 100644 index 994492cc91f48..0000000000000 --- a/site/src/pages/SettingsPages/AccountPage/LinkedAccountsPage.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react" -import { Section } from "../../../components/Section/Section" - -const Language = { - title: "Linked Accounts", - description: - "Linking your Coder account will add your workspace SSH key, allowing you to perform Git actions on all your workspaces.", -} - -export const LinkedAccountsPage: React.FC = () => { - return
-} diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx new file mode 100644 index 0000000000000..da2be6eac2ec6 --- /dev/null +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx @@ -0,0 +1,107 @@ +import { fireEvent, screen, waitFor } from "@testing-library/react" +import React from "react" +import * as API from "../../../api/api" +import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" +import * as AccountForm from "../../../components/SettingsAccountForm/SettingsAccountForm" +import { renderWithAuth } from "../../../testHelpers/renderHelpers" +import * as AuthXService from "../../../xServices/auth/authXService" +import { SecurityPage, Language } from "./SecurityPage" + +const renderPage = () => { + return renderWithAuth( + <> + + + , + ) +} + +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 } }) + fireEvent.change(screen.getByLabelText("Username"), { target: { value: newData.username } }) + fireEvent.click(screen.getByText(AccountForm.Language.updateSettings)) +} + +describe("AccountPage", () => { + describe("when it is a success", () => { + it("shows the success message", async () => { + jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) => + Promise.resolve({ + id: userId, + created_at: new Date().toString(), + status: "active", + organization_ids: ["123"], + roles: [], + ...data, + }), + ) + const { user } = renderPage() + await fillAndSubmitForm() + + const successMessage = await screen.findByText(AuthXService.Language.successProfileUpdate) + expect(successMessage).toBeDefined() + expect(API.updateProfile).toBeCalledTimes(1) + expect(API.updateProfile).toBeCalledWith(user.id, newData) + }) + }) + + 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({ + isAxiosError: true, + response: { + data: { message: "Invalid profile", errors: [{ detail: "Username is already in use", field: "username" }] }, + }, + }) + + const { user } = renderPage() + await fillAndSubmitForm() + + const errorMessage = await screen.findByText("Username is already in use") + expect(errorMessage).toBeDefined() + expect(API.updateProfile).toBeCalledTimes(1) + expect(API.updateProfile).toBeCalledWith(user.id, newData) + }) + }) + + describe("when it is an unknown error", () => { + it("shows a generic error message", async () => { + jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ + data: "unknown error", + }) + + const { user } = renderPage() + await fillAndSubmitForm() + + const errorMessage = await screen.findByText(Language.unknownError) + expect(errorMessage).toBeDefined() + expect(API.updateProfile).toBeCalledTimes(1) + expect(API.updateProfile).toBeCalledWith(user.id, newData) + }) + }) +}) diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx new file mode 100644 index 0000000000000..2e16d0c94fe87 --- /dev/null +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx @@ -0,0 +1,42 @@ +import { useActor } from "@xstate/react" +import React, { useContext } from "react" +import { isApiError, mapApiErrorToFieldErrors } from "../../../api/errors" +import { Section } from "../../../components/Section/Section" +import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm" +import { XServiceContext } from "../../../xServices/StateContext" + +export const Language = { + title: "Security", + unknownError: "Oops, an unknown error occurred.", +} + +export const AccountPage: React.FC = () => { + const xServices = useContext(XServiceContext) + const [authState, authSend] = useActor(xServices.authXService) + const { me, updateProfileError } = authState.context + const hasError = !!updateProfileError + const formErrors = + hasError && isApiError(updateProfileError) ? mapApiErrorToFieldErrors(updateProfileError.response.data) : undefined + const hasUnknownError = hasError && !isApiError(updateProfileError) + + if (!me) { + throw new Error("No current user found") + } + + return ( +
+ { + authSend({ + type: "UPDATE_PASSWORD", + data, + }) + }} + /> +
+ ) +} diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index d0671b068e3c2..29ced9a6dd6a4 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -198,7 +198,7 @@ describe("Users Page", () => { // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(expect.any(String), MockUser.id) + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {password: expect.any(String), old_password: ""}) }) }) diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index 3e634fc8b58d8..a98993b803765 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -50,6 +50,7 @@ export interface AuthContext { getMethodsError?: Error | unknown authError?: Error | unknown updateProfileError?: Error | unknown + updateSecurityError?: Error | unknown me?: TypesGen.User methods?: TypesGen.AuthMethods permissions?: Permissions @@ -64,6 +65,7 @@ export type AuthEvent = | { type: "SIGN_OUT" } | { type: "SIGN_IN"; email: string; password: string } | { type: "UPDATE_PROFILE"; data: TypesGen.UpdateUserProfileRequest } + | { type: "UPDATE_SECURITY"; data: TypesGen.UpdateUserPasswordRequest } | { type: "GET_SSH_KEY" } | { type: "REGENERATE_SSH_KEY" } | { type: "CONFIRM_REGENERATE_SSH_KEY" } @@ -145,6 +147,7 @@ export const authMachine = getUserError: undefined, authError: undefined, updateProfileError: undefined, + updateSecurityError: undefined, methods: undefined, getMethodsError: undefined, }, @@ -165,6 +168,9 @@ export const authMachine = updateProfile: { data: TypesGen.User } + updateSecurity: { + data: TypesGen.UpdateUserPasswordRequest + } checkPermissions: { data: TypesGen.UserAuthorizationResponse } @@ -279,6 +285,9 @@ export const authMachine = UPDATE_PROFILE: { target: "updatingProfile", }, + UPDATE_SECURITY: { + target: "updatingSecurity", + }, }, }, updatingProfile: { @@ -345,6 +354,13 @@ export const authMachine = return API.updateProfile(context.me.id, event.data) }, + updateSecurity: async (context, event) => { + if (!context.me) { + throw new Error("No current user found") + } + + return API.updateUserPassword(context.me.id, event.data) + }, checkPermissions: async (context) => { if (!context.me) { throw new Error("No current user found") diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index ba467c03df237..281c22328a03b 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -221,7 +221,7 @@ export const usersMachine = createMachine( throw new Error("newUserPassword not generated") } - return API.updateUserPassword(context.newUserPassword, context.userIdToResetPassword) + return API.updateUserPassword(context.userIdToResetPassword, {password: context.newUserPassword, old_password: ""}) }, updateUserRoles: (context, event) => { if (!context.userIdToUpdateRoles) { From 6a3a1bd318f7acbb4b14f84d3609ca3fbbdcdb55 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 21:41:43 +0000 Subject: [PATCH 2/3] complete page --- site/src/AppRouter.tsx | 2 + .../SettingsLayout/SettingsLayout.tsx | 2 + .../SettingsSecurityForm.tsx | 3 + .../SecurityPage/SecurityPage.test.tsx | 67 +++++++++---------- .../SecurityPage/SecurityPage.tsx | 14 ++-- site/src/pages/UsersPage/UsersPage.test.tsx | 2 +- site/src/xServices/auth/authXService.ts | 51 ++++++++++++-- 7 files changed, 92 insertions(+), 49 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 90cf9c5472764..978ed067e9f4f 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -9,6 +9,7 @@ import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage" import { HealthzPage } from "./pages/HealthzPage/HealthzPage" import { LoginPage } from "./pages/LoginPage/LoginPage" import { AccountPage } from "./pages/SettingsPages/AccountPage/AccountPage" +import { SecurityPage } from "./pages/SettingsPages/SecurityPage/SecurityPage" import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage" import { TemplatePage } from "./pages/TemplatePage/TemplatePage" import TemplatesPage from "./pages/TemplatesPage/TemplatesPage" @@ -126,6 +127,7 @@ export const AppRouter: React.FC = () => ( }> } /> + } /> } /> diff --git a/site/src/components/SettingsLayout/SettingsLayout.tsx b/site/src/components/SettingsLayout/SettingsLayout.tsx index 0a1344b99c43d..786e7bc027418 100644 --- a/site/src/components/SettingsLayout/SettingsLayout.tsx +++ b/site/src/components/SettingsLayout/SettingsLayout.tsx @@ -7,12 +7,14 @@ import { TabPanel } from "../TabPanel/TabPanel" export const Language = { accountLabel: "Account", + securityLabel: "Security", sshKeysLabel: "SSH keys", settingsLabel: "Settings", } const menuItems = [ { label: Language.accountLabel, path: "/settings/account" }, + { label: Language.securityLabel, path: "/settings/security" }, { label: Language.sshKeysLabel, path: "/settings/ssh-keys" }, ] diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx index 56902a6c223a9..cd6a579e5816d 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -68,6 +68,7 @@ export const SecurityForm: React.FC = ({ fullWidth label={Language.oldPasswordLabel} variant="outlined" + type="password" /> = ({ fullWidth label={Language.newPasswordLabel} variant="outlined" + type="password" /> = ({ fullWidth label={Language.confirmPasswordLabel} variant="outlined" + type="password" /> {error && {error}} diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx index da2be6eac2ec6..7fe626ab1bfbf 100644 --- a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx @@ -2,7 +2,7 @@ import { fireEvent, screen, waitFor } from "@testing-library/react" import React from "react" import * as API from "../../../api/api" import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" -import * as AccountForm from "../../../components/SettingsAccountForm/SettingsAccountForm" +import * as SecurityForm from "../../../components/SettingsSecurityForm/SettingsSecurityForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" import { SecurityPage, Language } from "./SecurityPage" @@ -10,88 +10,83 @@ import { SecurityPage, Language } from "./SecurityPage" const renderPage = () => { return renderWithAuth( <> - + , ) } const newData = { - email: "user@coder.com", - username: "user", + old_password: "password1", + password: "password2", + confirm_password: "password2" } const fillAndSubmitForm = async () => { - await waitFor(() => screen.findByLabelText("Email")) - fireEvent.change(screen.getByLabelText("Email"), { target: { value: newData.email } }) - fireEvent.change(screen.getByLabelText("Username"), { target: { value: newData.username } }) - fireEvent.click(screen.getByText(AccountForm.Language.updateSettings)) + await waitFor(() => screen.findByLabelText("Old Password")) + fireEvent.change(screen.getByLabelText("Old Password"), { target: { value: newData.old_password } }) + fireEvent.change(screen.getByLabelText("New Password"), { target: { value: newData.password } }) + fireEvent.change(screen.getByLabelText("Confirm Password"), { target: { value: newData.confirm_password } }) + fireEvent.click(screen.getByText(SecurityForm.Language.updatePassword)) } -describe("AccountPage", () => { +describe("SecurityPage", () => { describe("when it is a success", () => { it("shows the success message", async () => { - jest.spyOn(API, "updateProfile").mockImplementationOnce((userId, data) => - Promise.resolve({ - id: userId, - created_at: new Date().toString(), - status: "active", - organization_ids: ["123"], - roles: [], - ...data, - }), + jest.spyOn(API, "updateUserPassword").mockImplementationOnce((_userId, _data) => + Promise.resolve(undefined), ) const { user } = renderPage() await fillAndSubmitForm() - const successMessage = await screen.findByText(AuthXService.Language.successProfileUpdate) + const successMessage = await screen.findByText(AuthXService.Language.successSecurityUpdate) expect(successMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) + expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) }) - describe("when the email is already taken", () => { + describe("when the old_password is incorrect", () => { it("shows an error", async () => { - jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ isAxiosError: true, response: { - data: { message: "Invalid profile", errors: [{ detail: "Email is already in use", field: "email" }] }, + data: { message: "Incorrect password.", errors: [{ detail: "Incorrect password.", field: "old_password" }] }, }, }) const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText("Email is already in use") + const errorMessage = await screen.findByText("Incorrect password.") expect(errorMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) + expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) }) - describe("when the username is already taken", () => { + describe("when the password is invalid", () => { it("shows an error", async () => { - jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ isAxiosError: true, response: { - data: { message: "Invalid profile", errors: [{ detail: "Username is already in use", field: "username" }] }, + data: { message: "Invalid password.", errors: [{ detail: "Invalid password.", field: "password" }] }, }, }) const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText("Username is already in use") + const errorMessage = await screen.findByText("Invalid password.") expect(errorMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) + expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) }) describe("when it is an unknown error", () => { it("shows a generic error message", async () => { - jest.spyOn(API, "updateProfile").mockRejectedValueOnce({ + jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({ data: "unknown error", }) @@ -100,8 +95,8 @@ describe("AccountPage", () => { const errorMessage = await screen.findByText(Language.unknownError) expect(errorMessage).toBeDefined() - expect(API.updateProfile).toBeCalledTimes(1) - expect(API.updateProfile).toBeCalledWith(user.id, newData) + expect(API.updateUserPassword).toBeCalledTimes(1) + expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) }) }) diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx index 2e16d0c94fe87..16cc939f780bf 100644 --- a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx @@ -10,14 +10,14 @@ export const Language = { unknownError: "Oops, an unknown error occurred.", } -export const AccountPage: React.FC = () => { +export const SecurityPage: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) - const { me, updateProfileError } = authState.context - const hasError = !!updateProfileError + const { me, updateSecurityError } = authState.context + const hasError = !!updateSecurityError const formErrors = - hasError && isApiError(updateProfileError) ? mapApiErrorToFieldErrors(updateProfileError.response.data) : undefined - const hasUnknownError = hasError && !isApiError(updateProfileError) + hasError && isApiError(updateSecurityError) ? mapApiErrorToFieldErrors(updateSecurityError.response.data) : undefined + const hasUnknownError = hasError && !isApiError(updateSecurityError) if (!me) { throw new Error("No current user found") @@ -28,11 +28,11 @@ export const AccountPage: React.FC = () => { { authSend({ - type: "UPDATE_PASSWORD", + type: "UPDATE_SECURITY", data, }) }} diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 29ced9a6dd6a4..5de9ec8e38e47 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -220,7 +220,7 @@ describe("Users Page", () => { // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(expect.any(String), MockUser.id) + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {password: expect.any(String), old_password: ""}) }) }) }) diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index a98993b803765..fecc8420baad8 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -5,6 +5,7 @@ import { displayError, displaySuccess } from "../../components/GlobalSnackbar/ut export const Language = { successProfileUpdate: "Updated settings.", + successSecurityUpdate: "Updated password.", successRegenerateSSHKey: "SSH Key regenerated successfully", errorRegenerateSSHKey: "Error on regenerate the SSH Key", } @@ -147,7 +148,6 @@ export const authMachine = getUserError: undefined, authError: undefined, updateProfileError: undefined, - updateSecurityError: undefined, methods: undefined, getMethodsError: undefined, }, @@ -169,7 +169,7 @@ export const authMachine = data: TypesGen.User } updateSecurity: { - data: TypesGen.UpdateUserPasswordRequest + data: any } checkPermissions: { data: TypesGen.UserAuthorizationResponse @@ -285,9 +285,6 @@ export const authMachine = UPDATE_PROFILE: { target: "updatingProfile", }, - UPDATE_SECURITY: { - target: "updatingSecurity", - }, }, }, updatingProfile: { @@ -311,6 +308,41 @@ export const authMachine = }, }, ssh: sshState, + security: { + initial: "idle", + states: { + idle: { + initial: "noError", + states: { + noError: {}, + error: {}, + }, + on: { + UPDATE_SECURITY: { + target: "updatingSecurity", + }, + }, + }, + updatingSecurity: { + entry: "clearUpdateSecurityError", + invoke: { + src: "updateSecurity", + onDone: [ + { + actions: ["notifySuccessSecurityUpdate"], + target: "#authState.signedIn.security.idle.noError", + }, + ], + onError: [ + { + actions: "assignUpdateSecurityError", + target: "#authState.signedIn.security.idle.error", + }, + ], + }, + }, + }, + }, }, on: { SIGN_OUT: { @@ -415,6 +447,15 @@ export const authMachine = clearUpdateProfileError: assign({ updateProfileError: (_) => undefined, }), + clearUpdateSecurityError: assign({ + updateSecurityError: (_) => undefined, + }), + notifySuccessSecurityUpdate: () => { + displaySuccess(Language.successSecurityUpdate) + }, + assignUpdateSecurityError: assign({ + updateSecurityError: (_, event) => event.data, + }), assignPermissions: assign({ // Setting event.data as Permissions to be more stricted. So we know // what permissions we asked for. From c485f1a3ef89de30b07bbfcbddbdc9edc6d6a4b9 Mon Sep 17 00:00:00 2001 From: Garrett Date: Fri, 27 May 2022 21:49:01 +0000 Subject: [PATCH 3/3] lint --- site/src/api/api.ts | 6 ++++-- .../SettingsSecurityForm.tsx | 16 +++++++++++----- .../SecurityPage/SecurityPage.test.tsx | 8 +++----- .../SettingsPages/SecurityPage/SecurityPage.tsx | 4 +++- site/src/pages/UsersPage/UsersPage.test.tsx | 4 ++-- site/src/xServices/auth/authXService.ts | 2 +- site/src/xServices/users/usersXService.ts | 5 ++++- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 315ad07ef46f0..54ce7828f874d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -223,8 +223,10 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise => - axios.put(`/api/v2/users/${userId}/password`, updatePassword) +export const updateUserPassword = async ( + userId: TypesGen.User["id"], + updatePassword: TypesGen.UpdateUserPasswordRequest, +): Promise => axios.put(`/api/v2/users/${userId}/password`, updatePassword) export const getSiteRoles = async (): Promise> => { const response = await axios.get>(`/api/v2/users/roles`) diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx index cd6a579e5816d..d10726227f427 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { LoadingButton } from "../LoadingButton/LoadingButton" import { Stack } from "../Stack/Stack" @@ -28,10 +28,16 @@ export const Language = { const validationSchema = Yup.object({ old_password: Yup.string().trim().required(Language.oldPasswordRequired), - password: Yup.string().trim().min(8, Language.passwordMinLength).max(64, Language.passwordMaxLength).required(Language.newPasswordRequired), - confirm_password: Yup.string().trim().test("passwords-match", Language.confirmPasswordMatch, function (value) { - return (this.parent as SecurityFormValues).password === value - }) + password: Yup.string() + .trim() + .min(8, Language.passwordMinLength) + .max(64, Language.passwordMaxLength) + .required(Language.newPasswordRequired), + confirm_password: Yup.string() + .trim() + .test("passwords-match", Language.confirmPasswordMatch, function (value) { + return (this.parent as SecurityFormValues).password === value + }), }) export type SecurityFormErrors = FormikErrors diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx index 7fe626ab1bfbf..b6301fc0601c1 100644 --- a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.test.tsx @@ -5,7 +5,7 @@ import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackba import * as SecurityForm from "../../../components/SettingsSecurityForm/SettingsSecurityForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" -import { SecurityPage, Language } from "./SecurityPage" +import { Language, SecurityPage } from "./SecurityPage" const renderPage = () => { return renderWithAuth( @@ -19,7 +19,7 @@ const renderPage = () => { const newData = { old_password: "password1", password: "password2", - confirm_password: "password2" + confirm_password: "password2", } const fillAndSubmitForm = async () => { @@ -33,9 +33,7 @@ const fillAndSubmitForm = async () => { describe("SecurityPage", () => { describe("when it is a success", () => { it("shows the success message", async () => { - jest.spyOn(API, "updateUserPassword").mockImplementationOnce((_userId, _data) => - Promise.resolve(undefined), - ) + jest.spyOn(API, "updateUserPassword").mockImplementationOnce((_userId, _data) => Promise.resolve(undefined)) const { user } = renderPage() await fillAndSubmitForm() diff --git a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx index 16cc939f780bf..49157467b51c6 100644 --- a/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx +++ b/site/src/pages/SettingsPages/SecurityPage/SecurityPage.tsx @@ -16,7 +16,9 @@ export const SecurityPage: React.FC = () => { const { me, updateSecurityError } = authState.context const hasError = !!updateSecurityError const formErrors = - hasError && isApiError(updateSecurityError) ? mapApiErrorToFieldErrors(updateSecurityError.response.data) : undefined + hasError && isApiError(updateSecurityError) + ? mapApiErrorToFieldErrors(updateSecurityError.response.data) + : undefined const hasUnknownError = hasError && !isApiError(updateSecurityError) if (!me) { diff --git a/site/src/pages/UsersPage/UsersPage.test.tsx b/site/src/pages/UsersPage/UsersPage.test.tsx index 5de9ec8e38e47..e50388f89b3b2 100644 --- a/site/src/pages/UsersPage/UsersPage.test.tsx +++ b/site/src/pages/UsersPage/UsersPage.test.tsx @@ -198,7 +198,7 @@ describe("Users Page", () => { // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {password: expect.any(String), old_password: ""}) + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "" }) }) }) @@ -220,7 +220,7 @@ describe("Users Page", () => { // Check if the API was called correctly expect(API.updateUserPassword).toBeCalledTimes(1) - expect(API.updateUserPassword).toBeCalledWith(MockUser.id, {password: expect.any(String), old_password: ""}) + expect(API.updateUserPassword).toBeCalledWith(MockUser.id, { password: expect.any(String), old_password: "" }) }) }) }) diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index fecc8420baad8..cca8aae0a8f1c 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -169,7 +169,7 @@ export const authMachine = data: TypesGen.User } updateSecurity: { - data: any + data: undefined } checkPermissions: { data: TypesGen.UserAuthorizationResponse diff --git a/site/src/xServices/users/usersXService.ts b/site/src/xServices/users/usersXService.ts index 281c22328a03b..c21219ae6e1be 100644 --- a/site/src/xServices/users/usersXService.ts +++ b/site/src/xServices/users/usersXService.ts @@ -221,7 +221,10 @@ export const usersMachine = createMachine( throw new Error("newUserPassword not generated") } - return API.updateUserPassword(context.userIdToResetPassword, {password: context.newUserPassword, old_password: ""}) + return API.updateUserPassword(context.userIdToResetPassword, { + password: context.newUserPassword, + old_password: "", + }) }, updateUserRoles: (context, event) => { if (!context.userIdToUpdateRoles) {