This commit is contained in:
zomars 2023-01-17 14:45:53 -07:00
parent 94e6007245
commit 1c12339479
114 changed files with 764 additions and 1331 deletions

View File

@ -1,512 +1 @@
import { IdentityProvider, UserPermissionRole } from "@prisma/client";
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import NextAuth, { Session } from "next-auth";
import { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import CalComAdapter from "@calcom/lib/auth/next-auth-custom-adapter";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}";
const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
const IS_SAML_LOGIN_ENABLED = !!(process.env.SAML_DATABASE_URL && process.env.SAML_ADMINS);
const NEXTAUTH_URL = process.env.NEXTAUTH_URL || WEBAPP_URL;
const transporter = nodemailer.createTransport<TransportOptions>({
...(serverConfig.transport as TransportOptions),
} as TransportOptions);
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
name: "Cal.com",
type: "credentials",
credentials: {
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
},
async authorize(credentials) {
if (!credentials) {
console.error(`For some reason credentials are missing`);
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email.toLowerCase(),
},
select: {
role: true,
id: true,
username: true,
name: true,
email: true,
identityProvider: true,
password: true,
twoFactorEnabled: true,
twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
},
});
if (!user) {
throw new Error(ErrorCode.UserNotFound);
}
if (user.identityProvider !== IdentityProvider.CAL) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
}
if (!user.password) {
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);
}
if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
const isValidToken = authenticator.check(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: "INACTIVE_ADMIN",
belongsToActiveTeam: hasActiveTeams,
};
}
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
belongsToActiveTeam: hasActiveTeams,
};
},
}),
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
providers.push(
GoogleProvider({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
})
);
}
if (isSAMLLoginEnabled) {
providers.push({
id: "saml",
name: "BoxyHQ",
type: "oauth",
version: "2.0",
checks: ["pkce", "state"],
authorization: {
url: `${NEXTAUTH_URL}/api/auth/saml/authorize`,
params: {
scope: "",
response_type: "code",
provider: "saml",
},
},
token: {
url: `${NEXTAUTH_URL}/api/auth/saml/token`,
params: { grant_type: "authorization_code" },
},
userinfo: `${NEXTAUTH_URL}/api/auth/saml/userinfo`,
profile: (profile) => {
return {
id: profile.id || "",
firstName: profile.firstName || "",
lastName: profile.lastName || "",
email: profile.email || "",
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email_verified: true,
};
},
options: {
clientId: "dummy",
clientSecret: "dummy",
},
});
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
providers.push(
EmailProvider({
type: "email",
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
sendVerificationRequest: ({ identifier, url }) => {
const originalUrl = new URL(url);
const webappUrl = new URL(NEXTAUTH_URL);
if (originalUrl.origin !== webappUrl.origin) {
url = url.replace(originalUrl.origin, webappUrl.origin);
}
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
encoding: "utf8",
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier,
subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({
base_url: NEXTAUTH_URL,
signin_url: url,
email: identifier,
}),
});
},
})
);
}
const calcomAdapter = CalComAdapter(prisma);
export default NextAuth({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
adapter: calcomAdapter,
session: {
strategy: "jwt",
},
cookies: defaultCookies(NEXTAUTH_URL?.startsWith("https://")),
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify",
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers,
callbacks: {
async jwt({ token, user, account }) {
const autoMergeIdentities = async () => {
const existingUser = await prisma.user.findFirst({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
where: { email: token.email! },
select: {
id: true,
username: true,
name: true,
email: true,
role: true,
},
});
if (!existingUser) {
return token;
}
return {
...existingUser,
...token,
};
};
if (!user) {
return await autoMergeIdentities();
}
if (account && account.type === "credentials") {
return {
...token,
id: user.id,
name: user.name,
username: user.username,
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
};
}
// The arguments above are from the provider so we need to look up the
// user based on those values in order to construct a JWT.
if (account && account.type === "oauth" && account.provider && account.providerAccountId) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
identityProvider: idP,
},
{
identityProviderId: account.providerAccountId as string,
},
],
},
});
if (!existingUser) {
return await autoMergeIdentities();
}
return {
...token,
id: existingUser.id,
name: existingUser.name,
username: existingUser.username,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
};
}
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const calendsoSession: Session = {
...session,
hasValidLicense,
user: {
...session.user,
id: token.id as number,
name: token.name,
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
},
};
return calendsoSession;
},
async signIn(params) {
const { user, account, profile } = params;
if (account?.provider === "email") {
return true;
}
// In this case we've already verified the credentials in the authorize
// callback so we can sign the user in.
if (account?.type === "credentials") {
return true;
}
if (account?.type !== "oauth") {
return false;
}
if (!user.email) {
return false;
}
if (!user.name) {
return false;
}
if (account?.provider) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error TODO validate email_verified key on profile
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
if (!user.email_verified) {
return "/auth/error?error=unverified-email";
}
// Only google oauth on this path
const provider = account.provider.toUpperCase() as IdentityProvider;
const existingUser = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: account.provider,
},
},
},
where: {
identityProvider: provider,
identityProviderId: account.providerAccountId,
},
});
if (existingUser) {
// In this case there's an existing user and their email address
// hasn't changed since they last logged in.
if (existingUser.email === user.email) {
try {
// If old user without Account entry we link their google account
if (existingUser.accounts.length === 0) {
const linkAccountWithUserData = { ...account, userId: existingUser.id };
await calcomAdapter.linkAccount(linkAccountWithUserData);
}
} catch (error) {
if (error instanceof Error) {
console.error("Error while linking account of already existing user");
}
}
return true;
}
// If the email address doesn't match, check if an account already exists
// with the new email address. If it does, for now we return an error. If
// not, update the email of their account and log them in.
const userWithNewEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (!userWithNewEmail) {
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
return true;
} else {
return "/auth/error?error=new-email-conflict";
}
}
// If there's no existing user for this identity provider and id, create
// a new account. If an account already exists with the incoming email
// address return an error for now.
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (existingUserWithEmail) {
// if self-hosted then we can allow auto-merge of identity providers if email is verified
if (!hostedCal && existingUserWithEmail.emailVerified) {
return true;
}
// check if user was invited
if (
!existingUserWithEmail.password &&
!existingUserWithEmail.emailVerified &&
!existingUserWithEmail.username
) {
await prisma.user.update({
where: { email: user.email },
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
return true;
}
if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
return "/auth/error?error=use-password-login";
}
return "/auth/error?error=use-identity-login";
}
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
const linkAccountNewUserData = { ...account, userId: newUser.id };
await calcomAdapter.linkAccount(linkAccountNewUserData);
return true;
}
return false;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same domain
else if (new URL(url).hostname === new URL(NEXTAUTH_URL).hostname) return url;
return baseUrl;
},
},
});
export { default } from "@calcom/features/auth/api/dynamicHandler";

View File

@ -1,82 +0,0 @@
import { ResetPasswordRequest } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import prisma from "@calcom/prisma";
import { getTranslation } from "@calcom/server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");
if (req.method !== "POST") {
return res.status(405).end();
}
try {
const maybeUser = await prisma.user.findUnique({
where: {
email: req.body?.email?.toLowerCase(),
},
select: {
name: true,
identityProvider: true,
email: true,
},
});
if (!maybeUser) {
// Don't leak information about whether an email is registered or not
return res
.status(200)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}
const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({
where: {
email: maybeUser.email,
expires: {
gt: new Date(),
},
},
});
let passwordRequest: ResetPasswordRequest;
if (maybePreviousRequest && maybePreviousRequest?.length >= 1) {
passwordRequest = maybePreviousRequest[0];
} else {
const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate();
const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({
data: {
email: maybeUser.email,
expires: expiry,
},
});
passwordRequest = createdResetPasswordRequest;
}
const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`;
await sendPasswordResetEmail({
language: t,
user: maybeUser,
resetLink,
});
/** So we can test the password reset flow on CI */
if (process.env.NEXT_PUBLIC_IS_E2E) {
return res.status(201).json({
message: "If this email exists in our system, you should receive a Reset email.",
resetLink,
});
} else {
return res
.status(201)
.json({ message: "If this email exists in our system, you should receive a Reset email." });
}
} catch (reason) {
// console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
}
}

View File

@ -1,55 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(400).json({ message: "" });
}
try {
const rawPassword = req.body?.password;
const rawRequestId = req.body?.requestId;
if (!rawPassword || !rawRequestId) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const maybeRequest = await prisma.resetPasswordRequest.findUnique({
where: {
id: rawRequestId,
},
});
if (!maybeRequest) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const maybeUser = await prisma.user.findUnique({
where: {
email: maybeRequest.email,
},
});
if (!maybeUser) {
return res.status(400).json({ message: "Couldn't find an account for this email" });
}
const hashedPassword = await hashPassword(rawPassword);
await prisma.user.update({
where: {
id: maybeUser.id,
},
data: {
password: hashedPassword,
},
});
return res.status(201).json({ message: "Password reset." });
} catch (reason) {
console.error(reason);
return res.status(500).json({ message: "Unable to create password reset request" });
}
}

View File

@ -1,86 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode, getSession, verifyPassword } from "@calcom/lib/auth";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
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) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
if (!user.twoFactorEnabled) {
return res.json({ message: "Two factor disabled" });
}
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
// if user has 2fa
if (user.twoFactorEnabled) {
if (!req.body.code) {
return res.status(400).json({ error: ErrorCode.SecondFactorRequired });
// throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
// If user has 2fa enabled, check if body.code is correct
const isValidToken = authenticator.check(req.body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
// throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
// If it is, disable users 2fa
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
twoFactorEnabled: false,
twoFactorSecret: null,
},
});
return res.json({ message: "Two factor disabled" });
}

View File

@ -1,65 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode, getSession } from "@calcom/lib/auth";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
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.twoFactorEnabled) {
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
}
if (!user.twoFactorSecret) {
return res.status(400).json({ error: ErrorCode.TwoFactorSetupRequired });
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with two factor setup.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const isValidToken = authenticator.check(req.body.code, secret);
if (!isValidToken) {
return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode });
}
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
twoFactorEnabled: true,
},
});
return res.json({ message: "Two-factor enabled" });
}

View File

@ -1,72 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
import { ErrorCode, getSession, verifyPassword } from "@calcom/lib/auth";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
if (!session.user?.id) {
console.error("Session is missing a user id.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
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.identityProvider !== IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
}
if (!user.password) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
if (user.twoFactorEnabled) {
return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled });
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error("Missing encryption key; cannot proceed with two factor setup.");
return res.status(500).json({ error: ErrorCode.InternalServerError });
}
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
if (!isCorrectPassword) {
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
}
// This generates a secret 32 characters in length. Do not modify the number of
// bytes without updating the sanity checks in the enable and login endpoints.
const secret = authenticator.generateSecret(20);
await prisma.user.update({
where: {
id: session.user.id,
},
data: {
twoFactorEnabled: false,
twoFactorSecret: symmetricEncrypt(secret, process.env.CALENDSO_ENCRYPTION_KEY),
},
});
const name = user.email || user.username || user.id.toString();
const keyUri = authenticator.keyuri(name, "Cal", secret);
const dataUri = await qrcode.toDataURL(keyUri);
return res.json({ secret, keyUri, dataUri });
}

1
apps/auth/pages/auth Symbolic link
View File

@ -0,0 +1 @@
../../../packages/features/auth/pages

View File

@ -1,3 +1,4 @@
import { UserPermissionRole } from "@prisma/client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import React, { ComponentProps, useEffect } from "react";
@ -6,8 +7,6 @@ import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import Shell from "@calcom/features/shell/Shell";
import { ErrorBoundary } from "@calcom/ui";
import { UserPermissionRole } from ".prisma/client";
export default function AdminLayout({
children,
...rest

View File

@ -1,6 +1,6 @@
import { SyntheticEvent, useState } from "react";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, showToast } from "@calcom/ui";

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";

View File

@ -1,7 +1,7 @@
import React, { BaseSyntheticEvent, useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";

View File

@ -1,7 +1,7 @@
import React, { BaseSyntheticEvent, useState } from "react";
import { useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { ErrorCode } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";

View File

@ -3,24 +3,24 @@ import { compare, hash } from "bcryptjs";
import { Session } from "next-auth";
import { getSession as getSessionInner, GetSessionParams } from "next-auth/react";
/** @deprecated use the one from `@calcom/lib/auth` */
/** @deprecated use the one from `@calcom/features/auth/lib` */
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}
/** @deprecated use the one from `@calcom/lib/auth` */
/** @deprecated use the one from `@calcom/features/auth/lib` */
export async function verifyPassword(password: string, hashedPassword: string) {
const isValid = await compare(password, hashedPassword);
return isValid;
}
/** @deprecated use the one from `@calcom/lib/auth` */
/** @deprecated use the one from `@calcom/features/auth/lib` */
export async function getSession(options: GetSessionParams): Promise<Session | null> {
const session = await getSessionInner(options);
// that these are equal are ensured in `[...nextauth]`'s callback
return session as Session | null;
}
/** @deprecated use the one from `@calcom/lib/auth` */
/** @deprecated use the one from `@calcom/features/auth/lib` */
export enum ErrorCode {
UserNotFound = "user-not-found",
IncorrectPassword = "incorrect-password",
@ -36,7 +36,7 @@ export enum ErrorCode {
RateLimitExceeded = "rate-limit-exceeded",
InvalidPassword = "invalid-password",
}
/** @deprecated use the one from `@calcom/lib/auth` */
/** @deprecated use the one from `@calcom/features/auth/lib` */
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
[IdentityProvider.CAL]: "Cal",
[IdentityProvider.GOOGLE]: "Google",

View File

@ -23,7 +23,6 @@
"yarn": ">=1.19.0 < 2.0.0"
},
"dependencies": {
"@boxyhq/saml-jackson": "1.3.6",
"@calcom/app-store": "*",
"@calcom/app-store-cli": "*",
"@calcom/core": "*",

View File

@ -22,6 +22,7 @@ import defaultEvents, {
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { ssrInit } from "@calcom/lib/server/ssr";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
@ -33,8 +34,6 @@ import { EmbedProps } from "@lib/withEmbedSsr";
import { AvatarSSR } from "@components/ui/AvatarSSR";
import { ssrInit } from "@server/lib/ssr";
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group

View File

@ -10,6 +10,7 @@ import {
getUsernameList,
} from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { bookEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -20,8 +21,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type BookPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: BookPageProps) {

View File

@ -0,0 +1 @@
export { default } from "@calcom/features/auth/api/dynamicHandler";

View File

@ -1 +0,0 @@
export { default } from "@calcom/auth/pages/api/auth/[...nextauth]";

View File

@ -1,66 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { getSession } from "@lib/auth";
import { ErrorCode, hashPassword, verifyPassword } from "../../../lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });
if (!session || !session.user || !session.user.email) {
res.status(401).json({ message: "Not authenticated" });
return;
}
const user = await prisma.user.findFirst({
where: {
email: session.user.email,
},
select: {
id: true,
password: true,
identityProvider: true,
},
});
if (!user) {
res.status(404).json({ message: "User not found" });
return;
}
if (user.identityProvider !== IdentityProvider.CAL) {
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
}
const oldPassword = req.body.oldPassword;
const newPassword = req.body.newPassword;
const currentPassword = user.password;
if (!currentPassword) {
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
}
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
if (!passwordsMatch) {
return res.status(403).json({ error: ErrorCode.IncorrectPassword });
}
if (oldPassword === newPassword) {
return res.status(400).json({ error: ErrorCode.NewPasswordMatchesOld });
}
const hashedPassword = await hashPassword(newPassword);
await prisma.user.update({
where: {
id: user.id,
},
data: {
password: hashedPassword,
},
});
res.status(200).json({ message: "Password updated successfully" });
}

View File

@ -1,24 +0,0 @@
import { OAuthReq } from "@boxyhq/saml-jackson";
import { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@lib/core/http/error";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
if (req.method !== "GET") {
return res.status(400).send("Method not allowed");
}
try {
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq);
return res.redirect(302, redirect_url as string);
} catch (err) {
const { message, statusCode = 500 } = err as HttpError;
return res.status(statusCode).send(message);
}
}

View File

@ -1,14 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
const { oauthController } = await jackson();
const { redirect_url } = await oauthController.samlResponse(req.body);
if (redirect_url) return res.redirect(302, redirect_url);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@ -1,13 +0,0 @@
import { NextApiRequest } from "next";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function postHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
return await oauthController.token(req.body);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
});

View File

@ -1,34 +0,0 @@
import { NextApiRequest } from "next";
import z from "zod";
import jackson from "@calcom/features/ee/sso/lib/jackson";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const extractAuthToken = (req: NextApiRequest) => {
const authHeader = req.headers["authorization"];
const parts = (authHeader || "").split(" ");
if (parts.length > 1) return parts[1];
// check for query param
let arr: string[] = [];
const { access_token } = requestQuery.parse(req.query);
arr = arr.concat(access_token);
if (arr[0].length > 0) return arr[0];
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
};
const requestQuery = z.object({
access_token: z.string(),
});
async function getHandler(req: NextApiRequest) {
const { oauthController } = await jackson();
const token = extractAuthToken(req);
return await oauthController.userInfo(token);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});

View File

@ -1,58 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/lib/auth";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
const querySchema = z.object({
username: z
.string()
.refine((val) => val.trim().length >= 1, { message: "Please enter at least one character" }),
full_name: z.string().min(3, "Please enter at least 3 characters"),
email_address: z.string().email({ message: "Please enter a valid email" }),
password: z.string().refine((val) => isPasswordValid(val.trim(), false, true), {
message:
"The password must be a minimum of 15 characters long containing at least one number and have a mixture of uppercase and lowercase letters",
}),
});
async function handler(req: NextApiRequest) {
const userCount = await prisma.user.count();
if (userCount !== 0) {
throw new HttpError({ statusCode: 400, message: "No setup needed." });
}
const parsedQuery = querySchema.safeParse(req.body);
if (!parsedQuery.success) {
throw new HttpError({ statusCode: 422, message: parsedQuery.error.message });
}
const username = slugify(parsedQuery.data.username.trim());
const userEmail = parsedQuery.data.email_address.toLowerCase();
const hashedPassword = await hashPassword(parsedQuery.data.password);
await prisma.user.create({
data: {
username,
email: userEmail,
password: hashedPassword,
role: "ADMIN",
name: parsedQuery.data.full_name,
emailVerified: new Date(),
locale: "en", // TODO: We should revisit this
identityProvider: IdentityProvider.CAL,
},
});
return { message: "First admin user created successfuly." };
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});

View File

@ -1,100 +0,0 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";
import slugify from "@lib/slugify";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return;
}
if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true") {
res.status(403).json({ message: "Signup is disabled" });
return;
}
const data = req.body;
const { email, password } = data;
const username = slugify(data.username);
const userEmail = email.toLowerCase();
if (!username) {
res.status(422).json({ message: "Invalid username" });
return;
}
if (!userEmail || !userEmail.includes("@")) {
res.status(422).json({ message: "Invalid email" });
return;
}
if (!password || password.trim().length < 7) {
res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." });
return;
}
// There is actually an existingUser if username matches
// OR if email matches and both username and password are set
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ username },
{
AND: [{ email: userEmail }, { password: { not: null } }, { username: { not: null } }],
},
],
},
});
if (existingUser) {
const message: string =
existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
const hashedPassword = await hashPassword(password);
const user = await prisma.user.upsert({
where: { email: userEmail },
update: {
username,
password: hashedPassword,
emailVerified: new Date(Date.now()),
identityProvider: IdentityProvider.CAL,
},
create: {
username,
email: userEmail,
password: hashedPassword,
identityProvider: IdentityProvider.CAL,
},
});
// If user has been invitedTo a team, we accept the membership
if (user.invitedTo) {
const team = await prisma.team.findFirst({
where: { id: user.invitedTo },
});
if (team) {
const membership = await prisma.membership.update({
where: {
userId_teamId: { userId: user.id, teamId: user.invitedTo },
},
data: {
accepted: true,
},
});
// Sync Services: Close.com
closeComUpsertTeamUser(team, user, membership.role);
}
}
res.status(201).json({ message: "Created user" });
}

View File

@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {

View File

@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
async function handler(req: NextApiRequest & { userId?: number }) {

View File

@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { fetcher } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const getAccessLinkSchema = z.union([

View File

@ -2,7 +2,7 @@ import { NextApiRequest, NextApiResponse } from "next";
import type { Session } from "next-auth";
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import prisma from "@calcom/prisma";
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";

View File

@ -3,14 +3,13 @@ import { useRouter } from "next/router";
import RoutingFormsRoutingConfig from "@calcom/app-store/ee/routing-forms/pages/app-routing.config";
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
import { ssrInit } from "@calcom/lib/server/ssr";
import prisma from "@calcom/prisma";
import { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
import { AppProps } from "@lib/app-providers";
import { getSession } from "@lib/auth";
import { ssrInit } from "@server/lib/ssr";
type AppPageType = {
getServerSideProps: AppGetServerSideProps;
// A component than can accept any properties

View File

@ -3,7 +3,7 @@ import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
import { classNames } from "@calcom/lib";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/client";
import { inferSSRProps } from "@calcom/types/inferSSRProps";

1
apps/web/pages/auth Symbolic link
View File

@ -0,0 +1 @@
../../../packages/features/auth/pages

View File

@ -8,6 +8,7 @@ import Shell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
@ -31,8 +32,6 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
import { ssrInit } from "@server/lib/ssr";
const querySchema = z.object({
schedule: stringOrNumber,
});

View File

@ -4,6 +4,7 @@ import { GetServerSidePropsContext } from "next";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { EmptyScreen, Icon, showToast } from "@calcom/ui";
@ -12,8 +13,6 @@ import { HttpError } from "@lib/core/http/error";
import SkeletonLoader from "@components/availability/SkeletonLoader";
import { ssrInit } from "@server/lib/ssr";
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale();
const utils = trpc.useContext();

View File

@ -43,7 +43,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
function redirectToExternalUrl(url: string) {
window.parent.location.href = url;

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { ssrInit } from "@calcom/lib/server/ssr";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -14,8 +15,6 @@ import { EmbedProps } from "@lib/withEmbedSsr";
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
import { ssrInit } from "@server/lib/ssr";
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
export default function Type(props: DynamicAvailabilityPageProps) {

View File

@ -2,6 +2,7 @@ import { GetServerSidePropsContext } from "next";
import { JSONObject } from "superjson/dist/types";
import { parseRecurringEvent } from "@calcom/lib";
import { ssrInit } from "@calcom/lib/server/ssr";
import prisma from "@calcom/prisma";
import { bookEventTypeSelect } from "@calcom/prisma/selects";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
@ -11,8 +12,6 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
import { ssrInit } from "@server/lib/ssr";
export type HashLinkPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Book(props: HashLinkPageProps) {

View File

@ -16,6 +16,7 @@ import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { ssrInit } from "@calcom/lib/server/ssr";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc, RouterOutputs } from "@calcom/trpc/react";
@ -38,8 +39,6 @@ import { EventTeamWebhooksTab } from "@components/eventtype/EventTeamWebhooksTab
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
import { ssrInit } from "@server/lib/ssr";
export type FormValues = {
title: string;
eventTitle: string;

View File

@ -11,6 +11,7 @@ import {
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Badge,
@ -40,8 +41,6 @@ import { HttpError } from "@lib/core/http/error";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import { ssrInit } from "@server/lib/ssr";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];

View File

@ -4,7 +4,7 @@ import Head from "next/head";
import { useRouter } from "next/router";
import { z } from "zod";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";

View File

@ -2,12 +2,11 @@ import { GetServerSidePropsContext } from "next";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() {
const { t } = useLocale();
return (

View File

@ -3,12 +3,11 @@ import { signIn } from "next-auth/react";
import { useRef } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { Button, Meta, TextField } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
function AdminView() {
const { t } = useLocale();
const usernameRef = useRef<HTMLInputElement>(null);

View File

@ -1,11 +1,10 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@calcom/lib/server/ssr";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
function AdminAppsView() {
return (
<>

View File

@ -1,12 +1,11 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
function AdminUsersView() {
const { t } = useLocale();
return (

View File

@ -7,11 +7,10 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import { Button, Icon, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
interface CtaRowProps {
title: string;
description: string;

View File

@ -8,6 +8,7 @@ import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListIt
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import {
Button,
@ -19,8 +20,6 @@ import {
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ApiKeysView = () => {
const { t } = useLocale();

View File

@ -5,6 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import {
Button,
@ -19,8 +20,6 @@ import {
UpgradeTeamsBadge,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>

View File

@ -8,6 +8,7 @@ import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIn
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
@ -30,8 +31,6 @@ import { QueryCell } from "@lib/QueryCell";
import { CalendarSwitch } from "@components/settings/CalendarSwitch";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = () => {
return (
<SkeletonContainer>

View File

@ -3,6 +3,7 @@ import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import {
Button,
@ -26,8 +27,6 @@ import {
SkeletonText,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>

View File

@ -5,6 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import {
Button,
@ -22,8 +23,6 @@ import {
import { withQuery } from "@lib/QueryCell";
import { nameOfDay } from "@lib/core/i18n/weekday";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>

View File

@ -5,10 +5,11 @@ import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/features/auth/lib";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
@ -38,8 +39,6 @@ import {
import TwoFactor from "@components/auth/TwoFactor";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
return (
<SkeletonContainer>

View File

@ -3,11 +3,10 @@ import { useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, Label, Meta, showToast, Skeleton, Switch } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
const ProfileImpersonationView = () => {
const { t } = useLocale();
const utils = trpc.useContext();

View File

@ -3,14 +3,13 @@ import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useForm } from "react-hook-form";
import { identityProviderNameMap } from "@calcom/features/auth/lib";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { identityProviderNameMap } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, Meta, PasswordField, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
type ChangePasswordFormValues = {
oldPassword: string;
newPassword: string;

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/sso/page/user-sso-view";

View File

@ -3,14 +3,13 @@ import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { trpc } from "@calcom/trpc/react";
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = () => {
return (
<SkeletonContainer>

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-appearance-view";

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-billing-view";

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-members-view";

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/teams/pages/team-profile-view";

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/sso/page/teams-sso-view";

View File

@ -3,11 +3,10 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { getLayout } from "@components/layouts/WizardLayout";
import { ssrInit } from "@server/lib/ssr";
const CreateNewTeamPage = () => {
const { t } = useLocale();
return (

View File

@ -13,7 +13,7 @@ import { asStringOrNull } from "@calcom/web/lib/asStringOrNull";
import { WEBAPP_URL } from "@calcom/web/lib/config/constants";
import prisma from "@calcom/web/lib/prisma";
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
import { ssrInit } from "@calcom/web/server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
type FormValues = {
username: string;

View File

@ -4,10 +4,9 @@ import { TeamsListing } from "@calcom/features/ee/teams/components";
import Shell from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { Button, Icon } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
function Teams() {
const { t } = useLocale();
return (

View File

@ -1,6 +1,6 @@
import { GetServerSidePropsContext } from "next";
import { ssrInit } from "@server/lib/ssr";
import { ssrInit } from "@calcom/lib/server/ssr";
export { default } from "@calcom/features/ee/workflows/pages/index";

@ -1 +1 @@
Subproject commit 8f5d66f2cc4a7ed4f8c05d37dbf0417df5a57cd9
Subproject commit 8ee1ce1292ffac42d2abfd1ea34c0f3bc3b5e0f8

View File

@ -6,6 +6,7 @@
"license": "MIT",
"files": [
"eslint-preset.js",
"next-i18next.config.js",
"prettier-preset.js"
],
"dependencies": {

View File

@ -1,7 +1,7 @@
import { IdentityProvider } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession, ErrorCode, hashPassword, verifyPassword } from "@calcom/lib/auth";
import { getSession, ErrorCode, hashPassword, verifyPassword } from "@calcom/features/auth/lib";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -4,10 +4,9 @@ import { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { sendPasswordResetEmail } from "@calcom/emails";
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { getTranslation } from "@server/lib/i18n";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(req.body.language ?? "en", "common");

View File

@ -0,0 +1,512 @@
import { IdentityProvider, UserPermissionRole } from "@prisma/client";
import { readFileSync } from "fs";
import Handlebars from "handlebars";
import NextAuth, { Session } from "next-auth";
import { Provider } from "next-auth/providers";
import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import GoogleProvider from "next-auth/providers/google";
import nodemailer, { TransportOptions } from "nodemailer";
import { authenticator } from "otplib";
import path from "path";
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/features/auth/lib";
import CalComAdapter from "@calcom/features/auth/lib/next-auth-custom-adapter";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import { randomString } from "@calcom/lib/random";
import rateLimit from "@calcom/lib/rateLimit";
import { serverConfig } from "@calcom/lib/serverConfig";
import slugify from "@calcom/lib/slugify";
import prisma from "@calcom/prisma";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}";
const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } =
JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {};
const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true";
const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED);
const IS_SAML_LOGIN_ENABLED = !!(process.env.SAML_DATABASE_URL && process.env.SAML_ADMINS);
const NEXTAUTH_URL = process.env.NEXTAUTH_URL || WEBAPP_URL;
const transporter = nodemailer.createTransport<TransportOptions>({
...(serverConfig.transport as TransportOptions),
} as TransportOptions);
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
const providers: Provider[] = [
CredentialsProvider({
id: "credentials",
name: "Cal.com",
type: "credentials",
credentials: {
email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" },
password: { label: "Password", type: "password", placeholder: "Your super secure password" },
totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" },
},
async authorize(credentials) {
if (!credentials) {
console.error(`For some reason credentials are missing`);
throw new Error(ErrorCode.InternalServerError);
}
const user = await prisma.user.findUnique({
where: {
email: credentials.email.toLowerCase(),
},
select: {
role: true,
id: true,
username: true,
name: true,
email: true,
identityProvider: true,
password: true,
twoFactorEnabled: true,
twoFactorSecret: true,
teams: {
include: {
team: true,
},
},
},
});
if (!user) {
throw new Error(ErrorCode.UserNotFound);
}
if (user.identityProvider !== IdentityProvider.CAL) {
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
}
if (!user.password) {
throw new Error(ErrorCode.UserMissingPassword);
}
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
if (!isCorrectPassword) {
throw new Error(ErrorCode.IncorrectPassword);
}
if (user.twoFactorEnabled) {
if (!credentials.totpCode) {
throw new Error(ErrorCode.SecondFactorRequired);
}
if (!user.twoFactorSecret) {
console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
throw new Error(ErrorCode.InternalServerError);
}
if (!process.env.CALENDSO_ENCRYPTION_KEY) {
console.error(`"Missing encryption key; cannot proceed with two factor login."`);
throw new Error(ErrorCode.InternalServerError);
}
const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
if (secret.length !== 32) {
console.error(
`Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
);
throw new Error(ErrorCode.InternalServerError);
}
const isValidToken = authenticator.check(credentials.totpCode, secret);
if (!isValidToken) {
throw new Error(ErrorCode.IncorrectTwoFactorCode);
}
}
const limiter = rateLimit({
intervalInMs: 60 * 1000, // 1 minute
});
await limiter.check(10, user.email); // 10 requests per minute
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) {
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: "INACTIVE_ADMIN",
belongsToActiveTeam: hasActiveTeams,
};
}
return {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
belongsToActiveTeam: hasActiveTeams,
};
},
}),
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
providers.push(
GoogleProvider({
clientId: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
})
);
}
if (isSAMLLoginEnabled) {
providers.push({
id: "saml",
name: "BoxyHQ",
type: "oauth",
version: "2.0",
checks: ["pkce", "state"],
authorization: {
url: `${NEXTAUTH_URL}/api/auth/saml/authorize`,
params: {
scope: "",
response_type: "code",
provider: "saml",
},
},
token: {
url: `${NEXTAUTH_URL}/api/auth/saml/token`,
params: { grant_type: "authorization_code" },
},
userinfo: `${NEXTAUTH_URL}/api/auth/saml/userinfo`,
profile: (profile) => {
return {
id: profile.id || "",
firstName: profile.firstName || "",
lastName: profile.lastName || "",
email: profile.email || "",
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
email_verified: true,
};
},
options: {
clientId: "dummy",
clientSecret: "dummy",
},
});
}
if (true) {
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
providers.push(
EmailProvider({
type: "email",
maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only
// Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify.
sendVerificationRequest: ({ identifier, url }) => {
const originalUrl = new URL(url);
const webappUrl = new URL(NEXTAUTH_URL);
if (originalUrl.origin !== webappUrl.origin) {
url = url.replace(originalUrl.origin, webappUrl.origin);
}
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
encoding: "utf8",
});
const emailTemplate = Handlebars.compile(emailFile);
transporter.sendMail({
from: `${process.env.EMAIL_FROM}` || APP_NAME,
to: identifier,
subject: "Your sign-in link for " + APP_NAME,
html: emailTemplate({
base_url: NEXTAUTH_URL,
signin_url: url,
email: identifier,
}),
});
},
})
);
}
const calcomAdapter = CalComAdapter(prisma);
export default NextAuth({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
adapter: calcomAdapter,
session: {
strategy: "jwt",
},
cookies: defaultCookies(NEXTAUTH_URL?.startsWith("https://")),
pages: {
signIn: "/auth/login",
signOut: "/auth/logout",
error: "/auth/error", // Error code passed in query string as ?error=
verifyRequest: "/auth/verify",
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
},
providers,
callbacks: {
async jwt({ token, user, account }) {
const autoMergeIdentities = async () => {
const existingUser = await prisma.user.findFirst({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
where: { email: token.email! },
select: {
id: true,
username: true,
name: true,
email: true,
role: true,
},
});
if (!existingUser) {
return token;
}
return {
...existingUser,
...token,
};
};
if (!user) {
return await autoMergeIdentities();
}
if (account && account.type === "credentials") {
return {
...token,
id: user.id,
name: user.name,
username: user.username,
email: user.email,
role: user.role,
impersonatedByUID: user?.impersonatedByUID,
belongsToActiveTeam: user?.belongsToActiveTeam,
};
}
// The arguments above are from the provider so we need to look up the
// user based on those values in order to construct a JWT.
if (account && account.type === "oauth" && account.provider && account.providerAccountId) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
const existingUser = await prisma.user.findFirst({
where: {
AND: [
{
identityProvider: idP,
},
{
identityProviderId: account.providerAccountId as string,
},
],
},
});
if (!existingUser) {
return await autoMergeIdentities();
}
return {
...token,
id: existingUser.id,
name: existingUser.name,
username: existingUser.username,
email: existingUser.email,
role: existingUser.role,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
};
}
return token;
},
async session({ session, token }) {
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
const calendsoSession: Session = {
...session,
hasValidLicense,
user: {
...session.user,
id: token.id as number,
name: token.name,
username: token.username as string,
role: token.role as UserPermissionRole,
impersonatedByUID: token.impersonatedByUID as number,
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
},
};
return calendsoSession;
},
async signIn(params) {
const { user, account, profile } = params;
if (account?.provider === "email") {
return true;
}
// In this case we've already verified the credentials in the authorize
// callback so we can sign the user in.
if (account?.type === "credentials") {
return true;
}
if (account?.type !== "oauth") {
return false;
}
if (!user.email) {
return false;
}
if (!user.name) {
return false;
}
if (account?.provider) {
let idP: IdentityProvider = IdentityProvider.GOOGLE;
if (account.provider === "saml") {
idP = IdentityProvider.SAML;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error TODO validate email_verified key on profile
user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified;
if (!user.email_verified) {
return "/auth/error?error=unverified-email";
}
// Only google oauth on this path
const provider = account.provider.toUpperCase() as IdentityProvider;
const existingUser = await prisma.user.findFirst({
include: {
accounts: {
where: {
provider: account.provider,
},
},
},
where: {
identityProvider: provider,
identityProviderId: account.providerAccountId,
},
});
if (existingUser) {
// In this case there's an existing user and their email address
// hasn't changed since they last logged in.
if (existingUser.email === user.email) {
try {
// If old user without Account entry we link their google account
if (existingUser.accounts.length === 0) {
const linkAccountWithUserData = { ...account, userId: existingUser.id };
await calcomAdapter.linkAccount(linkAccountWithUserData);
}
} catch (error) {
if (error instanceof Error) {
console.error("Error while linking account of already existing user");
}
}
return true;
}
// If the email address doesn't match, check if an account already exists
// with the new email address. If it does, for now we return an error. If
// not, update the email of their account and log them in.
const userWithNewEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (!userWithNewEmail) {
await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } });
return true;
} else {
return "/auth/error?error=new-email-conflict";
}
}
// If there's no existing user for this identity provider and id, create
// a new account. If an account already exists with the incoming email
// address return an error for now.
const existingUserWithEmail = await prisma.user.findFirst({
where: { email: user.email },
});
if (existingUserWithEmail) {
// if self-hosted then we can allow auto-merge of identity providers if email is verified
if (!hostedCal && existingUserWithEmail.emailVerified) {
return true;
}
// check if user was invited
if (
!existingUserWithEmail.password &&
!existingUserWithEmail.emailVerified &&
!existingUserWithEmail.username
) {
await prisma.user.update({
where: { email: user.email },
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
return true;
}
if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
return "/auth/error?error=use-password-login";
}
return "/auth/error?error=use-identity-login";
}
const newUser = await prisma.user.create({
data: {
// Slugify the incoming name and append a few random characters to
// prevent conflicts for users with the same name.
username: usernameSlug(user.name),
emailVerified: new Date(Date.now()),
name: user.name,
email: user.email,
identityProvider: idP,
identityProviderId: String(user.id),
},
});
const linkAccountNewUserData = { ...account, userId: newUser.id };
await calcomAdapter.linkAccount(linkAccountNewUserData);
return true;
}
return false;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same domain
else if (new URL(url).hostname === new URL(NEXTAUTH_URL).hostname) return url;
return baseUrl;
},
},
});

View File

@ -1,9 +1,8 @@
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/features/auth/lib";
import prisma from "@calcom/prisma";
import { hashPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(400).json({ message: "" });

View File

@ -2,7 +2,7 @@ import { IdentityProvider } from "@prisma/client";
import { NextApiRequest } from "next";
import z from "zod";
import { hashPassword, isPasswordValid } from "@calcom/lib/auth";
import { hashPassword, isPasswordValid } from "@calcom/features/auth/lib";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import slugify from "@calcom/lib/slugify";

View File

@ -1,7 +1,7 @@
import { IdentityProvider } from "@prisma/client";
import { NextApiRequest, NextApiResponse } from "next";
import { hashPassword } from "@calcom/lib/auth";
import { hashPassword } from "@calcom/features/auth/lib";
import slugify from "@calcom/lib/slugify";
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
import prisma from "@calcom/prisma";

View File

@ -1,11 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode, getSession, verifyPassword } from "@calcom/features/auth/lib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });

View File

@ -1,11 +1,10 @@
import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import { ErrorCode, getSession } from "@calcom/features/auth/lib";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });

View File

@ -3,11 +3,10 @@ import { NextApiRequest, NextApiResponse } from "next";
import { authenticator } from "otplib";
import qrcode from "qrcode";
import { ErrorCode, getSession, verifyPassword } from "@calcom/features/auth/lib";
import { symmetricEncrypt } from "@calcom/lib/crypto";
import prisma from "@calcom/prisma";
import { ErrorCode, getSession, verifyPassword } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });

View File

@ -0,0 +1,39 @@
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { HttpError } from "@calcom/lib/http-error";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
// Check that user is authenticated
const { nextauth } = req.query;
if (!Array.isArray(nextauth)) {
return res.status(404).json({ message: `API route not found` });
}
try {
/* Absolute path didn't work */
const handlerMap = (await import("./index")).authApiHandlers;
const handlerKey = nextauth.join("/");
let handler: NextApiHandler;
if (handlerKey in handlerMap) {
handler = (await handlerMap[handlerKey as keyof typeof handlerMap])?.default;
} else {
handler = (await handlerMap.default).default;
}
if (typeof handler !== "function") {
return res.status(404).json({ message: `API handler not found` });
}
return await handler(req, res);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode).json({ message: error.message });
}
if (error instanceof Error) {
return res.status(400).json({ message: error.message });
}
return res.status(404).json({ message: `API handler not found` });
}
};
export default handler;

View File

@ -0,0 +1,15 @@
export const authApiHandlers = {
default: import("./auth/next-auth"),
changepw: import("./auth/changepw"),
"forgot-password": import("./auth/forgot-password"),
"reset-password": import("./auth/reset-password"),
setup: import("./auth/setup"),
signup: import("./auth/signup"),
"two-factor/totp/disable": import("./auth/two-factor/totp/disable"),
"two-factor/totp/enable": import("./auth/two-factor/totp/enable"),
"two-factor/totp/setup": import("./auth/two-factor/totp/setup"),
"saml/authorize": import("./auth/saml/authorize"),
"saml/callback": import("./auth/saml/callback"),
"saml/token": import("./auth/saml/token"),
"saml/userinfo": import("./auth/saml/userinfo"),
};

View File

@ -1,9 +1,7 @@
import classNames from "classnames";
import { APP_NAME, LOGO } from "@calcom/lib/constants";
import { Credits, HeadSeo } from "@calcom/ui";
import Loader from "@components/Loader";
import { Credits, HeadSeo, Loader } from "@calcom/ui";
interface Props {
title: string;

View File

@ -1,8 +1,7 @@
import { UserPermissionRole } from "@prisma/client";
import { useSession } from "next-auth/react";
import { FC, Fragment } from "react";
import { UserPermissionRole } from ".prisma/client";
type AdminRequiredProps = {
as?: keyof JSX.IntrinsicElements;
children?: React.ReactNode;

View File

@ -5,7 +5,7 @@ import { useRouter } from "next/router";
import { Controller, FormProvider, useForm } from "react-hook-form";
import * as z from "zod";
import { isPasswordValid } from "@calcom/lib/auth";
import { isPasswordValid } from "@calcom/features/auth/lib";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";

View File

@ -4,7 +4,7 @@ import type { NextApiRequest } from "next";
import type { Session } from "next-auth";
import { getSession as getSessionInner, GetSessionParams } from "next-auth/react";
import { HttpError } from "../http-error";
import { HttpError } from "@calcom/lib/http-error";
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);

View File

@ -6,10 +6,10 @@ import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon, SkeletonText } from "@calcom/ui";
import AuthContainer from "@components/ui/AuthContainer";
import { ssgInit } from "@server/lib/ssg";
import AuthContainer from "../components/AuthContainer";
const querySchema = z.object({
error: z.string().optional(),
});

View File

@ -6,12 +6,11 @@ import Link from "next/link";
import React, { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import AuthContainer from "@components/ui/AuthContainer";
import AuthContainer from "../../components/AuthContainer";
type Props = {
id: string;

View File

@ -5,12 +5,11 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { SyntheticEvent } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
import { getSession } from "@lib/auth";
import { useLocale } from "@lib/hooks/useLocale";
import AuthContainer from "@components/ui/AuthContainer";
import AuthContainer from "../../components/AuthContainer";
import { getSession } from "../../lib";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useLocale();

View File

@ -7,24 +7,24 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { SAMLLogin } from "@calcom/features/auth/components/SAMLLogin";
import { ErrorCode, getSession } from "@calcom/features/auth/lib";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/lib/server/ssr";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
import { ErrorCode, getSession } from "@lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
import { inferSSRProps } from "@lib/types/inferSSRProps";
// TODO: Fix this import
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
import AddToHomescreen from "@components/AddToHomescreen";
import TwoFactor from "@components/auth/TwoFactor";
import AuthContainer from "@components/ui/AuthContainer";
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
import { ssrInit } from "@server/lib/ssr";
import AuthContainer from "../components/AuthContainer";
import TwoFactor from "../components/TwoFactor";
interface LoginValues {
email: string;

View File

@ -9,9 +9,9 @@ import { Button, Icon } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AuthContainer from "@components/ui/AuthContainer";
import { ssrInit } from "@calcom/lib/server/ssr";
import { ssrInit } from "@server/lib/ssr";
import AuthContainer from "../components/AuthContainer";
type Props = inferSSRProps<typeof getServerSideProps>;

View File

@ -3,14 +3,14 @@ import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getSession } from "@calcom/lib/auth";
import { getSession } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import SetupFormStep1 from "./steps/SetupFormStep1";
import StepDone from "./steps/StepDone";
import SetupFormStep1 from "../../components/SetupFormStep1";
import StepDone from "../../components/StepDone";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();

View File

@ -2,6 +2,7 @@ import { GetServerSidePropsContext } from "next";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { z } from "zod";
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
import stripe from "@calcom/features/ee/payments/server/stripe";
@ -14,14 +15,13 @@ import {
} from "@calcom/features/ee/sso/lib/saml";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
// TODO: Fix this import
import { ssrInit } from "@calcom/lib/server/ssr";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { getSession } from "../../lib";
import { ssrInit } from "@server/lib/ssr";
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
export default function Provider(props: SSOProviderPageProps) {
const router = useRouter();
@ -49,12 +49,18 @@ export default function Provider(props: SSOProviderPageProps) {
return null;
}
const querySchema = z.object({
provider: z.union([z.string(), z.null()]).optional().default(null),
email: z.union([z.string(), z.null()]).optional().default(null),
username: z.union([z.string(), z.null()]).optional().default(null),
});
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
// get query params and typecast them to string
// (would be even better to assert them instead of typecasting)
const providerParam = asStringOrNull(context.query.provider);
const emailParam = asStringOrNull(context.query.email);
const usernameParam = asStringOrNull(context.query.username);
const {
provider: providerParam,
email: emailParam,
username: usernameParam,
} = querySchema.parse(context.query);
const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : "");
if (!providerParam) {
throw new Error(`File is not named sso/[provider]`);

View File

@ -1,16 +1,13 @@
import { CheckIcon, MailOpenIcon, ExclamationIcon } from "@heroicons/react/outline";
import { CheckIcon, ExclamationIcon, MailOpenIcon } from "@heroicons/react/outline";
import { signIn } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
import * as React from "react";
import { useEffect, useState, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import Loader from "@components/Loader";
import { Button, Loader, showToast } from "@calcom/ui";
async function sendVerificationLogin(email: string, username: string) {
await signIn("email", {

Some files were not shown because too many files have changed in this diff Show More