mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-27 00:40:58 +01:00
Merge pull request #464 from homarr-labs/ajnart/fix-duplicate-users
This commit is contained in:
@@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user