2023-02-16 22:39:57 +00:00
|
|
|
import type { UserPermissionRole } from "@prisma/client";
|
|
|
|
import { IdentityProvider } from "@prisma/client";
|
2022-04-21 20:32:25 +00:00
|
|
|
import { readFileSync } from "fs";
|
|
|
|
import Handlebars from "handlebars";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { Session } from "next-auth";
|
|
|
|
import NextAuth from "next-auth";
|
2023-02-02 14:26:00 +00:00
|
|
|
import { encode } from "next-auth/jwt";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { Provider } from "next-auth/providers";
|
2022-01-07 20:23:37 +00:00
|
|
|
import CredentialsProvider from "next-auth/providers/credentials";
|
2022-04-21 20:32:25 +00:00
|
|
|
import EmailProvider from "next-auth/providers/email";
|
2022-01-13 20:05:23 +00:00
|
|
|
import GoogleProvider from "next-auth/providers/google";
|
2023-02-16 22:39:57 +00:00
|
|
|
import type { TransportOptions } from "nodemailer";
|
|
|
|
import nodemailer from "nodemailer";
|
2021-09-21 09:29:20 +00:00
|
|
|
import { authenticator } from "otplib";
|
2022-04-21 20:32:25 +00:00
|
|
|
import path from "path";
|
2021-09-22 19:52:38 +00:00
|
|
|
|
2022-07-28 19:58:26 +00:00
|
|
|
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
|
|
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
2022-10-18 20:34:32 +00:00
|
|
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
2022-11-30 21:52:56 +00:00
|
|
|
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
2022-12-09 22:33:49 +00:00
|
|
|
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
2022-03-23 22:00:30 +00:00
|
|
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
2022-04-21 20:32:25 +00:00
|
|
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
2023-01-26 22:51:03 +00:00
|
|
|
import { randomString } from "@calcom/lib/random";
|
2022-08-30 19:58:35 +00:00
|
|
|
import rateLimit from "@calcom/lib/rateLimit";
|
2022-04-21 20:32:25 +00:00
|
|
|
import { serverConfig } from "@calcom/lib/serverConfig";
|
2023-01-26 22:51:03 +00:00
|
|
|
import slugify from "@calcom/lib/slugify";
|
2022-07-28 19:58:26 +00:00
|
|
|
import prisma from "@calcom/prisma";
|
2023-01-31 20:44:14 +00:00
|
|
|
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
2022-03-23 22:00:30 +00:00
|
|
|
|
2022-04-26 15:12:08 +00:00
|
|
|
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
2022-01-13 20:05:23 +00:00
|
|
|
|
|
|
|
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
|
|
|
|
2022-04-21 20:32:25 +00:00
|
|
|
const transporter = nodemailer.createTransport<TransportOptions>({
|
|
|
|
...(serverConfig.transport as TransportOptions),
|
|
|
|
} as TransportOptions);
|
|
|
|
|
2022-02-18 17:32:12 +00:00
|
|
|
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
|
|
|
|
2022-01-13 20:05:23 +00:00
|
|
|
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(),
|
|
|
|
},
|
2022-11-09 16:23:39 +00:00
|
|
|
select: {
|
|
|
|
role: true,
|
|
|
|
id: true,
|
|
|
|
username: true,
|
|
|
|
name: true,
|
|
|
|
email: true,
|
2023-01-31 20:44:14 +00:00
|
|
|
metadata: true,
|
2022-11-09 16:23:39 +00:00
|
|
|
identityProvider: true,
|
|
|
|
password: true,
|
|
|
|
twoFactorEnabled: true,
|
|
|
|
twoFactorSecret: true,
|
2022-12-07 15:04:04 +00:00
|
|
|
teams: {
|
|
|
|
include: {
|
|
|
|
team: true,
|
|
|
|
},
|
|
|
|
},
|
2022-11-09 16:23:39 +00:00
|
|
|
},
|
2022-01-13 20:05:23 +00:00
|
|
|
});
|
|
|
|
|
2023-01-30 18:37:03 +00:00
|
|
|
// Don't leak information about it being username or password that is invalid
|
2022-01-13 20:05:23 +00:00
|
|
|
if (!user) {
|
2023-01-30 18:37:03 +00:00
|
|
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
2022-01-13 20:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (user.identityProvider !== IdentityProvider.CAL) {
|
|
|
|
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user.password) {
|
2023-01-30 18:37:03 +00:00
|
|
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
2022-01-13 20:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
|
|
|
if (!isCorrectPassword) {
|
2023-01-30 18:37:03 +00:00
|
|
|
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
2022-01-13 20:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-30 19:58:35 +00:00
|
|
|
const limiter = rateLimit({
|
|
|
|
intervalInMs: 60 * 1000, // 1 minute
|
|
|
|
});
|
|
|
|
await limiter.check(10, user.email); // 10 requests per minute
|
2022-12-07 15:04:04 +00:00
|
|
|
// Check if the user you are logging into has any active teams
|
|
|
|
const hasActiveTeams =
|
|
|
|
user.teams.filter((m) => {
|
2022-12-09 22:33:49 +00:00
|
|
|
if (!IS_TEAM_BILLING_ENABLED) return true;
|
2022-12-07 15:04:04 +00:00
|
|
|
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
|
2022-12-09 22:33:49 +00:00
|
|
|
if (metadata.success && metadata.data?.subscriptionId) return true;
|
|
|
|
return false;
|
2022-12-07 15:04:04 +00:00
|
|
|
}).length > 0;
|
2022-08-30 19:58:35 +00:00
|
|
|
|
2022-11-09 16:23:39 +00:00
|
|
|
// 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,
|
2023-01-01 11:19:58 +00:00
|
|
|
role: "INACTIVE_ADMIN",
|
2022-12-07 15:04:04 +00:00
|
|
|
belongsToActiveTeam: hasActiveTeams,
|
2022-11-09 16:23:39 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-01-13 20:05:23 +00:00
|
|
|
return {
|
|
|
|
id: user.id,
|
|
|
|
username: user.username,
|
|
|
|
email: user.email,
|
|
|
|
name: user.name,
|
2022-04-26 08:48:17 +00:00
|
|
|
role: user.role,
|
2022-12-07 15:04:04 +00:00
|
|
|
belongsToActiveTeam: hasActiveTeams,
|
2022-01-13 20:05:23 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
}),
|
2022-09-17 21:09:06 +00:00
|
|
|
ImpersonationProvider,
|
2022-01-13 20:05:23 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
if (IS_GOOGLE_LOGIN_ENABLED) {
|
|
|
|
providers.push(
|
|
|
|
GoogleProvider({
|
|
|
|
clientId: GOOGLE_CLIENT_ID,
|
|
|
|
clientSecret: GOOGLE_CLIENT_SECRET,
|
2023-02-08 18:39:56 +00:00
|
|
|
allowDangerousEmailAccountLinking: true,
|
2022-01-13 20:05:23 +00:00
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isSAMLLoginEnabled) {
|
|
|
|
providers.push({
|
|
|
|
id: "saml",
|
|
|
|
name: "BoxyHQ",
|
|
|
|
type: "oauth",
|
|
|
|
version: "2.0",
|
|
|
|
checks: ["pkce", "state"],
|
|
|
|
authorization: {
|
2022-10-18 20:34:32 +00:00
|
|
|
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
2022-01-13 20:05:23 +00:00
|
|
|
params: {
|
|
|
|
scope: "",
|
|
|
|
response_type: "code",
|
|
|
|
provider: "saml",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
token: {
|
2022-10-18 20:34:32 +00:00
|
|
|
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
2022-01-13 20:05:23 +00:00
|
|
|
params: { grant_type: "authorization_code" },
|
|
|
|
},
|
2022-10-18 20:34:32 +00:00
|
|
|
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
|
2022-01-13 20:05:23 +00:00
|
|
|
profile: (profile) => {
|
|
|
|
return {
|
|
|
|
id: profile.id || "",
|
2022-02-02 18:33:27 +00:00
|
|
|
firstName: profile.firstName || "",
|
|
|
|
lastName: profile.lastName || "",
|
2022-01-13 20:05:23 +00:00
|
|
|
email: profile.email || "",
|
2022-02-02 18:33:27 +00:00
|
|
|
name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(),
|
2022-01-13 20:05:23 +00:00
|
|
|
email_verified: true,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
options: {
|
|
|
|
clientId: "dummy",
|
|
|
|
clientSecret: "dummy",
|
|
|
|
},
|
2023-02-08 18:39:56 +00:00
|
|
|
allowDangerousEmailAccountLinking: true,
|
2022-01-13 20:05:23 +00:00
|
|
|
});
|
|
|
|
}
|
2021-03-24 15:03:04 +00:00
|
|
|
|
2022-04-21 20:32:25 +00:00
|
|
|
if (true) {
|
2022-06-06 17:49:56 +00:00
|
|
|
const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates");
|
2022-04-21 20:32:25 +00:00
|
|
|
providers.push(
|
|
|
|
EmailProvider({
|
2022-08-06 03:09:52 +00:00
|
|
|
type: "email",
|
2022-04-21 20:32:25 +00:00
|
|
|
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 }) => {
|
2022-08-06 00:08:05 +00:00
|
|
|
const originalUrl = new URL(url);
|
|
|
|
const webappUrl = new URL(WEBAPP_URL);
|
|
|
|
if (originalUrl.origin !== webappUrl.origin) {
|
|
|
|
url = url.replace(originalUrl.origin, webappUrl.origin);
|
|
|
|
}
|
2022-04-21 20:32:25 +00:00
|
|
|
const emailFile = readFileSync(path.join(emailsDir, "confirm-email.html"), {
|
|
|
|
encoding: "utf8",
|
|
|
|
});
|
|
|
|
const emailTemplate = Handlebars.compile(emailFile);
|
|
|
|
transporter.sendMail({
|
2022-11-30 21:52:56 +00:00
|
|
|
from: `${process.env.EMAIL_FROM}` || APP_NAME,
|
2022-04-21 20:32:25 +00:00
|
|
|
to: identifier,
|
2022-11-30 21:52:56 +00:00
|
|
|
subject: "Your sign-in link for " + APP_NAME,
|
2022-04-21 20:32:25 +00:00
|
|
|
html: emailTemplate({
|
2022-08-02 16:05:09 +00:00
|
|
|
base_url: WEBAPP_URL,
|
2022-04-21 20:32:25 +00:00
|
|
|
signin_url: url,
|
|
|
|
email: identifier,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2023-02-02 14:26:00 +00:00
|
|
|
|
|
|
|
function isNumber(n: string) {
|
|
|
|
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
|
|
|
}
|
|
|
|
|
2022-04-26 15:12:08 +00:00
|
|
|
const calcomAdapter = CalComAdapter(prisma);
|
2023-02-02 14:26:00 +00:00
|
|
|
|
2021-03-24 15:03:04 +00:00
|
|
|
export default NextAuth({
|
2022-05-17 20:43:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
2022-04-26 15:12:08 +00:00
|
|
|
// @ts-ignore
|
|
|
|
adapter: calcomAdapter,
|
2021-08-18 11:52:25 +00:00
|
|
|
session: {
|
2022-01-07 20:23:37 +00:00
|
|
|
strategy: "jwt",
|
2021-09-23 09:02:53 +00:00
|
|
|
},
|
2023-01-31 20:44:14 +00:00
|
|
|
jwt: {
|
2023-02-02 14:26:00 +00:00
|
|
|
// decorate the native JWT encode function
|
|
|
|
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
|
|
|
|
encode: async ({ token, maxAge, secret }) => {
|
|
|
|
if (token?.sub && isNumber(token.sub)) {
|
|
|
|
const user = await prisma.user.findFirst({
|
|
|
|
where: { id: Number(token.sub) },
|
|
|
|
select: { metadata: true },
|
|
|
|
});
|
|
|
|
// if no user is found, we still don't want to crash here.
|
|
|
|
if (user) {
|
|
|
|
const metadata = userMetadata.parse(user.metadata);
|
|
|
|
if (metadata?.sessionTimeout) {
|
|
|
|
maxAge = metadata.sessionTimeout / 60;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return encode({ secret, token, maxAge });
|
2023-01-31 20:44:14 +00:00
|
|
|
},
|
|
|
|
},
|
2022-08-06 00:08:05 +00:00
|
|
|
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
|
2021-08-18 11:52:25 +00:00
|
|
|
pages: {
|
|
|
|
signIn: "/auth/login",
|
|
|
|
signOut: "/auth/logout",
|
|
|
|
error: "/auth/error", // Error code passed in query string as ?error=
|
2022-08-06 03:09:52 +00:00
|
|
|
verifyRequest: "/auth/verify",
|
2022-05-16 15:34:13 +00:00
|
|
|
// newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest)
|
2021-08-18 11:52:25 +00:00
|
|
|
},
|
2022-01-13 20:05:23 +00:00
|
|
|
providers,
|
|
|
|
callbacks: {
|
2022-01-20 01:24:00 +00:00
|
|
|
async jwt({ token, user, account }) {
|
2022-02-02 18:33:27 +00:00
|
|
|
const autoMergeIdentities = async () => {
|
2022-09-05 17:39:16 +00:00
|
|
|
const existingUser = await prisma.user.findFirst({
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
|
|
where: { email: token.email! },
|
2022-11-09 16:23:39 +00:00
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
username: true,
|
|
|
|
name: true,
|
|
|
|
email: true,
|
|
|
|
role: true,
|
|
|
|
},
|
2022-09-05 17:39:16 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!existingUser) {
|
|
|
|
return token;
|
2022-02-02 18:33:27 +00:00
|
|
|
}
|
|
|
|
|
2022-09-05 17:39:16 +00:00
|
|
|
return {
|
2022-11-09 16:23:39 +00:00
|
|
|
...existingUser,
|
|
|
|
...token,
|
2022-09-05 17:39:16 +00:00
|
|
|
};
|
2022-02-02 18:33:27 +00:00
|
|
|
};
|
2022-11-09 16:23:39 +00:00
|
|
|
|
2022-02-02 18:33:27 +00:00
|
|
|
if (!user) {
|
|
|
|
return await autoMergeIdentities();
|
2022-01-13 20:05:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (account && account.type === "credentials") {
|
|
|
|
return {
|
2022-11-09 16:23:39 +00:00
|
|
|
...token,
|
2022-01-13 20:05:23 +00:00
|
|
|
id: user.id,
|
2022-03-16 15:11:21 +00:00
|
|
|
name: user.name,
|
2022-01-13 20:05:23 +00:00
|
|
|
username: user.username,
|
|
|
|
email: user.email,
|
2022-04-26 08:48:17 +00:00
|
|
|
role: user.role,
|
2022-11-23 18:35:08 +00:00
|
|
|
impersonatedByUID: user?.impersonatedByUID,
|
2022-12-07 15:04:04 +00:00
|
|
|
belongsToActiveTeam: user?.belongsToActiveTeam,
|
2022-01-13 20:05:23 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2022-01-20 01:24:00 +00:00
|
|
|
if (account && account.type === "oauth" && account.provider && account.providerAccountId) {
|
2022-01-13 20:05:23 +00:00
|
|
|
let idP: IdentityProvider = IdentityProvider.GOOGLE;
|
|
|
|
if (account.provider === "saml") {
|
|
|
|
idP = IdentityProvider.SAML;
|
2022-01-07 20:23:37 +00:00
|
|
|
}
|
2022-01-13 20:05:23 +00:00
|
|
|
const existingUser = await prisma.user.findFirst({
|
2021-08-18 11:52:25 +00:00
|
|
|
where: {
|
2022-01-13 20:05:23 +00:00
|
|
|
AND: [
|
|
|
|
{
|
|
|
|
identityProvider: idP,
|
|
|
|
},
|
|
|
|
{
|
2022-01-20 01:24:00 +00:00
|
|
|
identityProviderId: account.providerAccountId as string,
|
2022-01-13 20:05:23 +00:00
|
|
|
},
|
|
|
|
],
|
2021-08-18 11:52:25 +00:00
|
|
|
},
|
|
|
|
});
|
2021-03-24 15:03:04 +00:00
|
|
|
|
2022-01-13 20:05:23 +00:00
|
|
|
if (!existingUser) {
|
2022-02-02 18:33:27 +00:00
|
|
|
return await autoMergeIdentities();
|
2021-08-18 11:52:25 +00:00
|
|
|
}
|
2021-03-24 15:03:04 +00:00
|
|
|
|
2021-08-18 11:52:25 +00:00
|
|
|
return {
|
2022-11-09 16:23:39 +00:00
|
|
|
...token,
|
2022-01-13 20:05:23 +00:00
|
|
|
id: existingUser.id,
|
2022-03-16 15:11:21 +00:00
|
|
|
name: existingUser.name,
|
2022-01-13 20:05:23 +00:00
|
|
|
username: existingUser.username,
|
|
|
|
email: existingUser.email,
|
2022-04-26 08:48:17 +00:00
|
|
|
role: existingUser.role,
|
|
|
|
impersonatedByUID: token.impersonatedByUID as number,
|
2022-12-07 15:04:04 +00:00
|
|
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
2021-08-18 11:52:25 +00:00
|
|
|
};
|
|
|
|
}
|
2022-01-13 20:05:23 +00:00
|
|
|
|
2021-08-18 11:52:25 +00:00
|
|
|
return token;
|
|
|
|
},
|
2022-01-07 20:23:37 +00:00
|
|
|
async session({ session, token }) {
|
Admin Wizard Choose License (#6574)
* Implementation
* i18n
* More i18n
* extracted i18n, needs api to get most recent price, added hint: update later
* Fixing i18n var
* Fix booking filters not working for admin (#6576)
* fix: react-select overflow issue in some modals. (#6587)
* feat: add a disable overflow prop
* feat: use the disable overflow prop
* Tailwind Merge (#6596)
* Tailwind Merge
* Fix merge classNames
* [CAL-808] /availability/single - UI issue on buttons beside time inputs (#6561)
* [CAL-808] /availability/single - UI issue on buttons beside time inputs
* Update apps/web/public/static/locales/en/common.json
* Update packages/features/schedules/components/Schedule.tsx
* create new translation for tooltip
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
* Bye bye submodules (#6585)
* WIP
* Uses ssh instead
* Update .gitignore
* Update .gitignore
* Update Makefile
* Update git-setup.sh
* Update git-setup.sh
* Replaced Makefile with bash script
* Update package.json
* fix: show button on empty string (#6601)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* fix: add delete in dropdown (#6599)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Update README.md
* Update README.md
* Changed a neutral- classes to gray (#6603)
* Changed a neutral- classes to gray
* Changed all border-1 to border
* Update package.json
* Test fixes
* Yarn lock fixes
* Fix string equality check in git-setup.sh
* [CAL-811] Avatar icon not redirecting user back to the main page (#6586)
* Remove cursor-pointer, remove old Avatar* files
* Fixed styling for checkedSelect + some cleanup
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
* Harsh/add member invite (#6598)
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
* Regenerated lockfile without upgrade (#6610)
* fix: remove annoying outline when <Button /> clicked (#6537)
* fix: remove annoying outline when <Button /> clicked
* Delete yarn.lock
* remove 1 on 1 icon (#6609)
* removed 1-on-1 badge
* changed user to users for group events
* fix: case-sensitivity in apps path (#6552)
* fix: lowercase slug
* fix: make fallback blocking
* Fix FAB (#6611)
* feat: add LocationSelect component (#6571)
* feat: add LocationSelect component
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* fix: type error
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* chore: type error
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
* Update booking filters design (#6543)
* Update booking filters
* Add filter on YOUR bookings
* Fix pending members showing up in list
* Reduce the avatar size to 'sm' for now
* Bugfix/dropdown menu trigger as child remove class names (#6614)
* Fix UsernameTextfield to take right height
* Remove className side-effect
* Incorrect resolution version fixed
* Converted mobile DropdownMenuTrigger styles into Button
* v2.5.3
* fix: use items-center (#6618)
* fix tooltip and modal stacking issues (#6491)
* fix tooltip and modal stacking issues
* use z-index in larger screens and less
Co-authored-by: Alex van Andel <me@alexvanandel.com>
* Temporary fix (#6626)
* Fix Ga4 tracking (#6630)
* generic <UpgradeScreen> component (#6594)
* first attempt of <UpgradeScreen>
* changes to icons
* reverted changes back to initial state, needs fix: teams not showing
* WIP
* Fix weird reactnode error
* Fix loading text
* added upgradeTip to routing forms
* icon colors
* create and use hook to check if user has team plan
* use useTeamPlan for upgradeTeamsBadge
* replace huge svg with compressed jpeg
* responsive fixes
* Update packages/ui/components/badge/UpgradeTeamsBadge.tsx
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
* Give team plan features to E2E tests
* Allow option to make a user part of team int ests
* Remove flash of paywall for team user
* Add team user for typeform tests as well
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
* Removing env var to rely on db
* Restoring i18n keys, set loading moved
* Fixing tailwind-preset glob
* Wizard width fix for md+ screens
* Converting licenses options to radix radio
* Applying feedback + other tweaks
* Reverting this, not this PR related
* Unneeded code removal
* Reverting unneeded style change
* Applying feedback
* Removing licenseType
* Upgrades typescript
* Update yarn lock
* Typings
* Hotfix: ping,riverside,whereby and around not showing up in list (#6712)
* Hotfix: ping,riverside,whereby and around not showing up in list (#6712) (#6713)
* Adds deployment settings to DB (#6706)
* WIP
* Adds DeploymentTheme
* Add missing migrations
* Adds client extensions for deployment
* Cleanup
* Delete migration.sql
* Relying on both, env var and new model
* Restoring env example doc for backward compat
* Maximum call stack size exceeded fix?
* Revert upgrade
* Update index.ts
* Delete index.ts
* Not exposing license key, fixed radio behavior
* Covering undefined env var
* Self contained checkLicense
* Feedback
* Moar feedback
* Feedback
* Feedback
* Feedback
* Cleanup
---------
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Nafees Nazik <84864519+G3root@users.noreply.github.com>
Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com>
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Harsh Singh <51085015+harshsinghatz@users.noreply.github.com>
Co-authored-by: Guest <guest@pop-os.localdomain>
Co-authored-by: root <harsh.singh@gocomet.com>
Co-authored-by: Luis Cadillo <luiscaf3r@gmail.com>
Co-authored-by: Mohammed Cherfaoui <hi@cherfaoui.dev>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
2023-02-08 00:23:42 +00:00
|
|
|
const hasValidLicense = await checkLicense(prisma);
|
2021-08-18 12:15:22 +00:00
|
|
|
const calendsoSession: Session = {
|
2021-08-18 11:52:25 +00:00
|
|
|
...session,
|
2022-05-26 17:07:14 +00:00
|
|
|
hasValidLicense,
|
2021-08-18 11:52:25 +00:00
|
|
|
user: {
|
|
|
|
...session.user,
|
|
|
|
id: token.id as number,
|
2022-03-16 15:11:21 +00:00
|
|
|
name: token.name,
|
2021-08-18 11:52:25 +00:00
|
|
|
username: token.username as string,
|
2022-04-26 08:48:17 +00:00
|
|
|
role: token.role as UserPermissionRole,
|
|
|
|
impersonatedByUID: token.impersonatedByUID as number,
|
2022-12-07 15:04:04 +00:00
|
|
|
belongsToActiveTeam: token?.belongsToActiveTeam as boolean,
|
2021-05-05 20:01:56 +00:00
|
|
|
},
|
2021-08-18 11:52:25 +00:00
|
|
|
};
|
|
|
|
return calendsoSession;
|
2021-05-05 20:01:56 +00:00
|
|
|
},
|
2022-04-21 20:32:25 +00:00
|
|
|
async signIn(params) {
|
|
|
|
const { user, account, profile } = params;
|
2022-04-26 15:12:08 +00:00
|
|
|
|
2022-11-23 18:35:08 +00:00
|
|
|
if (account?.provider === "email") {
|
2022-04-21 20:32:25 +00:00
|
|
|
return true;
|
|
|
|
}
|
2022-01-13 20:05:23 +00:00
|
|
|
// In this case we've already verified the credentials in the authorize
|
|
|
|
// callback so we can sign the user in.
|
2022-11-23 18:35:08 +00:00
|
|
|
if (account?.type === "credentials") {
|
2022-01-13 20:05:23 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-11-23 18:35:08 +00:00
|
|
|
if (account?.type !== "oauth") {
|
2022-01-13 20:05:23 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user.email) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!user.name) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-11-23 18:35:08 +00:00
|
|
|
if (account?.provider) {
|
2022-01-13 20:05:23 +00:00
|
|
|
let idP: IdentityProvider = IdentityProvider.GOOGLE;
|
|
|
|
if (account.provider === "saml") {
|
|
|
|
idP = IdentityProvider.SAML;
|
|
|
|
}
|
2022-11-23 18:35:08 +00:00
|
|
|
// 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;
|
2022-01-13 20:05:23 +00:00
|
|
|
|
|
|
|
if (!user.email_verified) {
|
|
|
|
return "/auth/error?error=unverified-email";
|
|
|
|
}
|
2022-04-26 15:12:08 +00:00
|
|
|
// Only google oauth on this path
|
|
|
|
const provider = account.provider.toUpperCase() as IdentityProvider;
|
2022-01-13 20:05:23 +00:00
|
|
|
|
|
|
|
const existingUser = await prisma.user.findFirst({
|
2022-04-26 15:12:08 +00:00
|
|
|
include: {
|
|
|
|
accounts: {
|
|
|
|
where: {
|
|
|
|
provider: account.provider,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2022-01-13 20:05:23 +00:00
|
|
|
where: {
|
2022-04-26 15:12:08 +00:00
|
|
|
identityProvider: provider,
|
|
|
|
identityProviderId: account.providerAccountId,
|
2022-01-13 20:05:23 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
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) {
|
2022-04-26 15:12:08 +00:00
|
|
|
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");
|
|
|
|
}
|
|
|
|
}
|
2022-01-13 20:05:23 +00:00
|
|
|
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) {
|
2022-02-02 18:33:27 +00:00
|
|
|
// if self-hosted then we can allow auto-merge of identity providers if email is verified
|
|
|
|
if (!hostedCal && existingUserWithEmail.emailVerified) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-01-13 20:05:23 +00:00
|
|
|
// 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.
|
2022-02-18 17:32:12 +00:00
|
|
|
username: usernameSlug(user.name),
|
2022-01-13 20:05:23 +00:00
|
|
|
emailVerified: new Date(Date.now()),
|
|
|
|
name: user.name,
|
|
|
|
identityProvider: idP,
|
2022-11-23 18:35:08 +00:00
|
|
|
identityProviderId: String(user.id),
|
2022-01-13 20:05:23 +00:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2023-02-08 18:39:56 +00:00
|
|
|
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
|
|
|
if (
|
|
|
|
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
|
|
|
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
|
|
|
) {
|
2023-02-17 18:59:34 +00:00
|
|
|
await prisma.user.update({
|
|
|
|
where: { email: existingUserWithEmail.email },
|
|
|
|
data: { password: null },
|
|
|
|
});
|
2023-02-08 18:39:56 +00:00
|
|
|
return true;
|
|
|
|
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
2022-01-13 20:05:23 +00:00
|
|
|
return "/auth/error?error=use-password-login";
|
|
|
|
}
|
|
|
|
|
|
|
|
return "/auth/error?error=use-identity-login";
|
|
|
|
}
|
|
|
|
|
2022-04-26 15:12:08 +00:00
|
|
|
const newUser = await prisma.user.create({
|
2022-01-13 20:05:23 +00:00
|
|
|
data: {
|
|
|
|
// Slugify the incoming name and append a few random characters to
|
|
|
|
// prevent conflicts for users with the same name.
|
2022-02-18 17:32:12 +00:00
|
|
|
username: usernameSlug(user.name),
|
2022-01-13 20:05:23 +00:00
|
|
|
emailVerified: new Date(Date.now()),
|
|
|
|
name: user.name,
|
|
|
|
email: user.email,
|
|
|
|
identityProvider: idP,
|
2022-11-23 18:35:08 +00:00
|
|
|
identityProviderId: String(user.id),
|
2022-01-13 20:05:23 +00:00
|
|
|
},
|
|
|
|
});
|
2022-04-26 15:12:08 +00:00
|
|
|
const linkAccountNewUserData = { ...account, userId: newUser.id };
|
|
|
|
await calcomAdapter.linkAccount(linkAccountNewUserData);
|
2022-01-13 20:05:23 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
2022-05-16 17:44:44 +00:00
|
|
|
async redirect({ url, baseUrl }) {
|
|
|
|
// Allows relative callback URLs
|
|
|
|
if (url.startsWith("/")) return `${baseUrl}${url}`;
|
2022-08-06 00:08:05 +00:00
|
|
|
// Allows callback URLs on the same domain
|
|
|
|
else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url;
|
2022-05-16 17:44:44 +00:00
|
|
|
return baseUrl;
|
|
|
|
},
|
2021-08-18 11:52:25 +00:00
|
|
|
},
|
2021-05-05 20:01:56 +00:00
|
|
|
});
|