Merge pull request #464 from homarr-labs/ajnart/fix-duplicate-users

This commit is contained in:
Thomas Camlong
2024-05-19 23:44:46 +02:00
committed by GitHub
6 changed files with 70 additions and 12 deletions

View File

@@ -44,10 +44,15 @@ export const RegistrationForm = ({ invite }: RegistrationFormProps) => {
});
router.push("/auth/login");
},
onError() {
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("error.usernameTaken")
: t("action.register.notification.error.message");
showErrorNotification({
title: t("action.register.notification.error.title"),
message: t("action.register.notification.error.message"),
message,
});
},
},

View File

@@ -22,16 +22,25 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
async onSettled() {
await revalidatePathActionAsync("/manage/users");
},
onSuccess() {
onSuccess(_, variables) {
// Reset form initial values to reset dirty state
form.setInitialValues({
name: variables.name,
email: variables.email ?? "",
});
showSuccessNotification({
title: t("common.notification.update.success"),
message: t("user.action.editProfile.notification.success.message"),
});
},
onError() {
onError(error) {
const message =
error.data?.code === "CONFLICT"
? t("user.error.usernameTaken")
: t("user.action.editProfile.notification.error.message");
showErrorNotification({
title: t("common.notification.update.error"),
message: t("user.action.editProfile.notification.error.message"),
message,
});
},
});
@@ -59,7 +68,7 @@ export const UserProfileForm = ({ user }: UserProfileFormProps) => {
<TextInput label={t("user.field.email.label")} {...form.getInputProps("email")} />
<Group justify="end">
<Button type="submit" color="teal" loading={isPending}>
<Button type="submit" color="teal" disabled={!form.isDirty()} loading={isPending}>
{t("common.action.saveChanges")}
</Button>
</Group>

View File

@@ -6,6 +6,7 @@ import { IconUserCheck } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useZodForm } from "@homarr/form";
import { showErrorNotification } from "@homarr/notifications";
import { useScopedI18n } from "@homarr/translation/client";
import { validation, z } from "@homarr/validation";
import { createCustomErrorParams } from "@homarr/validation/form";
@@ -26,7 +27,16 @@ export const UserCreateStepperComponent = () => {
const hasNext = active < stepperMax;
const hasPrevious = active > 0;
const { mutateAsync, isPending } = clientApi.user.create.useMutation();
const { mutateAsync, isPending } = clientApi.user.create.useMutation({
onError(error) {
showErrorNotification({
autoClose: false,
id: "create-user-error",
title: t("step.error.title"),
message: error.message,
});
},
});
const generalForm = useZodForm(
z.object({

View File

@@ -44,11 +44,23 @@ export const userRouter = createTRPCRouter({
});
}
if (!dbInvite || dbInvite.expirationDate < new Date()) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid invite",
});
}
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
await createUserAsync(ctx.db, input);
// Delete invite as it's used
await ctx.db.delete(invites).where(inviteWhere);
}),
create: publicProcedure.input(validation.user.create).mutation(async ({ ctx, input }) => {
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
await createUserAsync(ctx.db, input);
}),
setProfileImage: protectedProcedure
@@ -148,6 +160,8 @@ export const userRouter = createTRPCRouter({
});
}
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.name, input.id);
const emailDirty = input.email && user.email !== input.email;
await ctx.db
.update(users)
@@ -227,12 +241,28 @@ const createUserAsync = async (db: Database, input: z.infer<typeof validation.us
const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt);
const username = input.username.toLowerCase();
const userId = createId();
await db.insert(schema.users).values({
id: userId,
name: input.username,
name: username,
email: input.email,
password: hashedPassword,
salt,
});
};
const checkUsernameAlreadyTakenAndThrowAsync = async (db: Database, username: string, ignoreId?: string) => {
const user = await db.query.users.findFirst({
where: eq(users.name, username.toLowerCase()),
});
if (!user) return;
if (ignoreId && user.id === ignoreId) return;
throw new TRPCError({
code: "CONFLICT",
message: "Username already taken",
});
};

View File

@@ -2,16 +2,14 @@ import type { NotificationData } from "@mantine/notifications";
import { notifications } from "@mantine/notifications";
import { IconCheck, IconX } from "@tabler/icons-react";
type CommonNotificationProps = Pick<NotificationData, "title" | "message">;
export const showSuccessNotification = (props: CommonNotificationProps) =>
export const showSuccessNotification = (props: NotificationData) =>
notifications.show({
...props,
color: "teal",
icon: <IconCheck size={20} />,
});
export const showErrorNotification = (props: CommonNotificationProps) =>
export const showErrorNotification = (props: NotificationData) =>
notifications.show({
...props,
color: "red",

View File

@@ -36,6 +36,9 @@ export default {
label: "Previous password",
},
},
error: {
usernameTaken: "Username already taken",
},
action: {
login: {
label: "Login",
@@ -1219,6 +1222,9 @@ export default {
completed: {
title: "User created",
},
error: {
title: "User creation failed",
},
},
action: {
createAnother: "Create another user",