mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 08:50:56 +01:00
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";
|
||||
|
||||
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>
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -429,6 +429,9 @@ export default {
|
||||
password: {
|
||||
label: "Password",
|
||||
},
|
||||
confirmPassword: {
|
||||
label: "Confirm password",
|
||||
},
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user