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:
@@ -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>
|
||||||
@@ -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");
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -429,6 +429,9 @@ export default {
|
|||||||
password: {
|
password: {
|
||||||
label: "Password",
|
label: "Password",
|
||||||
},
|
},
|
||||||
|
confirmPassword: {
|
||||||
|
label: "Confirm password",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user