feat: use password input (#163)

* feat: use password input

* chore: address pull request feedback

* fix: typo in function name

* fix: deepsource issues

---------

Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
Manuel
2024-03-02 17:46:03 +01:00
committed by GitHub
parent 990be660c5
commit 2a83df3485
5 changed files with 69 additions and 40 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { clientApi } from "@homarr/api/client";
import { useForm, zodResolver } from "@homarr/form";
@@ -25,10 +25,15 @@ export const UserCreateStepperComponent = () => {
const stepperMax = 4;
const [active, setActive] = useState(0);
const nextStep = () =>
setActive((current) => (current < stepperMax ? current + 1 : current));
const prevStep = () =>
setActive((current) => (current > 0 ? current - 1 : current));
const nextStep = useCallback(
() =>
setActive((current) => (current < stepperMax ? current + 1 : current)),
[setActive],
);
const prevStep = useCallback(
() => setActive((current) => (current > 0 ? current - 1 : current)),
[setActive],
);
const hasNext = active < stepperMax;
const hasPrevious = active > 0;
@@ -52,39 +57,51 @@ export const UserCreateStepperComponent = () => {
const securityForm = useForm({
initialValues: {
password: "",
confirmPassword: "",
},
validate: zodResolver(
z.object({
password: validation.user.password,
}),
z
.object({
password: validation.user.password,
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
}),
),
validateInputOnBlur: true,
validateInputOnChange: true,
});
const allForms = [generalForm, securityForm];
const allForms = useMemo(
() => [generalForm, securityForm],
[generalForm, securityForm],
);
const isCurrentFormValid = allForms[active]
? (allForms[active]!.isValid satisfies () => boolean)
: () => true;
const canNavigateToNextStep = isCurrentFormValid();
const controlledGoToNextStep = async () => {
const controlledGoToNextStep = useCallback(async () => {
if (active + 1 === stepperMax) {
await mutateAsync({
name: generalForm.values.username,
username: generalForm.values.username,
email: generalForm.values.email,
password: securityForm.values.password,
confirmPassword: securityForm.values.confirmPassword,
});
}
nextStep();
};
}, [active, generalForm, mutateAsync, securityForm, nextStep]);
const reset = () => {
const reset = useCallback(() => {
setActive(0);
allForms.forEach((form) => {
form.reset();
});
};
}, [allForms]);
return (
<>
@@ -134,6 +151,12 @@ export const UserCreateStepperComponent = () => {
withAsterisk
{...securityForm.getInputProps("password")}
/>
<PasswordInput
label={t("step.security.field.confirmPassword.label")}
variant="filled"
withAsterisk
{...securityForm.getInputProps("confirmPassword")}
/>
</Stack>
</Card>
</form>

View File

@@ -1,6 +1,6 @@
import { getScopedI18n } from "@homarr/translation/server";
import { UserCreateStepperComponent } from "./_components/stepper.component";
import { UserCreateStepperComponent } from "./_components/create-user-stepper";
export async function generateMetadata() {
const t = await getScopedI18n("management.page.user.create");

View File

@@ -3,6 +3,7 @@ import "server-only";
import { TRPCError } from "@trpc/server";
import { createSalt, hashPassword } from "@homarr/auth";
import type { Database } from "@homarr/db";
import { createId, db, eq, schema } from "@homarr/db";
import { users } from "@homarr/db/schema/sqlite";
import { validation, z } from "@homarr/validation";
@@ -26,16 +27,12 @@ export const userRouter = createTRPCRouter({
});
}
const salt = await createSalt();
const hashedPassword = await hashPassword(input.password, salt);
const userId = createId();
await ctx.db.insert(schema.users).values({
id: userId,
name: input.username,
password: hashedPassword,
salt,
});
await createUser(ctx.db, input);
}),
create: publicProcedure
.input(validation.user.create)
.mutation(async ({ ctx, input }) => {
await createUser(ctx.db, input);
}),
getAll: publicProcedure.query(async () => {
return db.query.users.findMany({
@@ -62,18 +59,20 @@ export const userRouter = createTRPCRouter({
where: eq(users.id, input.userId),
});
}),
create: publicProcedure
.input(
z.object({
name: z.string(),
email: z.string().email().or(z.string().length(0).optional()),
}),
)
.mutation(async ({ input }) => {
await db.insert(users).values({
id: createId(),
name: input.name,
email: input.email,
});
}),
});
const createUser = async (
db: Database,
input: z.infer<typeof validation.user.create>,
) => {
const salt = await createSalt();
const hashedPassword = await hashPassword(input.password, salt);
const userId = createId();
await db.insert(schema.users).values({
id: userId,
name: input.username,
password: hashedPassword,
salt,
});
};

View File

@@ -429,6 +429,9 @@ export default {
password: {
label: "Password",
},
confirmPassword: {
label: "Confirm password",
},
},
},
permissions: {

View File

@@ -3,17 +3,20 @@ import { z } from "zod";
const usernameSchema = z.string().min(3).max(255);
const passwordSchema = z.string().min(8).max(255);
const initUserSchema = z
const createUserSchema = z
.object({
username: usernameSchema,
password: passwordSchema,
confirmPassword: z.string(),
email: z.string().email().optional(),
})
.refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"],
message: "Passwords do not match",
});
const initUserSchema = createUserSchema;
const signInSchema = z.object({
name: z.string(),
password: z.string(),
@@ -22,5 +25,6 @@ const signInSchema = z.object({
export const userSchemas = {
signIn: signInSchema,
init: initUserSchema,
create: createUserSchema,
password: passwordSchema,
};