feat: add change password form (#199)
This commit is contained in:
@@ -1,15 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
import { Button, Divider, Group, Stack, Text } from "@homarr/ui";
|
import { Button, Divider, Group, Stack, Text } from "@homarr/ui";
|
||||||
|
|
||||||
import { revalidatePathAction } from "~/app/revalidatePathAction";
|
|
||||||
|
|
||||||
interface DangerZoneAccordionProps {
|
interface DangerZoneAccordionProps {
|
||||||
user: NonNullable<RouterOutputs["user"]["getById"]>;
|
user: NonNullable<RouterOutputs["user"]["getById"]>;
|
||||||
}
|
}
|
||||||
@@ -19,9 +17,8 @@ export const DangerZoneAccordion = ({ user }: DangerZoneAccordionProps) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync: mutateUserDeletionAsync } =
|
const { mutateAsync: mutateUserDeletionAsync } =
|
||||||
clientApi.user.delete.useMutation({
|
clientApi.user.delete.useMutation({
|
||||||
onSettled: async () => {
|
onSettled: () => {
|
||||||
await router.push("/manage/users");
|
router.push("/manage/users");
|
||||||
await revalidatePathAction("/manage/users");
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { api } from "~/trpc/server";
|
import { api } from "~/trpc/server";
|
||||||
import { DangerZoneAccordion } from "./_components/dangerZone.accordion";
|
import { DangerZoneAccordion } from "./_components/dangerZone.accordion";
|
||||||
import { ProfileAccordion } from "./_components/profile.accordion";
|
import { ProfileAccordion } from "./_components/profile.accordion";
|
||||||
|
import { SecurityAccordionComponent } from "./_components/security.accordion";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: {
|
params: {
|
||||||
@@ -80,12 +81,14 @@ export default async function EditUserPage({ params }: Props) {
|
|||||||
{t("section.security.title")}
|
{t("section.security.title")}
|
||||||
</Text>
|
</Text>
|
||||||
</AccordionControl>
|
</AccordionControl>
|
||||||
<AccordionPanel></AccordionPanel>
|
<AccordionPanel>
|
||||||
|
<SecurityAccordionComponent user={user} />
|
||||||
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem
|
<AccordionItem
|
||||||
styles={{
|
styles={{
|
||||||
item: {
|
item: {
|
||||||
"--__item-border-color": "rgba(248,81,73,0.4)",
|
borderColor: "rgba(248,81,73,0.4)",
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ export const userRouter = createTRPCRouter({
|
|||||||
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
|
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
|
||||||
await ctx.db.delete(users).where(eq(users.id, input));
|
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 (
|
const createUser = async (
|
||||||
|
|||||||
@@ -585,6 +585,17 @@ export default {
|
|||||||
},
|
},
|
||||||
security: {
|
security: {
|
||||||
title: "Security",
|
title: "Security",
|
||||||
|
changePassword: {
|
||||||
|
title: "Change password",
|
||||||
|
form: {
|
||||||
|
password: {
|
||||||
|
label: "Password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
passwordUpdated: "Updated password",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dangerZone: {
|
dangerZone: {
|
||||||
title: "Danger zone",
|
title: "Danger zone",
|
||||||
|
|||||||
@@ -33,10 +33,16 @@ const editProfileSchema = z.object({
|
|||||||
.nullable(),
|
.nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changePasswordSchema = z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
password: passwordSchema,
|
||||||
|
});
|
||||||
|
|
||||||
export const userSchemas = {
|
export const userSchemas = {
|
||||||
signIn: signInSchema,
|
signIn: signInSchema,
|
||||||
init: initUserSchema,
|
init: initUserSchema,
|
||||||
create: createUserSchema,
|
create: createUserSchema,
|
||||||
password: passwordSchema,
|
password: passwordSchema,
|
||||||
editProfile: editProfileSchema,
|
editProfile: editProfileSchema,
|
||||||
|
changePassword: changePasswordSchema,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user