refactor: migrates user password to it's own table (#13628)
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -652,7 +652,6 @@ type SupportedTestWorkflows = PrismaType.WorkflowCreateInput;
|
||||
|
||||
type CustomUserOptsKeys =
|
||||
| "username"
|
||||
| "password"
|
||||
| "completedOnboarding"
|
||||
| "locale"
|
||||
| "name"
|
||||
@@ -669,6 +668,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
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<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password?: string | null },
|
||||
page: Page
|
||||
) {
|
||||
// get locators
|
||||
@@ -812,7 +816,7 @@ export async function login(
|
||||
}
|
||||
|
||||
export async function apiLogin(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password: string | null },
|
||||
page: Page
|
||||
) {
|
||||
const csrfToken = await page
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -224,7 +224,6 @@ export const buildUser = <T extends Partial<UserPayload>>(
|
||||
invitedTo: null,
|
||||
locale: "en",
|
||||
metadata: null,
|
||||
password: null,
|
||||
role: "USER",
|
||||
schedules: [],
|
||||
selectedCalendars: [],
|
||||
|
||||
@@ -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 } }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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";
|
||||
@@ -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?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
+35
-7
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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<Membership, "userId" | "teamId" | "accepted" | "role">[];
|
||||
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: {
|
||||
|
||||
@@ -73,7 +73,6 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
|
||||
id: true,
|
||||
movedToProfileId: true,
|
||||
email: true,
|
||||
password: true,
|
||||
username: true,
|
||||
completedOnboarding: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user