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

View File

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

View File

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

View File

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