+ );
+}
+
+export default signin;
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getSession(context);
+ const csrfToken = await getCsrfToken(context);
+ const providers = await getProviders();
+ if (session) {
+ return {
+ redirect: { destination: "/" },
+ };
+ }
+ return {
+ props: {
+ csrfToken,
+ providers,
+ },
+ };
+}
diff --git a/apps/auth/pages/auth/sso/[provider].tsx b/apps/auth/pages/auth/sso/[provider].tsx
index 25fe19818b..9adbc85ef8 100644
--- a/apps/auth/pages/auth/sso/[provider].tsx
+++ b/apps/auth/pages/auth/sso/[provider].tsx
@@ -1 +1,180 @@
-export { default, getServerSideProps } from "@calcom/features/auth/pages/sso/[provider]";
+import { GetServerSidePropsContext } from "next";
+import { signIn } from "next-auth/react";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+import { z } from "zod";
+
+import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
+import stripe from "@calcom/features/ee/payments/server/stripe";
+import {
+ hostedCal,
+ isSAMLLoginEnabled,
+ samlProductID,
+ samlTenantID,
+ samlTenantProduct,
+} from "@calcom/features/ee/sso/lib/saml";
+import { checkUsername } from "@calcom/lib/server/checkUsername";
+import prisma from "@calcom/prisma";
+// TODO: Fix this import
+import { ssrInit } from "@calcom/trpc/server/ssr";
+import { inferSSRProps } from "@calcom/types/inferSSRProps";
+
+import { getSession } from "../../lib";
+
+type SSOProviderPageProps = inferSSRProps;
+
+export default function Provider(props: SSOProviderPageProps) {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (props.provider === "saml") {
+ const email = typeof router.query?.email === "string" ? router.query?.email : null;
+
+ if (!email) {
+ router.push("/auth/error?error=" + "Email not provided");
+ return;
+ }
+
+ if (!props.isSAMLLoginEnabled) {
+ router.push("/auth/error?error=" + "SAML login not enabled");
+ return;
+ }
+
+ signIn("saml", {}, { tenant: props.tenant, product: props.product });
+ } else {
+ signIn(props.provider);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ return null;
+}
+
+const querySchema = z.object({
+ provider: z.union([z.string(), z.null()]).optional().default(null),
+ email: z.union([z.string(), z.null()]).optional().default(null),
+ username: z.union([z.string(), z.null()]).optional().default(null),
+});
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const {
+ provider: providerParam,
+ email: emailParam,
+ username: usernameParam,
+ } = querySchema.parse(context.query);
+ const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : "");
+ if (!providerParam) {
+ throw new Error(`File is not named sso/[provider]`);
+ }
+
+ const { req } = context;
+
+ const session = await getSession({ req });
+ const ssr = await ssrInit(context);
+
+ 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);
+ if (availability.available && availability.premium) {
+ 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 samlTenantProduct(prisma, emailParam);
+ tenant = ret.tenant;
+ product = ret.product;
+ } catch (e: any) {
+ 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/auth/pages/auth/verify.tsx b/apps/auth/pages/auth/verify.tsx
index e93b29337d..21c3c6b1b2 100644
--- a/apps/auth/pages/auth/verify.tsx
+++ b/apps/auth/pages/auth/verify.tsx
@@ -1 +1,176 @@
-export { default } from "@calcom/features/auth/pages/verify";
+import { CheckIcon, ExclamationIcon, MailOpenIcon } from "@heroicons/react/outline";
+import { signIn } from "next-auth/react";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import { useEffect, useRef, useState } from "react";
+import z from "zod";
+
+import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
+import { trpc } from "@calcom/trpc/react";
+import { Button, Loader, showToast } from "@calcom/ui";
+
+async function sendVerificationLogin(email: string, username: string) {
+ await signIn("email", {
+ email: email.toLowerCase(),
+ username: username.toLowerCase(),
+ redirect: false,
+ callbackUrl: WEBAPP_URL || "https://app.cal.com",
+ })
+ .then(() => {
+ showToast("Verification email sent", "success");
+ })
+ .catch((err) => {
+ showToast(err, "error");
+ });
+}
+
+function useSendFirstVerificationLogin({
+ email,
+ username,
+}: {
+ email: string | undefined;
+ username: string | undefined;
+}) {
+ const sent = useRef(false);
+ useEffect(() => {
+ if (!email || !username || sent.current) {
+ return;
+ }
+ (async () => {
+ await sendVerificationLogin(email, username);
+ sent.current = true;
+ })();
+ }, [email, username]);
+}
+
+const querySchema = z.object({
+ stripeCustomerId: z.string().optional(),
+ sessionId: z.string().optional(),
+ t: z.string().optional(),
+});
+
+export default function Verify() {
+ const router = useRouter();
+ const { t, sessionId, stripeCustomerId } = querySchema.parse(router.query);
+ const [secondsLeft, setSecondsLeft] = useState(30);
+ const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery({
+ stripeCustomerId,
+ checkoutSessionId: sessionId,
+ });
+ useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username });
+ // @note: check for t=timestamp and apply disabled state and secondsLeft accordingly
+ // to avoid refresh to skip waiting 30 seconds to re-send email
+ useEffect(() => {
+ const lastSent = new Date(parseInt(`${t}`));
+ // @note: This double round() looks ugly but it's the only way I came up to get the time difference in seconds
+ const difference = Math.round(Math.round(new Date().getTime() - lastSent.getTime()) / 1000);
+ if (difference < 30) {
+ // If less than 30 seconds, set the seconds left to 30 - difference
+ setSecondsLeft(30 - difference);
+ } else {
+ // else set the seconds left to 0 and disabled false
+ setSecondsLeft(0);
+ }
+ }, [t]);
+ // @note: here we make sure each second is decremented if disabled up to 0.
+ useEffect(() => {
+ if (secondsLeft > 0) {
+ const interval = setInterval(() => {
+ if (secondsLeft > 0) {
+ setSecondsLeft(secondsLeft - 1);
+ }
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [secondsLeft]);
+
+ if (!router.isReady || !data) {
+ // Loading state
+ return ;
+ }
+ const { valid, hasPaymentFailed, customer } = data;
+ if (!valid) {
+ throw new Error("Invalid session or customer id");
+ }
+
+ if (!stripeCustomerId && !sessionId) {
+ return
Invalid Link
;
+ }
+
+ return (
+
+
+
+ {/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
+ it or too hard to read. */}
+ {hasPaymentFailed
+ ? "Your payment failed"
+ : sessionId
+ ? "Payment successful!"
+ : "Verify your email" + " | " + APP_NAME}
+
+
+
Your account has been created, but your premium has not been reserved.
+ )}
+
+ We have sent an email to {customer?.email} with a link to activate your account.{" "}
+ {hasPaymentFailed &&
+ "Once you activate your account you will be able to try purchase your premium username again or select a different one."}
+
+
+ Don't see an email? Click the button below to send another email.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/pages/auth/error.tsx b/apps/web/pages/auth/error.tsx
index 9ebab32841..50d709b635 100644
--- a/apps/web/pages/auth/error.tsx
+++ b/apps/web/pages/auth/error.tsx
@@ -1 +1,57 @@
-export { default, getStaticProps } from "@calcom/features/auth/pages/error";
+import { GetStaticPropsContext } from "next";
+import Link from "next/link";
+import { useRouter } from "next/router";
+import z from "zod";
+
+import AuthContainer from "@calcom/features/auth/components/AuthContainer";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { ssgInit } from "@calcom/trpc/server/ssg";
+import { Button, Icon, SkeletonText } from "@calcom/ui";
+
+const querySchema = z.object({
+ error: z.string().optional(),
+});
+
+export default function Error() {
+ const { t } = useLocale();
+ const router = useRouter();
+ const { error } = querySchema.parse(router.query);
+ const isTokenVerificationError = error?.toLowerCase() === "verification";
+ let errorMsg = ;
+ if (router.isReady) {
+ errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
+ }
+
+ return (
+
+
+ );
+}
+
+export default signin;
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getSession(context);
+ const csrfToken = await getCsrfToken(context);
+ const providers = await getProviders();
+ if (session) {
+ return {
+ redirect: { destination: "/" },
+ };
+ }
+ return {
+ props: {
+ csrfToken,
+ providers,
+ },
+ };
+}
diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx
index 25fe19818b..9adbc85ef8 100644
--- a/apps/web/pages/auth/sso/[provider].tsx
+++ b/apps/web/pages/auth/sso/[provider].tsx
@@ -1 +1,180 @@
-export { default, getServerSideProps } from "@calcom/features/auth/pages/sso/[provider]";
+import { GetServerSidePropsContext } from "next";
+import { signIn } from "next-auth/react";
+import { useRouter } from "next/router";
+import { useEffect } from "react";
+import { z } from "zod";
+
+import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
+import stripe from "@calcom/features/ee/payments/server/stripe";
+import {
+ hostedCal,
+ isSAMLLoginEnabled,
+ samlProductID,
+ samlTenantID,
+ samlTenantProduct,
+} from "@calcom/features/ee/sso/lib/saml";
+import { checkUsername } from "@calcom/lib/server/checkUsername";
+import prisma from "@calcom/prisma";
+// TODO: Fix this import
+import { ssrInit } from "@calcom/trpc/server/ssr";
+import { inferSSRProps } from "@calcom/types/inferSSRProps";
+
+import { getSession } from "../../lib";
+
+type SSOProviderPageProps = inferSSRProps;
+
+export default function Provider(props: SSOProviderPageProps) {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (props.provider === "saml") {
+ const email = typeof router.query?.email === "string" ? router.query?.email : null;
+
+ if (!email) {
+ router.push("/auth/error?error=" + "Email not provided");
+ return;
+ }
+
+ if (!props.isSAMLLoginEnabled) {
+ router.push("/auth/error?error=" + "SAML login not enabled");
+ return;
+ }
+
+ signIn("saml", {}, { tenant: props.tenant, product: props.product });
+ } else {
+ signIn(props.provider);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ return null;
+}
+
+const querySchema = z.object({
+ provider: z.union([z.string(), z.null()]).optional().default(null),
+ email: z.union([z.string(), z.null()]).optional().default(null),
+ username: z.union([z.string(), z.null()]).optional().default(null),
+});
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const {
+ provider: providerParam,
+ email: emailParam,
+ username: usernameParam,
+ } = querySchema.parse(context.query);
+ const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : "");
+ if (!providerParam) {
+ throw new Error(`File is not named sso/[provider]`);
+ }
+
+ const { req } = context;
+
+ const session = await getSession({ req });
+ const ssr = await ssrInit(context);
+
+ 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);
+ if (availability.available && availability.premium) {
+ 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 samlTenantProduct(prisma, emailParam);
+ tenant = ret.tenant;
+ product = ret.product;
+ } catch (e: any) {
+ 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/pages/auth/verify.tsx b/apps/web/pages/auth/verify.tsx
index e93b29337d..21c3c6b1b2 100644
--- a/apps/web/pages/auth/verify.tsx
+++ b/apps/web/pages/auth/verify.tsx
@@ -1 +1,176 @@
-export { default } from "@calcom/features/auth/pages/verify";
+import { CheckIcon, ExclamationIcon, MailOpenIcon } from "@heroicons/react/outline";
+import { signIn } from "next-auth/react";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import { useEffect, useRef, useState } from "react";
+import z from "zod";
+
+import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
+import { trpc } from "@calcom/trpc/react";
+import { Button, Loader, showToast } from "@calcom/ui";
+
+async function sendVerificationLogin(email: string, username: string) {
+ await signIn("email", {
+ email: email.toLowerCase(),
+ username: username.toLowerCase(),
+ redirect: false,
+ callbackUrl: WEBAPP_URL || "https://app.cal.com",
+ })
+ .then(() => {
+ showToast("Verification email sent", "success");
+ })
+ .catch((err) => {
+ showToast(err, "error");
+ });
+}
+
+function useSendFirstVerificationLogin({
+ email,
+ username,
+}: {
+ email: string | undefined;
+ username: string | undefined;
+}) {
+ const sent = useRef(false);
+ useEffect(() => {
+ if (!email || !username || sent.current) {
+ return;
+ }
+ (async () => {
+ await sendVerificationLogin(email, username);
+ sent.current = true;
+ })();
+ }, [email, username]);
+}
+
+const querySchema = z.object({
+ stripeCustomerId: z.string().optional(),
+ sessionId: z.string().optional(),
+ t: z.string().optional(),
+});
+
+export default function Verify() {
+ const router = useRouter();
+ const { t, sessionId, stripeCustomerId } = querySchema.parse(router.query);
+ const [secondsLeft, setSecondsLeft] = useState(30);
+ const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery({
+ stripeCustomerId,
+ checkoutSessionId: sessionId,
+ });
+ useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username });
+ // @note: check for t=timestamp and apply disabled state and secondsLeft accordingly
+ // to avoid refresh to skip waiting 30 seconds to re-send email
+ useEffect(() => {
+ const lastSent = new Date(parseInt(`${t}`));
+ // @note: This double round() looks ugly but it's the only way I came up to get the time difference in seconds
+ const difference = Math.round(Math.round(new Date().getTime() - lastSent.getTime()) / 1000);
+ if (difference < 30) {
+ // If less than 30 seconds, set the seconds left to 30 - difference
+ setSecondsLeft(30 - difference);
+ } else {
+ // else set the seconds left to 0 and disabled false
+ setSecondsLeft(0);
+ }
+ }, [t]);
+ // @note: here we make sure each second is decremented if disabled up to 0.
+ useEffect(() => {
+ if (secondsLeft > 0) {
+ const interval = setInterval(() => {
+ if (secondsLeft > 0) {
+ setSecondsLeft(secondsLeft - 1);
+ }
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [secondsLeft]);
+
+ if (!router.isReady || !data) {
+ // Loading state
+ return ;
+ }
+ const { valid, hasPaymentFailed, customer } = data;
+ if (!valid) {
+ throw new Error("Invalid session or customer id");
+ }
+
+ if (!stripeCustomerId && !sessionId) {
+ return
Invalid Link
;
+ }
+
+ return (
+
+
+
+ {/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
+ it or too hard to read. */}
+ {hasPaymentFailed
+ ? "Your payment failed"
+ : sessionId
+ ? "Payment successful!"
+ : "Verify your email" + " | " + APP_NAME}
+
+
+
Your account has been created, but your premium has not been reserved.
+ )}
+
+ We have sent an email to {customer?.email} with a link to activate your account.{" "}
+ {hasPaymentFailed &&
+ "Once you activate your account you will be able to try purchase your premium username again or select a different one."}
+
+
+ Don't see an email? Click the button below to send another email.
+