mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
Add reset user password action (#1320)
This commit is contained in:
@ -361,10 +361,10 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
func (api *api) putUserPassword(rw http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
user = httpmw.UserParam(r)
|
user = httpmw.UserParam(r)
|
||||||
params codersdk.UpdateUserPasswordRequest
|
params codersdk.UpdateUserPasswordRequest
|
||||||
)
|
)
|
||||||
if !httpapi.Read(rw, r, ¶ms) {
|
if !httpapi.Read(rw, r, ¶ms) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import "@testing-library/jest-dom"
|
import "@testing-library/jest-dom"
|
||||||
|
import crypto from "crypto"
|
||||||
import { server } from "./src/testHelpers/server"
|
import { server } from "./src/testHelpers/server"
|
||||||
|
|
||||||
|
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||||
|
Object.defineProperty(global.self, "crypto", {
|
||||||
|
value: {
|
||||||
|
getRandomValues: function (buffer: Buffer) {
|
||||||
|
return crypto.randomFillSync(buffer)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Establish API mocking before all tests through MSW.
|
// Establish API mocking before all tests through MSW.
|
||||||
beforeAll(() =>
|
beforeAll(() =>
|
||||||
server.listen({
|
server.listen({
|
||||||
|
@ -155,3 +155,6 @@ export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen
|
|||||||
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
|
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateUserPassword = async (password: string, userId: TypesGen.User["id"]): Promise<undefined> =>
|
||||||
|
axios.put(`/api/v2/users/${userId}/password`, { password })
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||||
|
import { combineClasses } from "../../util/combineClasses"
|
||||||
|
|
||||||
export interface CodeBlockProps {
|
export interface CodeBlockProps {
|
||||||
lines: string[]
|
lines: string[]
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines }) => {
|
export const CodeBlock: React.FC<CodeBlockProps> = ({ lines, className = "" }) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={combineClasses([styles.root, className])}>
|
||||||
{lines.map((line, idx) => (
|
{lines.map((line, idx) => (
|
||||||
<div className={styles.line} key={idx}>
|
<div className={styles.line} key={idx}>
|
||||||
{line}
|
{line}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { Story } from "@storybook/react"
|
||||||
|
import React from "react"
|
||||||
|
import { MockUser } from "../../testHelpers"
|
||||||
|
import { generateRandomString } from "../../util/random"
|
||||||
|
import { ResetPasswordDialog, ResetPasswordDialogProps } from "./ResetPasswordDialog"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "components/ResetPasswordDialog",
|
||||||
|
component: ResetPasswordDialog,
|
||||||
|
argTypes: {
|
||||||
|
onClose: { action: "onClose" },
|
||||||
|
onConfirm: { action: "onConfirm" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Template: Story<ResetPasswordDialogProps> = (args: ResetPasswordDialogProps) => <ResetPasswordDialog {...args} />
|
||||||
|
|
||||||
|
export const Example = Template.bind({})
|
||||||
|
Example.args = {
|
||||||
|
open: true,
|
||||||
|
user: MockUser,
|
||||||
|
newPassword: generateRandomString(12),
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
import DialogActions from "@material-ui/core/DialogActions"
|
||||||
|
import DialogContent from "@material-ui/core/DialogContent"
|
||||||
|
import DialogContentText from "@material-ui/core/DialogContentText"
|
||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
|
import React from "react"
|
||||||
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
|
import { CodeBlock } from "../CodeBlock/CodeBlock"
|
||||||
|
import { Dialog, DialogActionButtons, DialogTitle } from "../Dialog/Dialog"
|
||||||
|
|
||||||
|
export interface ResetPasswordDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onConfirm: () => void
|
||||||
|
user?: TypesGen.User
|
||||||
|
newPassword?: string
|
||||||
|
loading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
title: "Reset password",
|
||||||
|
message: (username?: string): JSX.Element => (
|
||||||
|
<>
|
||||||
|
You will need to send <strong>{username}</strong> the following password:
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
confirmText: "Reset password",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ResetPasswordDialog: React.FC<ResetPasswordDialogProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
user,
|
||||||
|
newPassword,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose}>
|
||||||
|
<DialogTitle title={Language.title} />
|
||||||
|
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText variant="subtitle2">{Language.message(user?.username)}</DialogContentText>
|
||||||
|
|
||||||
|
<DialogContentText component="div">
|
||||||
|
<CodeBlock lines={[newPassword ?? ""]} className={styles.codeBlock} />
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<DialogActionButtons
|
||||||
|
onCancel={onClose}
|
||||||
|
confirmText={Language.confirmText}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
confirmLoading={loading}
|
||||||
|
/>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles(() => ({
|
||||||
|
codeBlock: {
|
||||||
|
minHeight: "auto",
|
||||||
|
userSelect: "all",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}))
|
@ -11,6 +11,7 @@ export const Language = {
|
|||||||
emptyMessage: "No users found",
|
emptyMessage: "No users found",
|
||||||
usernameLabel: "User",
|
usernameLabel: "User",
|
||||||
suspendMenuItem: "Suspend",
|
suspendMenuItem: "Suspend",
|
||||||
|
resetPasswordMenuItem: "Reset password",
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyState = <EmptyState message={Language.emptyMessage} />
|
const emptyState = <EmptyState message={Language.emptyMessage} />
|
||||||
@ -28,9 +29,10 @@ const columns: Column<UserResponse>[] = [
|
|||||||
export interface UsersTableProps {
|
export interface UsersTableProps {
|
||||||
users: UserResponse[]
|
users: UserResponse[]
|
||||||
onSuspendUser: (user: UserResponse) => void
|
onSuspendUser: (user: UserResponse) => void
|
||||||
|
onResetUserPassword: (user: UserResponse) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser }) => {
|
export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser, onResetUserPassword }) => {
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@ -45,6 +47,10 @@ export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser })
|
|||||||
label: Language.suspendMenuItem,
|
label: Language.suspendMenuItem,
|
||||||
onClick: onSuspendUser,
|
onClick: onSuspendUser,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: Language.resetPasswordMenuItem,
|
||||||
|
onClick: onResetUserPassword,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -2,6 +2,7 @@ import { fireEvent, screen, waitFor, within } from "@testing-library/react"
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
import * as API from "../../api"
|
import * as API from "../../api"
|
||||||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
|
||||||
|
import { Language as ResetPasswordDialogLanguage } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
|
||||||
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
|
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
|
||||||
import { MockUser, MockUser2, render } from "../../testHelpers"
|
import { MockUser, MockUser2, render } from "../../testHelpers"
|
||||||
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
|
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
|
||||||
@ -34,6 +35,33 @@ const suspendUser = async (setupActionSpies: () => void) => {
|
|||||||
fireEvent.click(confirmButton)
|
fireEvent.click(confirmButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetUserPassword = async (setupActionSpies: () => void) => {
|
||||||
|
// Get the first user in the table
|
||||||
|
const users = await screen.findAllByText(/.*@coder.com/)
|
||||||
|
const firstUserRow = users[0].closest("tr")
|
||||||
|
if (!firstUserRow) {
|
||||||
|
throw new Error("Error on get the first user row")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click on the "more" button to display the "Suspend" option
|
||||||
|
const moreButton = within(firstUserRow).getByLabelText("more")
|
||||||
|
fireEvent.click(moreButton)
|
||||||
|
const menu = screen.getByRole("menu")
|
||||||
|
const resetPasswordButton = within(menu).getByText(UsersTableLanguage.resetPasswordMenuItem)
|
||||||
|
fireEvent.click(resetPasswordButton)
|
||||||
|
|
||||||
|
// Check if the confirm message is displayed
|
||||||
|
const confirmDialog = screen.getByRole("dialog")
|
||||||
|
expect(confirmDialog).toHaveTextContent(`You will need to send ${MockUser.username} the following password:`)
|
||||||
|
|
||||||
|
// Setup spies to check the actions after
|
||||||
|
setupActionSpies()
|
||||||
|
|
||||||
|
// Click on the "Confirm" button
|
||||||
|
const confirmButton = within(confirmDialog).getByRole("button", { name: ResetPasswordDialogLanguage.confirmText })
|
||||||
|
fireEvent.click(confirmButton)
|
||||||
|
}
|
||||||
|
|
||||||
describe("Users Page", () => {
|
describe("Users Page", () => {
|
||||||
it("shows users", async () => {
|
it("shows users", async () => {
|
||||||
render(<UsersPage />)
|
render(<UsersPage />)
|
||||||
@ -81,7 +109,7 @@ describe("Users Page", () => {
|
|||||||
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({})
|
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if the success message is displayed
|
// Check if the error message is displayed
|
||||||
await screen.findByText(usersXServiceLanguage.suspendUserError)
|
await screen.findByText(usersXServiceLanguage.suspendUserError)
|
||||||
|
|
||||||
// Check if the API was called correctly
|
// Check if the API was called correctly
|
||||||
@ -90,4 +118,50 @@ describe("Users Page", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("reset user password", () => {
|
||||||
|
describe("when it is success", () => {
|
||||||
|
it("shows a success message", async () => {
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<UsersPage />
|
||||||
|
<GlobalSnackbar />
|
||||||
|
</>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await resetUserPassword(() => {
|
||||||
|
jest.spyOn(API, "updateUserPassword").mockResolvedValueOnce(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if the success message is displayed
|
||||||
|
await screen.findByText(usersXServiceLanguage.resetUserPasswordSuccess)
|
||||||
|
|
||||||
|
// Check if the API was called correctly
|
||||||
|
expect(API.updateUserPassword).toBeCalledTimes(1)
|
||||||
|
expect(API.updateUserPassword).toBeCalledWith(expect.any(String), MockUser.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("when it fails", () => {
|
||||||
|
it("shows an error message", async () => {
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<UsersPage />
|
||||||
|
<GlobalSnackbar />
|
||||||
|
</>,
|
||||||
|
)
|
||||||
|
|
||||||
|
await resetUserPassword(() => {
|
||||||
|
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Check if the error message is displayed
|
||||||
|
await screen.findByText(usersXServiceLanguage.resetUserPasswordError)
|
||||||
|
|
||||||
|
// Check if the API was called correctly
|
||||||
|
expect(API.updateUserPassword).toBeCalledTimes(1)
|
||||||
|
expect(API.updateUserPassword).toBeCalledWith(expect.any(String), MockUser.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -3,6 +3,7 @@ import React, { useContext, useEffect } from "react"
|
|||||||
import { useNavigate } from "react-router"
|
import { useNavigate } from "react-router"
|
||||||
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
|
import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog"
|
||||||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
|
||||||
|
import { ResetPasswordDialog } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
|
||||||
import { XServiceContext } from "../../xServices/StateContext"
|
import { XServiceContext } from "../../xServices/StateContext"
|
||||||
import { UsersPageView } from "./UsersPageView"
|
import { UsersPageView } from "./UsersPageView"
|
||||||
|
|
||||||
@ -15,9 +16,10 @@ export const Language = {
|
|||||||
export const UsersPage: React.FC = () => {
|
export const UsersPage: React.FC = () => {
|
||||||
const xServices = useContext(XServiceContext)
|
const xServices = useContext(XServiceContext)
|
||||||
const [usersState, usersSend] = useActor(xServices.usersXService)
|
const [usersState, usersSend] = useActor(xServices.usersXService)
|
||||||
const { users, getUsersError, userIdToSuspend } = usersState.context
|
const { users, getUsersError, userIdToSuspend, userIdToResetPassword, newUserPassword } = usersState.context
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
|
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
|
||||||
|
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch users on component mount
|
* Fetch users on component mount
|
||||||
@ -39,6 +41,9 @@ export const UsersPage: React.FC = () => {
|
|||||||
onSuspendUser={(user) => {
|
onSuspendUser={(user) => {
|
||||||
usersSend({ type: "SUSPEND_USER", userId: user.id })
|
usersSend({ type: "SUSPEND_USER", userId: user.id })
|
||||||
}}
|
}}
|
||||||
|
onResetUserPassword={(user) => {
|
||||||
|
usersSend({ type: "RESET_USER_PASSWORD", userId: user.id })
|
||||||
|
}}
|
||||||
error={getUsersError}
|
error={getUsersError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -61,6 +66,19 @@ export const UsersPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ResetPasswordDialog
|
||||||
|
loading={usersState.matches("resettingUserPassword")}
|
||||||
|
user={userToResetPassword}
|
||||||
|
newPassword={newUserPassword}
|
||||||
|
open={usersState.matches("confirmUserPasswordReset")}
|
||||||
|
onClose={() => {
|
||||||
|
usersSend("CANCEL_USER_PASSWORD_RESET")
|
||||||
|
}}
|
||||||
|
onConfirm={() => {
|
||||||
|
usersSend("CONFIRM_USER_PASSWORD_RESET")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ export interface UsersPageViewProps {
|
|||||||
users: UserResponse[]
|
users: UserResponse[]
|
||||||
openUserCreationDialog: () => void
|
openUserCreationDialog: () => void
|
||||||
onSuspendUser: (user: UserResponse) => void
|
onSuspendUser: (user: UserResponse) => void
|
||||||
|
onResetUserPassword: (user: UserResponse) => void
|
||||||
error?: unknown
|
error?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,13 +23,18 @@ export const UsersPageView: React.FC<UsersPageViewProps> = ({
|
|||||||
users,
|
users,
|
||||||
openUserCreationDialog,
|
openUserCreationDialog,
|
||||||
onSuspendUser,
|
onSuspendUser,
|
||||||
|
onResetUserPassword,
|
||||||
error,
|
error,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<Header title={Language.pageTitle} action={{ text: Language.newUserButton, onClick: openUserCreationDialog }} />
|
<Header title={Language.pageTitle} action={{ text: Language.newUserButton, onClick: openUserCreationDialog }} />
|
||||||
<Margins>
|
<Margins>
|
||||||
{error ? <ErrorSummary error={error} /> : <UsersTable users={users} onSuspendUser={onSuspendUser} />}
|
{error ? (
|
||||||
|
<ErrorSummary error={error} />
|
||||||
|
) : (
|
||||||
|
<UsersTable users={users} onSuspendUser={onSuspendUser} onResetUserPassword={onResetUserPassword} />
|
||||||
|
)}
|
||||||
</Margins>
|
</Margins>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
19
site/src/util/random.ts
Normal file
19
site/src/util/random.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Generate a cryptographically secure random string using the specified number
|
||||||
|
* of bytes then encode with base64.
|
||||||
|
*
|
||||||
|
* Base64 encodes 6 bits per character and pads with = so the length will not
|
||||||
|
* equal the number of randomly generated bytes.
|
||||||
|
* @see <https://developer.mozilla.org/en-US/docs/Glossary/Base64#encoded_size_increase>
|
||||||
|
*/
|
||||||
|
export const generateRandomString = (bytes: number): string => {
|
||||||
|
const byteArr = window.crypto.getRandomValues(new Uint8Array(bytes))
|
||||||
|
// The types for `map` don't seem to support mapping from one array type to
|
||||||
|
// another and `String.fromCharCode.apply` wants `number[]` so loop like this
|
||||||
|
// instead.
|
||||||
|
const strArr: string[] = []
|
||||||
|
for (const byte of byteArr) {
|
||||||
|
strArr.push(String.fromCharCode(byte))
|
||||||
|
}
|
||||||
|
return btoa(strArr.join(""))
|
||||||
|
}
|
@ -4,28 +4,42 @@ import { ApiError, FieldErrors, isApiError, mapApiErrorToFieldErrors } from "../
|
|||||||
import * as Types from "../../api/types"
|
import * as Types from "../../api/types"
|
||||||
import * as TypesGen from "../../api/typesGenerated"
|
import * as TypesGen from "../../api/typesGenerated"
|
||||||
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
|
import { displayError, displaySuccess } from "../../components/GlobalSnackbar/utils"
|
||||||
|
import { generateRandomString } from "../../util/random"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
createUserSuccess: "Successfully created user.",
|
createUserSuccess: "Successfully created user.",
|
||||||
suspendUserSuccess: "Successfully suspended the user.",
|
suspendUserSuccess: "Successfully suspended the user.",
|
||||||
suspendUserError: "Error on suspend the user",
|
suspendUserError: "Error on suspend the user.",
|
||||||
|
resetUserPasswordSuccess: "Successfully updated the user password.",
|
||||||
|
resetUserPasswordError: "Error on reset the user password.",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersContext {
|
export interface UsersContext {
|
||||||
|
// Get users
|
||||||
users?: TypesGen.User[]
|
users?: TypesGen.User[]
|
||||||
userIdToSuspend?: TypesGen.User["id"]
|
|
||||||
getUsersError?: Error | unknown
|
getUsersError?: Error | unknown
|
||||||
createUserError?: Error | unknown
|
createUserError?: Error | unknown
|
||||||
createUserFormErrors?: FieldErrors
|
createUserFormErrors?: FieldErrors
|
||||||
|
// Suspend user
|
||||||
|
userIdToSuspend?: TypesGen.User["id"]
|
||||||
suspendUserError?: Error | unknown
|
suspendUserError?: Error | unknown
|
||||||
|
// Reset user password
|
||||||
|
userIdToResetPassword?: TypesGen.User["id"]
|
||||||
|
resetUserPasswordError?: Error | unknown
|
||||||
|
newUserPassword?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UsersEvent =
|
export type UsersEvent =
|
||||||
| { type: "GET_USERS" }
|
| { type: "GET_USERS" }
|
||||||
| { type: "CREATE"; user: Types.CreateUserRequest }
|
| { type: "CREATE"; user: Types.CreateUserRequest }
|
||||||
|
// Suspend events
|
||||||
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
|
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
|
||||||
| { type: "CONFIRM_USER_SUSPENSION" }
|
| { type: "CONFIRM_USER_SUSPENSION" }
|
||||||
| { type: "CANCEL_USER_SUSPENSION" }
|
| { type: "CANCEL_USER_SUSPENSION" }
|
||||||
|
// Reset password events
|
||||||
|
| { type: "RESET_USER_PASSWORD"; userId: TypesGen.User["id"] }
|
||||||
|
| { type: "CONFIRM_USER_PASSWORD_RESET" }
|
||||||
|
| { type: "CANCEL_USER_PASSWORD_RESET" }
|
||||||
|
|
||||||
export const usersMachine = createMachine(
|
export const usersMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -43,6 +57,9 @@ export const usersMachine = createMachine(
|
|||||||
suspendUser: {
|
suspendUser: {
|
||||||
data: TypesGen.User
|
data: TypesGen.User
|
||||||
}
|
}
|
||||||
|
updateUserPassword: {
|
||||||
|
data: undefined
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
id: "usersState",
|
id: "usersState",
|
||||||
@ -59,6 +76,10 @@ export const usersMachine = createMachine(
|
|||||||
target: "confirmUserSuspension",
|
target: "confirmUserSuspension",
|
||||||
actions: ["assignUserIdToSuspend"],
|
actions: ["assignUserIdToSuspend"],
|
||||||
},
|
},
|
||||||
|
RESET_USER_PASSWORD: {
|
||||||
|
target: "confirmUserPasswordReset",
|
||||||
|
actions: ["assignUserIdToResetPassword", "generateRandomPassword"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gettingUsers: {
|
gettingUsers: {
|
||||||
@ -124,6 +145,27 @@ export const usersMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
confirmUserPasswordReset: {
|
||||||
|
on: {
|
||||||
|
CONFIRM_USER_PASSWORD_RESET: "resettingUserPassword",
|
||||||
|
CANCEL_USER_PASSWORD_RESET: "idle",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resettingUserPassword: {
|
||||||
|
entry: "clearResetUserPasswordError",
|
||||||
|
invoke: {
|
||||||
|
src: "resetUserPassword",
|
||||||
|
id: "resetUserPassword",
|
||||||
|
onDone: {
|
||||||
|
target: "idle",
|
||||||
|
actions: ["displayResetPasswordSuccess"],
|
||||||
|
},
|
||||||
|
onError: {
|
||||||
|
target: "idle",
|
||||||
|
actions: ["assignResetUserPasswordError", "displayResetPasswordErrorMessage"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
error: {
|
error: {
|
||||||
on: {
|
on: {
|
||||||
GET_USERS: "gettingUsers",
|
GET_USERS: "gettingUsers",
|
||||||
@ -145,6 +187,17 @@ export const usersMachine = createMachine(
|
|||||||
|
|
||||||
return API.suspendUser(context.userIdToSuspend)
|
return API.suspendUser(context.userIdToSuspend)
|
||||||
},
|
},
|
||||||
|
resetUserPassword: (context) => {
|
||||||
|
if (!context.userIdToResetPassword) {
|
||||||
|
throw new Error("userIdToResetPassword is undefined")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.newUserPassword) {
|
||||||
|
throw new Error("newUserPassword not generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
return API.updateUserPassword(context.newUserPassword, context.userIdToResetPassword)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
isFormError: (_, event) => isApiError(event.data),
|
isFormError: (_, event) => isApiError(event.data),
|
||||||
@ -159,6 +212,9 @@ export const usersMachine = createMachine(
|
|||||||
assignUserIdToSuspend: assign({
|
assignUserIdToSuspend: assign({
|
||||||
userIdToSuspend: (_, event) => event.userId,
|
userIdToSuspend: (_, event) => event.userId,
|
||||||
}),
|
}),
|
||||||
|
assignUserIdToResetPassword: assign({
|
||||||
|
userIdToResetPassword: (_, event) => event.userId,
|
||||||
|
}),
|
||||||
clearGetUsersError: assign((context: UsersContext) => ({
|
clearGetUsersError: assign((context: UsersContext) => ({
|
||||||
...context,
|
...context,
|
||||||
getUsersError: undefined,
|
getUsersError: undefined,
|
||||||
@ -173,6 +229,9 @@ export const usersMachine = createMachine(
|
|||||||
assignSuspendUserError: assign({
|
assignSuspendUserError: assign({
|
||||||
suspendUserError: (_, event) => event.data,
|
suspendUserError: (_, event) => event.data,
|
||||||
}),
|
}),
|
||||||
|
assignResetUserPasswordError: assign({
|
||||||
|
resetUserPasswordError: (_, event) => event.data,
|
||||||
|
}),
|
||||||
clearCreateUserError: assign((context: UsersContext) => ({
|
clearCreateUserError: assign((context: UsersContext) => ({
|
||||||
...context,
|
...context,
|
||||||
createUserError: undefined,
|
createUserError: undefined,
|
||||||
@ -180,6 +239,9 @@ export const usersMachine = createMachine(
|
|||||||
clearSuspendUserError: assign({
|
clearSuspendUserError: assign({
|
||||||
suspendUserError: (_) => undefined,
|
suspendUserError: (_) => undefined,
|
||||||
}),
|
}),
|
||||||
|
clearResetUserPasswordError: assign({
|
||||||
|
resetUserPasswordError: (_) => undefined,
|
||||||
|
}),
|
||||||
displayCreateUserSuccess: () => {
|
displayCreateUserSuccess: () => {
|
||||||
displaySuccess(Language.createUserSuccess)
|
displaySuccess(Language.createUserSuccess)
|
||||||
},
|
},
|
||||||
@ -189,6 +251,15 @@ export const usersMachine = createMachine(
|
|||||||
displaySuspendedErrorMessage: () => {
|
displaySuspendedErrorMessage: () => {
|
||||||
displayError(Language.suspendUserError)
|
displayError(Language.suspendUserError)
|
||||||
},
|
},
|
||||||
|
displayResetPasswordSuccess: () => {
|
||||||
|
displaySuccess(Language.resetUserPasswordSuccess)
|
||||||
|
},
|
||||||
|
displayResetPasswordErrorMessage: () => {
|
||||||
|
displayError(Language.resetUserPasswordError)
|
||||||
|
},
|
||||||
|
generateRandomPassword: assign({
|
||||||
|
newUserPassword: (_) => generateRandomString(12),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user