From 28c3f57f31ce6bb2314c8f5cd7118063370661cb Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Tue, 1 Oct 2024 06:22:12 +0200 Subject: [PATCH] New contact channels (#287) * removed contact channels from otp * fixed types * fixed bugs * fixed bug * fixed bugs * updated user contact channel * updated tests * updated tests * added unique key to otp and password auth * removed contact channel from user object --- .../migration.sql | 64 +++++ apps/backend/prisma/schema.prisma | 48 ++-- apps/backend/prisma/seed.ts | 4 +- .../v1/auth/otp/send-sign-in-code/route.tsx | 31 ++- .../otp/sign-in/verification-code-handler.tsx | 36 +-- .../api/v1/auth/password/sign-in/route.tsx | 33 ++- .../src/app/api/v1/internal/projects/crud.tsx | 4 +- .../src/app/api/v1/projects/current/crud.tsx | 5 +- apps/backend/src/app/api/v1/users/crud.tsx | 141 ++-------- .../endpoints/api/v1/auth/oauth/token.test.ts | 11 - .../api/v1/auth/password/sign-in.test.ts | 34 ++- .../api/v1/auth/password/sign-up.test.ts | 33 ++- .../endpoints/api/v1/team-memberships.test.ts | 30 --- .../backend/endpoints/api/v1/users.test.ts | 242 ------------------ .../src/interface/crud/current-user.ts | 2 - .../stack-shared/src/interface/crud/users.ts | 46 +--- 16 files changed, 216 insertions(+), 548 deletions(-) create mode 100644 apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql diff --git a/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql b/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql new file mode 100644 index 000000000..0f49bc9d2 --- /dev/null +++ b/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql @@ -0,0 +1,64 @@ +/* + Warnings: + + - You are about to drop the column `contactChannelId` on the `OtpAuthMethod` table. All the data in the column will be lost. + - You are about to drop the column `identifier` on the `PasswordAuthMethod` table. All the data in the column will be lost. + - A unique constraint covering the columns `[projectId,type,value,usedForAuth]` on the table `ContactChannel` will be added. If there are existing duplicate values, this will fail. + - You are about to drop the column `identifierType` on the `PasswordAuthMethod` table. All the data in the column will be lost. + - You are about to drop the column `identifierType` on the `PasswordAuthMethodConfig` table. All the data in the column will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "OtpAuthMethod" DROP CONSTRAINT "OtpAuthMethod_projectId_projectUserId_contactChannelId_fkey"; + +-- DropIndex +DROP INDEX "OtpAuthMethod_projectId_contactChannelId_key"; + +-- DropIndex +DROP INDEX "PasswordAuthMethod_projectId_identifierType_identifier_key"; + +-- AlterTable +ALTER TABLE "ContactChannel" ADD COLUMN "usedForAuth" "BooleanTrue"; + +-- Set the usedForAuth value to "TRUE" if the contact channel is used in `OtpAuthMethod` or the value is the same as the `PasswordAuthMethod` of the same user +UPDATE "ContactChannel" cc +SET "usedForAuth" = 'TRUE' +WHERE EXISTS ( + SELECT 1 + FROM "OtpAuthMethod" oam + WHERE oam."projectId" = cc."projectId" + AND oam."projectUserId" = cc."projectUserId" +) +OR EXISTS ( + SELECT 1 + FROM "PasswordAuthMethod" pam + WHERE pam."projectId" = cc."projectId" + AND pam."projectUserId" = cc."projectUserId" + AND pam."identifier" = cc."value" +); + + +-- AlterTable +ALTER TABLE "OtpAuthMethod" DROP COLUMN "contactChannelId"; + +-- AlterTable +ALTER TABLE "PasswordAuthMethod" DROP COLUMN "identifier"; + +-- CreateIndex +CREATE UNIQUE INDEX "ContactChannel_projectId_type_value_usedForAuth_key" ON "ContactChannel"("projectId", "type", "value", "usedForAuth"); + +-- AlterTable +ALTER TABLE "PasswordAuthMethod" DROP COLUMN "identifierType"; + +-- AlterTable +ALTER TABLE "PasswordAuthMethodConfig" DROP COLUMN "identifierType"; + +-- DropEnum +DROP TYPE "PasswordAuthMethodIdentifierType"; + +-- CreateIndex +CREATE UNIQUE INDEX "OtpAuthMethod_projectId_projectUserId_key" ON "OtpAuthMethod"("projectId", "projectUserId"); + +-- CreateIndex +CREATE UNIQUE INDEX "PasswordAuthMethod_projectId_projectUserId_key" ON "PasswordAuthMethod"("projectId", "projectUserId"); + diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 2597946f0..aa403777c 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -290,20 +290,22 @@ model ContactChannel { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - type ContactChannelType - isPrimary BooleanTrue? - isVerified Boolean - value String + type ContactChannelType + isPrimary BooleanTrue? + usedForAuth BooleanTrue? + isVerified Boolean + value String - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) - otpAuthMethod OtpAuthMethod[] + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) @@id([projectId, projectUserId, id]) // each user has at most one primary contact channel of each type @@unique([projectId, projectUserId, type, isPrimary]) // value must be unique per user per type @@unique([projectId, projectUserId, type, value]) + // only one contact channel per project with the same value and type can be used for auth + @@unique([projectId, type, value, usedForAuth]) } model ConnectedAccountConfig { @@ -379,11 +381,6 @@ model OtpAuthMethodConfig { @@id([projectConfigId, authMethodConfigId]) } -enum PasswordAuthMethodIdentifierType { - EMAIL - // USERNAME -} - model PasswordAuthMethodConfig { projectConfigId String @db.Uuid authMethodConfigId String @db.Uuid @@ -393,8 +390,6 @@ model PasswordAuthMethodConfig { authMethodConfig AuthMethodConfig @relation(fields: [projectConfigId, authMethodConfigId], references: [projectConfigId, id], onDelete: Cascade) - identifierType PasswordAuthMethodIdentifierType - @@id([projectConfigId, authMethodConfigId]) } @@ -498,21 +493,19 @@ model AuthMethod { } model OtpAuthMethod { - projectId String - authMethodId String @db.Uuid - contactChannelId String @db.Uuid - projectUserId String @db.Uuid + projectId String + authMethodId String @db.Uuid + projectUserId String @db.Uuid createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - contactChannel ContactChannel @relation(fields: [projectId, projectUserId, contactChannelId], references: [projectId, projectUserId, id], onDelete: Cascade) - authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade) - projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) + authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade) + projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) @@id([projectId, authMethodId]) - // each contact channel can only be used once per project as an otp method - @@unique([projectId, contactChannelId]) + // a user can only have one OTP auth method + @@unique([projectId, projectUserId]) } model PasswordAuthMethod { @@ -523,17 +516,14 @@ model PasswordAuthMethod { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - identifierType PasswordAuthMethodIdentifierType - // The identifier is the email or username, depending on the type. - identifier String - passwordHash String + passwordHash String authMethod AuthMethod @relation(fields: [projectId, authMethodId], references: [projectId, id], onDelete: Cascade) projectUser ProjectUser @relation(fields: [projectId, projectUserId], references: [projectId, projectUserId], onDelete: Cascade) @@id([projectId, authMethodId]) - // each identifier of each type can only occur once per project - @@unique([projectId, identifierType, identifier]) + // a user can only have one password auth method + @@unique([projectId, projectUserId]) } // This connects to projectUserOauthAccount, which might be shared between auth method and connected account. diff --git a/apps/backend/prisma/seed.ts b/apps/backend/prisma/seed.ts index 86ea5689e..95df930a9 100644 --- a/apps/backend/prisma/seed.ts +++ b/apps/backend/prisma/seed.ts @@ -81,9 +81,7 @@ async function seed() { }, { passwordConfig: { - create: { - identifierType: 'EMAIL', - } + create: {} } }, ...(['github', 'spotify', 'google', 'microsoft'] as const).map((id) => ({ diff --git a/apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx b/apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx index e662138e2..83c6df4c1 100644 --- a/apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx +++ b/apps/backend/src/app/api/v1/auth/otp/send-sign-in-code/route.tsx @@ -39,33 +39,38 @@ export const POST = createSmartRouteHandler({ throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project"); } - const authMethods = await prismaClient.otpAuthMethod.findMany({ + const contactChannel = await prismaClient.contactChannel.findUnique({ where: { - projectId: project.id, - contactChannel: { + projectId_type_value_usedForAuth: { + projectId: project.id, type: "EMAIL", value: email, - }, + usedForAuth: "TRUE", + } }, include: { - projectUser: true, - contactChannel: true, + projectUser: { + include: { + authMethods: { + include: { + otpAuthMethod: true, + } + } + } + } } }); - if (authMethods.length > 1) { - throw new StackAssertionError("Tried to send OTP sign in code but found multiple auth methods? The uniqueness on the DB schema should prevent this"); - } - const authMethod = authMethods.length === 1 ? authMethods[0] : null; + const otpAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod; - const isNewUser = !authMethod; + const isNewUser = !otpAuthMethod; if (isNewUser && !project.config.sign_up_enabled) { throw new KnownErrors.SignUpNotEnabled(); } let user; - if (!authMethod) { + if (!otpAuthMethod) { // TODO this should be in the same transaction as the read above user = await usersCrudHandlers.adminCreate({ project, @@ -79,7 +84,7 @@ export const POST = createSmartRouteHandler({ } else { user = await usersCrudHandlers.adminRead({ project, - user_id: authMethod.projectUser.projectUserId, + user_id: contactChannel.projectUser.projectUserId, }); } diff --git a/apps/backend/src/app/api/v1/auth/otp/sign-in/verification-code-handler.tsx b/apps/backend/src/app/api/v1/auth/otp/sign-in/verification-code-handler.tsx index d62ab4106..63b959f10 100644 --- a/apps/backend/src/app/api/v1/auth/otp/sign-in/verification-code-handler.tsx +++ b/apps/backend/src/app/api/v1/auth/otp/sign-in/verification-code-handler.tsx @@ -55,33 +55,39 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ }; }, async handler(project, { email }, data) { - const authMethods = await prismaClient.otpAuthMethod.findMany({ + const contactChannel = await prismaClient.contactChannel.findUnique({ where: { - projectId: project.id, - contactChannel: { + projectId_type_value_usedForAuth: { + projectId: project.id, type: "EMAIL", value: email, - }, + usedForAuth: "TRUE", + } }, include: { - projectUser: true, + projectUser: { + include: { + authMethods: { + include: { + otpAuthMethod: true, + } + } + } + } } }); - if (authMethods.length === 0) { + const otpAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod; + + if (!contactChannel || !otpAuthMethod) { throw new StackAssertionError("Tried to use OTP sign in but auth method was not found?"); } - if (authMethods.length > 1) { - throw new StackAssertionError("Tried to use OTP sign in but found multiple auth methods? The uniqueness on the DB schema should prevent this"); - } - - const authMethod = authMethods[0]; - if (authMethod.projectUser.requiresTotpMfa) { + if (contactChannel.projectUser.requiresTotpMfa) { throw await createMfaRequiredError({ project, isNewUser: data.is_new_user, - userId: authMethod.projectUserId, + userId: contactChannel.projectUser.projectUserId, }); } @@ -89,7 +95,7 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ where: { projectId_projectUserId_type_value: { projectId: project.id, - projectUserId: authMethod.projectUserId, + projectUserId: contactChannel.projectUser.projectUserId, type: "EMAIL", value: email, } @@ -101,7 +107,7 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ const { refreshToken, accessToken } = await createAuthTokens({ projectId: project.id, - projectUserId: authMethod.projectUserId, + projectUserId: contactChannel.projectUser.projectUserId, }); return { diff --git a/apps/backend/src/app/api/v1/auth/password/sign-in/route.tsx b/apps/backend/src/app/api/v1/auth/password/sign-in/route.tsx index 4bda81be8..abb33957f 100644 --- a/apps/backend/src/app/api/v1/auth/password/sign-in/route.tsx +++ b/apps/backend/src/app/api/v1/auth/password/sign-in/route.tsx @@ -37,39 +37,50 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.PasswordAuthenticationNotEnabled(); } - const authMethod = await prismaClient.passwordAuthMethod.findUnique({ + const contactChannel = await prismaClient.contactChannel.findUnique({ where: { - projectId_identifierType_identifier: { + projectId_type_value_usedForAuth: { projectId: project.id, - identifierType: "EMAIL", - identifier: email, + type: "EMAIL", + value: email, + usedForAuth: "TRUE", } }, include: { - projectUser: true, + projectUser: { + include: { + authMethods: { + include: { + passwordAuthMethod: true, + } + } + } + } } }); + const passwordAuthMethod = contactChannel?.projectUser.authMethods.find((m) => m.passwordAuthMethod)?.passwordAuthMethod; + // we compare the password even if the authMethod doesn't exist to prevent timing attacks - if (!await comparePassword(password, authMethod?.passwordHash || "")) { + if (!await comparePassword(password, passwordAuthMethod?.passwordHash || "")) { throw new KnownErrors.EmailPasswordMismatch(); } - if (!authMethod) { + if (!contactChannel || !passwordAuthMethod) { throw new StackAssertionError("This should never happen (the comparePassword call should've already caused this to fail)"); } - if (authMethod.projectUser.requiresTotpMfa) { + if (contactChannel.projectUser.requiresTotpMfa) { throw await createMfaRequiredError({ project, isNewUser: false, - userId: authMethod.projectUser.projectUserId, + userId: contactChannel.projectUser.projectUserId, }); } const { refreshToken, accessToken } = await createAuthTokens({ projectId: project.id, - projectUserId: authMethod.projectUser.projectUserId, + projectUserId: contactChannel.projectUser.projectUserId, }); return { @@ -78,7 +89,7 @@ export const POST = createSmartRouteHandler({ body: { access_token: accessToken, refresh_token: refreshToken, - user_id: authMethod.projectUser.projectUserId, + user_id: contactChannel.projectUser.projectUserId, } }; }, diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index d0798a57d..94810978c 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -141,9 +141,7 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand ...(data.config?.credential_enabled ?? true) ? [{ enabled: true, passwordConfig: { - create: { - identifierType: 'EMAIL', - } + create: {} }, }] : [], ] diff --git a/apps/backend/src/app/api/v1/projects/current/crud.tsx b/apps/backend/src/app/api/v1/projects/current/crud.tsx index 9b846f929..03bd65f61 100644 --- a/apps/backend/src/app/api/v1/projects/current/crud.tsx +++ b/apps/backend/src/app/api/v1/projects/current/crud.tsx @@ -339,7 +339,6 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro const passwordAuth = await tx.passwordAuthMethodConfig.findFirst({ where: { projectConfigId: oldProject.config.id, - identifierType: "EMAIL", }, }); if (data.config?.credential_enabled !== undefined) { @@ -349,9 +348,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro projectConfigId: oldProject.config.id, enabled: data.config.credential_enabled, passwordConfig: { - create: { - identifierType: "EMAIL", - }, + create: {}, }, }, }); diff --git a/apps/backend/src/app/api/v1/users/crud.tsx b/apps/backend/src/app/api/v1/users/crud.tsx index 3993c0ad1..03511e79e 100644 --- a/apps/backend/src/app/api/v1/users/crud.tsx +++ b/apps/backend/src/app/api/v1/users/crud.tsx @@ -22,37 +22,14 @@ export const userFullInclude = { providerConfig: true, }, }, - contactChannels: true, authMethods: { include: { passwordAuthMethod: true, - oauthAuthMethod: { - include: { - oauthProviderConfig: { - include: { - proxiedOAuthConfig: true, - standardOAuthConfig: true, - }, - } - } - }, - otpAuthMethod: { - include: { - contactChannel: true, - } - } - } - }, - connectedAccounts: { - include: { - oauthProviderConfig: { - include: { - proxiedOAuthConfig: true, - standardOAuthConfig: true, - }, - } + otpAuthMethod: true, + oauthAuthMethod: true, } }, + contactChannels: true, teamMembers: { include: { team: true, @@ -70,8 +47,12 @@ export const contactChannelToCrud = (channel: Prisma.ContactChannelGetPayload<{} } return { + id: channel.id, type: 'email', - email: channel.value, + value: channel.value, + is_primary: !!channel.isPrimary, + is_verified: channel.isVerified, + used_for_auth: !!channel.usedForAuth, }; }; @@ -105,49 +86,6 @@ export const userPrismaToCrud = ( throw new StackAssertionError("User cannot have more than one selected team; this should never happen"); } - const authMethods: UsersCrud["Admin"]["Read"]["auth_methods"] = prisma.authMethods - .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - .map((m) => { - if ([m.passwordAuthMethod, m.otpAuthMethod, m.oauthAuthMethod].filter(Boolean).length > 1) { - throw new StackAssertionError(`AuthMethod ${m.id} violates the union constraint`, m); - } - - if (m.passwordAuthMethod) { - return { - type: 'password', - identifier: m.passwordAuthMethod.identifier, - }; - } else if (m.otpAuthMethod) { - return { - type: 'otp', - contact_channel: { - type: 'email', - email: m.otpAuthMethod.contactChannel.value, - }, - }; - } else if (m.oauthAuthMethod) { - return { - type: 'oauth', - provider: { - ...oauthProviderConfigToCrud(m.oauthAuthMethod.oauthProviderConfig), - provider_user_id: m.oauthAuthMethod.providerAccountId, - }, - }; - } else { - throw new StackAssertionError("AuthMethod has no auth methods", m); - } - }); - - const connectedAccounts: UsersCrud["Admin"]["Read"]["connected_accounts"] = prisma.connectedAccounts.map((a) => { - return { - type: 'oauth', - provider: { - ...oauthProviderConfigToCrud(a.oauthProviderConfig), - provider_user_id: a.providerAccountId, - }, - }; - }); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const primaryEmailContactChannel = prisma.contactChannels.find((c) => c.type === 'EMAIL' && c.isPrimary); const passwordAuth = prisma.authMethods.find((m) => m.passwordAuthMethod); @@ -171,8 +109,6 @@ export const userPrismaToCrud = ( account_id: a.providerAccountId, email: a.email, })), - auth_methods: authMethods, - connected_accounts: connectedAccounts, selected_team_id: selectedTeamMembers[0]?.teamId ?? null, selected_team: selectedTeamMembers[0] ? teamPrismaToCrud(selectedTeamMembers[0]?.team) : null, last_active_at_millis: lastActiveAtMillis, @@ -201,30 +137,18 @@ async function checkAuthData( } if (data.primaryEmailAuthEnabled) { if (!data.oldPrimaryEmail || data.oldPrimaryEmail !== data.primaryEmail) { - const otpAuth = await tx.otpAuthMethod.findFirst({ + const otpAuth = await tx.contactChannel.findFirst({ where: { projectId: data.projectId, - contactChannel: { - type: 'EMAIL', - value: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"), - }, + type: 'EMAIL', + value: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"), + usedForAuth: BooleanTrue.TRUE, } }); if (otpAuth) { throw new KnownErrors.UserEmailAlreadyExists(); } - - const passwordAuth = await tx.passwordAuthMethod.findFirst({ - where: { - projectId: data.projectId, - identifier: data.primaryEmail || throwErr("primary_email_auth_enabled is true but primary_email is not set"), - } - }); - - if (passwordAuth) { - throw new KnownErrors.UserEmailAlreadyExists(); - } } } } @@ -460,7 +384,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC } if (data.primary_email) { - const contactChannel = await tx.contactChannel.create({ + await tx.contactChannel.create({ data: { projectUserId: newUser.projectUserId, projectId: auth.project.id, @@ -468,6 +392,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC value: data.primary_email || throwErr("primary_email_auth_enabled is true but primary_email is not set"), isVerified: data.primary_email_verified ?? false, isPrimary: "TRUE", + usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null, } }); @@ -484,7 +409,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC otpAuthMethod: { create: { projectUserId: newUser.projectUserId, - contactChannelId: contactChannel.id, } } } @@ -507,9 +431,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC authMethodConfigId: passwordConfig.authMethodConfigId, passwordAuthMethod: { create: { - identifier: data.primary_email || throwErr("password is set but primary_email is not"), passwordHash: await hashPassword(data.password), - identifierType: 'EMAIL', projectUserId: newUser.projectUserId, } } @@ -612,8 +534,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const primaryEmailContactChannel = oldUser.contactChannels.find((c) => c.type === 'EMAIL' && c.isPrimary); - const otpAuth = oldUser.authMethods.find((m) => m.otpAuthMethod && m.otpAuthMethod.contactChannel.id === primaryEmailContactChannel?.id)?.otpAuthMethod; - const passwordAuth = oldUser.authMethods.find((m) => m.passwordAuthMethod && m.passwordAuthMethod.identifier === primaryEmailContactChannel?.value)?.passwordAuthMethod; + const otpAuth = oldUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod; + const passwordAuth = oldUser.authMethods.find((m) => m.passwordAuthMethod)?.passwordAuthMethod; await checkAuthData(tx, { projectId: auth.project.id, @@ -627,10 +549,8 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC // if there is a new primary email // - create a new primary email contact channel if it doesn't exist // - update the primary email contact channel if it exists - // - update the password auth method if it exists // if the primary email is null // - delete the primary email contact channel if it exists (note that this will also delete the related auth methods) - // - delete the password auth method if it exists if (data.primary_email !== undefined) { if (data.primary_email === null) { await tx.contactChannel.delete({ @@ -643,17 +563,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }, }, }); - - if (passwordAuth) { - await tx.authMethod.delete({ - where: { - projectId_id: { - projectId: auth.project.id, - id: passwordAuth.authMethodId, - }, - }, - }); - } } else { await tx.contactChannel.upsert({ where: { @@ -674,22 +583,9 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }, update: { value: data.primary_email, + usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null, } }); - - if (passwordAuth) { - await tx.passwordAuthMethod.update({ - where: { - projectId_authMethodId: { - projectId: auth.project.id, - authMethodId: passwordAuth.authMethodId, - }, - }, - data: { - identifier: data.primary_email, - } - }); - } } } @@ -743,7 +639,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC otpAuthMethod: { create: { projectUserId: params.user_id, - contactChannelId: primaryEmailChannel.id, } } } @@ -821,9 +716,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC authMethodConfigId: passwordConfig.authMethodConfigId, passwordAuthMethod: { create: { - identifier: primaryEmailChannel.value, passwordHash: await hashPassword(data.password), - identifierType: 'EMAIL', projectUserId: params.user_id, } } diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/token.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/token.test.ts index f50db2a26..ed423f089 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/token.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/oauth/token.test.ts @@ -34,20 +34,9 @@ describe("with grant_type === 'authorization_code'", async () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "provider": { - "id": "spotify", - "provider_user_id": "@stack-generated.example.com", - "type": "spotify", - }, - "type": "oauth", - }, - ], "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": null, "has_password": false, "id": "", diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts index a212cc688..f52ea9507 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts @@ -18,22 +18,30 @@ it("should allow signing in to existing accounts", async ({ expect }) => { } `); const response = await niceBackendFetch("/api/v1/users/me", { accessType: "client" }); - expect(response.body.auth_methods).toMatchInlineSnapshot(` - [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - { - "identifier": "@stack-generated.example.com", - "type": "password", + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 200, + "body": { + "auth_with_email": true, + "client_metadata": null, + "client_read_only_metadata": null, + "display_name": null, + "has_password": true, + "id": "", + "oauth_providers": [], + "primary_email": "@stack-generated.example.com", + "primary_email_verified": false, + "profile_image_url": null, + "requires_totp_mfa": false, + "selected_team": null, + "selected_team_id": null, + "signed_up_at_millis": , }, - ] + "headers": Headers {