diff --git a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx
index e91e78de0..b1986f12c 100644
--- a/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx
+++ b/apps/nextjs/src/app/[locale]/auth/invite/[id]/_registration-form.tsx
@@ -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,
});
},
},
diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx
index 54f45526b..44ba9ddb7 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx
@@ -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) => {
-
diff --git a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx
index 5a27d206c..eadf51ddd 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/create/_components/create-user-stepper.tsx
@@ -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({
diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts
index edffc2ae5..7d3fbd100 100644
--- a/packages/api/src/router/user.ts
+++ b/packages/api/src/router/user.ts
@@ -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 {
+ 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",
+ });
+};
diff --git a/packages/notifications/src/index.tsx b/packages/notifications/src/index.tsx
index f63139abe..071496b20 100644
--- a/packages/notifications/src/index.tsx
+++ b/packages/notifications/src/index.tsx
@@ -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;
-
-export const showSuccessNotification = (props: CommonNotificationProps) =>
+export const showSuccessNotification = (props: NotificationData) =>
notifications.show({
...props,
color: "teal",
icon: ,
});
-export const showErrorNotification = (props: CommonNotificationProps) =>
+export const showErrorNotification = (props: NotificationData) =>
notifications.show({
...props,
color: "red",
diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts
index 757502ffb..3ba679d51 100644
--- a/packages/translation/src/lang/en.ts
+++ b/packages/translation/src/lang/en.ts
@@ -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",