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

Commitcf5aca7

Browse files
Add reset user password action (#1320)
1 parent57bb108 commitcf5aca7

File tree

12 files changed

+313
-12
lines changed

12 files changed

+313
-12
lines changed

‎coderd/users.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,10 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
361361
}
362362

363363
func (api*api)putUserPassword(rw http.ResponseWriter,r*http.Request) {
364-
var (
365-
user=httpmw.UserParam(r)
366-
params codersdk.UpdateUserPasswordRequest
367-
)
364+
var (
365+
user=httpmw.UserParam(r)
366+
params codersdk.UpdateUserPasswordRequest
367+
)
368368
if!httpapi.Read(rw,r,&params) {
369369
return
370370
}

‎site/jest.setup.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import"@testing-library/jest-dom"
2+
importcryptofrom"crypto"
23
import{server}from"./src/testHelpers/server"
34

5+
// Polyfill the getRandomValues that is used on utils/random.ts
6+
Object.defineProperty(global.self,"crypto",{
7+
value:{
8+
getRandomValues:function(buffer:Buffer){
9+
returncrypto.randomFillSync(buffer)
10+
},
11+
},
12+
})
13+
414
// Establish API mocking before all tests through MSW.
515
beforeAll(()=>
616
server.listen({

‎site/src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,6 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
155155
constresponse=awaitaxios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
156156
returnresponse.data
157157
}
158+
159+
exportconstupdateUserPassword=async(password:string,userId:TypesGen.User["id"]):Promise<undefined>=>
160+
axios.put(`/api/v2/users/${userId}/password`,{ password})

‎site/src/components/CodeBlock/CodeBlock.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import{makeStyles}from"@material-ui/core/styles"
22
importReactfrom"react"
33
import{MONOSPACE_FONT_FAMILY}from"../../theme/constants"
4+
import{combineClasses}from"../../util/combineClasses"
45

56
exportinterfaceCodeBlockProps{
67
lines:string[]
8+
className?:string
79
}
810

9-
exportconstCodeBlock:React.FC<CodeBlockProps>=({ lines})=>{
11+
exportconstCodeBlock:React.FC<CodeBlockProps>=({ lines, className=""})=>{
1012
conststyles=useStyles()
1113

1214
return(
13-
<divclassName={styles.root}>
15+
<divclassName={combineClasses([styles.root,className])}>
1416
{lines.map((line,idx)=>(
1517
<divclassName={styles.line}key={idx}>
1618
{line}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import{Story}from"@storybook/react"
2+
importReactfrom"react"
3+
import{MockUser}from"../../testHelpers"
4+
import{generateRandomString}from"../../util/random"
5+
import{ResetPasswordDialog,ResetPasswordDialogProps}from"./ResetPasswordDialog"
6+
7+
exportdefault{
8+
title:"components/ResetPasswordDialog",
9+
component:ResetPasswordDialog,
10+
argTypes:{
11+
onClose:{action:"onClose"},
12+
onConfirm:{action:"onConfirm"},
13+
},
14+
}
15+
16+
constTemplate:Story<ResetPasswordDialogProps>=(args:ResetPasswordDialogProps)=><ResetPasswordDialog{...args}/>
17+
18+
exportconstExample=Template.bind({})
19+
Example.args={
20+
open:true,
21+
user:MockUser,
22+
newPassword:generateRandomString(12),
23+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
importDialogActionsfrom"@material-ui/core/DialogActions"
2+
importDialogContentfrom"@material-ui/core/DialogContent"
3+
importDialogContentTextfrom"@material-ui/core/DialogContentText"
4+
import{makeStyles}from"@material-ui/core/styles"
5+
importReactfrom"react"
6+
import*asTypesGenfrom"../../api/typesGenerated"
7+
import{CodeBlock}from"../CodeBlock/CodeBlock"
8+
import{Dialog,DialogActionButtons,DialogTitle}from"../Dialog/Dialog"
9+
10+
exportinterfaceResetPasswordDialogProps{
11+
open:boolean
12+
onClose:()=>void
13+
onConfirm:()=>void
14+
user?:TypesGen.User
15+
newPassword?:string
16+
loading:boolean
17+
}
18+
19+
exportconstLanguage={
20+
title:"Reset password",
21+
message:(username?:string):JSX.Element=>(
22+
<>
23+
You will need to send<strong>{username}</strong> the following password:
24+
</>
25+
),
26+
confirmText:"Reset password",
27+
}
28+
29+
exportconstResetPasswordDialog:React.FC<ResetPasswordDialogProps>=({
30+
open,
31+
onClose,
32+
onConfirm,
33+
user,
34+
newPassword,
35+
loading,
36+
})=>{
37+
conststyles=useStyles()
38+
39+
return(
40+
<Dialogopen={open}onClose={onClose}>
41+
<DialogTitletitle={Language.title}/>
42+
43+
<DialogContent>
44+
<DialogContentTextvariant="subtitle2">{Language.message(user?.username)}</DialogContentText>
45+
46+
<DialogContentTextcomponent="div">
47+
<CodeBlocklines={[newPassword??""]}className={styles.codeBlock}/>
48+
</DialogContentText>
49+
</DialogContent>
50+
51+
<DialogActions>
52+
<DialogActionButtons
53+
onCancel={onClose}
54+
confirmText={Language.confirmText}
55+
onConfirm={onConfirm}
56+
confirmLoading={loading}
57+
/>
58+
</DialogActions>
59+
</Dialog>
60+
)
61+
}
62+
63+
constuseStyles=makeStyles(()=>({
64+
codeBlock:{
65+
minHeight:"auto",
66+
userSelect:"all",
67+
width:"100%",
68+
},
69+
}))

‎site/src/components/UsersTable/UsersTable.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const Language = {
1111
emptyMessage:"No users found",
1212
usernameLabel:"User",
1313
suspendMenuItem:"Suspend",
14+
resetPasswordMenuItem:"Reset password",
1415
}
1516

1617
constemptyState=<EmptyStatemessage={Language.emptyMessage}/>
@@ -28,9 +29,10 @@ const columns: Column<UserResponse>[] = [
2829
exportinterfaceUsersTableProps{
2930
users:UserResponse[]
3031
onSuspendUser:(user:UserResponse)=>void
32+
onResetUserPassword:(user:UserResponse)=>void
3133
}
3234

33-
exportconstUsersTable:React.FC<UsersTableProps>=({ users, onSuspendUser})=>{
35+
exportconstUsersTable:React.FC<UsersTableProps>=({ users, onSuspendUser, onResetUserPassword})=>{
3436
return(
3537
<Table
3638
columns={columns}
@@ -45,6 +47,10 @@ export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser })
4547
label:Language.suspendMenuItem,
4648
onClick:onSuspendUser,
4749
},
50+
{
51+
label:Language.resetPasswordMenuItem,
52+
onClick:onResetUserPassword,
53+
},
4854
]}
4955
/>
5056
)}

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

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react"
22
importReactfrom"react"
33
import*asAPIfrom"../../api"
44
import{GlobalSnackbar}from"../../components/GlobalSnackbar/GlobalSnackbar"
5+
import{LanguageasResetPasswordDialogLanguage}from"../../components/ResetPasswordDialog/ResetPasswordDialog"
56
import{LanguageasUsersTableLanguage}from"../../components/UsersTable/UsersTable"
67
import{MockUser,MockUser2,render}from"../../testHelpers"
78
import{LanguageasusersXServiceLanguage}from"../../xServices/users/usersXService"
@@ -34,6 +35,33 @@ const suspendUser = async (setupActionSpies: () => void) => {
3435
fireEvent.click(confirmButton)
3536
}
3637

38+
constresetUserPassword=async(setupActionSpies:()=>void)=>{
39+
// Get the first user in the table
40+
constusers=awaitscreen.findAllByText(/.*@coder.com/)
41+
constfirstUserRow=users[0].closest("tr")
42+
if(!firstUserRow){
43+
thrownewError("Error on get the first user row")
44+
}
45+
46+
// Click on the "more" button to display the "Suspend" option
47+
constmoreButton=within(firstUserRow).getByLabelText("more")
48+
fireEvent.click(moreButton)
49+
constmenu=screen.getByRole("menu")
50+
constresetPasswordButton=within(menu).getByText(UsersTableLanguage.resetPasswordMenuItem)
51+
fireEvent.click(resetPasswordButton)
52+
53+
// Check if the confirm message is displayed
54+
constconfirmDialog=screen.getByRole("dialog")
55+
expect(confirmDialog).toHaveTextContent(`You will need to send${MockUser.username} the following password:`)
56+
57+
// Setup spies to check the actions after
58+
setupActionSpies()
59+
60+
// Click on the "Confirm" button
61+
constconfirmButton=within(confirmDialog).getByRole("button",{name:ResetPasswordDialogLanguage.confirmText})
62+
fireEvent.click(confirmButton)
63+
}
64+
3765
describe("Users Page",()=>{
3866
it("shows users",async()=>{
3967
render(<UsersPage/>)
@@ -81,7 +109,7 @@ describe("Users Page", () => {
81109
jest.spyOn(API,"suspendUser").mockRejectedValueOnce({})
82110
})
83111

84-
// Check if thesuccess message is displayed
112+
// Check if theerror message is displayed
85113
awaitscreen.findByText(usersXServiceLanguage.suspendUserError)
86114

87115
// Check if the API was called correctly
@@ -90,4 +118,50 @@ describe("Users Page", () => {
90118
})
91119
})
92120
})
121+
122+
describe("reset user password",()=>{
123+
describe("when it is success",()=>{
124+
it("shows a success message",async()=>{
125+
render(
126+
<>
127+
<UsersPage/>
128+
<GlobalSnackbar/>
129+
</>,
130+
)
131+
132+
awaitresetUserPassword(()=>{
133+
jest.spyOn(API,"updateUserPassword").mockResolvedValueOnce(undefined)
134+
})
135+
136+
// Check if the success message is displayed
137+
awaitscreen.findByText(usersXServiceLanguage.resetUserPasswordSuccess)
138+
139+
// Check if the API was called correctly
140+
expect(API.updateUserPassword).toBeCalledTimes(1)
141+
expect(API.updateUserPassword).toBeCalledWith(expect.any(String),MockUser.id)
142+
})
143+
})
144+
145+
describe("when it fails",()=>{
146+
it("shows an error message",async()=>{
147+
render(
148+
<>
149+
<UsersPage/>
150+
<GlobalSnackbar/>
151+
</>,
152+
)
153+
154+
awaitresetUserPassword(()=>{
155+
jest.spyOn(API,"updateUserPassword").mockRejectedValueOnce({})
156+
})
157+
158+
// Check if the error message is displayed
159+
awaitscreen.findByText(usersXServiceLanguage.resetUserPasswordError)
160+
161+
// Check if the API was called correctly
162+
expect(API.updateUserPassword).toBeCalledTimes(1)
163+
expect(API.updateUserPassword).toBeCalledWith(expect.any(String),MockUser.id)
164+
})
165+
})
166+
})
93167
})

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useContext, useEffect } from "react"
33
import{useNavigate}from"react-router"
44
import{ConfirmDialog}from"../../components/ConfirmDialog/ConfirmDialog"
55
import{FullScreenLoader}from"../../components/Loader/FullScreenLoader"
6+
import{ResetPasswordDialog}from"../../components/ResetPasswordDialog/ResetPasswordDialog"
67
import{XServiceContext}from"../../xServices/StateContext"
78
import{UsersPageView}from"./UsersPageView"
89

@@ -15,9 +16,10 @@ export const Language = {
1516
exportconstUsersPage:React.FC=()=>{
1617
constxServices=useContext(XServiceContext)
1718
const[usersState,usersSend]=useActor(xServices.usersXService)
18-
const{ users, getUsersError, userIdToSuspend}=usersState.context
19+
const{ users, getUsersError, userIdToSuspend, userIdToResetPassword, newUserPassword}=usersState.context
1920
constnavigate=useNavigate()
2021
constuserToBeSuspended=users?.find((u)=>u.id===userIdToSuspend)
22+
constuserToResetPassword=users?.find((u)=>u.id===userIdToResetPassword)
2123

2224
/**
2325
* Fetch users on component mount
@@ -39,6 +41,9 @@ export const UsersPage: React.FC = () => {
3941
onSuspendUser={(user)=>{
4042
usersSend({type:"SUSPEND_USER",userId:user.id})
4143
}}
44+
onResetUserPassword={(user)=>{
45+
usersSend({type:"RESET_USER_PASSWORD",userId:user.id})
46+
}}
4247
error={getUsersError}
4348
/>
4449

@@ -61,6 +66,19 @@ export const UsersPage: React.FC = () => {
6166
</>
6267
}
6368
/>
69+
70+
<ResetPasswordDialog
71+
loading={usersState.matches("resettingUserPassword")}
72+
user={userToResetPassword}
73+
newPassword={newUserPassword}
74+
open={usersState.matches("confirmUserPasswordReset")}
75+
onClose={()=>{
76+
usersSend("CANCEL_USER_PASSWORD_RESET")
77+
}}
78+
onConfirm={()=>{
79+
usersSend("CONFIRM_USER_PASSWORD_RESET")
80+
}}
81+
/>
6482
</>
6583
)
6684
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@ export interface UsersPageViewProps {
1515
users:UserResponse[]
1616
openUserCreationDialog:()=>void
1717
onSuspendUser:(user:UserResponse)=>void
18+
onResetUserPassword:(user:UserResponse)=>void
1819
error?:unknown
1920
}
2021

2122
exportconstUsersPageView:React.FC<UsersPageViewProps>=({
2223
users,
2324
openUserCreationDialog,
2425
onSuspendUser,
26+
onResetUserPassword,
2527
error,
2628
})=>{
2729
return(
2830
<Stackspacing={4}>
2931
<Headertitle={Language.pageTitle}action={{text:Language.newUserButton,onClick:openUserCreationDialog}}/>
3032
<Margins>
31-
{error ?<ErrorSummaryerror={error}/> :<UsersTableusers={users}onSuspendUser={onSuspendUser}/>}
33+
{error ?(
34+
<ErrorSummaryerror={error}/>
35+
) :(
36+
<UsersTableusers={users}onSuspendUser={onSuspendUser}onResetUserPassword={onResetUserPassword}/>
37+
)}
3238
</Margins>
3339
</Stack>
3440
)

‎site/src/util/random.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Generate a cryptographically secure random string using the specified number
3+
* of bytes then encode with base64.
4+
*
5+
* Base64 encodes 6 bits per character and pads with = so the length will not
6+
* equal the number of randomly generated bytes.
7+
*@see <https://developer.mozilla.org/en-US/docs/Glossary/Base64#encoded_size_increase>
8+
*/
9+
exportconstgenerateRandomString=(bytes:number):string=>{
10+
constbyteArr=window.crypto.getRandomValues(newUint8Array(bytes))
11+
// The types for `map` don't seem to support mapping from one array type to
12+
// another and `String.fromCharCode.apply` wants `number[]` so loop like this
13+
// instead.
14+
conststrArr:string[]=[]
15+
for(constbyteofbyteArr){
16+
strArr.push(String.fromCharCode(byte))
17+
}
18+
returnbtoa(strArr.join(""))
19+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp