From 8ad851d4b083ba4dac2b741274d346d5e0cf2583 Mon Sep 17 00:00:00 2001 From: Vladyslav Matsiiako Date: Tue, 18 Jul 2023 18:36:34 -0700 Subject: [PATCH] added the ability to change user name --- backend/src/controllers/v2/usersController.ts | 26 +++++- backend/src/routes/v2/users.ts | 10 +++ frontend/src/hooks/api/users/queries.tsx | 13 +++ frontend/src/hooks/api/users/types.ts | 4 + .../ChangeLanguageSection.tsx | 2 +- .../PersonalSecurityTab.tsx | 2 + .../PersonalTabGroup/PersonalTabGroup.tsx | 2 +- .../UserNameSection/UserNameSection.tsx | 82 +++++++++++++++++++ .../UserNameSection/index.tsx | 1 + .../ProjectSettingsPage.tsx | 6 +- .../ProjectNameChangeSection.tsx | 4 +- 11 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/UserNameSection.tsx create mode 100644 frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/index.tsx diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 19d88d56d..b5849f2e7 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -3,10 +3,10 @@ import { Types } from "mongoose"; import crypto from "crypto"; import bcrypt from "bcrypt"; import { - MembershipOrg, - User, APIKeyData, - TokenVersion + MembershipOrg, + TokenVersion, + User } from "../../models"; import { getSaltRounds } from "../../config"; @@ -80,6 +80,26 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => { }); } +/** + * Update the current user's name [firstName, lastName]. + * @param req + * @param res + * @returns + */ +export const updateName = async (req: Request, res: Response) => { + const { firstName, lastName }: { firstName: string; lastName: string; } = req.body; + req.user.firstName = firstName; + req.user.lastName = lastName || ""; + + await req.user.save(); + + const user = req.user; + + return res.status(200).send({ + user, + }); +} + /** * Return organizations that the current user is part of. * @param req diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index f5a1401dc..7815513ec 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -29,6 +29,16 @@ router.patch( usersController.updateMyMfaEnabled ); +router.patch( + "/me/name", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + }), + body("firstName").exists(), + validateRequest, + usersController.updateName +); + router.get( "/me/organizations", requireAuth({ diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index c1bcf43a2..fe9efd73f 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -16,6 +16,7 @@ import { CreateAPIKeyRes, DeletOrgMembershipDTO, OrgUser, + RenameUserDTO, TokenVersion, UpdateOrgUserRoleDTO, User} from "./types"; @@ -45,6 +46,18 @@ const fetchUserAction = async (action: string) => { return data.userAction; }; +export const useRenameUser = () => { + const queryClient = useQueryClient(); + + return useMutation<{}, {}, RenameUserDTO>({ + mutationFn: ({ newName }) => + apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }), + onSuccess: () => { + queryClient.invalidateQueries(userKeys.getUser); + } + }); +}; + export const useGetUserAction = (action: string) => useQuery({ queryKey: userKeys.userAction, diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index e272e7bfd..31a33c756 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -67,6 +67,10 @@ export type CreateAPIKeyRes = { apiKeyData: APIKeyData; } +export type RenameUserDTO = { + newName: string; +}; + export type APIKeyData = { _id: string; name: string; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangeLanguageSection/ChangeLanguageSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangeLanguageSection/ChangeLanguageSection.tsx index 7d2dfb604..c64050c41 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangeLanguageSection/ChangeLanguageSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangeLanguageSection/ChangeLanguageSection.tsx @@ -15,7 +15,7 @@ export const ChangeLanguageSection = () => { }; return ( -
+

{t("settings.personal.change-language")}

diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx index 4c0b21454..abfec566a 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalSecurityTab/PersonalSecurityTab.tsx @@ -3,10 +3,12 @@ import { ChangePasswordSection } from "../ChangePasswordSection"; import { EmergencyKitSection } from "../EmergencyKitSection"; import { SecuritySection } from "../SecuritySection"; import { SessionsSection } from "../SessionsSection"; +import { UserNameSection } from "../UserNameSection"; export const PersonalSecurityTab = () => { return (
+ diff --git a/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx b/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx index 178633143..6f3edef97 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/PersonalTabGroup/PersonalTabGroup.tsx @@ -12,7 +12,7 @@ const tabs = [ export const PersonalTabGroup = () => { return ( - + {tabs.map((tab) => ( {({ selected }) => ( diff --git a/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/UserNameSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/UserNameSection.tsx new file mode 100644 index 000000000..bf03877a8 --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/UserNameSection.tsx @@ -0,0 +1,82 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useUser } from "@app/context"; +import { useRenameUser } from "@app/hooks/api/users/queries"; + +const formSchema = yup.object({ + name: yup.string().required().label("User Name"), +}); + +type FormData = yup.InferType; + +export const UserNameSection = (): JSX.Element => { + const { user } = useUser(); + const { createNotification } = useNotificationContext(); + const { + handleSubmit, + control, + reset + } = useForm({ resolver: yupResolver(formSchema) }); + const { mutateAsync, isLoading } = useRenameUser(); + + useEffect(() => { + if (user) { + reset({ name: `${user?.firstName}${user?.lastName && " "}${user?.lastName}` }); + } + }, [user]); + + const onFormSubmit = async ({ name }: FormData) => { + try { + if (!user?._id) return; + if (name === "") return; + + await mutateAsync({ newName: name}); + createNotification({ + text: "Successfully renamed user", + type: "success" + }); + } catch (error) { + console.error(error); + createNotification({ + text: "Failed to rename user", + type: "error" + }); + } + }; + + return ( +
+

+ User name +

+
+ ( + + + + )} + control={control} + name="name" + /> +
+ +
+ ); +}; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/index.tsx b/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/index.tsx new file mode 100644 index 000000000..bbd771d3e --- /dev/null +++ b/frontend/src/views/Settings/PersonalSettingsPage/UserNameSection/index.tsx @@ -0,0 +1 @@ +export { UserNameSection } from "./UserNameSection"; \ No newline at end of file diff --git a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx index 75f67aa6a..aa5383bd9 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/ProjectSettingsPage.tsx @@ -26,15 +26,13 @@ export const ProjectSettingsPage = () => {

{t("settings.project.title")}

- + {tabs.map((tab) => ( {({ selected }) => ( diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx index a4a606476..66ad4c5b0 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/ProjectNameChangeSection/ProjectNameChangeSection.tsx @@ -1,6 +1,5 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; @@ -27,7 +26,6 @@ export const ProjectNameChangeSection = () => { control, reset } = useForm({ resolver: yupResolver(formSchema) }); - const { t } = useTranslation(); useEffect(() => { if (currentWorkspace) { @@ -67,7 +65,7 @@ export const ProjectNameChangeSection = () => { className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600" >

- {t("common.display-name")} + Project Name