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",