From d7e3f95b12e7715e15c1dc79780f407a94a4eaac Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 12:42:04 -0700 Subject: [PATCH 01/64] removed contact channels from otp --- .../migration.sql | 28 +++++++++++++++ apps/backend/prisma/schema.prisma | 34 ++++++++----------- 2 files changed, 42 insertions(+), 20 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..72c2df382 --- /dev/null +++ b/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql @@ -0,0 +1,28 @@ +/* + 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. + +*/ +-- 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"; + +-- 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"); diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 2597946f0..65be68e52 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 { @@ -498,21 +500,17 @@ 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]) } model PasswordAuthMethod { @@ -524,16 +522,12 @@ model PasswordAuthMethod { updatedAt DateTime @updatedAt identifierType PasswordAuthMethodIdentifierType - // The identifier is the email or username, depending on the type. - identifier 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]) } // This connects to projectUserOauthAccount, which might be shared between auth method and connected account. From 97b94fd3c7273f395932219c47b6ea149e201b2e Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 13:21:43 -0700 Subject: [PATCH 02/64] fixed types --- .../migration.sql | 30 ++++++++++ apps/backend/prisma/schema.prisma | 10 +--- .../v1/auth/otp/send-sign-in-code/route.tsx | 45 ++++++++++----- .../otp/sign-in/verification-code-handler.tsx | 47 ++++++++++----- .../api/v1/auth/password/sign-in/route.tsx | 33 +++++++---- .../src/app/api/v1/projects/current/crud.tsx | 5 +- apps/backend/src/app/api/v1/users/crud.tsx | 57 +++---------------- .../stack-shared/src/interface/crud/users.ts | 5 -- 8 files changed, 127 insertions(+), 105 deletions(-) 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 index 72c2df382..bdeca3fd1 100644 --- a/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql +++ b/apps/backend/prisma/migrations/20240929194058_remove_otp_contact_channel/migration.sql @@ -4,6 +4,8 @@ - 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 @@ -18,6 +20,24 @@ 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"; @@ -26,3 +46,13 @@ 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"; + diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 65be68e52..d1c43758a 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -381,11 +381,6 @@ model OtpAuthMethodConfig { @@id([projectConfigId, authMethodConfigId]) } -enum PasswordAuthMethodIdentifierType { - EMAIL - // USERNAME -} - model PasswordAuthMethodConfig { projectConfigId String @db.Uuid authMethodConfigId String @db.Uuid @@ -395,8 +390,6 @@ model PasswordAuthMethodConfig { authMethodConfig AuthMethodConfig @relation(fields: [projectConfigId, authMethodConfigId], references: [projectConfigId, id], onDelete: Cascade) - identifierType PasswordAuthMethodIdentifierType - @@id([projectConfigId, authMethodConfigId]) } @@ -521,8 +514,7 @@ model PasswordAuthMethod { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - identifierType PasswordAuthMethodIdentifierType - 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) 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..fcc7a5023 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,52 @@ export const POST = createSmartRouteHandler({ throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project"); } - const authMethods = await prismaClient.otpAuthMethod.findMany({ + // const authMethods = await prismaClient.otpAuthMethod.findMany({ + // where: { + // projectId: project.id, + // contactChannel: { + // type: "EMAIL", + // value: email, + // }, + // }, + // include: { + // projectUser: true, + // contactChannel: true, + // } + // }); + + 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 +98,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..2e72b5bb2 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,52 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ }; }, async handler(project, { email }, data) { - const authMethods = await prismaClient.otpAuthMethod.findMany({ + // const authMethods = await prismaClient.otpAuthMethod.findMany({ + // where: { + // projectId: project.id, + // contactChannel: { + // type: "EMAIL", + // value: email, + // }, + // }, + // include: { + // projectUser: true, + // } + // }); + + 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) { + if (!contactChannel) { 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]; + const otpAuthMethod = contactChannel.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod; - 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 +108,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 +120,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/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 e677e713a..1acfc7b6e 100644 --- a/apps/backend/src/app/api/v1/users/crud.tsx +++ b/apps/backend/src/app/api/v1/users/crud.tsx @@ -26,6 +26,7 @@ export const userFullInclude = { authMethods: { include: { passwordAuthMethod: true, + otpAuthMethod: true, oauthAuthMethod: { include: { oauthProviderConfig: { @@ -36,11 +37,6 @@ export const userFullInclude = { } } }, - otpAuthMethod: { - include: { - contactChannel: true, - } - } } }, connectedAccounts: { @@ -115,15 +111,10 @@ export const userPrismaToCrud = ( 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 { @@ -201,30 +192,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 +439,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, @@ -484,7 +463,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC otpAuthMethod: { create: { projectUserId: newUser.projectUserId, - contactChannelId: contactChannel.id, } } } @@ -507,9 +485,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, } } @@ -614,8 +590,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, @@ -678,20 +654,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC value: data.primary_email, } }); - - if (passwordAuth) { - await tx.passwordAuthMethod.update({ - where: { - projectId_authMethodId: { - projectId: auth.project.id, - authMethodId: passwordAuth.authMethodId, - }, - }, - data: { - identifier: data.primary_email, - } - }); - } } } @@ -745,7 +707,6 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC otpAuthMethod: { create: { projectUserId: params.user_id, - contactChannelId: primaryEmailChannel.id, } } } @@ -823,9 +784,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/packages/stack-shared/src/interface/crud/users.ts b/packages/stack-shared/src/interface/crud/users.ts index 730434f0f..39ffb1bea 100644 --- a/packages/stack-shared/src/interface/crud/users.ts +++ b/packages/stack-shared/src/interface/crud/users.ts @@ -46,14 +46,9 @@ export const usersCrudServerReadSchema = fieldSchema.yupObject({ auth_methods: fieldSchema.yupArray(fieldSchema.yupUnion( fieldSchema.yupObject({ type: fieldSchema.yupString().oneOf(['password']).required(), - identifier: fieldSchema.yupString().required(), }).required(), fieldSchema.yupObject({ type: fieldSchema.yupString().oneOf(['otp']).required(), - contact_channel: fieldSchema.yupObject({ - type: fieldSchema.yupString().oneOf(['email']).required(), - email: fieldSchema.yupString().required(), - }).required(), }).required(), fieldSchema.yupObject({ type: fieldSchema.yupString().oneOf(['oauth']).required(), From 2928f5b1fa76e46ce6bfb1817b3ec2e7d73ebaa5 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 13:43:01 -0700 Subject: [PATCH 03/64] fixed bugs --- .../v1/auth/otp/send-sign-in-code/route.tsx | 14 -------------- .../otp/sign-in/verification-code-handler.tsx | 19 +++---------------- 2 files changed, 3 insertions(+), 30 deletions(-) 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 fcc7a5023..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,20 +39,6 @@ export const POST = createSmartRouteHandler({ throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project"); } - // const authMethods = await prismaClient.otpAuthMethod.findMany({ - // where: { - // projectId: project.id, - // contactChannel: { - // type: "EMAIL", - // value: email, - // }, - // }, - // include: { - // projectUser: true, - // contactChannel: true, - // } - // }); - const contactChannel = await prismaClient.contactChannel.findUnique({ where: { projectId_type_value_usedForAuth: { 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 2e72b5bb2..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,19 +55,6 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ }; }, async handler(project, { email }, data) { - // const authMethods = await prismaClient.otpAuthMethod.findMany({ - // where: { - // projectId: project.id, - // contactChannel: { - // type: "EMAIL", - // value: email, - // }, - // }, - // include: { - // projectUser: true, - // } - // }); - const contactChannel = await prismaClient.contactChannel.findUnique({ where: { projectId_type_value_usedForAuth: { @@ -90,12 +77,12 @@ export const signInVerificationCodeHandler = createVerificationCodeHandler({ } }); - if (!contactChannel) { + 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?"); } - const otpAuthMethod = contactChannel.projectUser.authMethods.find((m) => m.otpAuthMethod)?.otpAuthMethod; - if (contactChannel.projectUser.requiresTotpMfa) { throw await createMfaRequiredError({ project, From cbaadeefec4afefb2a143eb8ed487daa51f2754d Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 14:01:32 -0700 Subject: [PATCH 04/64] fixed bug --- apps/backend/prisma/seed.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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) => ({ From 32ed487845d1ec776bb2370723243b33b07777c0 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 14:22:36 -0700 Subject: [PATCH 05/64] fixed bugs --- .../src/app/api/v1/internal/projects/crud.tsx | 4 +--- apps/backend/src/app/api/v1/users/crud.tsx | 15 ++------------- 2 files changed, 3 insertions(+), 16 deletions(-) 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/users/crud.tsx b/apps/backend/src/app/api/v1/users/crud.tsx index 1acfc7b6e..3c8ffd324 100644 --- a/apps/backend/src/app/api/v1/users/crud.tsx +++ b/apps/backend/src/app/api/v1/users/crud.tsx @@ -447,6 +447,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, } }); @@ -605,10 +606,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({ @@ -621,17 +620,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: { @@ -652,6 +640,7 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }, update: { value: data.primary_email, + usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null, } }); } From 02aed011b5f1cde84eddb8338c19aca04dee7885 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 14:32:14 -0700 Subject: [PATCH 06/64] updated user contact channel --- apps/backend/src/app/api/v1/users/crud.tsx | 47 +++-------------- .../stack-shared/src/interface/crud/users.ts | 51 ++++++++----------- 2 files changed, 28 insertions(+), 70 deletions(-) diff --git a/apps/backend/src/app/api/v1/users/crud.tsx b/apps/backend/src/app/api/v1/users/crud.tsx index 3c8ffd324..9e0752020 100644 --- a/apps/backend/src/app/api/v1/users/crud.tsx +++ b/apps/backend/src/app/api/v1/users/crud.tsx @@ -66,8 +66,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, }; }; @@ -101,43 +105,7 @@ 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', - }; - } else if (m.otpAuthMethod) { - return { - type: 'otp', - }; - } 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, - }, - }; - }); + const contactChannels = prisma.contactChannels.map(c => contactChannelToCrud(c)); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const primaryEmailContactChannel = prisma.contactChannels.find((c) => c.type === 'EMAIL' && c.isPrimary); @@ -162,11 +130,10 @@ 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, + contact_channels: contactChannels, }; }; diff --git a/packages/stack-shared/src/interface/crud/users.ts b/packages/stack-shared/src/interface/crud/users.ts index 39ffb1bea..c499ead6b 100644 --- a/packages/stack-shared/src/interface/crud/users.ts +++ b/packages/stack-shared/src/interface/crud/users.ts @@ -17,6 +17,15 @@ export const usersCrudServerUpdateSchema = fieldSchema.yupObject({ selected_team_id: fieldSchema.selectedTeamIdSchema.nullable().optional(), }).required(); +const contactChannelSchema = fieldSchema.yupObject({ + id: fieldSchema.yupString().required(), + type: fieldSchema.yupString().required(), + value: fieldSchema.yupString().required(), + is_primary: fieldSchema.yupBoolean().required(), + is_verified: fieldSchema.yupBoolean().required(), + used_for_auth: fieldSchema.yupBoolean().required(), +}).required(); + export const usersCrudServerReadSchema = fieldSchema.yupObject({ id: fieldSchema.userIdSchema.required(), primary_email: fieldSchema.primaryEmailSchema.nullable().defined(), @@ -27,6 +36,18 @@ export const usersCrudServerReadSchema = fieldSchema.yupObject({ profile_image_url: fieldSchema.profileImageUrlSchema.nullable().defined(), signed_up_at_millis: fieldSchema.signedUpAtMillisSchema.required(), has_password: fieldSchema.yupBoolean().required().meta({ openapiField: { description: 'Whether the user has a password associated with their account', exampleValue: true } }), + client_metadata: fieldSchema.userClientMetadataSchema, + client_read_only_metadata: fieldSchema.userClientReadOnlyMetadataSchema, + server_metadata: fieldSchema.userServerMetadataSchema, + last_active_at_millis: fieldSchema.userLastActiveAtMillisSchema.required(), + contact_channels: fieldSchema.yupArray(contactChannelSchema).required(), + + oauth_providers: fieldSchema.yupArray(fieldSchema.yupObject({ + id: fieldSchema.yupString().required(), + account_id: fieldSchema.yupString().required(), + email: fieldSchema.yupString().nullable(), + }).required()).required().meta({ openapiField: { hidden: true, description: 'A list of OAuth providers connected to this account', exampleValue: [{ id: 'google', account_id: '12345', email: 'john.doe@gmail.com' }] } }), + /** * @deprecated */ @@ -35,36 +56,6 @@ export const usersCrudServerReadSchema = fieldSchema.yupObject({ * @deprecated */ requires_totp_mfa: fieldSchema.yupBoolean().required().meta({ openapiField: { hidden: true, description: 'Whether the user is required to use TOTP MFA to sign in', exampleValue: false } }), - /** - * @deprecated - */ - oauth_providers: fieldSchema.yupArray(fieldSchema.yupObject({ - id: fieldSchema.yupString().required(), - account_id: fieldSchema.yupString().required(), - email: fieldSchema.yupString().nullable(), - }).required()).required().meta({ openapiField: { hidden: true, description: 'A list of OAuth providers connected to this account', exampleValue: [{ id: 'google', account_id: '12345', email: 'john.doe@gmail.com' }] } }), - auth_methods: fieldSchema.yupArray(fieldSchema.yupUnion( - fieldSchema.yupObject({ - type: fieldSchema.yupString().oneOf(['password']).required(), - }).required(), - fieldSchema.yupObject({ - type: fieldSchema.yupString().oneOf(['otp']).required(), - }).required(), - fieldSchema.yupObject({ - type: fieldSchema.yupString().oneOf(['oauth']).required(), - provider: fieldSchema.userOAuthProviderSchema.required(), - }).required(), - )).required().meta({ openapiField: { hidden: true, description: 'A list of authentication methods available for this user to sign in with', exampleValue: [ { "contact_channel": { "email": "john.doe@gmail.com", "type": "email", }, "type": "otp", } ] } }), - connected_accounts: fieldSchema.yupArray(fieldSchema.yupUnion( - fieldSchema.yupObject({ - type: fieldSchema.yupString().oneOf(['oauth']).required(), - provider: fieldSchema.userOAuthProviderSchema.required(), - }).required(), - )).required().meta({ openapiField: { hidden: true, description: 'A list of connected accounts to this user', exampleValue: [ { "provider": { "provider_user_id": "12345", "type": "google", }, "type": "oauth", } ] } }), - client_metadata: fieldSchema.userClientMetadataSchema, - client_read_only_metadata: fieldSchema.userClientReadOnlyMetadataSchema, - server_metadata: fieldSchema.userServerMetadataSchema, - last_active_at_millis: fieldSchema.userLastActiveAtMillisSchema.required(), }).required(); export const usersCrudServerCreateSchema = usersCrudServerUpdateSchema.omit(['selected_team_id']).concat(fieldSchema.yupObject({ From 7cc1bffeaa9bf848e46dcf1040c0705b8839011b Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 14:39:44 -0700 Subject: [PATCH 07/64] updated tests --- .../backend/endpoints/api/v1/users.test.ts | 413 ++++++++---------- .../src/interface/crud/current-user.ts | 2 - 2 files changed, 171 insertions(+), 244 deletions(-) diff --git a/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts index 71986b760..7de64bb9f 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts @@ -77,19 +77,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": null, "has_password": false, "id": "", @@ -117,19 +107,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": null, "has_password": false, "id": "", @@ -202,19 +182,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": "John Doe", "has_password": false, "id": "", @@ -241,19 +211,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": { "key": "value" }, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": "John Doe", "has_password": false, "id": "", @@ -390,19 +350,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": "John Doe", "has_password": false, "id": "", @@ -429,19 +379,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": null, "has_password": false, "id": "", @@ -566,19 +506,9 @@ describe("with client access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": { "key": "value" }, "client_read_only_metadata": null, - "connected_accounts": [], "display_name": null, "has_password": false, "id": "", @@ -682,19 +612,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -727,19 +657,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": "John Doe", "has_password": false, "id": "", @@ -788,19 +718,19 @@ describe("with server access", () => { "is_paginated": false, "items": [ { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -838,19 +768,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -881,11 +811,10 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [], "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [], "display_name": null, "has_password": false, "id": "", @@ -920,19 +849,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": "John Dough", "has_password": false, "id": "", @@ -967,23 +896,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - { - "identifier": "@stack-generated.example.com", - "type": "password", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": true, "id": "", @@ -1063,19 +988,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -1128,11 +1053,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [], "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": false, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -1164,23 +1097,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - { - "identifier": "@stack-generated.example.com", - "type": "password", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": true, "id": "", @@ -1227,23 +1156,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - { - "identifier": "@stack-generated.example.com", - "type": "password", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": true, "id": "", @@ -1272,11 +1197,19 @@ describe("with server access", () => { NiceResponse { "status": 201, "body": { - "auth_methods": [], "auth_with_email": false, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": false, + "type": "email", + "used_for_auth": false, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -1329,19 +1262,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": "John Doe", "has_password": false, "id": "", @@ -1366,19 +1299,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": "John Doe", "has_password": false, "id": "", @@ -1412,19 +1345,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": "John Doe", "has_password": false, "id": "", @@ -1458,23 +1391,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - { - "identifier": "@stack-generated.example.com", - "type": "password", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": true, "id": "", @@ -1557,19 +1486,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "@stack-generated.example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": { "key": "client value" }, "client_read_only_metadata": { "key": "client read only value" }, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": true, + "value": "@stack-generated.example.com", + }, + ], "display_name": null, "has_password": false, "id": "", @@ -1614,19 +1543,19 @@ describe("with server access", () => { NiceResponse { "status": 200, "body": { - "auth_methods": [ - { - "contact_channel": { - "email": "new-primary-email@example.com", - "type": "email", - }, - "type": "otp", - }, - ], "auth_with_email": true, "client_metadata": null, "client_read_only_metadata": null, - "connected_accounts": [], + "contact_channels": [ + { + "id": "", + "is_primary": true, + "is_verified": true, + "type": "email", + "used_for_auth": false, + "value": "new-primary-email@example.com", + }, + ], "display_name": null, "has_password": false, "id": "", diff --git a/packages/stack-shared/src/interface/crud/current-user.ts b/packages/stack-shared/src/interface/crud/current-user.ts index ebe722d58..9675f359b 100644 --- a/packages/stack-shared/src/interface/crud/current-user.ts +++ b/packages/stack-shared/src/interface/crud/current-user.ts @@ -26,8 +26,6 @@ const clientReadSchema = usersCrudServerReadSchema.pick([ "auth_with_email", "oauth_providers", "selected_team_id", - "auth_methods", - "connected_accounts", "requires_totp_mfa", ]).concat(yupObject({ selected_team: teamsCrudClientReadSchema.nullable().defined(), From 4bef06f5a2c0d6a60a4b79d0f9310e2d2bc4090c Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 29 Sep 2024 14:49:47 -0700 Subject: [PATCH 08/64] updated tests --- .../endpoints/api/v1/auth/oauth/token.test.ts | 11 ---- .../api/v1/auth/password/sign-in.test.ts | 33 ++++++---- .../api/v1/auth/password/sign-up.test.ts | 33 ++++++---- .../endpoints/api/v1/team-memberships.test.ts | 60 +++++++++---------- 4 files changed, 70 insertions(+), 67 deletions(-) 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..2b01de582 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,20 +18,27 @@ 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 {