diff --git a/apps/auth/next.config.js b/apps/auth/next.config.js index 3d3bc9990d..ecdcc6eaec 100644 --- a/apps/auth/next.config.js +++ b/apps/auth/next.config.js @@ -2,6 +2,20 @@ const nextConfig = { reactStrictMode: true, swcMinify: true, + transpilePackages: [ + "@calcom/app-store", + "@calcom/core", + "@calcom/dayjs", + "@calcom/emails", + "@calcom/embed-core", + "@calcom/embed-react", + "@calcom/embed-snippet", + "@calcom/features", + "@calcom/lib", + "@calcom/prisma", + "@calcom/trpc", + "@calcom/ui", + ], }; module.exports = nextConfig; diff --git a/apps/auth/pages/api/auth/[...nextauth].ts b/apps/auth/pages/api/auth/[...nextauth].ts index 1a4bf95bc9..0ad6d98f7e 100644 --- a/apps/auth/pages/api/auth/[...nextauth].ts +++ b/apps/auth/pages/api/auth/[...nextauth].ts @@ -2,11 +2,9 @@ import { IdentityProvider, UserPermissionRole } from "@prisma/client"; import { readFileSync } from "fs"; import Handlebars from "handlebars"; import NextAuth, { Session } from "next-auth"; -import type { Provider } from "next-auth/providers"; -import AppleProvider from "next-auth/providers/apple"; +import { Provider } from "next-auth/providers"; import CredentialsProvider from "next-auth/providers/credentials"; import EmailProvider from "next-auth/providers/email"; -import FacebookProvider from "next-auth/providers/facebook"; import GoogleProvider from "next-auth/providers/google"; import nodemailer, { TransportOptions } from "nodemailer"; import { authenticator } from "otplib"; @@ -27,7 +25,188 @@ import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; -const providers: Provider[] = []; +const GOOGLE_API_CREDENTIALS = process.env.GOOGLE_API_CREDENTIALS || "{}"; +const { client_id: GOOGLE_CLIENT_ID, client_secret: GOOGLE_CLIENT_SECRET } = + JSON.parse(GOOGLE_API_CREDENTIALS)?.web || {}; +const GOOGLE_LOGIN_ENABLED = process.env.GOOGLE_LOGIN_ENABLED === "true"; +const IS_GOOGLE_LOGIN_ENABLED = !!(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET && GOOGLE_LOGIN_ENABLED); +const IS_SAML_LOGIN_ENABLED = !!(process.env.SAML_DATABASE_URL && process.env.SAML_ADMINS); + +const NEXTAUTH_URL = process.env.NEXTAUTH_URL || WEBAPP_URL; + +const transporter = nodemailer.createTransport({ + ...(serverConfig.transport as TransportOptions), +} as TransportOptions); + +const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase(); + +const providers: Provider[] = [ + CredentialsProvider({ + id: "credentials", + name: "Cal.com", + type: "credentials", + credentials: { + email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" }, + password: { label: "Password", type: "password", placeholder: "Your super secure password" }, + totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" }, + }, + async authorize(credentials) { + if (!credentials) { + console.error(`For some reason credentials are missing`); + throw new Error(ErrorCode.InternalServerError); + } + + const user = await prisma.user.findUnique({ + where: { + email: credentials.email.toLowerCase(), + }, + select: { + role: true, + id: true, + username: true, + name: true, + email: true, + identityProvider: true, + password: true, + twoFactorEnabled: true, + twoFactorSecret: true, + teams: { + include: { + team: true, + }, + }, + }, + }); + + if (!user) { + throw new Error(ErrorCode.UserNotFound); + } + + if (user.identityProvider !== IdentityProvider.CAL) { + throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled); + } + + if (!user.password) { + throw new Error(ErrorCode.UserMissingPassword); + } + + const isCorrectPassword = await verifyPassword(credentials.password, user.password); + if (!isCorrectPassword) { + throw new Error(ErrorCode.IncorrectPassword); + } + + if (user.twoFactorEnabled) { + if (!credentials.totpCode) { + throw new Error(ErrorCode.SecondFactorRequired); + } + + if (!user.twoFactorSecret) { + console.error(`Two factor is enabled for user ${user.id} but they have no secret`); + throw new Error(ErrorCode.InternalServerError); + } + + if (!process.env.CALENDSO_ENCRYPTION_KEY) { + console.error(`"Missing encryption key; cannot proceed with two factor login."`); + throw new Error(ErrorCode.InternalServerError); + } + + const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); + if (secret.length !== 32) { + console.error( + `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` + ); + throw new Error(ErrorCode.InternalServerError); + } + + const isValidToken = authenticator.check(credentials.totpCode, secret); + if (!isValidToken) { + throw new Error(ErrorCode.IncorrectTwoFactorCode); + } + } + + const limiter = rateLimit({ + intervalInMs: 60 * 1000, // 1 minute + }); + await limiter.check(10, user.email); // 10 requests per minute + // Check if the user you are logging into has any active teams + const hasActiveTeams = + user.teams.filter((m) => { + if (!IS_TEAM_BILLING_ENABLED) return true; + const metadata = teamMetadataSchema.safeParse(m.team.metadata); + if (metadata.success && metadata.data?.subscriptionId) return true; + return false; + }).length > 0; + + // authentication success- but does it meet the minimum password requirements? + if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) { + return { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + role: "INACTIVE_ADMIN", + belongsToActiveTeam: hasActiveTeams, + }; + } + + return { + id: user.id, + username: user.username, + email: user.email, + name: user.name, + role: user.role, + belongsToActiveTeam: hasActiveTeams, + }; + }, + }), + ImpersonationProvider, +]; + +if (IS_GOOGLE_LOGIN_ENABLED) { + providers.push( + GoogleProvider({ + clientId: GOOGLE_CLIENT_ID, + clientSecret: GOOGLE_CLIENT_SECRET, + }) + ); +} + +if (isSAMLLoginEnabled) { + providers.push({ + id: "saml", + name: "BoxyHQ", + type: "oauth", + version: "2.0", + checks: ["pkce", "state"], + authorization: { + url: `${NEXTAUTH_URL}/api/auth/saml/authorize`, + params: { + scope: "", + response_type: "code", + provider: "saml", + }, + }, + token: { + url: `${NEXTAUTH_URL}/api/auth/saml/token`, + params: { grant_type: "authorization_code" }, + }, + userinfo: `${NEXTAUTH_URL}/api/auth/saml/userinfo`, + profile: (profile) => { + return { + id: profile.id || "", + firstName: profile.firstName || "", + lastName: profile.lastName || "", + email: profile.email || "", + name: `${profile.firstName || ""} ${profile.lastName || ""}`.trim(), + email_verified: true, + }; + }, + options: { + clientId: "dummy", + clientSecret: "dummy", + }, + }); +} if (true) { const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates"); @@ -38,7 +217,7 @@ if (true) { // 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); + const webappUrl = new URL(NEXTAUTH_URL); if (originalUrl.origin !== webappUrl.origin) { url = url.replace(originalUrl.origin, webappUrl.origin); } @@ -51,7 +230,7 @@ if (true) { to: identifier, subject: "Your sign-in link for " + APP_NAME, html: emailTemplate({ - base_url: WEBAPP_URL, + base_url: NEXTAUTH_URL, signin_url: url, email: identifier, }), @@ -60,21 +239,274 @@ if (true) { }) ); } - -// OAuth authentication providers... -if (!!process.env.APPLE_ID && process.env.APPLE_SECRET) { - providers.push( - AppleProvider({ - clientId: process.env.APPLE_ID!, - clientSecret: process.env.APPLE_SECRET!, - }) - ); -} - -const adapter = CalComAdapter(prisma); - +const calcomAdapter = CalComAdapter(prisma); export default NextAuth({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + adapter: calcomAdapter, + session: { + strategy: "jwt", + }, + cookies: defaultCookies(NEXTAUTH_URL?.startsWith("https://")), + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + error: "/auth/error", // Error code passed in query string as ?error= + verifyRequest: "/auth/verify", + // newUser: "/auth/new", // New users will be directed here on first sign in (leave the property out if not of interest) + }, providers, - // @ts-expect-error PrismaClient and PromiseLike signatures differ - adapter, + callbacks: { + async jwt({ token, user, account }) { + const autoMergeIdentities = async () => { + const existingUser = await prisma.user.findFirst({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + where: { email: token.email! }, + select: { + id: true, + username: true, + name: true, + email: true, + role: true, + }, + }); + + if (!existingUser) { + return token; + } + + return { + ...existingUser, + ...token, + }; + }; + + if (!user) { + return await autoMergeIdentities(); + } + + if (account && account.type === "credentials") { + return { + ...token, + id: user.id, + name: user.name, + username: user.username, + email: user.email, + role: user.role, + impersonatedByUID: user?.impersonatedByUID, + belongsToActiveTeam: user?.belongsToActiveTeam, + }; + } + + // The arguments above are from the provider so we need to look up the + // user based on those values in order to construct a JWT. + if (account && account.type === "oauth" && account.provider && account.providerAccountId) { + let idP: IdentityProvider = IdentityProvider.GOOGLE; + if (account.provider === "saml") { + idP = IdentityProvider.SAML; + } + const existingUser = await prisma.user.findFirst({ + where: { + AND: [ + { + identityProvider: idP, + }, + { + identityProviderId: account.providerAccountId as string, + }, + ], + }, + }); + + if (!existingUser) { + return await autoMergeIdentities(); + } + + return { + ...token, + id: existingUser.id, + name: existingUser.name, + username: existingUser.username, + email: existingUser.email, + role: existingUser.role, + impersonatedByUID: token.impersonatedByUID as number, + belongsToActiveTeam: token?.belongsToActiveTeam as boolean, + }; + } + + return token; + }, + async session({ session, token }) { + const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || ""); + const calendsoSession: Session = { + ...session, + hasValidLicense, + user: { + ...session.user, + id: token.id as number, + name: token.name, + username: token.username as string, + role: token.role as UserPermissionRole, + impersonatedByUID: token.impersonatedByUID as number, + belongsToActiveTeam: token?.belongsToActiveTeam as boolean, + }, + }; + return calendsoSession; + }, + async signIn(params) { + const { user, account, profile } = params; + + if (account?.provider === "email") { + return true; + } + // In this case we've already verified the credentials in the authorize + // callback so we can sign the user in. + if (account?.type === "credentials") { + return true; + } + + if (account?.type !== "oauth") { + return false; + } + + if (!user.email) { + return false; + } + + if (!user.name) { + return false; + } + + if (account?.provider) { + let idP: IdentityProvider = IdentityProvider.GOOGLE; + if (account.provider === "saml") { + idP = IdentityProvider.SAML; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore-error TODO validate email_verified key on profile + user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified; + + if (!user.email_verified) { + return "/auth/error?error=unverified-email"; + } + // Only google oauth on this path + const provider = account.provider.toUpperCase() as IdentityProvider; + + const existingUser = await prisma.user.findFirst({ + include: { + accounts: { + where: { + provider: account.provider, + }, + }, + }, + where: { + identityProvider: provider, + identityProviderId: account.providerAccountId, + }, + }); + + if (existingUser) { + // In this case there's an existing user and their email address + // hasn't changed since they last logged in. + if (existingUser.email === user.email) { + try { + // If old user without Account entry we link their google account + if (existingUser.accounts.length === 0) { + const linkAccountWithUserData = { ...account, userId: existingUser.id }; + await calcomAdapter.linkAccount(linkAccountWithUserData); + } + } catch (error) { + if (error instanceof Error) { + console.error("Error while linking account of already existing user"); + } + } + return true; + } + + // If the email address doesn't match, check if an account already exists + // with the new email address. If it does, for now we return an error. If + // not, update the email of their account and log them in. + const userWithNewEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (!userWithNewEmail) { + await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } }); + return true; + } else { + return "/auth/error?error=new-email-conflict"; + } + } + + // If there's no existing user for this identity provider and id, create + // a new account. If an account already exists with the incoming email + // address return an error for now. + const existingUserWithEmail = await prisma.user.findFirst({ + where: { email: user.email }, + }); + + if (existingUserWithEmail) { + // if self-hosted then we can allow auto-merge of identity providers if email is verified + if (!hostedCal && existingUserWithEmail.emailVerified) { + return true; + } + + // check if user was invited + if ( + !existingUserWithEmail.password && + !existingUserWithEmail.emailVerified && + !existingUserWithEmail.username + ) { + await prisma.user.update({ + where: { email: user.email }, + data: { + // Slugify the incoming name and append a few random characters to + // prevent conflicts for users with the same name. + username: usernameSlug(user.name), + emailVerified: new Date(Date.now()), + name: user.name, + identityProvider: idP, + identityProviderId: String(user.id), + }, + }); + + return true; + } + + if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) { + return "/auth/error?error=use-password-login"; + } + + return "/auth/error?error=use-identity-login"; + } + + const newUser = await prisma.user.create({ + data: { + // Slugify the incoming name and append a few random characters to + // prevent conflicts for users with the same name. + username: usernameSlug(user.name), + emailVerified: new Date(Date.now()), + name: user.name, + email: user.email, + identityProvider: idP, + identityProviderId: String(user.id), + }, + }); + const linkAccountNewUserData = { ...account, userId: newUser.id }; + await calcomAdapter.linkAccount(linkAccountNewUserData); + + return true; + } + + return false; + }, + async redirect({ url, baseUrl }) { + // Allows relative callback URLs + if (url.startsWith("/")) return `${baseUrl}${url}`; + // Allows callback URLs on the same domain + else if (new URL(url).hostname === new URL(NEXTAUTH_URL).hostname) return url; + return baseUrl; + }, + }, }); diff --git a/apps/auth/pages/api/auth/changepw.ts b/apps/auth/pages/api/auth/changepw.ts new file mode 100644 index 0000000000..48a8e26972 --- /dev/null +++ b/apps/auth/pages/api/auth/changepw.ts @@ -0,0 +1,63 @@ +import { IdentityProvider } from "@prisma/client"; +import type { NextApiRequest, NextApiResponse } from "next"; + +import { getSession, ErrorCode, hashPassword, verifyPassword } from "@calcom/lib/auth"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({ req: req }); + + if (!session || !session.user || !session.user.email) { + res.status(401).json({ message: "Not authenticated" }); + return; + } + + const user = await prisma.user.findFirst({ + where: { + email: session.user.email, + }, + select: { + id: true, + password: true, + identityProvider: true, + }, + }); + + if (!user) { + res.status(404).json({ message: "User not found" }); + return; + } + + if (user.identityProvider !== IdentityProvider.CAL) { + return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }); + } + + const oldPassword = req.body.oldPassword; + const newPassword = req.body.newPassword; + + const currentPassword = user.password; + if (!currentPassword) { + return res.status(400).json({ error: ErrorCode.UserMissingPassword }); + } + + const passwordsMatch = await verifyPassword(oldPassword, currentPassword); + if (!passwordsMatch) { + return res.status(403).json({ error: ErrorCode.IncorrectPassword }); + } + + if (oldPassword === newPassword) { + return res.status(400).json({ error: ErrorCode.NewPasswordMatchesOld }); + } + + const hashedPassword = await hashPassword(newPassword); + await prisma.user.update({ + where: { + id: user.id, + }, + data: { + password: hashedPassword, + }, + }); + + res.status(200).json({ message: "Password updated successfully" }); +} diff --git a/apps/auth/pages/api/auth/forgot-password.ts b/apps/auth/pages/api/auth/forgot-password.ts new file mode 100644 index 0000000000..7d47ff492d --- /dev/null +++ b/apps/auth/pages/api/auth/forgot-password.ts @@ -0,0 +1,82 @@ +import { ResetPasswordRequest } from "@prisma/client"; +import { NextApiRequest, NextApiResponse } from "next"; + +import dayjs from "@calcom/dayjs"; +import { sendPasswordResetEmail } from "@calcom/emails"; +import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email"; +import prisma from "@calcom/prisma"; +import { getTranslation } from "@calcom/server/lib/i18n"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const t = await getTranslation(req.body.language ?? "en", "common"); + + if (req.method !== "POST") { + return res.status(405).end(); + } + + try { + const maybeUser = await prisma.user.findUnique({ + where: { + email: req.body?.email?.toLowerCase(), + }, + select: { + name: true, + identityProvider: true, + email: true, + }, + }); + + if (!maybeUser) { + // Don't leak information about whether an email is registered or not + return res + .status(200) + .json({ message: "If this email exists in our system, you should receive a Reset email." }); + } + + const maybePreviousRequest = await prisma.resetPasswordRequest.findMany({ + where: { + email: maybeUser.email, + expires: { + gt: new Date(), + }, + }, + }); + + let passwordRequest: ResetPasswordRequest; + + if (maybePreviousRequest && maybePreviousRequest?.length >= 1) { + passwordRequest = maybePreviousRequest[0]; + } else { + const expiry = dayjs().add(PASSWORD_RESET_EXPIRY_HOURS, "hours").toDate(); + const createdResetPasswordRequest = await prisma.resetPasswordRequest.create({ + data: { + email: maybeUser.email, + expires: expiry, + }, + }); + passwordRequest = createdResetPasswordRequest; + } + + const resetLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/forgot-password/${passwordRequest.id}`; + await sendPasswordResetEmail({ + language: t, + user: maybeUser, + resetLink, + }); + + /** So we can test the password reset flow on CI */ + if (process.env.NEXT_PUBLIC_IS_E2E) { + return res.status(201).json({ + message: "If this email exists in our system, you should receive a Reset email.", + resetLink, + }); + } else { + return res + .status(201) + .json({ message: "If this email exists in our system, you should receive a Reset email." }); + } + } catch (reason) { + // console.error(reason); + return res.status(500).json({ message: "Unable to create password reset request" }); + } +} diff --git a/apps/auth/pages/api/auth/reset-password.ts b/apps/auth/pages/api/auth/reset-password.ts new file mode 100644 index 0000000000..71b91199be --- /dev/null +++ b/apps/auth/pages/api/auth/reset-password.ts @@ -0,0 +1,55 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import { hashPassword } from "@calcom/lib/auth"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(400).json({ message: "" }); + } + + try { + const rawPassword = req.body?.password; + const rawRequestId = req.body?.requestId; + + if (!rawPassword || !rawRequestId) { + return res.status(400).json({ message: "Couldn't find an account for this email" }); + } + + const maybeRequest = await prisma.resetPasswordRequest.findUnique({ + where: { + id: rawRequestId, + }, + }); + + if (!maybeRequest) { + return res.status(400).json({ message: "Couldn't find an account for this email" }); + } + + const maybeUser = await prisma.user.findUnique({ + where: { + email: maybeRequest.email, + }, + }); + + if (!maybeUser) { + return res.status(400).json({ message: "Couldn't find an account for this email" }); + } + + const hashedPassword = await hashPassword(rawPassword); + + await prisma.user.update({ + where: { + id: maybeUser.id, + }, + data: { + password: hashedPassword, + }, + }); + + return res.status(201).json({ message: "Password reset." }); + } catch (reason) { + console.error(reason); + return res.status(500).json({ message: "Unable to create password reset request" }); + } +} diff --git a/apps/auth/pages/api/auth/saml/authorize.ts b/apps/auth/pages/api/auth/saml/authorize.ts new file mode 100644 index 0000000000..bb16f83da9 --- /dev/null +++ b/apps/auth/pages/api/auth/saml/authorize.ts @@ -0,0 +1,23 @@ +import { OAuthReq } from "@boxyhq/saml-jackson"; +import { NextApiRequest, NextApiResponse } from "next"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { HttpError } from "@calcom/lib/http-error"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const { oauthController } = await jackson(); + + if (req.method !== "GET") { + return res.status(400).send("Method not allowed"); + } + + try { + const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq); + + return res.redirect(302, redirect_url as string); + } catch (err) { + const { message, statusCode = 500 } = err as HttpError; + + return res.status(statusCode).send(message); + } +} diff --git a/apps/auth/pages/api/auth/saml/callback.ts b/apps/auth/pages/api/auth/saml/callback.ts new file mode 100644 index 0000000000..f7c91928ff --- /dev/null +++ b/apps/auth/pages/api/auth/saml/callback.ts @@ -0,0 +1,14 @@ +import { NextApiRequest, NextApiResponse } from "next"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +async function postHandler(req: NextApiRequest, res: NextApiResponse) { + const { oauthController } = await jackson(); + const { redirect_url } = await oauthController.samlResponse(req.body); + if (redirect_url) return res.redirect(302, redirect_url); +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(postHandler) }), +}); diff --git a/apps/auth/pages/api/auth/saml/token.ts b/apps/auth/pages/api/auth/saml/token.ts new file mode 100644 index 0000000000..703a010850 --- /dev/null +++ b/apps/auth/pages/api/auth/saml/token.ts @@ -0,0 +1,13 @@ +import { NextApiRequest } from "next"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +async function postHandler(req: NextApiRequest) { + const { oauthController } = await jackson(); + return await oauthController.token(req.body); +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(postHandler) }), +}); diff --git a/apps/auth/pages/api/auth/saml/userinfo.ts b/apps/auth/pages/api/auth/saml/userinfo.ts new file mode 100644 index 0000000000..e408357a37 --- /dev/null +++ b/apps/auth/pages/api/auth/saml/userinfo.ts @@ -0,0 +1,34 @@ +import { NextApiRequest } from "next"; +import z from "zod"; + +import jackson from "@calcom/features/ee/sso/lib/jackson"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; + +const extractAuthToken = (req: NextApiRequest) => { + const authHeader = req.headers["authorization"]; + const parts = (authHeader || "").split(" "); + if (parts.length > 1) return parts[1]; + + // check for query param + let arr: string[] = []; + const { access_token } = requestQuery.parse(req.query); + arr = arr.concat(access_token); + if (arr[0].length > 0) return arr[0]; + + throw new HttpError({ statusCode: 401, message: "Unauthorized" }); +}; + +const requestQuery = z.object({ + access_token: z.string(), +}); + +async function getHandler(req: NextApiRequest) { + const { oauthController } = await jackson(); + const token = extractAuthToken(req); + return await oauthController.userInfo(token); +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/apps/auth/pages/api/auth/setup.ts b/apps/auth/pages/api/auth/setup.ts new file mode 100644 index 0000000000..8d04b7a95e --- /dev/null +++ b/apps/auth/pages/api/auth/setup.ts @@ -0,0 +1,57 @@ +import { IdentityProvider } from "@prisma/client"; +import { NextApiRequest } from "next"; +import z from "zod"; + +import { hashPassword, isPasswordValid } from "@calcom/lib/auth"; +import { HttpError } from "@calcom/lib/http-error"; +import { defaultHandler, defaultResponder } from "@calcom/lib/server"; +import slugify from "@calcom/lib/slugify"; +import prisma from "@calcom/prisma"; + +const querySchema = z.object({ + username: z + .string() + .refine((val) => val.trim().length >= 1, { message: "Please enter at least one character" }), + full_name: z.string().min(3, "Please enter at least 3 characters"), + email_address: z.string().email({ message: "Please enter a valid email" }), + password: z.string().refine((val) => isPasswordValid(val.trim(), false, true), { + message: + "The password must be a minimum of 15 characters long containing at least one number and have a mixture of uppercase and lowercase letters", + }), +}); + +async function handler(req: NextApiRequest) { + const userCount = await prisma.user.count(); + if (userCount !== 0) { + throw new HttpError({ statusCode: 400, message: "No setup needed." }); + } + + const parsedQuery = querySchema.safeParse(req.body); + if (!parsedQuery.success) { + throw new HttpError({ statusCode: 422, message: parsedQuery.error.message }); + } + + const username = slugify(parsedQuery.data.username.trim()); + const userEmail = parsedQuery.data.email_address.toLowerCase(); + + const hashedPassword = await hashPassword(parsedQuery.data.password); + + await prisma.user.create({ + data: { + username, + email: userEmail, + password: hashedPassword, + role: "ADMIN", + name: parsedQuery.data.full_name, + emailVerified: new Date(), + locale: "en", // TODO: We should revisit this + identityProvider: IdentityProvider.CAL, + }, + }); + + return { message: "First admin user created successfuly." }; +} + +export default defaultHandler({ + POST: Promise.resolve({ default: defaultResponder(handler) }), +}); diff --git a/apps/auth/pages/api/auth/signup.ts b/apps/auth/pages/api/auth/signup.ts new file mode 100644 index 0000000000..ceab470d17 --- /dev/null +++ b/apps/auth/pages/api/auth/signup.ts @@ -0,0 +1,94 @@ +import { IdentityProvider } from "@prisma/client"; +import { NextApiRequest, NextApiResponse } from "next"; + +import { hashPassword } from "@calcom/lib/auth"; +import slugify from "@calcom/lib/slugify"; +import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return; + } + + const data = req.body; + const { email, password } = data; + const username = slugify(data.username); + const userEmail = email.toLowerCase(); + + if (!username) { + res.status(422).json({ message: "Invalid username" }); + return; + } + + if (!userEmail || !userEmail.includes("@")) { + res.status(422).json({ message: "Invalid email" }); + return; + } + + if (!password || password.trim().length < 7) { + res.status(422).json({ message: "Invalid input - password should be at least 7 characters long." }); + return; + } + + // There is actually an existingUser if username matches + // OR if email matches and both username and password are set + const existingUser = await prisma.user.findFirst({ + where: { + OR: [ + { username }, + { + AND: [{ email: userEmail }, { password: { not: null } }, { username: { not: null } }], + }, + ], + }, + }); + + if (existingUser) { + const message: string = + existingUser.email !== userEmail ? "Username already taken" : "Email address is already registered"; + + return res.status(409).json({ message }); + } + + const hashedPassword = await hashPassword(password); + + const user = await prisma.user.upsert({ + where: { email: userEmail }, + update: { + username, + password: hashedPassword, + emailVerified: new Date(Date.now()), + identityProvider: IdentityProvider.CAL, + }, + create: { + username, + email: userEmail, + password: hashedPassword, + identityProvider: IdentityProvider.CAL, + }, + }); + + // If user has been invitedTo a team, we accept the membership + if (user.invitedTo) { + const team = await prisma.team.findFirst({ + where: { id: user.invitedTo }, + }); + + if (team) { + const membership = await prisma.membership.update({ + where: { + userId_teamId: { userId: user.id, teamId: user.invitedTo }, + }, + data: { + accepted: true, + }, + }); + + // Sync Services: Close.com + closeComUpsertTeamUser(team, user, membership.role); + } + } + + res.status(201).json({ message: "Created user" }); +} diff --git a/apps/auth/pages/api/auth/two-factor/totp/disable.ts b/apps/auth/pages/api/auth/two-factor/totp/disable.ts new file mode 100644 index 0000000000..dff5fbcf17 --- /dev/null +++ b/apps/auth/pages/api/auth/two-factor/totp/disable.ts @@ -0,0 +1,86 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { authenticator } from "otplib"; + +import { ErrorCode, getSession, verifyPassword } from "@calcom/lib/auth"; +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ message: "Not authenticated" }); + } + + if (!session.user?.id) { + console.error("Session is missing a user id."); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + if (!user) { + console.error(`Session references user that no longer exists.`); + return res.status(401).json({ message: "Not authenticated" }); + } + + if (!user.password) { + return res.status(400).json({ error: ErrorCode.UserMissingPassword }); + } + + if (!user.twoFactorEnabled) { + return res.json({ message: "Two factor disabled" }); + } + + const isCorrectPassword = await verifyPassword(req.body.password, user.password); + if (!isCorrectPassword) { + return res.status(400).json({ error: ErrorCode.IncorrectPassword }); + } + // if user has 2fa + if (user.twoFactorEnabled) { + if (!req.body.code) { + return res.status(400).json({ error: ErrorCode.SecondFactorRequired }); + // throw new Error(ErrorCode.SecondFactorRequired); + } + + if (!user.twoFactorSecret) { + console.error(`Two factor is enabled for user ${user.id} but they have no secret`); + throw new Error(ErrorCode.InternalServerError); + } + + if (!process.env.CALENDSO_ENCRYPTION_KEY) { + console.error(`"Missing encryption key; cannot proceed with two factor login."`); + throw new Error(ErrorCode.InternalServerError); + } + + const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); + if (secret.length !== 32) { + console.error( + `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` + ); + throw new Error(ErrorCode.InternalServerError); + } + + // If user has 2fa enabled, check if body.code is correct + const isValidToken = authenticator.check(req.body.code, secret); + if (!isValidToken) { + return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); + + // throw new Error(ErrorCode.IncorrectTwoFactorCode); + } + } + // If it is, disable users 2fa + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + twoFactorEnabled: false, + twoFactorSecret: null, + }, + }); + + return res.json({ message: "Two factor disabled" }); +} diff --git a/apps/auth/pages/api/auth/two-factor/totp/enable.ts b/apps/auth/pages/api/auth/two-factor/totp/enable.ts new file mode 100644 index 0000000000..543218e0a4 --- /dev/null +++ b/apps/auth/pages/api/auth/two-factor/totp/enable.ts @@ -0,0 +1,65 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { authenticator } from "otplib"; + +import { ErrorCode, getSession } from "@calcom/lib/auth"; +import { symmetricDecrypt } from "@calcom/lib/crypto"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ message: "Not authenticated" }); + } + + if (!session.user?.id) { + console.error("Session is missing a user id."); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + if (!user) { + console.error(`Session references user that no longer exists.`); + return res.status(401).json({ message: "Not authenticated" }); + } + + if (user.twoFactorEnabled) { + return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled }); + } + + if (!user.twoFactorSecret) { + return res.status(400).json({ error: ErrorCode.TwoFactorSetupRequired }); + } + + if (!process.env.CALENDSO_ENCRYPTION_KEY) { + console.error("Missing encryption key; cannot proceed with two factor setup."); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); + if (secret.length !== 32) { + console.error( + `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` + ); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const isValidToken = authenticator.check(req.body.code, secret); + if (!isValidToken) { + return res.status(400).json({ error: ErrorCode.IncorrectTwoFactorCode }); + } + + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + twoFactorEnabled: true, + }, + }); + + return res.json({ message: "Two-factor enabled" }); +} diff --git a/apps/auth/pages/api/auth/two-factor/totp/setup.ts b/apps/auth/pages/api/auth/two-factor/totp/setup.ts new file mode 100644 index 0000000000..f17a5c5092 --- /dev/null +++ b/apps/auth/pages/api/auth/two-factor/totp/setup.ts @@ -0,0 +1,72 @@ +import { IdentityProvider } from "@prisma/client"; +import { NextApiRequest, NextApiResponse } from "next"; +import { authenticator } from "otplib"; +import qrcode from "qrcode"; + +import { ErrorCode, getSession, verifyPassword } from "@calcom/lib/auth"; +import { symmetricEncrypt } from "@calcom/lib/crypto"; +import prisma from "@calcom/prisma"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const session = await getSession({ req }); + if (!session) { + return res.status(401).json({ message: "Not authenticated" }); + } + + if (!session.user?.id) { + console.error("Session is missing a user id."); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const user = await prisma.user.findUnique({ where: { id: session.user.id } }); + if (!user) { + console.error(`Session references user that no longer exists.`); + return res.status(401).json({ message: "Not authenticated" }); + } + + if (user.identityProvider !== IdentityProvider.CAL) { + return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled }); + } + + if (!user.password) { + return res.status(400).json({ error: ErrorCode.UserMissingPassword }); + } + + if (user.twoFactorEnabled) { + return res.status(400).json({ error: ErrorCode.TwoFactorAlreadyEnabled }); + } + + if (!process.env.CALENDSO_ENCRYPTION_KEY) { + console.error("Missing encryption key; cannot proceed with two factor setup."); + return res.status(500).json({ error: ErrorCode.InternalServerError }); + } + + const isCorrectPassword = await verifyPassword(req.body.password, user.password); + if (!isCorrectPassword) { + return res.status(400).json({ error: ErrorCode.IncorrectPassword }); + } + + // This generates a secret 32 characters in length. Do not modify the number of + // bytes without updating the sanity checks in the enable and login endpoints. + const secret = authenticator.generateSecret(20); + + await prisma.user.update({ + where: { + id: session.user.id, + }, + data: { + twoFactorEnabled: false, + twoFactorSecret: symmetricEncrypt(secret, process.env.CALENDSO_ENCRYPTION_KEY), + }, + }); + + const name = user.email || user.username || user.id.toString(); + const keyUri = authenticator.keyuri(name, "Cal", secret); + const dataUri = await qrcode.toDataURL(keyUri); + + return res.json({ secret, keyUri, dataUri }); +} diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 4c046f461d..09afeb576e 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -138,7 +138,7 @@ const nextConfig = { return config; }, async rewrites() { - return [ + const rewrites = [ { source: "/:user/avatar.png", destination: "/api/user/avatar?username=:user", @@ -175,6 +175,14 @@ const nextConfig = { destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?, }, */ ]; + // So that we can opt-in to use an external auth server + if (process.env.NEXTAUTH_URL !== process.env.NEXT_PUBLIC_WEBAPP_URL) { + rewrites.push({ + source: "/api/auth/:rest*", + destination: process.env.NEXTAUTH_URL + "/api/auth/:rest*", + }); + } + return rewrites; }, async redirects() { const redirects = [ diff --git a/apps/web/pages/api/auth/[...nextauth].tsx b/apps/web/pages/api/auth/[...nextauth].tsx index cf32db2440..b1d311d3b4 100644 --- a/apps/web/pages/api/auth/[...nextauth].tsx +++ b/apps/web/pages/api/auth/[...nextauth].tsx @@ -1,506 +1 @@ -import { IdentityProvider, UserPermissionRole } from "@prisma/client"; -import { readFileSync } from "fs"; -import Handlebars from "handlebars"; -import NextAuth, { Session } from "next-auth"; -import { Provider } from "next-auth/providers"; -import CredentialsProvider from "next-auth/providers/credentials"; -import EmailProvider from "next-auth/providers/email"; -import GoogleProvider from "next-auth/providers/google"; -import nodemailer, { TransportOptions } from "nodemailer"; -import { authenticator } from "otplib"; -import path from "path"; - -import checkLicense from "@calcom/features/ee/common/server/checkLicense"; -import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider"; -import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml"; -import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth"; -import CalComAdapter from "@calcom/lib/auth/next-auth-custom-adapter"; -import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants"; -import { symmetricDecrypt } from "@calcom/lib/crypto"; -import { defaultCookies } from "@calcom/lib/default-cookies"; -import rateLimit from "@calcom/lib/rateLimit"; -import { serverConfig } from "@calcom/lib/serverConfig"; -import prisma from "@calcom/prisma"; -import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; - -import { randomString } from "@lib/random"; -import slugify from "@lib/slugify"; - -import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants"; - -const transporter = nodemailer.createTransport({ - ...(serverConfig.transport as TransportOptions), -} as TransportOptions); - -const usernameSlug = (username: string) => slugify(username) + "-" + randomString(6).toLowerCase(); - -const providers: Provider[] = [ - CredentialsProvider({ - id: "credentials", - name: "Cal.com", - type: "credentials", - credentials: { - email: { label: "Email Address", type: "email", placeholder: "john.doe@example.com" }, - password: { label: "Password", type: "password", placeholder: "Your super secure password" }, - totpCode: { label: "Two-factor Code", type: "input", placeholder: "Code from authenticator app" }, - }, - async authorize(credentials) { - if (!credentials) { - console.error(`For some reason credentials are missing`); - throw new Error(ErrorCode.InternalServerError); - } - - const user = await prisma.user.findUnique({ - where: { - email: credentials.email.toLowerCase(), - }, - select: { - role: true, - id: true, - username: true, - name: true, - email: true, - identityProvider: true, - password: true, - twoFactorEnabled: true, - twoFactorSecret: true, - teams: { - include: { - team: true, - }, - }, - }, - }); - - if (!user) { - throw new Error(ErrorCode.UserNotFound); - } - - if (user.identityProvider !== IdentityProvider.CAL) { - throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled); - } - - if (!user.password) { - throw new Error(ErrorCode.UserMissingPassword); - } - - const isCorrectPassword = await verifyPassword(credentials.password, user.password); - if (!isCorrectPassword) { - throw new Error(ErrorCode.IncorrectPassword); - } - - if (user.twoFactorEnabled) { - if (!credentials.totpCode) { - throw new Error(ErrorCode.SecondFactorRequired); - } - - if (!user.twoFactorSecret) { - console.error(`Two factor is enabled for user ${user.id} but they have no secret`); - throw new Error(ErrorCode.InternalServerError); - } - - if (!process.env.CALENDSO_ENCRYPTION_KEY) { - console.error(`"Missing encryption key; cannot proceed with two factor login."`); - throw new Error(ErrorCode.InternalServerError); - } - - const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY); - if (secret.length !== 32) { - console.error( - `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}` - ); - throw new Error(ErrorCode.InternalServerError); - } - - const isValidToken = authenticator.check(credentials.totpCode, secret); - if (!isValidToken) { - throw new Error(ErrorCode.IncorrectTwoFactorCode); - } - } - - const limiter = rateLimit({ - intervalInMs: 60 * 1000, // 1 minute - }); - await limiter.check(10, user.email); // 10 requests per minute - // Check if the user you are logging into has any active teams - const hasActiveTeams = - user.teams.filter((m) => { - if (!IS_TEAM_BILLING_ENABLED) return true; - const metadata = teamMetadataSchema.safeParse(m.team.metadata); - if (metadata.success && metadata.data?.subscriptionId) return true; - return false; - }).length > 0; - - // authentication success- but does it meet the minimum password requirements? - if (user.role === "ADMIN" && !isPasswordValid(credentials.password, false, true)) { - return { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - role: "INACTIVE_ADMIN", - belongsToActiveTeam: hasActiveTeams, - }; - } - - return { - id: user.id, - username: user.username, - email: user.email, - name: user.name, - role: user.role, - belongsToActiveTeam: hasActiveTeams, - }; - }, - }), - ImpersonationProvider, -]; - -if (IS_GOOGLE_LOGIN_ENABLED) { - providers.push( - GoogleProvider({ - clientId: GOOGLE_CLIENT_ID, - clientSecret: GOOGLE_CLIENT_SECRET, - }) - ); -} - -if (isSAMLLoginEnabled) { - providers.push({ - id: "saml", - name: "BoxyHQ", - type: "oauth", - version: "2.0", - checks: ["pkce", "state"], - authorization: { - url: `${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: "dummy", - }, - }); -} - -if (true) { - const emailsDir = path.resolve(process.cwd(), "..", "..", "packages/emails", "templates"); - providers.push( - EmailProvider({ - type: "email", - maxAge: 10 * 60 * 60, // Magic links are valid for 10 min only - // Here we setup the sendVerificationRequest that calls the email template with the identifier (email) and token to verify. - sendVerificationRequest: ({ identifier, url }) => { - const originalUrl = new URL(url); - const webappUrl = new URL(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, - }), - }); - }, - }) - ); -} -const calcomAdapter = CalComAdapter(prisma); -export default NextAuth({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - adapter: calcomAdapter, - session: { - strategy: "jwt", - }, - cookies: defaultCookies(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 && account.type === "credentials") { - return { - ...token, - id: user.id, - name: user.name, - username: user.username, - email: user.email, - role: user.role, - impersonatedByUID: user?.impersonatedByUID, - belongsToActiveTeam: user?.belongsToActiveTeam, - }; - } - - // The arguments above are from the provider so we need to look up the - // user based on those values in order to construct a JWT. - if (account && account.type === "oauth" && account.provider && account.providerAccountId) { - let idP: IdentityProvider = IdentityProvider.GOOGLE; - if (account.provider === "saml") { - idP = IdentityProvider.SAML; - } - const existingUser = await prisma.user.findFirst({ - where: { - AND: [ - { - identityProvider: idP, - }, - { - identityProviderId: account.providerAccountId as string, - }, - ], - }, - }); - - if (!existingUser) { - return await autoMergeIdentities(); - } - - return { - ...token, - id: existingUser.id, - name: existingUser.name, - username: existingUser.username, - email: existingUser.email, - role: existingUser.role, - impersonatedByUID: token.impersonatedByUID as number, - belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - }; - } - - return token; - }, - async session({ session, token }) { - const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || ""); - const calendsoSession: Session = { - ...session, - hasValidLicense, - user: { - ...session.user, - id: token.id as number, - name: token.name, - username: token.username as string, - role: token.role as UserPermissionRole, - impersonatedByUID: token.impersonatedByUID as number, - belongsToActiveTeam: token?.belongsToActiveTeam as boolean, - }, - }; - return calendsoSession; - }, - async signIn(params) { - const { user, account, profile } = params; - - if (account?.provider === "email") { - return true; - } - // In this case we've already verified the credentials in the authorize - // callback so we can sign the user in. - if (account?.type === "credentials") { - return true; - } - - if (account?.type !== "oauth") { - return false; - } - - if (!user.email) { - return false; - } - - if (!user.name) { - return false; - } - - if (account?.provider) { - let idP: IdentityProvider = IdentityProvider.GOOGLE; - if (account.provider === "saml") { - idP = IdentityProvider.SAML; - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error TODO validate email_verified key on profile - user.email_verified = user.email_verified || !!user.emailVerified || profile.email_verified; - - if (!user.email_verified) { - return "/auth/error?error=unverified-email"; - } - // Only google oauth on this path - const provider = account.provider.toUpperCase() as IdentityProvider; - - const existingUser = await prisma.user.findFirst({ - include: { - accounts: { - where: { - provider: account.provider, - }, - }, - }, - where: { - identityProvider: provider, - identityProviderId: account.providerAccountId, - }, - }); - - if (existingUser) { - // In this case there's an existing user and their email address - // hasn't changed since they last logged in. - if (existingUser.email === user.email) { - try { - // If old user without Account entry we link their google account - if (existingUser.accounts.length === 0) { - const linkAccountWithUserData = { ...account, userId: existingUser.id }; - await calcomAdapter.linkAccount(linkAccountWithUserData); - } - } catch (error) { - if (error instanceof Error) { - console.error("Error while linking account of already existing user"); - } - } - return true; - } - - // If the email address doesn't match, check if an account already exists - // with the new email address. If it does, for now we return an error. If - // not, update the email of their account and log them in. - const userWithNewEmail = await prisma.user.findFirst({ - where: { email: user.email }, - }); - - if (!userWithNewEmail) { - await prisma.user.update({ where: { id: existingUser.id }, data: { email: user.email } }); - return true; - } else { - return "/auth/error?error=new-email-conflict"; - } - } - - // If there's no existing user for this identity provider and id, create - // a new account. If an account already exists with the incoming email - // address return an error for now. - const existingUserWithEmail = await prisma.user.findFirst({ - where: { email: user.email }, - }); - - if (existingUserWithEmail) { - // if self-hosted then we can allow auto-merge of identity providers if email is verified - if (!hostedCal && existingUserWithEmail.emailVerified) { - return true; - } - - // check if user was invited - if ( - !existingUserWithEmail.password && - !existingUserWithEmail.emailVerified && - !existingUserWithEmail.username - ) { - await prisma.user.update({ - where: { email: user.email }, - data: { - // Slugify the incoming name and append a few random characters to - // prevent conflicts for users with the same name. - username: usernameSlug(user.name), - emailVerified: new Date(Date.now()), - name: user.name, - identityProvider: idP, - identityProviderId: String(user.id), - }, - }); - - return true; - } - - if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) { - return "/auth/error?error=use-password-login"; - } - - return "/auth/error?error=use-identity-login"; - } - - const newUser = await prisma.user.create({ - data: { - // Slugify the incoming name and append a few random characters to - // prevent conflicts for users with the same name. - username: usernameSlug(user.name), - emailVerified: new Date(Date.now()), - name: user.name, - email: user.email, - identityProvider: idP, - identityProviderId: String(user.id), - }, - }); - const linkAccountNewUserData = { ...account, userId: newUser.id }; - await calcomAdapter.linkAccount(linkAccountNewUserData); - - return true; - } - - return false; - }, - async redirect({ url, baseUrl }) { - // Allows relative callback URLs - if (url.startsWith("/")) return `${baseUrl}${url}`; - // Allows callback URLs on the same domain - else if (new URL(url).hostname === new URL(WEBAPP_URL).hostname) return url; - return baseUrl; - }, - }, -}); +export { default } from "@calcom/auth/pages/api/auth/[...nextauth]"; diff --git a/package.json b/package.json index 1fc291ea9c..0c479d9af3 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"", "dev:swagger": "turbo run dev --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"", "dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"", + "dev:auth": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/auth\"", "dev": "turbo run dev --scope=\"@calcom/web\"", "docs-build": "turbo run build --scope=\"@calcom/docs\" --include-dependencies", "docs-dev": "turbo run dev --scope=\"@calcom/docs\"", diff --git a/yarn.lock b/yarn.lock index 22d6f448a7..81f9910606 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4945,11 +4945,6 @@ dependencies: webpack-bundle-analyzer "4.3.0" -"@next/env@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260" - integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg== - "@next/env@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/env/-/env-13.1.1.tgz#6ff26488dc7674ef2bfdd1ca28fe43eed1113bea" @@ -4962,131 +4957,66 @@ dependencies: glob "7.1.7" -"@next/swc-android-arm-eabi@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3" - integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ== - "@next/swc-android-arm-eabi@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-13.1.1.tgz#b5c3cd1f79d5c7e6a3b3562785d4e5ac3555b9e1" integrity sha512-qnFCx1kT3JTWhWve4VkeWuZiyjG0b5T6J2iWuin74lORCupdrNukxkq9Pm+Z7PsatxuwVJMhjUoYz7H4cWzx2A== -"@next/swc-android-arm64@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c" - integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q== - "@next/swc-android-arm64@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-13.1.1.tgz#e2ca9ccbba9ef770cb19fbe96d1ac00fe4cb330d" integrity sha512-eCiZhTzjySubNqUnNkQCjU3Fh+ep3C6b5DCM5FKzsTH/3Gr/4Y7EiaPZKILbvnXmhWtKPIdcY6Zjx51t4VeTfA== -"@next/swc-darwin-arm64@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae" - integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg== - "@next/swc-darwin-arm64@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.1.1.tgz#4af00877332231bbd5a3703435fdd0b011e74767" integrity sha512-9zRJSSIwER5tu9ADDkPw5rIZ+Np44HTXpYMr0rkM656IvssowPxmhK0rTreC1gpUCYwFsRbxarUJnJsTWiutPg== -"@next/swc-darwin-x64@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1" - integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA== - "@next/swc-darwin-x64@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.1.1.tgz#bf4cb09e7e6ec6d91e031118dde2dd17078bcbbc" integrity sha512-qWr9qEn5nrnlhB0rtjSdR00RRZEtxg4EGvicIipqZWEyayPxhUu6NwKiG8wZiYZCLfJ5KWr66PGSNeDMGlNaiA== -"@next/swc-freebsd-x64@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a" - integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q== - "@next/swc-freebsd-x64@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-13.1.1.tgz#6933ea1264328e8523e28818f912cd53824382d4" integrity sha512-UwP4w/NcQ7V/VJEj3tGVszgb4pyUCt3lzJfUhjDMUmQbzG9LDvgiZgAGMYH6L21MoyAATJQPDGiAMWAPKsmumA== -"@next/swc-linux-arm-gnueabihf@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298" - integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w== - "@next/swc-linux-arm-gnueabihf@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-13.1.1.tgz#b5896967aaba3873d809c3ad2e2039e89acde419" integrity sha512-CnsxmKHco9sosBs1XcvCXP845Db+Wx1G0qouV5+Gr+HT/ZlDYEWKoHVDgnJXLVEQzq4FmHddBNGbXvgqM1Gfkg== -"@next/swc-linux-arm64-gnu@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717" - integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ== - "@next/swc-linux-arm64-gnu@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.1.1.tgz#91b3e9ea8575b1ded421c0ea0739b7bccf228469" integrity sha512-JfDq1eri5Dif+VDpTkONRd083780nsMCOKoFG87wA0sa4xL8LGcXIBAkUGIC1uVy9SMsr2scA9CySLD/i+Oqiw== -"@next/swc-linux-arm64-musl@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3" - integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg== - "@next/swc-linux-arm64-musl@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.1.1.tgz#83149ea05d7d55f3664d608dbe004c0d125f9147" integrity sha512-GA67ZbDq2AW0CY07zzGt07M5b5Yaq5qUpFIoW3UFfjOPgb0Sqf3DAW7GtFMK1sF4ROHsRDMGQ9rnT0VM2dVfKA== -"@next/swc-linux-x64-gnu@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1" - integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA== - "@next/swc-linux-x64-gnu@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.1.1.tgz#d7d0777b56de0dd82b78055772e13e18594a15ca" integrity sha512-nnjuBrbzvqaOJaV+XgT8/+lmXrSCOt1YYZn/irbDb2fR2QprL6Q7WJNgwsZNxiLSfLdv+2RJGGegBx9sLBEzGA== -"@next/swc-linux-x64-musl@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305" - integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg== - "@next/swc-linux-x64-musl@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.1.1.tgz#41655722b127133cd95ab5bc8ca1473e9ab6876f" integrity sha512-CM9xnAQNIZ8zf/igbIT/i3xWbQZYaF397H+JroF5VMOCUleElaMdQLL5riJml8wUfPoN3dtfn2s4peSr3azz/g== -"@next/swc-win32-arm64-msvc@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade" - integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw== - "@next/swc-win32-arm64-msvc@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.1.1.tgz#f10da3dfc9b3c2bbd202f5d449a9b807af062292" integrity sha512-pzUHOGrbgfGgPlOMx9xk3QdPJoRPU+om84hqVoe6u+E0RdwOG0Ho/2UxCgDqmvpUrMab1Deltlt6RqcXFpnigQ== -"@next/swc-win32-ia32-msvc@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2" - integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA== - "@next/swc-win32-ia32-msvc@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.1.1.tgz#4c0102b9b18ece15c818056d07e3917ee9dade78" integrity sha512-WeX8kVS46aobM9a7Xr/kEPcrTyiwJqQv/tbw6nhJ4fH9xNZ+cEcyPoQkwPo570dCOLz3Zo9S2q0E6lJ/EAUOBg== -"@next/swc-win32-x64-msvc@12.3.1": - version "12.3.1" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" - integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== - "@next/swc-win32-x64-msvc@13.1.1": version "13.1.1" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.1.1.tgz#c209a37da13be27b722f9c40c40ab4b094866244" @@ -5237,7 +5167,7 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== -"@prisma/client@^4.2.1", "@prisma/client@^4.8.1": +"@prisma/client@^4.8.1": version "4.8.1" resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.8.1.tgz#51c16488dfac4e74a275a2753bf20262a65f2a2b" integrity sha512-d4xhZhETmeXK/yZ7K0KcVOzEfI5YKGGEr4F5SBV04/MU4ncN/HcE28sy3e4Yt8UFW0ZuImKFQJE+9rWt9WbGSQ== @@ -5267,11 +5197,6 @@ resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz#30401aba1029e7d32e3cb717e705a7c92ccc211e" integrity sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw== -"@prisma/engines@4.2.1": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.2.1.tgz#60c7d0acc1c0c5b70ece712e2cbe13f46a345d6e" - integrity sha512-0KqBwREUOjBiHwITsQzw2DWfLHjntvbqzGRawj4sBMnIiL5CXwyDUKeHOwXzKMtNr1rEjxEsypM14g0CzLRK3g== - "@prisma/engines@4.8.1": version "4.8.1" resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.8.1.tgz#8428f7dcd7912c6073024511376595017630dc85" @@ -7730,13 +7655,6 @@ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.35.0.tgz#f809e2e5e0a00f01aa12e8aed0b89d27728c05c0" integrity sha512-UIuzpbJqgXCTvJhY/aZYvBtaKdMfQgnIv6kkLlfRJ9smZcC4zoPvq3j7k9wobYI+idHAWP4BRiPnqA8lvzJCtg== -"@swc/helpers@0.4.11": - version "0.4.11" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" - integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== - dependencies: - tslib "^2.4.0" - "@swc/helpers@0.4.14": version "0.4.14" resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.14.tgz#1352ac6d95e3617ccb7c1498ff019654f1e12a74" @@ -8883,6 +8801,11 @@ resolved "https://registry.yarnpkg.com/@vanilla-extract/sprinkles/-/sprinkles-1.4.1.tgz#8c8703ddedaac355c1187db909119816c0fc771c" integrity sha512-aW6CfMMToX4a+baLuVxwcT0FSACjX3xrNt8wdi/3LLRlLAfhyue8OK7kJxhcYNZfydBeWTP59aRy8p5FUTIeew== +"@vercel/analytics@^0.1.6": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@vercel/analytics/-/analytics-0.1.8.tgz#71f1f8c7bb98ac0c5c47eb3fb8ccbe8141b9fe47" + integrity sha512-PQrOI8BJ9qUiVJuQfnKiJd15eDjDJH9TBKsNeMrtelT4NAk7d9mBVz1CoZkvoFnHQ0OW7Xnqmr1F2nScfAnznQ== + "@vercel/edge-functions-ui@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@vercel/edge-functions-ui/-/edge-functions-ui-0.2.1.tgz#8af0a5d8d4d544364fa79c4d075564e3a5bd972e" @@ -14685,6 +14608,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fathom-client@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/fathom-client/-/fathom-client-3.5.0.tgz#47bf3e67fa789ec415fe6efdc0ec02b9187b4b0d" + integrity sha512-BiRDS9Q9a8Zma0H717FWC5cvf545K/CsxBpxKT22TcSl1EbRhhlHWIJgrdeiQUfdorBK2ppy09TwMOhRsbos/A== + fault@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13" @@ -20359,13 +20287,6 @@ next-auth@^4.18.8: preact-render-to-string "^5.1.19" uuid "^8.3.2" -next-axiom@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/next-axiom/-/next-axiom-0.10.0.tgz#7cd2f52d9691cf9f7984ed325d58a6f93912eed3" - integrity sha512-QrOUqNmJ20StiR0b+/HMiW0o0w442DjfaOg4yH3hNJmAX0c9Afy6hiZ/j9D67XmqlpXeg83ESx89rt83u4/giA== - dependencies: - whatwg-fetch "^3.6.2" - next-axiom@^0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/next-axiom/-/next-axiom-0.16.0.tgz#0bdde740cf51ba9f3bff0e68061c488c9a150094" @@ -20393,11 +20314,6 @@ next-i18next@^11.3.0: i18next-fs-backend "^1.1.4" react-i18next "^11.18.0" -next-plausible@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/next-plausible/-/next-plausible-2.2.0.tgz#f825842f97bce0062bdaf897328c4908d7ce0a78" - integrity sha512-pIhs5MikL6ZMJvB7sxkM49xN06W1A6d6RYta5vrqwQmF2/oXoCG+IPoaPzyODZ/vo7f2/NMAOaUm5QM0dKqMdA== - next-seo@^4.26.0: version "4.29.0" resolved "https://registry.yarnpkg.com/next-seo/-/next-seo-4.29.0.tgz#d281e95ba47914117cc99e9e468599f0547d9b9b" @@ -20442,45 +20358,11 @@ next-transpile-modules@^8.0.0: enhanced-resolve "^5.7.0" escalade "^3.1.1" -next-transpile-modules@^9.0.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/next-transpile-modules/-/next-transpile-modules-9.1.0.tgz#dffd2563bf76f8afdb28f0611948f46252ca65ef" - integrity sha512-yzJji65xDqcIqjvx5vPJcs1M+MYQTzLM1pXH/qf8Q88ohx+bwVGDc1AeV+HKr1NwvMCNTpwVPSFI7cA5WdyeWA== - dependencies: - enhanced-resolve "^5.10.0" - escalade "^3.1.1" - next-validations@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/next-validations/-/next-validations-0.2.1.tgz#68010c9b017ba48eec4f404fd42eb9b0c7324737" integrity sha512-92pR14MPTTx0ynlvYH2TwMf7WiGiznNL/l0dtZyKPw3x48rcMhwEZrP1ZmsMJwzp5D+U+sY2deexeLWC8rlNtQ== -next@^12.3.1: - version "12.3.1" - resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1" - integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw== - dependencies: - "@next/env" "12.3.1" - "@swc/helpers" "0.4.11" - caniuse-lite "^1.0.30001406" - postcss "8.4.14" - styled-jsx "5.0.7" - use-sync-external-store "1.2.0" - optionalDependencies: - "@next/swc-android-arm-eabi" "12.3.1" - "@next/swc-android-arm64" "12.3.1" - "@next/swc-darwin-arm64" "12.3.1" - "@next/swc-darwin-x64" "12.3.1" - "@next/swc-freebsd-x64" "12.3.1" - "@next/swc-linux-arm-gnueabihf" "12.3.1" - "@next/swc-linux-arm64-gnu" "12.3.1" - "@next/swc-linux-arm64-musl" "12.3.1" - "@next/swc-linux-x64-gnu" "12.3.1" - "@next/swc-linux-x64-musl" "12.3.1" - "@next/swc-win32-arm64-msvc" "12.3.1" - "@next/swc-win32-ia32-msvc" "12.3.1" - "@next/swc-win32-x64-msvc" "12.3.1" - next@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/next/-/next-13.1.1.tgz#42b825f650410649aff1017d203a088d77c80b5b" @@ -22164,13 +22046,6 @@ prisma-field-encryption@^1.4.0: object-path "^0.11.8" zod "^3.17.3" -prisma@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.2.1.tgz#3558359f15021aa4767de8c6d0ca1f285cf33d65" - integrity sha512-HuYqnTDgH8atjPGtYmY0Ql9XrrJnfW7daG1PtAJRW0E6gJxc50lY3vrIDn0yjMR3TvRlypjTcspQX8DT+xD4Sg== - dependencies: - "@prisma/engines" "4.2.1" - prisma@^4.8.1: version "4.8.1" resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.8.1.tgz#ef93cd908809b7d02e9f4bead5eea7733ba50c68" @@ -25118,11 +24993,6 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -styled-jsx@5.0.7: - version "5.0.7" - resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" - integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== - styled-jsx@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" @@ -28127,11 +27997,6 @@ zod@^3.17.3, zod@^3.20.2: resolved "https://registry.yarnpkg.com/zod/-/zod-3.20.2.tgz#068606642c8f51b3333981f91c0a8ab37dfc2807" integrity sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ== -zod@^3.19.1: - version "3.19.1" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.19.1.tgz#112f074a97b50bfc4772d4ad1576814bd8ac4473" - integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA== - zustand@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.1.tgz#5a61cc755a002df5f041840a414ae6e9a99ee22b"