From 728a386bf090ff39837c5ecb87d5308c47fc91eb Mon Sep 17 00:00:00 2001 From: Sasha Date: Fri, 19 Jan 2024 23:00:36 +0200 Subject: [PATCH] chore: [app-router-migration 35] migrate the "sso" pages (#13200) ## What does this PR do? - This PR migrates the `/auth/sso/*` page group to the app directory (which runs under the App Router). - The migrated page exists under `/future/auth/sso/*` and is accessible at all times. ## Requirement/Documentation - If there is a requirement document, please, share it here. - If there is ab UI/UX design document, please, share it here. ## Type of change - [x] Chore (refactoring code, technical debt, workflow improvements) - [x] New feature (non-breaking change which adds functionality) ## How should this be tested? There should be no changes between the old sso and new sso routes in terms of content and no downgrading in terms of performance. The pages were not tested due to being unable to reproduce the flow locally. ## Mandatory Tasks - [x] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected. --- .../app/future/auth/sso/[provider]/page.tsx | 12 ++ apps/web/app/future/auth/sso/direct/page.tsx | 11 ++ apps/web/pages/auth/sso/[provider].tsx | 142 +----------------- apps/web/pages/auth/sso/direct.tsx | 16 +- .../sso/[provider]/getServerSideProps.tsx | 142 ++++++++++++++++++ .../auth/sso/direct/getServerSideProps.tsx | 10 ++ 6 files changed, 185 insertions(+), 148 deletions(-) create mode 100644 apps/web/app/future/auth/sso/[provider]/page.tsx create mode 100644 apps/web/app/future/auth/sso/direct/page.tsx create mode 100644 apps/web/server/lib/auth/sso/[provider]/getServerSideProps.tsx create mode 100644 apps/web/server/lib/auth/sso/direct/getServerSideProps.tsx diff --git a/apps/web/app/future/auth/sso/[provider]/page.tsx b/apps/web/app/future/auth/sso/[provider]/page.tsx new file mode 100644 index 0000000000..2a59c87a8a --- /dev/null +++ b/apps/web/app/future/auth/sso/[provider]/page.tsx @@ -0,0 +1,12 @@ +import Provider from "@pages/auth/sso/[provider]"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { WithLayout } from "app/layoutHOC"; +import type { InferGetServerSidePropsType } from "next"; + +import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps"; + +export default WithLayout({ + getLayout: null, + Page: Provider, + getData: withAppDirSsr>(getServerSideProps), +})<"P">; diff --git a/apps/web/app/future/auth/sso/direct/page.tsx b/apps/web/app/future/auth/sso/direct/page.tsx new file mode 100644 index 0000000000..0f69f324fe --- /dev/null +++ b/apps/web/app/future/auth/sso/direct/page.tsx @@ -0,0 +1,11 @@ +import DirectSSOLogin from "@pages/auth/sso/direct"; +import { withAppDirSsr } from "app/WithAppDirSsr"; +import { WithLayout } from "app/layoutHOC"; + +import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps"; + +export default WithLayout({ + getLayout: null, + Page: DirectSSOLogin, + getData: withAppDirSsr(getServerSideProps), +})<"P">; diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index f1a2e759ba..8aa522667f 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -1,25 +1,16 @@ -import type { GetServerSidePropsContext } from "next"; +"use client"; + import { signIn } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; -import stripe from "@calcom/features/ee/payments/server/stripe"; -import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; -import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso"; -import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; -import { checkUsername } from "@calcom/lib/server/checkUsername"; -import prisma from "@calcom/prisma"; -import { asStringOrNull } from "@lib/asStringOrNull"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import PageWrapper from "@components/PageWrapper"; -import { ssrInit } from "@server/lib/ssr"; +import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps"; export type SSOProviderPageProps = inferSSRProps; @@ -52,129 +43,4 @@ export default function Provider(props: SSOProviderPageProps) { Provider.PageWrapper = PageWrapper; -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - // get query params and typecast them to string - // (would be even better to assert them instead of typecasting) - const providerParam = asStringOrNull(context.query.provider); - const emailParam = asStringOrNull(context.query.email); - const usernameParam = asStringOrNull(context.query.username); - const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`; - if (!providerParam) { - throw new Error(`File is not named sso/[provider]`); - } - - const { req } = context; - - const session = await getServerSession({ req }); - const ssr = await ssrInit(context); - const { currentOrgDomain } = orgDomainConfig(context.req); - - if (session) { - // Validating if username is Premium, while this is true an email its required for stripe user confirmation - if (usernameParam && session.user.email) { - const availability = await checkUsername(usernameParam, currentOrgDomain); - if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) { - const stripePremiumUrl = await getStripePremiumUsernameUrl({ - userEmail: session.user.email, - username: usernameParam, - successDestination, - }); - if (stripePremiumUrl) { - return { - redirect: { - destination: stripePremiumUrl, - permanent: false, - }, - }; - } - } - } - - return { - redirect: { - destination: successDestination, - permanent: false, - }, - }; - } - - let error: string | null = null; - - let tenant = samlTenantID; - let product = samlProductID; - - if (providerParam === "saml" && hostedCal) { - if (!emailParam) { - error = "Email not provided"; - } else { - try { - const ret = await ssoTenantProduct(prisma, emailParam); - tenant = ret.tenant; - product = ret.product; - } catch (e) { - if (e instanceof Error) { - error = e.message; - } - } - } - } - - if (error) { - return { - redirect: { - destination: `/auth/error?error=${error}`, - permanent: false, - }, - }; - } - - return { - props: { - trpcState: ssr.dehydrate(), - provider: providerParam, - isSAMLLoginEnabled, - hostedCal, - tenant, - product, - error, - }, - }; -}; - -type GetStripePremiumUsernameUrl = { - userEmail: string; - username: string; - successDestination: string; -}; - -const getStripePremiumUsernameUrl = async ({ - userEmail, - username, - successDestination, -}: GetStripePremiumUsernameUrl): Promise => { - // @TODO: probably want to check if stripe user email already exists? or not - const customer = await stripe.customers.create({ - email: userEmail, - metadata: { - email: userEmail, - username, - }, - }); - - const checkoutSession = await stripe.checkout.sessions.create({ - mode: "subscription", - payment_method_types: ["card"], - customer: customer.id, - line_items: [ - { - price: getPremiumMonthlyPlanPriceId(), - quantity: 1, - }, - ], - success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}${successDestination}&session_id={CHECKOUT_SESSION_ID}`, - cancel_url: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com", - allow_promotion_codes: true, - }); - - return checkoutSession.url; -}; +export { getServerSideProps }; diff --git a/apps/web/pages/auth/sso/direct.tsx b/apps/web/pages/auth/sso/direct.tsx index d3b5ce8d84..b616c59250 100644 --- a/apps/web/pages/auth/sso/direct.tsx +++ b/apps/web/pages/auth/sso/direct.tsx @@ -1,14 +1,17 @@ +"use client"; + import { signIn } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; import PageWrapper from "@components/PageWrapper"; +import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps"; + // This page is used to initiate the SAML authentication flow by redirecting to the SAML provider. // Accessible only on self-hosted Cal.com instances. export default function Page({ samlTenantID, samlProductID }: inferSSRProps) { @@ -36,13 +39,6 @@ export default function Page({ samlTenantID, samlProductID }: inferSSRProps { + // get query params and typecast them to string + // (would be even better to assert them instead of typecasting) + const providerParam = asStringOrNull(context.query.provider); + const emailParam = asStringOrNull(context.query.email); + const usernameParam = asStringOrNull(context.query.username); + const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`; + if (!providerParam) { + throw new Error(`File is not named sso/[provider]`); + } + + const { req } = context; + + const session = await getServerSession({ req }); + const ssr = await ssrInit(context); + const { currentOrgDomain } = orgDomainConfig(context.req); + + if (session) { + // Validating if username is Premium, while this is true an email its required for stripe user confirmation + if (usernameParam && session.user.email) { + const availability = await checkUsername(usernameParam, currentOrgDomain); + if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) { + const stripePremiumUrl = await getStripePremiumUsernameUrl({ + userEmail: session.user.email, + username: usernameParam, + successDestination, + }); + if (stripePremiumUrl) { + return { + redirect: { + destination: stripePremiumUrl, + permanent: false, + }, + }; + } + } + } + + return { + redirect: { + destination: successDestination, + permanent: false, + }, + }; + } + + let error: string | null = null; + + let tenant = samlTenantID; + let product = samlProductID; + + if (providerParam === "saml" && hostedCal) { + if (!emailParam) { + error = "Email not provided"; + } else { + try { + const ret = await ssoTenantProduct(prisma, emailParam); + tenant = ret.tenant; + product = ret.product; + } catch (e) { + if (e instanceof Error) { + error = e.message; + } + } + } + } + + if (error) { + return { + redirect: { + destination: `/auth/error?error=${error}`, + permanent: false, + }, + }; + } + + return { + props: { + trpcState: ssr.dehydrate(), + provider: providerParam, + isSAMLLoginEnabled, + hostedCal, + tenant, + product, + error, + }, + }; +}; + +type GetStripePremiumUsernameUrl = { + userEmail: string; + username: string; + successDestination: string; +}; + +const getStripePremiumUsernameUrl = async ({ + userEmail, + username, + successDestination, +}: GetStripePremiumUsernameUrl): Promise => { + // @TODO: probably want to check if stripe user email already exists? or not + const customer = await stripe.customers.create({ + email: userEmail, + metadata: { + email: userEmail, + username, + }, + }); + + const checkoutSession = await stripe.checkout.sessions.create({ + mode: "subscription", + payment_method_types: ["card"], + customer: customer.id, + line_items: [ + { + price: getPremiumMonthlyPlanPriceId(), + quantity: 1, + }, + ], + success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}${successDestination}&session_id={CHECKOUT_SESSION_ID}`, + cancel_url: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com", + allow_promotion_codes: true, + }); + + return checkoutSession.url; +}; diff --git a/apps/web/server/lib/auth/sso/direct/getServerSideProps.tsx b/apps/web/server/lib/auth/sso/direct/getServerSideProps.tsx new file mode 100644 index 0000000000..e18ac44cc4 --- /dev/null +++ b/apps/web/server/lib/auth/sso/direct/getServerSideProps.tsx @@ -0,0 +1,10 @@ +import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml"; + +export async function getServerSideProps() { + return { + props: { + samlTenantID, + samlProductID, + }, + }; +}