diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index d7615c3c5..a0591a97a 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -254,7 +254,7 @@ async function checkAuthData( }); if (existingChannelUsedForAuth) { - throw new KnownErrors.UserWithEmailAlreadyExists(data.primaryEmail); + throw new KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse("email", data.primaryEmail); } } } diff --git a/apps/dashboard/src/components/user-dialog.tsx b/apps/dashboard/src/components/user-dialog.tsx index 01bc5f3a4..8a206bde8 100644 --- a/apps/dashboard/src/components/user-dialog.tsx +++ b/apps/dashboard/src/components/user-dialog.tsx @@ -2,7 +2,7 @@ import { useAdminApp } from "@/app/(main)/(protected)/projects/[projectId]/use-a import { ServerUser } from "@hexclave/next"; import { KnownErrors } from "@hexclave/shared"; import { countryCodeSchema, emailSchema, jsonStringOrEmptySchema, passwordSchema } from "@hexclave/shared/dist/schema-fields"; -import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Typography, useToast } from "@/components/ui"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Button, Typography } from "@/components/ui"; import * as yup from "yup"; import { FormDialog } from "./form-dialog"; import { CountryCodeField } from "./country-code-select"; @@ -22,7 +22,6 @@ export function UserDialog(props: { type: 'edit', user: ServerUser, })) { - const { toast } = useToast(); const adminApp = useAdminApp(); const project = adminApp.useProject(); @@ -128,13 +127,14 @@ export function UserDialog(props: { } } catch (error) { if (KnownErrors.UserWithEmailAlreadyExists.isInstance(error)) { - toast({ - title: "Email already exists", - description: "Please choose a different email address", - variant: "destructive", - }); + alert("Email already exists. Please choose a different email address."); return 'prevent-close'; } + if (KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse.isInstance(error)) { + alert("Email already used for authentication. This email is already used for sign-in by another account. Please choose a different email address."); + return 'prevent-close'; + } + throw error; } } diff --git a/apps/e2e/tests/backend/endpoints/api/v1/users-primary-email.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/users-primary-email.test.ts index fceb78f7d..0b21b9dc3 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/users-primary-email.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/users-primary-email.test.ts @@ -433,6 +433,57 @@ describe("updating primary_email via users/me endpoint", () => { expect(updateResponse.body.primary_email).toBe(newMailbox.emailAddress); expect(updateResponse.body.primary_email_auth_enabled).toBe(true); }); + + it("should return a clear error when changing primary_email to an email already used for auth by another user", async ({ expect }) => { + // Create first user with email used for auth (via OTP sign-in) + await Auth.Otp.signIn(); + const firstUserEmail = (await niceBackendFetch("/api/v1/users/me", { + accessType: "client", + })).body.primary_email; + + // Create second user via server + const secondMailbox = createMailbox(); + const createResponse = await niceBackendFetch("/api/v1/users", { + accessType: "server", + method: "POST", + body: { + primary_email: secondMailbox.emailAddress, + primary_email_auth_enabled: true, + }, + }); + expect(createResponse.status).toBe(201); + const secondUserId = createResponse.body.id; + + // Try to change second user's primary_email to first user's email (with auth enabled) + const updateResponse = await niceBackendFetch(`/api/v1/users/${secondUserId}`, { + accessType: "server", + method: "PATCH", + body: { + primary_email: firstUserEmail, + primary_email_auth_enabled: true, + }, + }); + + // Should get a clear error about the email being used for auth by another account + expect(updateResponse).toMatchInlineSnapshot(` + NiceResponse { + "status": 409, + "body": { + "code": "CONTACT_CHANNEL_ALREADY_USED_FOR_AUTH_BY_SOMEONE_ELSE", + "details": { + "contact_channel_value": "default-mailbox--@stack-generated.example.com", + "type": "email", + "would_work_if_email_was_verified": false, + }, + "error": "This email \\"(default-mailbox--@stack-generated.example.com)\\" is already used for authentication by another account.", + }, + "headers": Headers { + "x-stack-known-error": "CONTACT_CHANNEL_ALREADY_USED_FOR_AUTH_BY_SOMEONE_ELSE", +