Skip to content

Commit d35139c

Browse files
committed
Add FE for reset user password
1 parent d85092b commit d35139c

File tree

8 files changed

+185
-7
lines changed

8 files changed

+185
-7
lines changed

site/src/api/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,6 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
140140
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
141141
return response.data
142142
}
143+
144+
export const updateUserPassword = async (password: string, userId: TypesGen.User["id"]): Promise<undefined> =>
145+
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
import React from "react"
33
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
4+
import { combineClasses } from "../../util/combineClasses"
45

56
export interface CodeBlockProps {
67
lines: string[]
8+
className?: string
79
}
810

9-
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines }) => {
11+
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, className = "" }) => {
1012
const styles = useStyles()
1113

1214
return (
13-
<div className={styles.root}>
15+
<div className={combineClasses([styles.root, className])}>
1416
{lines.map((line, idx) => (
1517
<div className={styles.line} key={idx}>
1618
{line}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import DialogActions from "@material-ui/core/DialogActions"
2+
import DialogContent from "@material-ui/core/DialogContent"
3+
import DialogContentText from "@material-ui/core/DialogContentText"
4+
import { makeStyles } from "@material-ui/core/styles"
5+
import React from "react"
6+
import * as TypesGen from "../../api/typesGenerated"
7+
import { CodeBlock } from "../CodeBlock/CodeBlock"
8+
import { Dialog, DialogActionButtons, DialogTitle } from "../Dialog/Dialog"
9+
10+
interface ResetPasswordDialogProps {
11+
open: boolean
12+
onClose: () => void
13+
onConfirm: () => void
14+
user?: TypesGen.User
15+
newPassword?: string
16+
}
17+
18+
export const ResetPasswordDialog: React.FC<ResetPasswordDialogProps> = ({
19+
open,
20+
onClose,
21+
onConfirm,
22+
user,
23+
newPassword,
24+
}) => {
25+
const styles = useStyles()
26+
27+
return (
28+
<Dialog open={open} onClose={onClose}>
29+
<DialogTitle title="Reset password" />
30+
31+
<DialogContent>
32+
<DialogContentText variant="subtitle2">
33+
You will need to send <strong>{user?.username}</strong> the following password:
34+
</DialogContentText>
35+
36+
<DialogContentText component="div">
37+
<CodeBlock lines={[newPassword ?? ""]} className={styles.codeBlock} />
38+
</DialogContentText>
39+
</DialogContent>
40+
41+
<DialogActions>
42+
<DialogActionButtons onCancel={onClose} confirmText="Reset password" onConfirm={onConfirm} />
43+
</DialogActions>
44+
</Dialog>
45+
)
46+
}
47+
48+
const useStyles = makeStyles(() => ({
49+
codeBlock: {
50+
minHeight: "auto",
51+
userSelect: "all",
52+
width: "100%",
53+
},
54+
}))

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
const emptyState = <EmptyState message={Language.emptyMessage} />
@@ -28,9 +29,10 @@ const columns: Column<UserResponse>[] = [
2829
export interface UsersTableProps {
2930
users: UserResponse[]
3031
onSuspendUser: (user: UserResponse) => void
32+
onResetUserPassword: (user: UserResponse) => void
3133
}
3234

33-
export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser }) => {
35+
export const UsersTable: 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.tsx

Lines changed: 18 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
export const UsersPage: React.FC = () => {
1617
const xServices = 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
const navigate = useNavigate()
2021
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
22+
const userToResetPassword = 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,18 @@ export const UsersPage: React.FC = () => {
6166
</>
6267
}
6368
/>
69+
70+
<ResetPasswordDialog
71+
user={userToResetPassword}
72+
newPassword={newUserPassword}
73+
open={usersState.matches("confirmUserPasswordReset")}
74+
onClose={() => {
75+
usersSend("CANCEL_USER_PASSWORD_RESET")
76+
}}
77+
onConfirm={() => {
78+
usersSend("CONFIRM_USER_PASSWORD_RESET")
79+
}}
80+
/>
6481
</>
6582
)
6683
}

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
export const UsersPageView: React.FC<UsersPageViewProps> = ({
2223
users,
2324
openUserCreationDialog,
2425
onSuspendUser,
26+
onResetUserPassword,
2527
error,
2628
}) => {
2729
return (
2830
<Stack spacing={4}>
2931
<Header title={Language.pageTitle} action={{ text: Language.newUserButton, onClick: openUserCreationDialog }} />
3032
<Margins>
31-
{error ? <ErrorSummary error={error} /> : <UsersTable users={users} onSuspendUser={onSuspendUser} />}
33+
{error ? (
34+
<ErrorSummary error={error} />
35+
) : (
36+
<UsersTable users={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+
export const generateRandomString = (bytes: number): string => {
10+
const byteArr = window.crypto.getRandomValues(new Uint8Array(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+
const strArr: string[] = []
15+
for (const byte of byteArr) {
16+
strArr.push(String.fromCharCode(byte))
17+
}
18+
return btoa(strArr.join(""))
19+
}

site/src/xServices/users/usersXService.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,42 @@ import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../
44
import * as Types from "../../api/types"
55
import * as TypesGen from "../../api/typesGenerated"
66
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
7+
import { generateRandomString } from "../../util/random"
78

89
export const Language = {
910
createUserSuccess: "Successfully created user.",
1011
suspendUserSuccess: "Successfully suspended the user.",
11-
suspendUserError: "Error on suspend the user",
12+
suspendUserError: "Error on suspend the user.",
13+
resetUserPasswordSuccess: "Successfully updated the user password.",
14+
resetUserPasswordError: "Error on reset the user password.",
1215
}
1316

1417
export interface UsersContext {
18+
// Get users
1519
users?: TypesGen.User[]
16-
userIdToSuspend?: TypesGen.User["id"]
1720
getUsersError?: Error | unknown
1821
createUserError?: Error | unknown
1922
createUserFormErrors?: FieldErrors
23+
// Suspend user
24+
userIdToSuspend?: TypesGen.User["id"]
2025
suspendUserError?: Error | unknown
26+
// Reset user password
27+
userIdToResetPassword?: TypesGen.User["id"]
28+
resetUserPasswordError?: Error | unknown
29+
newUserPassword?: string
2130
}
2231

2332
export type UsersEvent =
2433
| { type: "GET_USERS" }
2534
| { type: "CREATE"; user: Types.CreateUserRequest }
35+
// Suspend events
2636
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
2737
| { type: "CONFIRM_USER_SUSPENSION" }
2838
| { type: "CANCEL_USER_SUSPENSION" }
39+
// Reset password events
40+
| { type: "RESET_USER_PASSWORD"; userId: TypesGen.User["id"] }
41+
| { type: "CONFIRM_USER_PASSWORD_RESET" }
42+
| { type: "CANCEL_USER_PASSWORD_RESET" }
2943

3044
export const usersMachine = createMachine(
3145
{
@@ -43,6 +57,9 @@ export const usersMachine = createMachine(
4357
suspendUser: {
4458
data: TypesGen.User
4559
}
60+
updateUserPassword: {
61+
data: undefined
62+
}
4663
},
4764
},
4865
id: "usersState",
@@ -59,6 +76,10 @@ export const usersMachine = createMachine(
5976
target: "confirmUserSuspension",
6077
actions: ["assignUserIdToSuspend"],
6178
},
79+
RESET_USER_PASSWORD: {
80+
target: "confirmUserPasswordReset",
81+
actions: ["assignUserIdToResetPassword", "generateRandomPassword"],
82+
},
6283
},
6384
},
6485
gettingUsers: {
@@ -124,6 +145,27 @@ export const usersMachine = createMachine(
124145
},
125146
},
126147
},
148+
confirmUserPasswordReset: {
149+
on: {
150+
CONFIRM_USER_PASSWORD_RESET: "resettingUserPassword",
151+
CANCEL_USER_PASSWORD_RESET: "idle",
152+
},
153+
},
154+
resettingUserPassword: {
155+
entry: "clearResetUserPasswordError",
156+
invoke: {
157+
src: "resetUserPassword",
158+
id: "resetUserPassword",
159+
onDone: {
160+
target: "idle",
161+
actions: ["displayResetPasswordSuccess"],
162+
},
163+
onError: {
164+
target: "idle",
165+
actions: ["assignResetUserPasswordError", "displayResetPasswordErrorMessage"],
166+
},
167+
},
168+
},
127169
error: {
128170
on: {
129171
GET_USERS: "gettingUsers",
@@ -145,6 +187,17 @@ export const usersMachine = createMachine(
145187

146188
return API.suspendUser(context.userIdToSuspend)
147189
},
190+
resetUserPassword: (context) => {
191+
if (!context.userIdToResetPassword) {
192+
throw new Error("userIdToResetPassword is undefined")
193+
}
194+
195+
if (!context.newUserPassword) {
196+
throw new Error("newUserPassword not generated")
197+
}
198+
199+
return API.updateUserPassword(context.newUserPassword, context.userIdToResetPassword)
200+
},
148201
},
149202
guards: {
150203
isFormError: (_, event) => isApiError(event.data),
@@ -159,6 +212,9 @@ export const usersMachine = createMachine(
159212
assignUserIdToSuspend: assign({
160213
userIdToSuspend: (_, event) => event.userId,
161214
}),
215+
assignUserIdToResetPassword: assign({
216+
userIdToResetPassword: (_, event) => event.userId,
217+
}),
162218
clearGetUsersError: assign((context: UsersContext) => ({
163219
...context,
164220
getUsersError: undefined,
@@ -173,13 +229,19 @@ export const usersMachine = createMachine(
173229
assignSuspendUserError: assign({
174230
suspendUserError: (_, event) => event.data,
175231
}),
232+
assignResetUserPasswordError: assign({
233+
resetUserPasswordError: (_, event) => event.data,
234+
}),
176235
clearCreateUserError: assign((context: UsersContext) => ({
177236
...context,
178237
createUserError: undefined,
179238
})),
180239
clearSuspendUserError: assign({
181240
suspendUserError: (_) => undefined,
182241
}),
242+
clearResetUserPasswordError: assign({
243+
resetUserPasswordError: (_) => undefined,
244+
}),
183245
displayCreateUserSuccess: () => {
184246
displaySuccess(Language.createUserSuccess)
185247
},
@@ -189,6 +251,15 @@ export const usersMachine = createMachine(
189251
displaySuspendedErrorMessage: () => {
190252
displayError(Language.suspendUserError)
191253
},
254+
displayResetPasswordSuccess: () => {
255+
displaySuccess(Language.resetUserPasswordSuccess)
256+
},
257+
displayResetPasswordErrorMessage: () => {
258+
displaySuccess(Language.resetUserPasswordError)
259+
},
260+
generateRandomPassword: assign({
261+
newUserPassword: (_) => generateRandomString(12),
262+
}),
192263
},
193264
},
194265
)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy