From 7b0d5959cbac211dcebf2c370b24d8657d28d7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Mon, 12 Feb 2024 14:13:21 -0700 Subject: [PATCH] refactor: migrates user password to it's own table (#13628) --- apps/web/pages/api/auth/changepw.ts | 8 ++-- apps/web/pages/api/auth/reset-password.ts | 6 ++- apps/web/pages/api/auth/setup.ts | 2 +- .../pages/api/auth/two-factor/totp/disable.ts | 8 ++-- .../pages/api/auth/two-factor/totp/setup.ts | 6 +-- .../playwright/auth/forgot-password.e2e.ts | 3 +- apps/web/playwright/fixtures/users.ts | 12 ++++-- .../features/auth/lib/next-auth-options.ts | 35 +++++++++------- .../auth/signup/handlers/calcomHandler.ts | 12 ++++-- .../auth/signup/handlers/selfHostedHandler.ts | 18 ++++++-- .../features/ee/users/server/trpc-router.ts | 7 +--- packages/lib/test/builder.ts | 1 - packages/lib/validateUsername.ts | 4 +- .../migration.sql | 31 ++++++++++++++ packages/prisma/schema.prisma | 9 +++- packages/prisma/seed-insights.ts | 18 ++++++-- packages/prisma/seed-utils.ts | 6 ++- packages/prisma/seed.ts | 42 +++++++++++++++---- .../loggedInViewer/deleteMe.handler.ts | 4 +- .../viewer/auth/changePassword.handler.ts | 17 +++----- .../viewer/auth/verifyPassword.handler.ts | 8 ++-- .../viewer/organizations/create.handler.ts | 2 +- .../organizations/setPassword.handler.ts | 11 ++--- .../viewer/teams/inviteMember/utils.ts | 7 ++-- .../viewer/teams/removeMember.handler.ts | 1 - 25 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 packages/prisma/migrations/20240209223121_adds_user_password/migration.sql diff --git a/apps/web/pages/api/auth/changepw.ts b/apps/web/pages/api/auth/changepw.ts index da9b7af6c1..5202221250 100644 --- a/apps/web/pages/api/auth/changepw.ts +++ b/apps/web/pages/api/auth/changepw.ts @@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const oldPassword = req.body.oldPassword; const newPassword = req.body.newPassword; - const currentPassword = user.password; + const currentPassword = user.password?.hash; if (!currentPassword) { return res.status(400).json({ error: ErrorCode.UserMissingPassword }); } @@ -53,12 +53,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } const hashedPassword = await hashPassword(newPassword); - await prisma.user.update({ + await prisma.userPassword.update({ where: { - id: user.id, + userId: user.id, }, data: { - password: hashedPassword, + hash: hashedPassword, }, }); diff --git a/apps/web/pages/api/auth/reset-password.ts b/apps/web/pages/api/auth/reset-password.ts index e2d1109c68..f510c96b23 100644 --- a/apps/web/pages/api/auth/reset-password.ts +++ b/apps/web/pages/api/auth/reset-password.ts @@ -41,7 +41,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) email: maybeRequest.email, }, data: { - password: hashedPassword, + password: { + update: { + hash: hashedPassword, + }, + }, emailVerified: new Date(), }, }); diff --git a/apps/web/pages/api/auth/setup.ts b/apps/web/pages/api/auth/setup.ts index f63f2bb9b4..fb196cc325 100644 --- a/apps/web/pages/api/auth/setup.ts +++ b/apps/web/pages/api/auth/setup.ts @@ -41,7 +41,7 @@ async function handler(req: NextApiRequest) { data: { username, email: userEmail, - password: hashedPassword, + password: { create: { hash: hashedPassword } }, role: "ADMIN", name: parsedQuery.data.full_name, emailVerified: new Date(), diff --git a/apps/web/pages/api/auth/two-factor/totp/disable.ts b/apps/web/pages/api/auth/two-factor/totp/disable.ts index fecb75d92f..bcb8fbe1bb 100644 --- a/apps/web/pages/api/auth/two-factor/totp/disable.ts +++ b/apps/web/pages/api/auth/two-factor/totp/disable.ts @@ -23,13 +23,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(500).json({ error: ErrorCode.InternalServerError }); } - const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); if (!user) { console.error(`Session references user that no longer exists.`); return res.status(401).json({ message: "Not authenticated" }); } - if (!user.password && user.identityProvider === IdentityProvider.CAL) { + if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) { return res.status(400).json({ error: ErrorCode.UserMissingPassword }); } @@ -37,8 +37,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.json({ message: "Two factor disabled" }); } - if (user.password && user.identityProvider === IdentityProvider.CAL) { - const isCorrectPassword = await verifyPassword(req.body.password, user.password); + if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) { + const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash); if (!isCorrectPassword) { return res.status(400).json({ error: ErrorCode.IncorrectPassword }); } diff --git a/apps/web/pages/api/auth/two-factor/totp/setup.ts b/apps/web/pages/api/auth/two-factor/totp/setup.ts index a6fbed0391..628346d2ca 100644 --- a/apps/web/pages/api/auth/two-factor/totp/setup.ts +++ b/apps/web/pages/api/auth/two-factor/totp/setup.ts @@ -25,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(500).json({ error: ErrorCode.InternalServerError }); } - const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } }); if (!user) { console.error(`Session references user that no longer exists.`); return res.status(401).json({ message: "Not authenticated" }); @@ -35,7 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }); } - if (!user.password) { + if (!user.password?.hash) { return res.status(400).json({ error: ErrorCode.UserMissingPassword }); } @@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(500).json({ error: ErrorCode.InternalServerError }); } - const isCorrectPassword = await verifyPassword(req.body.password, user.password); + const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash); if (!isCorrectPassword) { return res.status(400).json({ error: ErrorCode.IncorrectPassword }); } diff --git a/apps/web/playwright/auth/forgot-password.e2e.ts b/apps/web/playwright/auth/forgot-password.e2e.ts index 79bf74ccfa..7b7ceb349f 100644 --- a/apps/web/playwright/auth/forgot-password.e2e.ts +++ b/apps/web/playwright/auth/forgot-password.e2e.ts @@ -86,7 +86,8 @@ testBothFutureAndLegacyRoutes.describe("Forgot password", async () => { }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(await verifyPassword(newPassword, updatedUser.password!)).toBeTruthy(); + const updatedPassword = updatedUser.password!.hash; + expect(await verifyPassword(newPassword, updatedPassword)).toBeTruthy(); // finally, make sure the same URL cannot be used to reset the password again, as it should be expired. await page.goto(`/auth/forgot-password/${id}`); diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index c55ab2f9e4..1fa59debef 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -652,7 +652,6 @@ type SupportedTestWorkflows = PrismaType.WorkflowCreateInput; type CustomUserOptsKeys = | "username" - | "password" | "completedOnboarding" | "locale" | "name" @@ -669,6 +668,7 @@ type CustomUserOpts = Partial> & { useExactUsername?: boolean; roleInOrganization?: MembershipRole; schedule?: Schedule; + password?: string | null; }; // creates the actual user in the db. @@ -690,7 +690,11 @@ const createUser = ( username: uname, name: opts?.name, email: opts?.email ?? `${uname}@example.com`, - password: hashPassword(uname), + password: { + create: { + hash: hashPassword(uname), + }, + }, emailVerified: new Date(), completedOnboarding: opts?.completedOnboarding ?? true, timeZone: opts?.timeZone ?? TimeZoneEnum.UK, @@ -791,7 +795,7 @@ async function confirmPendingPayment(page: Page) { // login using a replay of an E2E routine. export async function login( - user: Pick & Partial>, + user: Pick & Partial> & { password?: string | null }, page: Page ) { // get locators @@ -812,7 +816,7 @@ export async function login( } export async function apiLogin( - user: Pick & Partial>, + user: Pick & Partial> & { password: string | null }, page: Page ) { const csrfToken = await page diff --git a/packages/features/auth/lib/next-auth-options.ts b/packages/features/auth/lib/next-auth-options.ts index 706e000b72..aba2c07697 100644 --- a/packages/features/auth/lib/next-auth-options.ts +++ b/packages/features/auth/lib/next-auth-options.ts @@ -42,8 +42,8 @@ const ORGANIZATIONS_AUTOLINK = const usernameSlug = (username: string) => `${slugify(username)}-${randomString(6).toLowerCase()}`; -const loginWithTotp = async (user: { email: string }) => - `/auth/login?totp=${await (await import("./signJwt")).default({ email: user.email })}`; +const loginWithTotp = async (email: string) => + `/auth/login?totp=${await (await import("./signJwt")).default({ email })}`; type UserTeams = { teams: (Membership & { @@ -126,18 +126,18 @@ const providers: Provider[] = [ if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled); } - if (!user.password && user.identityProvider == IdentityProvider.CAL) { + if (!user.password?.hash && user.identityProvider == IdentityProvider.CAL) { throw new Error(ErrorCode.IncorrectEmailPassword); } - if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { + if (!user.password?.hash && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) { throw new Error(ErrorCode.IncorrectEmailPassword); } - if (user.password && !credentials.totpCode) { - if (!user.password) { + if (user.password?.hash && !credentials.totpCode) { + if (!user.password?.hash) { throw new Error(ErrorCode.IncorrectEmailPassword); } - const isCorrectPassword = await verifyPassword(credentials.password, user.password); + const isCorrectPassword = await verifyPassword(credentials.password, user.password.hash); if (!isCorrectPassword) { throw new Error(ErrorCode.IncorrectEmailPassword); } @@ -717,7 +717,7 @@ export const AUTH_OPTIONS: AuthOptions = { } } if (existingUser.twoFactorEnabled && existingUser.identityProvider === idP) { - return loginWithTotp(existingUser); + return loginWithTotp(existingUser.email); } else { return true; } @@ -733,7 +733,7 @@ export const AUTH_OPTIONS: AuthOptions = { if (!userWithNewEmail) { await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } }); if (existingUser.twoFactorEnabled) { - return loginWithTotp(existingUser); + return loginWithTotp(existingUser.email); } else { return true; } @@ -752,6 +752,9 @@ export const AUTH_OPTIONS: AuthOptions = { mode: "insensitive", }, }, + include: { + password: true, + }, }); if (existingUserWithEmail) { @@ -762,7 +765,7 @@ export const AUTH_OPTIONS: AuthOptions = { existingUserWithEmail.identityProvider !== IdentityProvider.CAL ) { if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); + return loginWithTotp(existingUserWithEmail.email); } else { return true; } @@ -770,7 +773,7 @@ export const AUTH_OPTIONS: AuthOptions = { // check if user was invited if ( - !existingUserWithEmail.password && + !existingUserWithEmail.password?.hash && !existingUserWithEmail.emailVerified && !existingUserWithEmail.username ) { @@ -792,7 +795,7 @@ export const AUTH_OPTIONS: AuthOptions = { }); if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); + return loginWithTotp(existingUserWithEmail.email); } else { return true; } @@ -807,14 +810,16 @@ export const AUTH_OPTIONS: AuthOptions = { where: { email: existingUserWithEmail.email }, // also update email to the IdP email data: { - password: null, + password: { + delete: true, + }, email: user.email, identityProvider: idP, identityProviderId: account.providerAccountId, }, }); if (existingUserWithEmail.twoFactorEnabled) { - return loginWithTotp(existingUserWithEmail); + return loginWithTotp(existingUserWithEmail.email); } else { return true; } @@ -853,7 +858,7 @@ export const AUTH_OPTIONS: AuthOptions = { await calcomAdapter.linkAccount(linkAccountNewUserData); if (account.twoFactorEnabled) { - return loginWithTotp(newUser); + return loginWithTotp(newUser.email); } else { return true; } diff --git a/packages/features/auth/signup/handlers/calcomHandler.ts b/packages/features/auth/signup/handlers/calcomHandler.ts index b82299e3e6..b3aba50ab2 100644 --- a/packages/features/auth/signup/handlers/calcomHandler.ts +++ b/packages/features/auth/signup/handlers/calcomHandler.ts @@ -138,18 +138,22 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { where: { email }, update: { username, - password: hashedPassword, emailVerified: new Date(Date.now()), identityProvider: IdentityProvider.CAL, + password: { + upsert: { + create: { hash: hashedPassword }, + update: { hash: hashedPassword }, + }, + }, }, create: { username, email, - password: hashedPassword, identityProvider: IdentityProvider.CAL, + password: { create: { hash: hashedPassword } }, }, }); - // Wrapping in a transaction as if one fails we want to rollback the whole thing to preventa any data inconsistencies const { membership } = await createOrUpdateMemberships({ teamMetadata, @@ -180,7 +184,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) { data: { username, email, - password: hashedPassword, + password: { create: { hash: hashedPassword } }, metadata: { stripeCustomerId: customer.id, checkoutSessionId, diff --git a/packages/features/auth/signup/handlers/selfHostedHandler.ts b/packages/features/auth/signup/handlers/selfHostedHandler.ts index 26f48dc5e0..e98b5b52a1 100644 --- a/packages/features/auth/signup/handlers/selfHostedHandler.ts +++ b/packages/features/auth/signup/handlers/selfHostedHandler.ts @@ -79,14 +79,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) where: { email: userEmail }, update: { username: correctedUsername, - password: hashedPassword, + password: { + upsert: { + create: { hash: hashedPassword }, + update: { hash: hashedPassword }, + }, + }, emailVerified: new Date(Date.now()), identityProvider: IdentityProvider.CAL, }, create: { username: correctedUsername, email: userEmail, - password: hashedPassword, + password: { create: { hash: hashedPassword } }, identityProvider: IdentityProvider.CAL, }, }); @@ -128,14 +133,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) where: { email: userEmail }, update: { username: correctedUsername, - password: hashedPassword, + password: { + upsert: { + create: { hash: hashedPassword }, + update: { hash: hashedPassword }, + }, + }, emailVerified: new Date(Date.now()), identityProvider: IdentityProvider.CAL, }, create: { username: correctedUsername, email: userEmail, - password: hashedPassword, + password: { create: { hash: hashedPassword } }, identityProvider: IdentityProvider.CAL, }, }); diff --git a/packages/features/ee/users/server/trpc-router.ts b/packages/features/ee/users/server/trpc-router.ts index ff1a9edd40..001c214953 100644 --- a/packages/features/ee/users/server/trpc-router.ts +++ b/packages/features/ee/users/server/trpc-router.ts @@ -67,9 +67,7 @@ const authedAdminProcedureWithRequestedUser = authedAdminProcedure.use(async ({ return next({ ctx: { user: ctx.user, - requestedUser: - /** Don't leak the password */ - exclude(user, ["password"]), + requestedUser: user, }, }); }); @@ -84,8 +82,7 @@ export const userAdminRouter = router({ // TODO: Add search, pagination, etc. const users = await prisma.user.findMany(); return users.map((user) => ({ - /** Don't leak the password */ - ...exclude(user, ["password"]), + ...user, /** * FIXME: This should be either a prisma extension or middleware * @see https://www.prisma.io/docs/concepts/components/prisma-client/middleware diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 6ecabccea8..106547540a 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -224,7 +224,6 @@ export const buildUser = >( invitedTo: null, locale: "en", metadata: null, - password: null, role: "USER", schedules: [], selectedCalendars: [], diff --git a/packages/lib/validateUsername.ts b/packages/lib/validateUsername.ts index 56d0a10de9..4ebbd5741a 100644 --- a/packages/lib/validateUsername.ts +++ b/packages/lib/validateUsername.ts @@ -56,9 +56,7 @@ export const validateAndGetCorrectedUsernameAndEmail = async ({ { OR: [ { emailVerified: { not: null } }, - { - AND: [{ password: { not: null } }, { username: { not: null } }], - }, + { AND: [{ password: { isNot: null } }, { username: { not: null } }] }, ], }, ], diff --git a/packages/prisma/migrations/20240209223121_adds_user_password/migration.sql b/packages/prisma/migrations/20240209223121_adds_user_password/migration.sql new file mode 100644 index 0000000000..a5b700460f --- /dev/null +++ b/packages/prisma/migrations/20240209223121_adds_user_password/migration.sql @@ -0,0 +1,31 @@ +/* +Warnings: + +- You are about to drop the column `password` on the `users` table. All the data in the column will be lost. + + */ +-- CreateTable +CREATE TABLE + "UserPassword" ( + "hash" TEXT NOT NULL, + "userId" INTEGER NOT NULL + ); + +-- CreateIndex +CREATE UNIQUE INDEX "UserPassword_userId_key" ON "UserPassword" ("userId"); + +-- AddForeignKey +ALTER TABLE "UserPassword" ADD CONSTRAINT "UserPassword_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- Inserts current user password into new table +INSERT INTO + "UserPassword" ("hash", "userId") +SELECT + u."password", + u.id +FROM + users u; + +-- We rename instead of dropping the column to avoid possible data loss, will be dropped in the next migration. +ALTER TABLE "users" +RENAME COLUMN "password" TO "password_deprecated"; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 03fa98aff3..72b15b2643 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -191,6 +191,13 @@ enum UserPermissionRole { ADMIN } +// It holds the password of a User, separate from the User model to avoid leaking the password hash +model UserPassword { + hash String + userId Int @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + // It holds Personal Profiles of a User plus it has email, password and other core things model User { id Int @id @default(autoincrement()) @@ -199,7 +206,7 @@ model User { /// @zod.email() email String emailVerified DateTime? - password String? + password UserPassword? bio String? avatar String? avatarUrl String? diff --git a/packages/prisma/seed-insights.ts b/packages/prisma/seed-insights.ts index 440e0e2bfc..7f4ad14529 100644 --- a/packages/prisma/seed-insights.ts +++ b/packages/prisma/seed-insights.ts @@ -78,7 +78,11 @@ async function main() { insightsAdmin = await prisma.user.create({ data: { email: "insights@example.com", - password: await hashPassword("insightsinsightsinsights...!"), + password: { + create: { + hash: await hashPassword("insightsinsightsinsights...!"), + }, + }, name: "Insights Admin", role: "ADMIN", username: "insights-admin", @@ -102,7 +106,11 @@ async function main() { insightsUser = await prisma.user.create({ data: { email: "insightsuser@example.com", - password: await hashPassword("insightsuser"), + password: { + create: { + hash: await hashPassword("insightsuser"), + }, + }, name: "Insights User", role: "USER", username: "insights-user", @@ -415,7 +423,11 @@ async function createPerformanceData() { const email = `insightsuser${timestamp}@example.com`; const insightsUser = { email, - password: await hashPassword("insightsuser"), + password: { + create: { + hash: await hashPassword("insightsuser"), + }, + }, name: `Insights User ${timestamp}`, username: `insights-user-${timestamp}`, completedOnboarding: true, diff --git a/packages/prisma/seed-utils.ts b/packages/prisma/seed-utils.ts index d42741dc2b..4aac63a4f5 100644 --- a/packages/prisma/seed-utils.ts +++ b/packages/prisma/seed-utils.ts @@ -40,7 +40,11 @@ export async function createUserAndEventType({ }) { const userData = { ...user, - password: await hashPassword(user.password), + password: { + create: { + hash: await hashPassword(user.password), + }, + }, emailVerified: new Date(), completedOnboarding: user.completedOnboarding ?? true, locale: "en", diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index 6e0d6d2727..1fa05fd754 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -122,7 +122,11 @@ async function createOrganizationAndAddMembersAndTeams({ data: { ...member.memberData, emailVerified: new Date(), - password: await hashPassword(member.memberData.password), + password: { + create: { + hash: await hashPassword(member.memberData.password.create?.hash || ""), + }, + }, }, })), inTeams: member.inTeams, @@ -149,7 +153,11 @@ async function createOrganizationAndAddMembersAndTeams({ name: user.name, email: user.email, emailVerified: new Date(), - password: await hashPassword(user.username), + password: { + create: { + hash: await hashPassword(user.username), + }, + }, }, }); }), @@ -255,7 +263,11 @@ async function createOrganizationAndAddMembersAndTeams({ await prisma.user.create({ data: { ...nonOrgMember, - password: await hashPassword(nonOrgMember.password), + password: { + create: { + hash: await hashPassword(nonOrgMember.username), + }, + }, emailVerified: new Date(), }, }) @@ -807,7 +819,11 @@ async function main() { { memberData: { email: "owner1-acme@example.com", - password: "owner1-acme", + password: { + create: { + hash: "owner1-acme", + }, + }, username: "owner1-acme", name: "Owner 1", }, @@ -836,7 +852,11 @@ async function main() { nonOrgMembers: [ { email: "non-acme-member-1@example.com", - password: "non-acme-member-1", + password: { + create: { + hash: "non-acme-member-1", + }, + }, username: "non-acme-member-1", name: "NonAcme Member1", }, @@ -866,7 +886,11 @@ async function main() { { memberData: { email: "owner1-dunder@example.com", - password: "owner1-dunder", + password: { + create: { + hash: "owner1-dunder", + }, + }, username: "owner1-dunder", name: "Owner 1", }, @@ -895,7 +919,11 @@ async function main() { nonOrgMembers: [ { email: "non-dunder-member-1@example.com", - password: "non-dunder-member-1", + password: { + create: { + hash: "non-dunder-member-1", + }, + }, username: "non-dunder-member-1", name: "NonDunder Member1", }, diff --git a/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts b/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts index 723b4df4e0..5dac2356ad 100644 --- a/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/deleteMe.handler.ts @@ -43,11 +43,11 @@ export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => { throw new HttpError({ statusCode: 400, message: ErrorCode.ThirdPartyIdentityProviderEnabled }); } - if (!user.password) { + if (!user.password?.hash) { throw new HttpError({ statusCode: 400, message: ErrorCode.UserMissingPassword }); } - const isCorrectPassword = await verifyPassword(input.password, user.password); + const isCorrectPassword = await verifyPassword(input.password, user.password.hash); if (!isCorrectPassword) { throw new HttpError({ statusCode: 403, message: ErrorCode.IncorrectPassword }); } diff --git a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts index 022dfe8cec..83b6cfa876 100644 --- a/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/changePassword.handler.ts @@ -25,16 +25,11 @@ export const changePasswordHandler = async ({ input, ctx }: ChangePasswordOption throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" }); } - const currentPasswordQuery = await prisma.user.findFirst({ - where: { - id: user.id, - }, - select: { - password: true, - }, + const currentPasswordQuery = await prisma.userPassword.findFirst({ + where: { userId: user.id }, }); - const currentPassword = currentPasswordQuery?.password; + const currentPassword = currentPasswordQuery?.hash; if (!currentPassword) { throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" }); @@ -54,12 +49,12 @@ export const changePasswordHandler = async ({ input, ctx }: ChangePasswordOption } const hashedPassword = await hashPassword(newPassword); - await prisma.user.update({ + await prisma.userPassword.update({ where: { - id: user.id, + userId: user.id, }, data: { - password: hashedPassword, + hash: hashedPassword, }, }); }; diff --git a/packages/trpc/server/routers/viewer/auth/verifyPassword.handler.ts b/packages/trpc/server/routers/viewer/auth/verifyPassword.handler.ts index e663141c33..08f6a7e984 100644 --- a/packages/trpc/server/routers/viewer/auth/verifyPassword.handler.ts +++ b/packages/trpc/server/routers/viewer/auth/verifyPassword.handler.ts @@ -14,17 +14,17 @@ type VerifyPasswordOptions = { }; export const verifyPasswordHandler = async ({ input, ctx }: VerifyPasswordOptions) => { - const user = await prisma.user.findUnique({ + const userPassword = await prisma.userPassword.findUnique({ where: { - id: ctx.user.id, + userId: ctx.user.id, }, }); - if (!user?.password) { + if (!userPassword?.hash) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } - const passwordsMatch = await verifyPassword(input.passwordInput, user.password); + const passwordsMatch = await verifyPassword(input.passwordInput, userPassword.hash); if (!passwordsMatch) { throw new TRPCError({ code: "UNAUTHORIZED" }); diff --git a/packages/trpc/server/routers/viewer/organizations/create.handler.ts b/packages/trpc/server/routers/viewer/organizations/create.handler.ts index 02aa40dc65..dc0691dc23 100644 --- a/packages/trpc/server/routers/viewer/organizations/create.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/create.handler.ts @@ -119,7 +119,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => { username: slugify(adminUsername), email: adminEmail, emailVerified: new Date(), - password: hashedPassword, + password: { create: { hash: hashedPassword } }, organizationId: organization.id, // Default schedule schedules: { diff --git a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts index bb762f1aa8..60e9de5f84 100644 --- a/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/setPassword.handler.ts @@ -30,12 +30,13 @@ export const setPasswordHandler = async ({ ctx, input }: UpdateOptions) => { }); if (!user) throw new TRPCError({ code: "BAD_REQUEST", message: "User not found" }); - if (!user.password) throw new TRPCError({ code: "BAD_REQUEST", message: "Password not set by default" }); + if (!user.password?.hash) + throw new TRPCError({ code: "BAD_REQUEST", message: "Password not set by default" }); const generatedPassword = createHash("md5") .update(`${user?.email ?? ""}${process.env.CALENDSO_ENCRYPTION_KEY}`) .digest("hex"); - const isCorrectPassword = await verifyPassword(generatedPassword, user?.password); + const isCorrectPassword = await verifyPassword(generatedPassword, user.password.hash); if (!isCorrectPassword) throw new TRPCError({ @@ -44,12 +45,12 @@ export const setPasswordHandler = async ({ ctx, input }: UpdateOptions) => { }); const hashedPassword = await hashPassword(newPassword); - await prisma.user.update({ + await prisma.userPassword.update({ where: { - id: ctx.user.id, + userId: ctx.user.id, }, data: { - password: hashedPassword, + hash: hashedPassword, }, }); diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 2c8793231e..bfe6a35453 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -13,7 +13,7 @@ import { UserRepository } from "@calcom/lib/server/repository/user"; import slugify from "@calcom/lib/slugify"; import { prisma } from "@calcom/prisma"; import type { Membership, Team } from "@calcom/prisma/client"; -import { Prisma, type User as UserType } from "@calcom/prisma/client"; +import { Prisma, type User as UserType, type UserPassword } from "@calcom/prisma/client"; import type { Profile as ProfileType } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -27,12 +27,13 @@ import type { InviteMemberOptions, TeamWithParent } from "./types"; const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] }); export type Invitee = Pick< UserType, - "id" | "email" | "username" | "password" | "identityProvider" | "completedOnboarding" + "id" | "email" | "username" | "identityProvider" | "completedOnboarding" >; export type UserWithMembership = Invitee & { teams?: Pick[]; profiles: ProfileType[]; + password: UserPassword | null; }; export async function checkPermissions({ @@ -570,7 +571,7 @@ export const sendExistingUserTeamInviteEmails = async ({ * Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template. * This only changes if the user is a CAL user and has not completed onboarding and has no password */ - if (!user.completedOnboarding && !user.password && user.identityProvider === "CAL") { + if (!user.completedOnboarding && !user.password?.hash && user.identityProvider === "CAL") { const token = randomBytes(32).toString("hex"); await prisma.verificationToken.create({ data: { diff --git a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts index 701f3cd6f7..e71ded20d7 100644 --- a/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/removeMember.handler.ts @@ -73,7 +73,6 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) = id: true, movedToProfileId: true, email: true, - password: true, username: true, completedOnboarding: true, },