feat: add change password form (#199)

This commit is contained in:
Manuel
2024-03-06 21:20:41 +01:00
committed by GitHub
parent c0401702f0
commit beb7defd32
6 changed files with 117 additions and 8 deletions

View File

@@ -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<RouterOutputs["user"]["getById"]>;
}
@@ -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");
},
});

View File

@@ -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<RouterOutputs["user"]["getById"]>;
}
export const SecurityAccordionComponent = ({
user,
}: SecurityAccordionComponentProps) => {
return (
<Stack>
<ChangePasswordForm user={user} />
</Stack>
);
};
const ChangePasswordForm = ({
user,
}: {
user: NonNullable<RouterOutputs["user"]["getById"]>;
}) => {
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 (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Stack gap={0}>
<Title order={5}>
{t(
"management.page.user.edit.section.security.changePassword.title",
)}
</Title>
<PasswordInput
label={t(
"management.page.user.edit.section.security.changePassword.form.password.label",
)}
{...form.getInputProps("password")}
/>
</Stack>
<Button loading={isPending} type="submit" disabled={!form.isValid()}>
{t("common.action.confirm")}
</Button>
</Stack>
</form>
);
};

View File

@@ -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")}
</Text>
</AccordionControl>
<AccordionPanel></AccordionPanel>
<AccordionPanel>
<SecurityAccordionComponent user={user} />
</AccordionPanel>
</AccordionItem>
<AccordionItem
styles={{
item: {
"--__item-border-color": "rgba(248,81,73,0.4)",
borderColor: "rgba(248,81,73,0.4)",
borderWidth: 4,
},
}}

View File

@@ -87,6 +87,18 @@ export const userRouter = createTRPCRouter({
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
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 (

View File

@@ -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",

View File

@@ -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,
};