feat(web): improve session retrieval performance (#7584)
* feat(web): improve session retrieval performance Switch to using `getServerSession` which avoids a HTTP round trip to retrieve session details. Additionally, migrate deprecated `app/lib/auth` calls to to `@calcom/lib` package. * fix: update failing test and lint * Consolidates auth code in features * Update yarn.lock * Update packages/trpc/server/createContext.ts * Oopsie --------- Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { SyntheticEvent } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -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/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -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/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, Label, PasswordField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/lib/auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Dialog, DialogContent, Form, TextField } from "@calcom/ui";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from "react";
|
||||
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/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import type { Session } from "next-auth";
|
||||
import type { GetSessionParams } from "next-auth/react";
|
||||
import { getSession as getSessionInner } from "next-auth/react";
|
||||
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function hashPassword(password: string) {
|
||||
const hashedPassword = await hash(password, 12);
|
||||
return hashedPassword;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export async function verifyPassword(password: string, hashedPassword: string) {
|
||||
const isValid = await compare(password, hashedPassword);
|
||||
return isValid;
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
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` */
|
||||
export enum ErrorCode {
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
InvalidPassword = "invalid-password",
|
||||
}
|
||||
/** @deprecated use the one from `@calcom/lib/auth` */
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import parser from "accept-language-parser";
|
||||
import type { IncomingMessage } from "http";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { Maybe } from "@calcom/trpc/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { i18n } from "../../../next-i18next.config";
|
||||
|
||||
export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
@@ -15,9 +15,13 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
|
||||
return preferredLocale ?? i18n.defaultLocale;
|
||||
}
|
||||
|
||||
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const session = await getSession({ req });
|
||||
export const getOrSetUserLocaleFromHeaders = async (
|
||||
req: GetServerSidePropsContext["req"],
|
||||
res: GetServerSidePropsContext["res"]
|
||||
): Promise<string> => {
|
||||
const { default: prisma } = await import("@calcom/prisma");
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const preferredLocale = getLocaleFromHeaders(req);
|
||||
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"micro": "^10.0.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^13.2.1",
|
||||
"next-auth": "^4.18.8",
|
||||
"next-auth": "^4.20.1",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^11.3.0",
|
||||
|
||||
@@ -1,659 +1,5 @@
|
||||
import type { UserPermissionRole } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import { SignJWT } from "jose";
|
||||
import type { Session } from "next-auth";
|
||||
import NextAuth from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { 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 type { TransportOptions } from "nodemailer";
|
||||
import nodemailer 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 jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { hostedCal, isSAMLLoginEnabled, clientSecretVerifier } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_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, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
|
||||
|
||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
|
||||
const transporter = nodemailer.createTransport<TransportOptions>({
|
||||
...(serverConfig.transport as TransportOptions),
|
||||
} as TransportOptions);
|
||||
|
||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||
|
||||
const signJwt = async (payload: { email: string }) => {
|
||||
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject(payload.email)
|
||||
.setIssuedAt()
|
||||
.setIssuer(WEBSITE_URL)
|
||||
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||
.setExpirationTime("2m")
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||
|
||||
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,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.password) {
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
}
|
||||
|
||||
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: { team: { metadata: unknown } }) => {
|
||||
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,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isSAMLLoginEnabled) {
|
||||
providers.push({
|
||||
id: "saml",
|
||||
name: "BoxyHQ",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
checks: ["pkce", "state"],
|
||||
authorization: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
||||
params: {
|
||||
scope: "",
|
||||
response_type: "code",
|
||||
provider: "saml",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
||||
params: { grant_type: "authorization_code" },
|
||||
},
|
||||
userinfo: `${WEBAPP_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: clientSecretVerifier,
|
||||
},
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
});
|
||||
|
||||
// Idp initiated login
|
||||
providers.push(
|
||||
CredentialsProvider({
|
||||
id: "saml-idp",
|
||||
name: "IdP Login",
|
||||
credentials: {
|
||||
code: {},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { code } = credentials;
|
||||
|
||||
if (!code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
// Fetch access token
|
||||
const { access_token } = await oauthController.token({
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${process.env.NEXTAUTH_URL}`,
|
||||
client_id: "dummy",
|
||||
client_secret: clientSecretVerifier,
|
||||
});
|
||||
|
||||
if (!access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch user info
|
||||
const userInfo = await oauthController.userInfo(access_token);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, firstName, lastName, email } = userInfo;
|
||||
|
||||
return {
|
||||
id: id as unknown as number,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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(WEBAPP_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: WEBAPP_URL,
|
||||
signin_url: url,
|
||||
email: identifier,
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
}
|
||||
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
|
||||
export default NextAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: calcomAdapter,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
// 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 });
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_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) {
|
||||
return token;
|
||||
}
|
||||
if (account.type === "credentials") {
|
||||
// return token if credentials,saml-idp
|
||||
if (account.provider === "saml-idp") {
|
||||
return token;
|
||||
}
|
||||
// any other credentials, add user info
|
||||
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.type === "oauth") {
|
||||
if (!account.provider || !account.providerAccountId) {
|
||||
return token;
|
||||
}
|
||||
const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE;
|
||||
|
||||
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(prisma);
|
||||
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.
|
||||
// Only if provider is not saml-idp
|
||||
if (account?.provider !== "saml-idp") {
|
||||
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" || account.provider === "saml-idp") {
|
||||
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 existingUser = await prisma.user.findFirst({
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider: idP,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
identityProvider: idP as IdentityProvider,
|
||||
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");
|
||||
}
|
||||
}
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
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 } });
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
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) {
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
data: { password: null },
|
||||
});
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else 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);
|
||||
|
||||
if (account.twoFactorEnabled) {
|
||||
return loginWithTotp(newUser);
|
||||
} else {
|
||||
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(WEBAPP_URL).hostname) return url;
|
||||
return baseUrl;
|
||||
},
|
||||
},
|
||||
});
|
||||
export default NextAuth(AUTH_OPTIONS);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
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 });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session || !session.user || !session.user.email) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
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: "" });
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
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" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
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" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -3,17 +3,18 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { authenticator } from "otplib";
|
||||
import qrcode from "qrcode";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
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" });
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
const booking = await handleNewBooking(req, {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||
const session = await getSession({ req });
|
||||
async function handler(req: NextApiRequest & { userId?: number }, res: NextApiResponse) {
|
||||
const session = await getServerSession({ req, res });
|
||||
/* To mimic API behavior and comply with types */
|
||||
req.userId = session?.user?.id || -1;
|
||||
return await handleCancelBooking(req);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { 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 { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import { revalidateCalendarCache } from "@calcom/lib/server/revalidateCalendarCache";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -42,7 +42,7 @@ const defaultIntegrationAddHandler = async ({
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
// Check that user is authenticated
|
||||
req.session = await getSession({ req });
|
||||
req.session = await getServerSession({ req, res });
|
||||
|
||||
const { args } = req.query;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
|
||||
let isCold = true;
|
||||
@@ -9,7 +9,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const prePrismaDate = performance.now();
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const preSessionDate = performance.now();
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
if (!session) return res.status(409).json({ message: "Unauthorized" });
|
||||
const preUserDate = performance.now();
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
*/
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import * as trpcNext from "@calcom/trpc/server/adapters/next";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { createContext as createTrpcContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
@@ -12,7 +13,11 @@ export default trpcNext.createNextApiHandler({
|
||||
/**
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
createContext,
|
||||
createContext: ({ req, res }) => {
|
||||
const sessionGetter = () => getServerSession({ req, res });
|
||||
|
||||
return createTrpcContext({ req, res }, sessionGetter);
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/error-handling
|
||||
*/
|
||||
|
||||
@@ -3,11 +3,11 @@ import { useRouter } from "next/router";
|
||||
|
||||
import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
|
||||
import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -103,7 +103,7 @@ export async function getServerSideProps(
|
||||
appPages?: string[];
|
||||
}>
|
||||
) {
|
||||
const { params } = context;
|
||||
const { params, req, res } = context;
|
||||
if (!params) {
|
||||
return {
|
||||
notFound: true,
|
||||
@@ -120,7 +120,7 @@ export async function getServerSideProps(
|
||||
// appPages is actually hardcoded here and no matter the fileName the same variable would be used.
|
||||
// We can write some validation logic later on that ensures that [...appPages].tsx file exists
|
||||
params.appPages = pages.slice(1);
|
||||
const session = await getSession({ req: context.req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const user = session?.user;
|
||||
|
||||
const result = await route.getServerSideProps(
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/client";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -86,9 +86,11 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
let appStore;
|
||||
if (session?.user?.id) {
|
||||
|
||||
@@ -7,11 +7,10 @@ import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmailField } from "@calcom/ui";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
@@ -133,18 +132,21 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session) {
|
||||
res.writeHead(302, { Location: "/" });
|
||||
res.end();
|
||||
return;
|
||||
return { props: {} };
|
||||
}
|
||||
|
||||
return {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
...(await serverSideTranslations(context.locale || "en", ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,8 +9,9 @@ import { FormProvider, useForm } from "react-hook-form";
|
||||
import { FaGoogle } from "react-icons/fa";
|
||||
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { ErrorCode, getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -220,8 +221,9 @@ export default function Login({
|
||||
|
||||
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
|
||||
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const verifyJwt = (jwt: string) => {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -136,10 +136,12 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
|
||||
import { getProviders, signIn, getCsrfToken } from "next-auth/react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
type Provider = {
|
||||
@@ -25,7 +26,9 @@ function signin({ providers }: { providers: Provider[] }) {
|
||||
export default signin;
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getSession(context);
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const csrfToken = await getCsrfToken(context);
|
||||
const providers = await getProviders();
|
||||
if (session) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import {
|
||||
hostedCal,
|
||||
@@ -16,7 +17,6 @@ import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
@@ -60,9 +60,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
throw new Error(`File is not named sso/[provider]`);
|
||||
}
|
||||
|
||||
const { req } = context;
|
||||
const { req, res } = context;
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
if (session) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { z } from "zod";
|
||||
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||
@@ -26,7 +27,6 @@ import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||
@@ -371,8 +371,10 @@ const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) =
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, query } = context;
|
||||
const session = await getSession({ req });
|
||||
const { req, res, query } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
const typeParam = parseInt(asStringOrThrow(query.type));
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
@@ -393,9 +395,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
try {
|
||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
// session,
|
||||
type: typeParam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
initialData: {
|
||||
|
||||
@@ -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 { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -140,8 +140,10 @@ const OnboardingPage = (props: IOnboardingPageProps) => {
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const crypto = await import("crypto");
|
||||
const session = await getSession(context);
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { NextPageContext } from "next";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
|
||||
function RedirectPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
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 { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { identityProviderNameMap } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
@@ -82,6 +82,8 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const booking = await prisma.booking.findUnique({
|
||||
@@ -139,7 +141,8 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
});
|
||||
const session = await getSession();
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
// set meetingPassword to null for guests
|
||||
if (session?.user.id !== bookingObj.user?.id) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
@@ -12,7 +13,11 @@ import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||
* Make sure to `return { props: { trpcState: ssr.dehydrate() } }` at the end.
|
||||
*/
|
||||
export async function ssrInit(context: GetServerSidePropsContext) {
|
||||
const ctx = await createContext(context);
|
||||
const { req, res } = context;
|
||||
|
||||
const sessionGetter = () => getServerSession({ req, res });
|
||||
|
||||
const ctx = await createContext(context, sessionGetter);
|
||||
|
||||
const ssr = createProxySSGHelpers({
|
||||
router: appRouter,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
/**
|
||||
* !: Stops the `jose` dependency from bundling the browser version and breaking tests
|
||||
* @jest-environment node
|
||||
*/
|
||||
import type {
|
||||
EventType as PrismaEventType,
|
||||
User as PrismaUser,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { signIn } from "next-auth/react";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import z from "zod";
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
export enum ErrorCode {
|
||||
IncorrectUsernamePassword = "incorrect-username-password",
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
SocialIdentityProviderRequired = "social-identity-provider-required",
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
import { getSession } from "./getSession";
|
||||
|
||||
type CtxOrReq = { req: NextApiRequest; ctx?: never } | { ctx: { req: NextApiRequest }; req?: never };
|
||||
|
||||
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
|
||||
const session = await getSession(ctxOrReq);
|
||||
if (!session?.user.id) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
return session;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { getServerSession as getServerSessionInner } from "next-auth/next";
|
||||
|
||||
import { AUTH_OPTIONS } from "./next-auth-options";
|
||||
|
||||
export async function getServerSession(options: {
|
||||
req: NextApiRequest | GetServerSidePropsContext["req"];
|
||||
res: NextApiResponse | GetServerSidePropsContext["res"];
|
||||
authOptions?: AuthOptions;
|
||||
}) {
|
||||
const { req, res, authOptions = AUTH_OPTIONS } = options;
|
||||
|
||||
const session = await getServerSessionInner(req, res, authOptions);
|
||||
|
||||
// that these are equal are ensured in `[...nextauth]`'s callback
|
||||
return session as Session | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Session } from "next-auth";
|
||||
import type { GetSessionParams } from "next-auth/react";
|
||||
import { getSession as getSessionInner } from "next-auth/react";
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
export async function hashPassword(password: string) {
|
||||
const hashedPassword = await hash(password, 12);
|
||||
return hashedPassword;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
export function isPasswordValid(password: string): boolean;
|
||||
export function isPasswordValid(
|
||||
password: string,
|
||||
breakdown: boolean,
|
||||
strict?: boolean
|
||||
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
|
||||
export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) {
|
||||
let cap = false, // Has uppercase characters
|
||||
low = false, // Has lowercase characters
|
||||
num = false, // At least one number
|
||||
min = false, // Eight characters, or fifteen in strict mode.
|
||||
admin_min = false;
|
||||
if (password.length > 7 && (!strict || password.length > 14)) min = true;
|
||||
if (strict && password.length > 14) admin_min = true;
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
if (!isNaN(parseInt(password[i]))) num = true;
|
||||
else {
|
||||
if (password[i] === password[i].toUpperCase()) cap = true;
|
||||
if (password[i] === password[i].toLowerCase()) low = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);
|
||||
|
||||
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
|
||||
// Only return the admin key if strict mode is enabled.
|
||||
if (strict) errors = { ...errors, admin_min };
|
||||
|
||||
return errors;
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ import type {
|
||||
} from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
|
||||
import { identityProviderNameMap } from "@lib/auth";
|
||||
import { identityProviderNameMap } from "./identityProviderNameMap";
|
||||
|
||||
/** @return { import("next-auth/adapters").Adapter } */
|
||||
export default function CalComAdapter(prismaClient: PrismaClient) {
|
||||
@@ -0,0 +1,664 @@
|
||||
import type { UserPermissionRole } from "@prisma/client";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import { SignJWT } from "jose";
|
||||
import type { AuthOptions, Session } from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import type { 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 type { TransportOptions } from "nodemailer";
|
||||
import nodemailer 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 jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL, WEBSITE_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, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { ErrorCode } from "./ErrorCode";
|
||||
import { isPasswordValid } from "./isPasswordValid";
|
||||
import CalComAdapter from "./next-auth-custom-adapter";
|
||||
import { verifyPassword } from "./verifyPassword";
|
||||
|
||||
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 transporter = nodemailer.createTransport<TransportOptions>({
|
||||
...(serverConfig.transport as TransportOptions),
|
||||
} as TransportOptions);
|
||||
|
||||
const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase();
|
||||
|
||||
const signJwt = async (payload: { email: string }) => {
|
||||
const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
|
||||
return new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: "HS256" })
|
||||
.setSubject(payload.email)
|
||||
.setIssuedAt()
|
||||
.setIssuer(WEBSITE_URL)
|
||||
.setAudience(`${WEBSITE_URL}/auth/login`)
|
||||
.setExpirationTime("2m")
|
||||
.sign(secret);
|
||||
};
|
||||
|
||||
const loginWithTotp = async (user: { email: string }) =>
|
||||
`/auth/login?totp=${await signJwt({ email: user.email })}`;
|
||||
|
||||
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,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
twoFactorSecret: true,
|
||||
teams: {
|
||||
include: {
|
||||
team: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider !== IdentityProvider.CAL && !credentials.totpCode) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.password) {
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
}
|
||||
|
||||
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: { team: { metadata: unknown } }) => {
|
||||
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,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (isSAMLLoginEnabled) {
|
||||
providers.push({
|
||||
id: "saml",
|
||||
name: "BoxyHQ",
|
||||
type: "oauth",
|
||||
version: "2.0",
|
||||
checks: ["pkce", "state"],
|
||||
authorization: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
||||
params: {
|
||||
scope: "",
|
||||
response_type: "code",
|
||||
provider: "saml",
|
||||
},
|
||||
},
|
||||
token: {
|
||||
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
||||
params: { grant_type: "authorization_code" },
|
||||
},
|
||||
userinfo: `${WEBAPP_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: clientSecretVerifier,
|
||||
},
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
});
|
||||
|
||||
// Idp initiated login
|
||||
providers.push(
|
||||
CredentialsProvider({
|
||||
id: "saml-idp",
|
||||
name: "IdP Login",
|
||||
credentials: {
|
||||
code: {},
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { code } = credentials;
|
||||
|
||||
if (!code) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
// Fetch access token
|
||||
const { access_token } = await oauthController.token({
|
||||
code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: `${process.env.NEXTAUTH_URL}`,
|
||||
client_id: "dummy",
|
||||
client_secret: clientSecretVerifier,
|
||||
});
|
||||
|
||||
if (!access_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch user info
|
||||
const userInfo = await oauthController.userInfo(access_token);
|
||||
|
||||
if (!userInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { id, firstName, lastName, email } = userInfo;
|
||||
|
||||
return {
|
||||
id: id as unknown as number,
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
name: `${firstName} ${lastName}`.trim(),
|
||||
email_verified: true,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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(WEBAPP_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: WEBAPP_URL,
|
||||
signin_url: url,
|
||||
email: identifier,
|
||||
}),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
}
|
||||
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
|
||||
export const AUTH_OPTIONS: AuthOptions = {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
adapter: calcomAdapter,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
// 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 });
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_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) {
|
||||
return token;
|
||||
}
|
||||
if (account.type === "credentials") {
|
||||
// return token if credentials,saml-idp
|
||||
if (account.provider === "saml-idp") {
|
||||
return token;
|
||||
}
|
||||
// any other credentials, add user info
|
||||
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.type === "oauth") {
|
||||
if (!account.provider || !account.providerAccountId) {
|
||||
return token;
|
||||
}
|
||||
const idP = account.provider === "saml" ? IdentityProvider.SAML : IdentityProvider.GOOGLE;
|
||||
|
||||
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(prisma);
|
||||
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.
|
||||
// Only if provider is not saml-idp
|
||||
if (account?.provider !== "saml-idp") {
|
||||
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" || account.provider === "saml-idp") {
|
||||
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 existingUser = await prisma.user.findFirst({
|
||||
include: {
|
||||
accounts: {
|
||||
where: {
|
||||
provider: idP,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
identityProvider: idP as IdentityProvider,
|
||||
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");
|
||||
}
|
||||
}
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
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 } });
|
||||
if (existingUser.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUser);
|
||||
} else {
|
||||
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) {
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
) {
|
||||
await prisma.user.update({
|
||||
where: { email: existingUserWithEmail.email },
|
||||
data: { password: null },
|
||||
});
|
||||
if (existingUserWithEmail.twoFactorEnabled) {
|
||||
return loginWithTotp(existingUserWithEmail);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else 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);
|
||||
|
||||
if (account.twoFactorEnabled) {
|
||||
return loginWithTotp(newUser);
|
||||
} else {
|
||||
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(WEBAPP_URL).hostname) return url;
|
||||
return baseUrl;
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export function validPassword(password: string) {
|
||||
if (password.length < 7) return false;
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false;
|
||||
|
||||
if (!/\d+/.test(password)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { compare } from "bcryptjs";
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string) {
|
||||
const isValid = await compare(password, hashedPassword);
|
||||
return isValid;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@calcom/feature-auth",
|
||||
"sideEffects": false,
|
||||
"description": "Cal.com's main auth code",
|
||||
"authors": "Cal.com, Inc.",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"handlebars": "^4.7.7",
|
||||
"jose": "^4.13.1",
|
||||
"next-auth": "^4.20.1",
|
||||
"nodemailer": "^6.7.8",
|
||||
"otplib": "^12.0.1",
|
||||
"@calcom/dayjs": "*",
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/ui": "*"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { User } from "@prisma/client";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getSession } from "@calcom/features/auth/lib/getSession";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const teamIdschema = z.object({
|
||||
|
||||
@@ -3,8 +3,8 @@ import type Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getRequestedSlugError } from "@calcom/app-store/stripepayment/lib/team-billing";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
@@ -64,7 +64,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
closeComUpdateTeam(prevTeam, team);
|
||||
}
|
||||
|
||||
const session = await getSession({ req });
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
if (!session) return { message: "Team upgraded successfully" };
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { compare, hash } from "bcryptjs";
|
||||
import type { NextApiRequest } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
import type { GetSessionParams } from "next-auth/react";
|
||||
import { getSession as getSessionInner } from "next-auth/react";
|
||||
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
|
||||
export async function hashPassword(password: string) {
|
||||
const hashedPassword = await hash(password, 12);
|
||||
return hashedPassword;
|
||||
}
|
||||
|
||||
export async function verifyPassword(password: string, hashedPassword: string) {
|
||||
const isValid = await compare(password, hashedPassword);
|
||||
return isValid;
|
||||
}
|
||||
|
||||
export function validPassword(password: string) {
|
||||
if (password.length < 7) return false;
|
||||
|
||||
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password)) return false;
|
||||
|
||||
if (!/\d+/.test(password)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function isPasswordValid(password: string): boolean;
|
||||
export function isPasswordValid(
|
||||
password: string,
|
||||
breakdown: boolean,
|
||||
strict?: boolean
|
||||
): { caplow: boolean; num: boolean; min: boolean; admin_min: boolean };
|
||||
export function isPasswordValid(password: string, breakdown?: boolean, strict?: boolean) {
|
||||
let cap = false, // Has uppercase characters
|
||||
low = false, // Has lowercase characters
|
||||
num = false, // At least one number
|
||||
min = false, // Eight characters, or fifteen in strict mode.
|
||||
admin_min = false;
|
||||
if (password.length > 7 && (!strict || password.length > 14)) min = true;
|
||||
if (strict && password.length > 14) admin_min = true;
|
||||
for (let i = 0; i < password.length; i++) {
|
||||
if (!isNaN(parseInt(password[i]))) num = true;
|
||||
else {
|
||||
if (password[i] === password[i].toUpperCase()) cap = true;
|
||||
if (password[i] === password[i].toLowerCase()) low = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!breakdown) return cap && low && num && min && (strict ? admin_min : true);
|
||||
|
||||
let errors: Record<string, boolean> = { caplow: cap && low, num, min };
|
||||
// Only return the admin key if strict mode is enabled.
|
||||
if (strict) errors = { ...errors, admin_min };
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
type CtxOrReq = { req: NextApiRequest; ctx?: never } | { ctx: { req: NextApiRequest }; req?: never };
|
||||
|
||||
export const ensureSession = async (ctxOrReq: CtxOrReq) => {
|
||||
const session = await getSession(ctxOrReq);
|
||||
if (!session?.user.id) throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||
return session;
|
||||
};
|
||||
|
||||
export enum ErrorCode {
|
||||
IncorrectUsernamePassword = "incorrect-username-password",
|
||||
UserNotFound = "user-not-found",
|
||||
IncorrectPassword = "incorrect-password",
|
||||
UserMissingPassword = "missing-password",
|
||||
TwoFactorDisabled = "two-factor-disabled",
|
||||
TwoFactorAlreadyEnabled = "two-factor-already-enabled",
|
||||
TwoFactorSetupRequired = "two-factor-setup-required",
|
||||
SecondFactorRequired = "second-factor-required",
|
||||
IncorrectTwoFactorCode = "incorrect-two-factor-code",
|
||||
InternalServerError = "internal-server-error",
|
||||
NewPasswordMatchesOld = "new-password-matches-old",
|
||||
ThirdPartyIdentityProviderEnabled = "third-party-identity-provider-enabled",
|
||||
RateLimitExceeded = "rate-limit-exceeded",
|
||||
SocialIdentityProviderRequired = "social-identity-provider-required",
|
||||
}
|
||||
|
||||
export const identityProviderNameMap: { [key in IdentityProvider]: string } = {
|
||||
[IdentityProvider.CAL]: "Cal",
|
||||
[IdentityProvider.GOOGLE]: "Google",
|
||||
[IdentityProvider.SAML]: "SAML",
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import cache from "memory-cache";
|
||||
|
||||
import { ErrorCode } from "./auth";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
|
||||
const rateLimit = (options: { intervalInMs: number }) => {
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { BookingStatus, MembershipRole, Prisma, UserPermissionRole } from "@prisma/client";
|
||||
import type { Prisma, UserPermissionRole } from "@prisma/client";
|
||||
import { BookingStatus, MembershipRole } from "@prisma/client";
|
||||
import { uuid } from "short-uuid";
|
||||
|
||||
import dailyMeta from "@calcom/app-store/dailyvideo/_metadata";
|
||||
import googleMeetMeta from "@calcom/app-store/googlevideo/_metadata";
|
||||
import zoomMeta from "@calcom/app-store/zoomvideo/_metadata";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
|
||||
import prisma from ".";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import superjson from "superjson";
|
||||
|
||||
import { httpBatchLink } from "../client/links/httpBatchLink";
|
||||
import { httpLink } from "../client/links/httpLink";
|
||||
import { loggerLink } from "../client/links/loggerLink";
|
||||
import { splitLink } from "../client/links/splitLink";
|
||||
@@ -8,6 +7,7 @@ import { createTRPCNext } from "../next";
|
||||
// ℹ️ Type-only import:
|
||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
|
||||
import type { TRPCClientErrorLike } from "../react";
|
||||
import { httpBatchLink } from "../react";
|
||||
import type { inferRouterInputs, inferRouterOutputs, Maybe } from "../server";
|
||||
import type { AppRouter } from "../server/routers/_app";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { GetServerSidePropsContext, NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
|
||||
@@ -108,6 +108,15 @@ type CreateInnerContextOptions = {
|
||||
i18n: Awaited<ReturnType<typeof serverSideTranslations>>;
|
||||
} & Partial<CreateContextOptions>;
|
||||
|
||||
export type GetSessionFn =
|
||||
| ((_options: {
|
||||
req: GetServerSidePropsContext["req"] | NextApiRequest;
|
||||
res: GetServerSidePropsContext["res"] | NextApiResponse;
|
||||
}) => Promise<Session | null>)
|
||||
| (() => Promise<Session | null>);
|
||||
|
||||
const DEFAULT_SESSION_GETTER: GetSessionFn = ({ req, res }) => getServerSession({ req, res });
|
||||
|
||||
/**
|
||||
* Inner context. Will always be available in your procedures, in contrast to the outer context.
|
||||
*
|
||||
@@ -128,9 +137,12 @@ export async function createContextInner(opts: CreateInnerContextOptions) {
|
||||
* Creates context for an incoming request
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
export const createContext = async ({ req, res }: CreateContextOptions, sessionGetter = getSession) => {
|
||||
export const createContext = async (
|
||||
{ req, res }: CreateContextOptions,
|
||||
sessionGetter: GetSessionFn = DEFAULT_SESSION_GETTER
|
||||
) => {
|
||||
// for API-response caching see https://trpc.io/docs/caching
|
||||
const session = await sessionGetter({ req });
|
||||
const session = await sessionGetter({ req, res });
|
||||
|
||||
const user = await getUserFromSession({ session, req });
|
||||
const locale = user?.locale ?? getLocaleFromHeaders(req);
|
||||
|
||||
@@ -20,10 +20,11 @@ import {
|
||||
} from "@calcom/core/videoClient";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
|
||||
import { ErrorCode, verifyPassword } from "@calcom/lib/auth";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { hashPassword, validPassword, verifyPassword } from "@calcom/lib/auth";
|
||||
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
|
||||
import { validPassword } from "@calcom/features/auth/lib/validPassword";
|
||||
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -4160,9 +4160,9 @@
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@headlessui/react@^1.5.0":
|
||||
version "1.7.12"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.12.tgz#9ab2baa3c4f632782631e00937f9531a34033619"
|
||||
integrity sha512-FhSx5V+Qp0GvbTpaxyS+ymGDDNntCacClWsk/d8Upbr19g3AsPbjfPk4+m2CgJGcuCB5Dz7LpUIOAbvQTyjL2g==
|
||||
version "1.7.13"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.13.tgz#fd150b394954e9f1d86ed2340cffd1217d6e7628"
|
||||
integrity sha512-9n+EQKRtD9266xIHXdY5MfiXPDfYwl7zBM7KOx2Ae3Gdgxy8QML1FkCMjq6AsOf0l6N9uvI4HcFtuFlenaldKg==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
|
||||
@@ -5497,19 +5497,19 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-accordion@^1.0.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.0.tgz#8db063b9eaeb32ca90ffec74e190dab104b56522"
|
||||
integrity sha512-CNN9ZBgCK4i4SX7gFk5s8095j55DUWi85vwRNfkfBLs0QdAG5Tb4ku6sBeugCAiLvsmxw481GyNl+C3stoJVBQ==
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.1.1.tgz#fa1ab1b5c6a29aa75aefaf306a9e72fe3a482dbc"
|
||||
integrity sha512-TQtyyRubYe8DD6DYCovNLTjd2D+TFrNCpr99T5M3cYUbR7BsRxWsxfInjbQ1nHsdy2uPTcnJS5npyXPVfP0piw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-collapsible" "1.0.1"
|
||||
"@radix-ui/react-collection" "1.0.1"
|
||||
"@radix-ui/react-collapsible" "1.0.2"
|
||||
"@radix-ui/react-collection" "1.0.2"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-direction" "1.0.0"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
|
||||
"@radix-ui/react-arrow@1.0.0":
|
||||
@@ -5539,10 +5539,10 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
"@radix-ui/react-collapsible@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.1.tgz#7184312ce18e703628b9ef75cdc44b896d6f06b1"
|
||||
integrity sha512-0maX4q91iYa4gjt3PsNf7dq/yqSR+HGAE8I5p54dQ6gnveS+ETWlMoijxrhmgV1k8svxpm34mQAtqIrJt4XZmA==
|
||||
"@radix-ui/react-collapsible@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.0.2.tgz#0583470c7caa8cd1ab6f606416288d19b3baf777"
|
||||
integrity sha512-QNiDT6Au8jUU0K1WV+HEd4loH7C5CKQjeXxskwqyiyAkyCmW7qlQM5vSSJCIoQC+OVPyhgafSmGudRP8Qm1/gA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
@@ -5550,7 +5550,7 @@
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-presence" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
|
||||
@@ -5591,15 +5591,15 @@
|
||||
"@radix-ui/react-primitive" "1.0.0"
|
||||
"@radix-ui/react-slot" "1.0.0"
|
||||
|
||||
"@radix-ui/react-collection@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.1.tgz#259506f97c6703b36291826768d3c1337edd1de5"
|
||||
integrity sha512-uuiFbs+YCKjn3X1DTSx9G7BHApu4GHbi3kgiwsnFUbOKCrwejAJv4eE4Vc8C0Oaxt9T0aV4ox0WCOdx+39Xo+g==
|
||||
"@radix-ui/react-collection@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.2.tgz#d50da00bfa2ac14585319efdbbb081d4c5a29a97"
|
||||
integrity sha512-s8WdQQ6wNXpaxdZ308KSr8fEWGrg4un8i4r/w7fhiS4ElRNjk5rRcl0/C6TANG2LvLOGIxtzo/jAg6Qf73TEBw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
|
||||
"@radix-ui/react-compose-refs@0.1.0":
|
||||
@@ -5695,6 +5695,18 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.2"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.3.tgz#63844d8e6bbcd010a513e7176d051c3c4044e09e"
|
||||
integrity sha512-nXZOvFjOuHS1ovumntGV7NNoLaEp9JEvTht3MBjP44NSW5hUKj/8OnfN3+8WmB+CEhN44XaGhpHoSsUIEl5P7Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.2"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-1.0.0.tgz#687959e1bcdd5e8eb0de406484aff28d0974c593"
|
||||
@@ -5810,25 +5822,25 @@
|
||||
react-remove-scroll "2.5.4"
|
||||
|
||||
"@radix-ui/react-navigation-menu@^1.0.0":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.1.tgz#84f24d90e6448a0c83d3431c6eefbea73dc7522e"
|
||||
integrity sha512-Khgf+LwqYfUpbFAHcFPDMj6ZrWxnwCgC96liLYwE48x9YJbXGlutOWzZaSzrgl82xS+PwoPLQunfDe/i4ZITRA==
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.2.tgz#ca5ee35ddbc4e57a9403be92f9573435173e35c8"
|
||||
integrity sha512-1fSjOAGTThYSgJ5pENG2V8we7+6KDbfbiyt66HmLTeo0W3PAmmciclm0o97VlcVZW7q5Vg2hN7Cbj4XKo5u2sw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-collection" "1.0.1"
|
||||
"@radix-ui/react-collection" "1.0.2"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-direction" "1.0.0"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.2"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.3"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-presence" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
"@radix-ui/react-use-previous" "1.0.0"
|
||||
"@radix-ui/react-visually-hidden" "1.0.1"
|
||||
"@radix-ui/react-visually-hidden" "1.0.2"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.2":
|
||||
version "1.0.2"
|
||||
@@ -5942,6 +5954,14 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
|
||||
"@radix-ui/react-primitive@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz#54e22f49ca59ba88d8143090276d50b93f8a7053"
|
||||
integrity sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-slot" "1.0.1"
|
||||
|
||||
"@radix-ui/react-radio-group@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.0.0.tgz#16267bdfc3c1bd293a6ac1b34f3e9b484e88ebf8"
|
||||
@@ -5976,19 +5996,19 @@
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
|
||||
"@radix-ui/react-roving-focus@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz#d8ac2e3b8006697bdfc2b0eb06bef7e15b6245de"
|
||||
integrity sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==
|
||||
"@radix-ui/react-roving-focus@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz#0b4f4f9bd509f4510079e9e0734a734fd17cdce3"
|
||||
integrity sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-collection" "1.0.1"
|
||||
"@radix-ui/react-collection" "1.0.2"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-direction" "1.0.0"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
|
||||
@@ -6075,9 +6095,9 @@
|
||||
"@radix-ui/react-use-size" "1.0.0"
|
||||
|
||||
"@radix-ui/react-tabs@^1.0.0":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.2.tgz#8f5ec73ca41b151a413bdd6e00553408ff34ce07"
|
||||
integrity sha512-gOUwh+HbjCuL0UCo8kZ+kdUEG8QtpdO4sMQduJ34ZEz0r4922g9REOBM+vIsfwtGxSug4Yb1msJMJYN2Bk8TpQ==
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.3.tgz#8b4158160a7c6633c893c74641e929d2708e709a"
|
||||
integrity sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
@@ -6085,8 +6105,8 @@
|
||||
"@radix-ui/react-direction" "1.0.0"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-presence" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-roving-focus" "1.0.2"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
"@radix-ui/react-roving-focus" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
|
||||
"@radix-ui/react-toggle-group@^1.0.0":
|
||||
@@ -6254,13 +6274,13 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.0"
|
||||
|
||||
"@radix-ui/react-visually-hidden@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.1.tgz#9a4ac4fc97ae8d72a10e727f16b3121b5f0aa469"
|
||||
integrity sha512-K1hJcCMfWfiYUibRqf3V8r5Drpyf7rh44jnrwAbdvI5iCCijilBBeyQv9SKidYNZIopMdCyR9FnIjkHxHN0FcQ==
|
||||
"@radix-ui/react-visually-hidden@1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.2.tgz#29b117a59ef09a984bdad12cb98d81e8350be450"
|
||||
integrity sha512-qirnJxtYn73HEk1rXL12/mXnu2rwsNHDID10th2JGtdK25T9wX+mxRmGt7iPSahw512GbZOc0syZX1nLQGoEOg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.2"
|
||||
|
||||
"@radix-ui/rect@1.0.0":
|
||||
version "1.0.0"
|
||||
@@ -20700,7 +20720,7 @@ next-api-middleware@^1.0.1:
|
||||
dependencies:
|
||||
debug "^4.3.2"
|
||||
|
||||
next-auth@^4.18.8:
|
||||
next-auth@^4.20.1:
|
||||
version "4.20.1"
|
||||
resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.20.1.tgz#6e65c4fde14171f6ce64f05f672f80f39fc418c7"
|
||||
integrity sha512-ZcTUN4qzzZ/zJYgOW0hMXccpheWtAol8QOMdMts+LYRcsPGsqf2hEityyaKyECQVw1cWInb9dF3wYwI5GZdEmQ==
|
||||
@@ -22105,17 +22125,17 @@ playwright-core@1.25.0:
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.25.0.tgz#54dc867c6c2cc5e4233905e249206a02914d14f1"
|
||||
integrity sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q==
|
||||
|
||||
playwright-core@1.31.1:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.1.tgz#4deeebbb8fb73b512593fe24bea206d8fd85ff7f"
|
||||
integrity sha512-JTyX4kV3/LXsvpHkLzL2I36aCdml4zeE35x+G5aPc4bkLsiRiQshU5lWeVpHFAuC8xAcbI6FDcw/8z3q2xtJSQ==
|
||||
playwright-core@1.31.2:
|
||||
version "1.31.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.2.tgz#debf4b215d14cb619adb7e511c164d068075b2ed"
|
||||
integrity sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==
|
||||
|
||||
playwright@^1.29.2:
|
||||
version "1.31.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.1.tgz#66164cdc1506bc883c7a98b44714dfea50b22d50"
|
||||
integrity sha512-zKJabsIA2rvOwJ12lGTqWv4HVJzlfw2JtUvO4hAr7J8UXQZ1qEPpX20E1vcz/9fotnTkwgqp3CVdIBwptBN3Fg==
|
||||
version "1.31.2"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.2.tgz#4252280586c596746122cd1fdf9f8ff6a63fa852"
|
||||
integrity sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og==
|
||||
dependencies:
|
||||
playwright-core "1.31.1"
|
||||
playwright-core "1.31.2"
|
||||
|
||||
pngjs@^3.0.0, pngjs@^3.3.0, pngjs@^3.3.3:
|
||||
version "3.4.0"
|
||||
@@ -26711,9 +26731,9 @@ tzdata@^1.0.30:
|
||||
integrity sha512-QxRODDsXS8UVxlPazCZXplqVuD6mCe7tSyDE5SCSHXv1nmrfflXxNH4pD+fjU8KJwePhWqhp6n+FpygQeGLv7w==
|
||||
|
||||
ua-parser-js@^1.0.33:
|
||||
version "1.0.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.33.tgz#f21f01233e90e7ed0f059ceab46eb190ff17f8f4"
|
||||
integrity sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.34.tgz#b33f41c415325839f354005d25a2f588be296976"
|
||||
integrity sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==
|
||||
|
||||
uc.micro@^1.0.1, uc.micro@^1.0.5:
|
||||
version "1.0.6"
|
||||
@@ -28523,9 +28543,9 @@ zod-prisma@^0.5.4:
|
||||
ts-morph "^13.0.2"
|
||||
|
||||
zod@^3.17.3:
|
||||
version "3.20.6"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.6.tgz#2f2f08ff81291d47d99e86140fedb4e0db08361a"
|
||||
integrity sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==
|
||||
version "3.21.4"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
||||
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
||||
|
||||
zod@^3.20.2:
|
||||
version "3.20.2"
|
||||
|
||||
Reference in New Issue
Block a user