Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2fe3963

Browse files
f0sselkylecarbs
authored andcommitted
feat: add user password change page (#1866)
1 parentdc8deea commit2fe3963

File tree

10 files changed

+324
-17
lines changed

10 files changed

+324
-17
lines changed

‎site/src/AppRouter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CliAuthenticationPage } from "./pages/CliAuthPage/CliAuthPage"
99
import{HealthzPage}from"./pages/HealthzPage/HealthzPage"
1010
import{LoginPage}from"./pages/LoginPage/LoginPage"
1111
import{AccountPage}from"./pages/SettingsPages/AccountPage/AccountPage"
12+
import{SecurityPage}from"./pages/SettingsPages/SecurityPage/SecurityPage"
1213
import{SSHKeysPage}from"./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
1314
import{TemplatePage}from"./pages/TemplatePage/TemplatePage"
1415
importTemplatesPagefrom"./pages/TemplatesPage/TemplatesPage"
@@ -126,6 +127,7 @@ export const AppRouter: React.FC = () => (
126127

127128
<Routepath="settings"element={<SettingsLayout/>}>
128129
<Routepath="account"element={<AccountPage/>}/>
130+
<Routepath="security"element={<SecurityPage/>}/>
129131
<Routepath="ssh-keys"element={<SSHKeysPage/>}/>
130132
</Route>
131133

‎site/src/api/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,10 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
223223
returnresponse.data
224224
}
225225

226-
exportconstupdateUserPassword=async(password:string,userId:TypesGen.User["id"]):Promise<undefined>=>
227-
axios.put(`/api/v2/users/${userId}/password`,{ password})
226+
exportconstupdateUserPassword=async(
227+
userId:TypesGen.User["id"],
228+
updatePassword:TypesGen.UpdateUserPasswordRequest,
229+
):Promise<undefined>=>axios.put(`/api/v2/users/${userId}/password`,updatePassword)
228230

229231
exportconstgetSiteRoles=async():Promise<Array<TypesGen.Role>>=>{
230232
constresponse=awaitaxios.get<Array<TypesGen.Role>>(`/api/v2/users/roles`)

‎site/src/components/SettingsLayout/SettingsLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { TabPanel } from "../TabPanel/TabPanel"
77

88
exportconstLanguage={
99
accountLabel:"Account",
10+
securityLabel:"Security",
1011
sshKeysLabel:"SSH keys",
1112
settingsLabel:"Settings",
1213
}
1314

1415
constmenuItems=[
1516
{label:Language.accountLabel,path:"/settings/account"},
17+
{label:Language.securityLabel,path:"/settings/security"},
1618
{label:Language.sshKeysLabel,path:"/settings/ssh-keys"},
1719
]
1820

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
importFormHelperTextfrom"@material-ui/core/FormHelperText"
2+
importTextFieldfrom"@material-ui/core/TextField"
3+
import{FormikContextType,FormikErrors,useFormik}from"formik"
4+
importReactfrom"react"
5+
import*asYupfrom"yup"
6+
import{getFormHelpers,onChangeTrimmed}from"../../util/formUtils"
7+
import{LoadingButton}from"../LoadingButton/LoadingButton"
8+
import{Stack}from"../Stack/Stack"
9+
10+
interfaceSecurityFormValues{
11+
old_password:string
12+
password:string
13+
confirm_password:string
14+
}
15+
16+
exportconstLanguage={
17+
oldPasswordLabel:"Old Password",
18+
newPasswordLabel:"New Password",
19+
confirmPasswordLabel:"Confirm Password",
20+
oldPasswordRequired:"Old password is required",
21+
newPasswordRequired:"New password is required",
22+
confirmPasswordRequired:"Password confirmation is required",
23+
passwordMinLength:"Password must be at least 8 characters",
24+
passwordMaxLength:"Password must be no more than 64 characters",
25+
confirmPasswordMatch:"Password and confirmation must match",
26+
updatePassword:"Update password",
27+
}
28+
29+
constvalidationSchema=Yup.object({
30+
old_password:Yup.string().trim().required(Language.oldPasswordRequired),
31+
password:Yup.string()
32+
.trim()
33+
.min(8,Language.passwordMinLength)
34+
.max(64,Language.passwordMaxLength)
35+
.required(Language.newPasswordRequired),
36+
confirm_password:Yup.string()
37+
.trim()
38+
.test("passwords-match",Language.confirmPasswordMatch,function(value){
39+
return(this.parentasSecurityFormValues).password===value
40+
}),
41+
})
42+
43+
exporttypeSecurityFormErrors=FormikErrors<SecurityFormValues>
44+
exportinterfaceSecurityFormProps{
45+
isLoading:boolean
46+
initialValues:SecurityFormValues
47+
onSubmit:(values:SecurityFormValues)=>void
48+
formErrors?:SecurityFormErrors
49+
error?:string
50+
}
51+
52+
exportconstSecurityForm:React.FC<SecurityFormProps>=({
53+
isLoading,
54+
onSubmit,
55+
initialValues,
56+
formErrors={},
57+
error,
58+
})=>{
59+
constform:FormikContextType<SecurityFormValues>=useFormik<SecurityFormValues>({
60+
initialValues,
61+
validationSchema,
62+
onSubmit,
63+
})
64+
constgetFieldHelpers=getFormHelpers<SecurityFormValues>(form,formErrors)
65+
66+
return(
67+
<>
68+
<formonSubmit={form.handleSubmit}>
69+
<Stack>
70+
<TextField
71+
{...getFieldHelpers("old_password")}
72+
onChange={onChangeTrimmed(form)}
73+
autoComplete="old_password"
74+
fullWidth
75+
label={Language.oldPasswordLabel}
76+
variant="outlined"
77+
type="password"
78+
/>
79+
<TextField
80+
{...getFieldHelpers("password")}
81+
onChange={onChangeTrimmed(form)}
82+
autoComplete="password"
83+
fullWidth
84+
label={Language.newPasswordLabel}
85+
variant="outlined"
86+
type="password"
87+
/>
88+
<TextField
89+
{...getFieldHelpers("confirm_password")}
90+
onChange={onChangeTrimmed(form)}
91+
autoComplete="confirm_password"
92+
fullWidth
93+
label={Language.confirmPasswordLabel}
94+
variant="outlined"
95+
type="password"
96+
/>
97+
98+
{error&&<FormHelperTexterror>{error}</FormHelperText>}
99+
100+
<div>
101+
<LoadingButtonloading={isLoading}type="submit"variant="contained">
102+
{isLoading ?"" :Language.updatePassword}
103+
</LoadingButton>
104+
</div>
105+
</Stack>
106+
</form>
107+
</>
108+
)
109+
}

‎site/src/pages/SettingsPages/AccountPage/LinkedAccountsPage.tsx

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import{fireEvent,screen,waitFor}from"@testing-library/react"
2+
importReactfrom"react"
3+
import*asAPIfrom"../../../api/api"
4+
import{GlobalSnackbar}from"../../../components/GlobalSnackbar/GlobalSnackbar"
5+
import*asSecurityFormfrom"../../../components/SettingsSecurityForm/SettingsSecurityForm"
6+
import{renderWithAuth}from"../../../testHelpers/renderHelpers"
7+
import*asAuthXServicefrom"../../../xServices/auth/authXService"
8+
import{Language,SecurityPage}from"./SecurityPage"
9+
10+
constrenderPage=()=>{
11+
returnrenderWithAuth(
12+
<>
13+
<SecurityPage/>
14+
<GlobalSnackbar/>
15+
</>,
16+
)
17+
}
18+
19+
constnewData={
20+
old_password:"password1",
21+
password:"password2",
22+
confirm_password:"password2",
23+
}
24+
25+
constfillAndSubmitForm=async()=>{
26+
awaitwaitFor(()=>screen.findByLabelText("Old Password"))
27+
fireEvent.change(screen.getByLabelText("Old Password"),{target:{value:newData.old_password}})
28+
fireEvent.change(screen.getByLabelText("New Password"),{target:{value:newData.password}})
29+
fireEvent.change(screen.getByLabelText("Confirm Password"),{target:{value:newData.confirm_password}})
30+
fireEvent.click(screen.getByText(SecurityForm.Language.updatePassword))
31+
}
32+
33+
describe("SecurityPage",()=>{
34+
describe("when it is a success",()=>{
35+
it("shows the success message",async()=>{
36+
jest.spyOn(API,"updateUserPassword").mockImplementationOnce((_userId,_data)=>Promise.resolve(undefined))
37+
const{ user}=renderPage()
38+
awaitfillAndSubmitForm()
39+
40+
constsuccessMessage=awaitscreen.findByText(AuthXService.Language.successSecurityUpdate)
41+
expect(successMessage).toBeDefined()
42+
expect(API.updateUserPassword).toBeCalledTimes(1)
43+
expect(API.updateUserPassword).toBeCalledWith(user.id,newData)
44+
})
45+
})
46+
47+
describe("when the old_password is incorrect",()=>{
48+
it("shows an error",async()=>{
49+
jest.spyOn(API,"updateUserPassword").mockRejectedValueOnce({
50+
isAxiosError:true,
51+
response:{
52+
data:{message:"Incorrect password.",errors:[{detail:"Incorrect password.",field:"old_password"}]},
53+
},
54+
})
55+
56+
const{ user}=renderPage()
57+
awaitfillAndSubmitForm()
58+
59+
consterrorMessage=awaitscreen.findByText("Incorrect password.")
60+
expect(errorMessage).toBeDefined()
61+
expect(API.updateUserPassword).toBeCalledTimes(1)
62+
expect(API.updateUserPassword).toBeCalledWith(user.id,newData)
63+
})
64+
})
65+
66+
describe("when the password is invalid",()=>{
67+
it("shows an error",async()=>{
68+
jest.spyOn(API,"updateUserPassword").mockRejectedValueOnce({
69+
isAxiosError:true,
70+
response:{
71+
data:{message:"Invalid password.",errors:[{detail:"Invalid password.",field:"password"}]},
72+
},
73+
})
74+
75+
const{ user}=renderPage()
76+
awaitfillAndSubmitForm()
77+
78+
consterrorMessage=awaitscreen.findByText("Invalid password.")
79+
expect(errorMessage).toBeDefined()
80+
expect(API.updateUserPassword).toBeCalledTimes(1)
81+
expect(API.updateUserPassword).toBeCalledWith(user.id,newData)
82+
})
83+
})
84+
85+
describe("when it is an unknown error",()=>{
86+
it("shows a generic error message",async()=>{
87+
jest.spyOn(API,"updateUserPassword").mockRejectedValueOnce({
88+
data:"unknown error",
89+
})
90+
91+
const{ user}=renderPage()
92+
awaitfillAndSubmitForm()
93+
94+
consterrorMessage=awaitscreen.findByText(Language.unknownError)
95+
expect(errorMessage).toBeDefined()
96+
expect(API.updateUserPassword).toBeCalledTimes(1)
97+
expect(API.updateUserPassword).toBeCalledWith(user.id,newData)
98+
})
99+
})
100+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import{useActor}from"@xstate/react"
2+
importReact,{useContext}from"react"
3+
import{isApiError,mapApiErrorToFieldErrors}from"../../../api/errors"
4+
import{Section}from"../../../components/Section/Section"
5+
import{SecurityForm}from"../../../components/SettingsSecurityForm/SettingsSecurityForm"
6+
import{XServiceContext}from"../../../xServices/StateContext"
7+
8+
exportconstLanguage={
9+
title:"Security",
10+
unknownError:"Oops, an unknown error occurred.",
11+
}
12+
13+
exportconstSecurityPage:React.FC=()=>{
14+
constxServices=useContext(XServiceContext)
15+
const[authState,authSend]=useActor(xServices.authXService)
16+
const{ me, updateSecurityError}=authState.context
17+
consthasError=!!updateSecurityError
18+
constformErrors=
19+
hasError&&isApiError(updateSecurityError)
20+
?mapApiErrorToFieldErrors(updateSecurityError.response.data)
21+
:undefined
22+
consthasUnknownError=hasError&&!isApiError(updateSecurityError)
23+
24+
if(!me){
25+
thrownewError("No current user found")
26+
}
27+
28+
return(
29+
<Sectiontitle={Language.title}>
30+
<SecurityForm
31+
error={hasUnknownError ?Language.unknownError :undefined}
32+
formErrors={formErrors}
33+
isLoading={authState.matches("signedIn.security.updatingSecurity")}
34+
initialValues={{old_password:"",password:"",confirm_password:""}}
35+
onSubmit={(data)=>{
36+
authSend({
37+
type:"UPDATE_SECURITY",
38+
data,
39+
})
40+
}}
41+
/>
42+
</Section>
43+
)
44+
}

‎site/src/pages/UsersPage/UsersPage.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ describe("Users Page", () => {
198198

199199
// Check if the API was called correctly
200200
expect(API.updateUserPassword).toBeCalledTimes(1)
201-
expect(API.updateUserPassword).toBeCalledWith(expect.any(String),MockUser.id)
201+
expect(API.updateUserPassword).toBeCalledWith(MockUser.id,{password:expect.any(String),old_password:""})
202202
})
203203
})
204204

@@ -220,7 +220,7 @@ describe("Users Page", () => {
220220

221221
// Check if the API was called correctly
222222
expect(API.updateUserPassword).toBeCalledTimes(1)
223-
expect(API.updateUserPassword).toBeCalledWith(expect.any(String),MockUser.id)
223+
expect(API.updateUserPassword).toBeCalledWith(MockUser.id,{password:expect.any(String),old_password:""})
224224
})
225225
})
226226
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp