From beb7defd32561ca074b48b4aa72631fdb59ddbdc Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Wed, 6 Mar 2024 21:20:41 +0100 Subject: [PATCH] feat: add change password form (#199) --- .../_components/dangerZone.accordion.tsx | 9 +-- .../_components/security.accordion.tsx | 80 +++++++++++++++++++ .../[locale]/manage/users/[userId]/page.tsx | 7 +- packages/api/src/router/user.ts | 12 +++ packages/translation/src/lang/en.ts | 11 +++ packages/validation/src/user.ts | 6 ++ 6 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/security.accordion.tsx diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/dangerZone.accordion.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/dangerZone.accordion.tsx index af9de4f1d..0484390ec 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/dangerZone.accordion.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/dangerZone.accordion.tsx @@ -1,15 +1,13 @@ "use client"; import React from "react"; -import { useRouter } from "next/router"; +import { useRouter } from "next/navigation"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { useScopedI18n } from "@homarr/translation/client"; import { Button, Divider, Group, Stack, Text } from "@homarr/ui"; -import { revalidatePathAction } from "~/app/revalidatePathAction"; - interface DangerZoneAccordionProps { user: NonNullable; } @@ -19,9 +17,8 @@ export const DangerZoneAccordion = ({ user }: DangerZoneAccordionProps) => { const router = useRouter(); const { mutateAsync: mutateUserDeletionAsync } = clientApi.user.delete.useMutation({ - onSettled: async () => { - await router.push("/manage/users"); - await revalidatePathAction("/manage/users"); + onSettled: () => { + router.push("/manage/users"); }, }); diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/security.accordion.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/security.accordion.tsx new file mode 100644 index 000000000..b95289e35 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/_components/security.accordion.tsx @@ -0,0 +1,80 @@ +"use client"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { useForm, zodResolver } from "@homarr/form"; +import { showSuccessNotification } from "@homarr/notifications"; +import { useI18n } from "@homarr/translation/client"; +import { Button, PasswordInput, Stack, Title } from "@homarr/ui"; +import { validation } from "@homarr/validation"; + +import { revalidatePathAction } from "~/app/revalidatePathAction"; + +interface SecurityAccordionComponentProps { + user: NonNullable; +} + +export const SecurityAccordionComponent = ({ + user, +}: SecurityAccordionComponentProps) => { + return ( + + + + ); +}; + +const ChangePasswordForm = ({ + user, +}: { + user: NonNullable; +}) => { + const t = useI18n(); + const { mutate, isPending } = clientApi.user.changePassword.useMutation({ + onSettled: async () => { + await revalidatePathAction(`/manage/users/${user.id}`); + showSuccessNotification({ + title: t( + "management.page.user.edit.section.security.changePassword.message.passwordUpdated", + ), + message: "", + }); + }, + }); + const form = useForm({ + initialValues: { + userId: user.id, + password: "", + }, + validate: zodResolver(validation.user.changePassword), + validateInputOnBlur: true, + validateInputOnChange: true, + }); + + const handleSubmit = () => { + mutate(form.values); + }; + + return ( +
+ + + + {t( + "management.page.user.edit.section.security.changePassword.title", + )} + + + + + +
+ ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx index 5798a31cd..c318a6ad1 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx @@ -20,6 +20,7 @@ import { import { api } from "~/trpc/server"; import { DangerZoneAccordion } from "./_components/dangerZone.accordion"; import { ProfileAccordion } from "./_components/profile.accordion"; +import { SecurityAccordionComponent } from "./_components/security.accordion"; interface Props { params: { @@ -80,12 +81,14 @@ export default async function EditUserPage({ params }: Props) { {t("section.security.title")} - + + + { await ctx.db.delete(users).where(eq(users.id, input)); }), + changePassword: publicProcedure + .input(validation.user.changePassword) + .mutation(async ({ ctx, input }) => { + const salt = await createSalt(); + const hashedPassword = await hashPassword(input.password, salt); + await ctx.db + .update(users) + .set({ + password: hashedPassword, + }) + .where(eq(users.id, input.userId)); + }), }); const createUser = async ( diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index d9d10c256..d7fc4ac81 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -585,6 +585,17 @@ export default { }, security: { title: "Security", + changePassword: { + title: "Change password", + form: { + password: { + label: "Password", + }, + }, + message: { + passwordUpdated: "Updated password", + }, + }, }, dangerZone: { title: "Danger zone", diff --git a/packages/validation/src/user.ts b/packages/validation/src/user.ts index 807308d01..008905a87 100644 --- a/packages/validation/src/user.ts +++ b/packages/validation/src/user.ts @@ -33,10 +33,16 @@ const editProfileSchema = z.object({ .nullable(), }); +const changePasswordSchema = z.object({ + userId: z.string(), + password: passwordSchema, +}); + export const userSchemas = { signIn: signInSchema, init: initUserSchema, create: createUserSchema, password: passwordSchema, editProfile: editProfileSchema, + changePassword: changePasswordSchema, };