revert: UserPassword (#13680)

This commit is contained in:
Alex van Andel
2024-02-14 12:36:29 +00:00
committed by GitHub
parent bb9f8e5d38
commit 0a68c6d048
25 changed files with 89 additions and 190 deletions
+4 -4
View File
@@ -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?.hash;
const currentPassword = user.password;
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.userPassword.update({
await prisma.user.update({
where: {
userId: user.id,
id: user.id,
},
data: {
hash: hashedPassword,
password: hashedPassword,
},
});
+1 -5
View File
@@ -41,11 +41,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
email: maybeRequest.email,
},
data: {
password: {
update: {
hash: hashedPassword,
},
},
password: hashedPassword,
emailVerified: new Date(),
},
});
+1 -1
View File
@@ -41,7 +41,7 @@ async function handler(req: NextApiRequest) {
data: {
username,
email: userEmail,
password: { create: { hash: hashedPassword } },
password: hashedPassword,
role: "ADMIN",
name: parsedQuery.data.full_name,
emailVerified: new Date(),
@@ -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 }, include: { password: true } });
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
if (!user) {
console.error(`Session references user that no longer exists.`);
return res.status(401).json({ message: "Not authenticated" });
}
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
if (!user.password && 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?.hash && user.identityProvider === IdentityProvider.CAL) {
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
if (user.password && user.identityProvider === IdentityProvider.CAL) {
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
@@ -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 }, include: { password: true } });
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
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?.hash) {
if (!user.password) {
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.hash);
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
@@ -86,8 +86,7 @@ testBothFutureAndLegacyRoutes.describe("Forgot password", async () => {
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const updatedPassword = updatedUser.password!.hash;
expect(await verifyPassword(newPassword, updatedPassword)).toBeTruthy();
expect(await verifyPassword(newPassword, updatedUser.password!)).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}`);
+4 -8
View File
@@ -652,6 +652,7 @@ type SupportedTestWorkflows = PrismaType.WorkflowCreateInput;
type CustomUserOptsKeys =
| "username"
| "password"
| "completedOnboarding"
| "locale"
| "name"
@@ -668,7 +669,6 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
useExactUsername?: boolean;
roleInOrganization?: MembershipRole;
schedule?: Schedule;
password?: string | null;
};
// creates the actual user in the db.
@@ -690,11 +690,7 @@ const createUser = (
username: uname,
name: opts?.name,
email: opts?.email ?? `${uname}@example.com`,
password: {
create: {
hash: hashPassword(uname),
},
},
password: hashPassword(uname),
emailVerified: new Date(),
completedOnboarding: opts?.completedOnboarding ?? true,
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
@@ -795,7 +791,7 @@ async function confirmPendingPayment(page: Page) {
// login using a replay of an E2E routine.
export async function login(
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password?: string | null },
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
page: Page
) {
// get locators
@@ -816,7 +812,7 @@ export async function login(
}
export async function apiLogin(
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password: string | null },
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
page: Page
) {
const csrfToken = await page
+15 -20
View File
@@ -42,8 +42,8 @@ const ORGANIZATIONS_AUTOLINK =
const usernameSlug = (username: string) => `${slugify(username)}-${randomString(6).toLowerCase()}`;
const loginWithTotp = async (email: string) =>
`/auth/login?totp=${await (await import("./signJwt")).default({ email })}`;
const loginWithTotp = async (user: { email: string }) =>
`/auth/login?totp=${await (await import("./signJwt")).default({ email: user.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?.hash && user.identityProvider == IdentityProvider.CAL) {
if (!user.password && user.identityProvider == IdentityProvider.CAL) {
throw new Error(ErrorCode.IncorrectEmailPassword);
}
if (!user.password?.hash && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
throw new Error(ErrorCode.IncorrectEmailPassword);
}
if (user.password?.hash && !credentials.totpCode) {
if (!user.password?.hash) {
if (user.password && !credentials.totpCode) {
if (!user.password) {
throw new Error(ErrorCode.IncorrectEmailPassword);
}
const isCorrectPassword = await verifyPassword(credentials.password, user.password.hash);
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
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.email);
return loginWithTotp(existingUser);
} 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.email);
return loginWithTotp(existingUser);
} else {
return true;
}
@@ -752,9 +752,6 @@ export const AUTH_OPTIONS: AuthOptions = {
mode: "insensitive",
},
},
include: {
password: true,
},
});
if (existingUserWithEmail) {
@@ -765,7 +762,7 @@ export const AUTH_OPTIONS: AuthOptions = {
existingUserWithEmail.identityProvider !== IdentityProvider.CAL
) {
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail.email);
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
@@ -773,7 +770,7 @@ export const AUTH_OPTIONS: AuthOptions = {
// check if user was invited
if (
!existingUserWithEmail.password?.hash &&
!existingUserWithEmail.password &&
!existingUserWithEmail.emailVerified &&
!existingUserWithEmail.username
) {
@@ -795,7 +792,7 @@ export const AUTH_OPTIONS: AuthOptions = {
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail.email);
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
@@ -810,16 +807,14 @@ export const AUTH_OPTIONS: AuthOptions = {
where: { email: existingUserWithEmail.email },
// also update email to the IdP email
data: {
password: {
delete: true,
},
password: null,
email: user.email,
identityProvider: idP,
identityProviderId: account.providerAccountId,
},
});
if (existingUserWithEmail.twoFactorEnabled) {
return loginWithTotp(existingUserWithEmail.email);
return loginWithTotp(existingUserWithEmail);
} else {
return true;
}
@@ -858,7 +853,7 @@ export const AUTH_OPTIONS: AuthOptions = {
await calcomAdapter.linkAccount(linkAccountNewUserData);
if (account.twoFactorEnabled) {
return loginWithTotp(newUser.email);
return loginWithTotp(newUser);
} else {
return true;
}
@@ -138,22 +138,18 @@ 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,
@@ -184,7 +180,7 @@ async function handler(req: RequestWithUsernameStatus, res: NextApiResponse) {
data: {
username,
email,
password: { create: { hash: hashedPassword } },
password: hashedPassword,
metadata: {
stripeCustomerId: customer.id,
checkoutSessionId,
@@ -79,19 +79,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
where: { email: userEmail },
update: {
username: correctedUsername,
password: {
upsert: {
create: { hash: hashedPassword },
update: { hash: hashedPassword },
},
},
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username: correctedUsername,
email: userEmail,
password: { create: { hash: hashedPassword } },
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
@@ -133,19 +128,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
where: { email: userEmail },
update: {
username: correctedUsername,
password: {
upsert: {
create: { hash: hashedPassword },
update: { hash: hashedPassword },
},
},
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username: correctedUsername,
email: userEmail,
password: { create: { hash: hashedPassword } },
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
@@ -67,7 +67,9 @@ const authedAdminProcedureWithRequestedUser = authedAdminProcedure.use(async ({
return next({
ctx: {
user: ctx.user,
requestedUser: user,
requestedUser:
/** Don't leak the password */
exclude(user, ["password"]),
},
});
});
@@ -82,7 +84,8 @@ export const userAdminRouter = router({
// TODO: Add search, pagination, etc.
const users = await prisma.user.findMany();
return users.map((user) => ({
...user,
/** Don't leak the password */
...exclude(user, ["password"]),
/**
* FIXME: This should be either a prisma extension or middleware
* @see https://www.prisma.io/docs/concepts/components/prisma-client/middleware
+1
View File
@@ -229,6 +229,7 @@ export const buildUser = <T extends Partial<UserPayload>>(
invitedTo: null,
locale: "en",
metadata: null,
password: null,
role: "USER",
schedules: [],
selectedCalendars: [],
+3 -1
View File
@@ -56,7 +56,9 @@ export const validateAndGetCorrectedUsernameAndEmail = async ({
{
OR: [
{ emailVerified: { not: null } },
{ AND: [{ password: { isNot: null } }, { username: { not: null } }] },
{
AND: [{ password: { not: null } }, { username: { not: null } }],
},
],
},
],
@@ -1,32 +0,0 @@
/*
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
WHERE
u."password" IS NOT NULL;
-- TODO for next migration.
-- ALTER TABLE "users" DROP COLUMN "password";
+1 -8
View File
@@ -194,13 +194,6 @@ 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())
@@ -209,7 +202,7 @@ model User {
/// @zod.email()
email String
emailVerified DateTime?
password UserPassword?
password String?
bio String?
avatar String?
avatarUrl String?
+3 -15
View File
@@ -78,11 +78,7 @@ async function main() {
insightsAdmin = await prisma.user.create({
data: {
email: "insights@example.com",
password: {
create: {
hash: await hashPassword("insightsinsightsinsights...!"),
},
},
password: await hashPassword("insightsinsightsinsights...!"),
name: "Insights Admin",
role: "ADMIN",
username: "insights-admin",
@@ -106,11 +102,7 @@ async function main() {
insightsUser = await prisma.user.create({
data: {
email: "insightsuser@example.com",
password: {
create: {
hash: await hashPassword("insightsuser"),
},
},
password: await hashPassword("insightsuser"),
name: "Insights User",
role: "USER",
username: "insights-user",
@@ -423,11 +415,7 @@ async function createPerformanceData() {
const email = `insightsuser${timestamp}@example.com`;
const insightsUser = {
email,
password: {
create: {
hash: await hashPassword("insightsuser"),
},
},
password: await hashPassword("insightsuser"),
name: `Insights User ${timestamp}`,
username: `insights-user-${timestamp}`,
completedOnboarding: true,
+1 -5
View File
@@ -40,11 +40,7 @@ export async function createUserAndEventType({
}) {
const userData = {
...user,
password: {
create: {
hash: await hashPassword(user.password),
},
},
password: await hashPassword(user.password),
emailVerified: new Date(),
completedOnboarding: user.completedOnboarding ?? true,
locale: "en",
+7 -35
View File
@@ -122,11 +122,7 @@ async function createOrganizationAndAddMembersAndTeams({
data: {
...member.memberData,
emailVerified: new Date(),
password: {
create: {
hash: await hashPassword(member.memberData.password.create?.hash || ""),
},
},
password: await hashPassword(member.memberData.password),
},
})),
inTeams: member.inTeams,
@@ -153,11 +149,7 @@ async function createOrganizationAndAddMembersAndTeams({
name: user.name,
email: user.email,
emailVerified: new Date(),
password: {
create: {
hash: await hashPassword(user.username),
},
},
password: await hashPassword(user.username),
},
});
}),
@@ -263,11 +255,7 @@ async function createOrganizationAndAddMembersAndTeams({
await prisma.user.create({
data: {
...nonOrgMember,
password: {
create: {
hash: await hashPassword(nonOrgMember.username),
},
},
password: await hashPassword(nonOrgMember.password),
emailVerified: new Date(),
},
})
@@ -819,11 +807,7 @@ async function main() {
{
memberData: {
email: "owner1-acme@example.com",
password: {
create: {
hash: "owner1-acme",
},
},
password: "owner1-acme",
username: "owner1-acme",
name: "Owner 1",
},
@@ -852,11 +836,7 @@ async function main() {
nonOrgMembers: [
{
email: "non-acme-member-1@example.com",
password: {
create: {
hash: "non-acme-member-1",
},
},
password: "non-acme-member-1",
username: "non-acme-member-1",
name: "NonAcme Member1",
},
@@ -886,11 +866,7 @@ async function main() {
{
memberData: {
email: "owner1-dunder@example.com",
password: {
create: {
hash: "owner1-dunder",
},
},
password: "owner1-dunder",
username: "owner1-dunder",
name: "Owner 1",
},
@@ -919,11 +895,7 @@ async function main() {
nonOrgMembers: [
{
email: "non-dunder-member-1@example.com",
password: {
create: {
hash: "non-dunder-member-1",
},
},
password: "non-dunder-member-1",
username: "non-dunder-member-1",
name: "NonDunder Member1",
},
@@ -43,11 +43,11 @@ export const deleteMeHandler = async ({ ctx, input }: DeleteMeOptions) => {
throw new HttpError({ statusCode: 400, message: ErrorCode.ThirdPartyIdentityProviderEnabled });
}
if (!user.password?.hash) {
if (!user.password) {
throw new HttpError({ statusCode: 400, message: ErrorCode.UserMissingPassword });
}
const isCorrectPassword = await verifyPassword(input.password, user.password.hash);
const isCorrectPassword = await verifyPassword(input.password, user.password);
if (!isCorrectPassword) {
throw new HttpError({ statusCode: 403, message: ErrorCode.IncorrectPassword });
}
@@ -25,11 +25,16 @@ export const changePasswordHandler = async ({ input, ctx }: ChangePasswordOption
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
}
const currentPasswordQuery = await prisma.userPassword.findFirst({
where: { userId: user.id },
const currentPasswordQuery = await prisma.user.findFirst({
where: {
id: user.id,
},
select: {
password: true,
},
});
const currentPassword = currentPasswordQuery?.hash;
const currentPassword = currentPasswordQuery?.password;
if (!currentPassword) {
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
@@ -49,12 +54,12 @@ export const changePasswordHandler = async ({ input, ctx }: ChangePasswordOption
}
const hashedPassword = await hashPassword(newPassword);
await prisma.userPassword.update({
await prisma.user.update({
where: {
userId: user.id,
id: user.id,
},
data: {
hash: hashedPassword,
password: hashedPassword,
},
});
};
@@ -14,17 +14,17 @@ type VerifyPasswordOptions = {
};
export const verifyPasswordHandler = async ({ input, ctx }: VerifyPasswordOptions) => {
const userPassword = await prisma.userPassword.findUnique({
const user = await prisma.user.findUnique({
where: {
userId: ctx.user.id,
id: ctx.user.id,
},
});
if (!userPassword?.hash) {
if (!user?.password) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
const passwordsMatch = await verifyPassword(input.passwordInput, userPassword.hash);
const passwordsMatch = await verifyPassword(input.passwordInput, user.password);
if (!passwordsMatch) {
throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -119,7 +119,7 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
username: slugify(adminUsername),
email: adminEmail,
emailVerified: new Date(),
password: { create: { hash: hashedPassword } },
password: hashedPassword,
organizationId: organization.id,
// Default schedule
schedules: {
@@ -30,13 +30,12 @@ export const setPasswordHandler = async ({ ctx, input }: UpdateOptions) => {
});
if (!user) throw new TRPCError({ code: "BAD_REQUEST", message: "User not found" });
if (!user.password?.hash)
throw new TRPCError({ code: "BAD_REQUEST", message: "Password not set by default" });
if (!user.password) 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.hash);
const isCorrectPassword = await verifyPassword(generatedPassword, user?.password);
if (!isCorrectPassword)
throw new TRPCError({
@@ -45,12 +44,12 @@ export const setPasswordHandler = async ({ ctx, input }: UpdateOptions) => {
});
const hashedPassword = await hashPassword(newPassword);
await prisma.userPassword.update({
await prisma.user.update({
where: {
userId: ctx.user.id,
id: ctx.user.id,
},
data: {
hash: hashedPassword,
password: hashedPassword,
},
});
@@ -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, type UserPassword } from "@calcom/prisma/client";
import { Prisma, type User as UserType } 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,13 +27,12 @@ import type { InviteMemberOptions, TeamWithParent } from "./types";
const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] });
export type Invitee = Pick<
UserType,
"id" | "email" | "username" | "identityProvider" | "completedOnboarding"
"id" | "email" | "username" | "password" | "identityProvider" | "completedOnboarding"
>;
export type UserWithMembership = Invitee & {
teams?: Pick<Membership, "userId" | "teamId" | "accepted" | "role">[];
profiles: ProfileType[];
password: UserPassword | null;
};
export async function checkPermissions({
@@ -571,7 +570,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?.hash && user.identityProvider === "CAL") {
if (!user.completedOnboarding && !user.password && user.identityProvider === "CAL") {
const token = randomBytes(32).toString("hex");
await prisma.verificationToken.create({
data: {
@@ -73,6 +73,7 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
id: true,
movedToProfileId: true,
email: true,
password: true,
username: true,
completedOnboarding: true,
},