diff --git a/apps/backend/src/app/api/v1/contact-channels/crud.tsx b/apps/backend/src/app/api/v1/contact-channels/crud.tsx index b8bc6c086..c131aa556 100644 --- a/apps/backend/src/app/api/v1/contact-channels/crud.tsx +++ b/apps/backend/src/app/api/v1/contact-channels/crud.tsx @@ -129,12 +129,29 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl } const updatedContactChannel = await prismaClient.$transaction(async (tx) => { - await ensureContactChannelExists(tx, { + const existingContactChannel = await ensureContactChannelExists(tx, { projectId: auth.project.id, userId: params.user_id, contactChannelId: params.contact_channel_id || throwErr("Missing contact channel id"), }); + // if usedForAuth is set to true, make sure no other account uses this channel for auth + if (data.used_for_auth) { + const existingWithSameChannel = await tx.contactChannel.findUnique({ + where: { + projectId_type_value_usedForAuth: { + projectId: auth.project.id, + type: data.type !== undefined ? crudContactChannelTypeToPrisma(data.type) : existingContactChannel.type, + value: data.value !== undefined ? data.value : existingContactChannel.value, + usedForAuth: 'TRUE', + }, + }, + }); + if (existingWithSameChannel && existingWithSameChannel.id !== existingContactChannel.id) { + throw new KnownErrors.ContactChannelAlreadyUsedForAuthBySomeoneElse(data.type ?? prismaContactChannelTypeToCrud(existingContactChannel.type)); + } + } + if (data.is_primary) { // mark all other channels as not primary await tx.contactChannel.updateMany({ @@ -215,3 +232,12 @@ export const contactChannelsCrudHandlers = createLazyProxy(() => createCrudHandl }; } })); + + +function crudContactChannelTypeToPrisma(type: "email") { + return typedToUppercase(type); +} + +function prismaContactChannelTypeToCrud(type: "EMAIL") { + return typedToLowercase(type); +} diff --git a/apps/backend/src/lib/request-checks.tsx b/apps/backend/src/lib/request-checks.tsx index d7cf8be1d..a6a58cbde 100644 --- a/apps/backend/src/lib/request-checks.tsx +++ b/apps/backend/src/lib/request-checks.tsx @@ -1,11 +1,11 @@ import { ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; +import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { ProviderType, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/utils/oauth"; +import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; import { listUserTeamPermissions } from "./permissions"; import { PrismaTransaction } from "./types"; -import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; -import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; async function _getTeamMembership( @@ -198,4 +198,6 @@ export async function ensureContactChannelExists( if (!contactChannel) { throw new StatusError(StatusError.BadRequest, 'Contact channel not found'); } + + return contactChannel; } diff --git a/packages/stack-shared/src/known-errors.tsx b/packages/stack-shared/src/known-errors.tsx index f2da01733..c900d0ad6 100644 --- a/packages/stack-shared/src/known-errors.tsx +++ b/packages/stack-shared/src/known-errors.tsx @@ -1,7 +1,6 @@ import { StackAssertionError, StatusError, throwErr } from "./utils/errors"; import { identityArgs } from "./utils/functions"; import { Json } from "./utils/json"; -import { filterUndefined } from "./utils/objects"; import { deindent } from "./utils/strings"; export type KnownErrorJson = { @@ -1187,6 +1186,17 @@ const OAuthProviderAccessDenied = createKnownErrorConstructor( () => [] as const, ); +const ContactChannelAlreadyUsedForAuthBySomeoneElse = createKnownErrorConstructor( + KnownError, + "CONTACT_CHANNEL_ALREADY_USED_FOR_AUTH_BY_SOMEONE_ELSE", + (type: "email") => [ + 400, + `This ${type} is already used for authentication by another account.`, + { type }, + ] as const, + (json) => [json.type] as const, +); + export type KnownErrors = { [K in keyof typeof KnownErrors]: InstanceType; }; @@ -1283,6 +1293,7 @@ export const KnownErrors = { InvalidAuthorizationCode, TeamPermissionNotFound, OAuthProviderAccessDenied, + ContactChannelAlreadyUsedForAuthBySomeoneElse, } satisfies Record>;