Revert more stuff

This commit is contained in:
zomars 2023-01-17 16:18:54 -07:00
parent 2bba63f9f0
commit 213489c202
23 changed files with 17 additions and 2354 deletions

View File

@ -3,17 +3,20 @@ const nextConfig = {
reactStrictMode: true,
swcMinify: true,
transpilePackages: [
"@calcom/app-store-cli",
"@calcom/app-store",
"@calcom/config",
"@calcom/core",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/embed-snippet",
"@calcom/features",
"@calcom/types",
"@calcom/lib",
"@calcom/prisma",
"@calcom/trpc",
"@calcom/tsconfig",
"@calcom/ui",
],
};

View File

@ -1,10 +1,13 @@
import { TooltipProvider } from "@radix-ui/react-tooltip";
import { SessionProvider } from "next-auth/react";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<SessionProvider session={session}>
<Component {...pageProps} />
<TooltipProvider>
<Component {...pageProps} />
</TooltipProvider>
</SessionProvider>
);
}

View File

@ -1,57 +0,0 @@
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 = <SkeletonText />;
if (router.isReady) {
errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
}
return (
<AuthContainer title="" description="">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<Icon.FiX className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{error}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{errorMsg}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login" passHref legacyBehavior>
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
</Link>
</div>
</AuthContainer>
);
}
export const getStaticProps = async (context: GetStaticPropsContext) => {
const ssr = await ssgInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};

View File

@ -1,194 +0,0 @@
import { ResetPasswordRequest } from "@prisma/client";
import debounce from "lodash/debounce";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import Link from "next/link";
import React, { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import AuthContainer from "@calcom/features/auth/components/AuthContainer";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
type Props = {
id: string;
resetPasswordRequest: ResetPasswordRequest;
csrfToken: string;
};
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId: requestId, password: password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
};
const debouncedChangePassword = debounce(submitChangePassword, 250);
const Success = () => {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal mt-6 text-center text-3xl font-extrabold text-gray-900">
{t("password_updated")}
</h2>
</div>
<Button href="/auth/login" className="w-full justify-center">
{t("login")}
</Button>
</div>
</>
);
};
const Expired = () => {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal mt-6 text-center text-3xl font-extrabold text-gray-900">{t("whoops")}</h2>
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
</div>
<p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<button
type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
{t("try_again")}
</button>
</Link>
</div>
</>
);
};
const isRequestExpired = useMemo(() => {
const now = dayjs();
return dayjs(resetPasswordRequest.expires).isBefore(now);
}, [resetPasswordRequest]);
return (
<AuthContainer
showLogo
title={t("reset_password")}
description={t("change_your_password")}
heading={!success ? t("reset_password") : undefined}>
{isRequestExpired && <Expired />}
{!isRequestExpired && !success && (
<>
<form
className="space-y-6"
onSubmit={async (e) => {
e.preventDefault();
if (!password) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
}}
action="#">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div className="mt-1">
<TextField
label={t("new_password")}
onChange={(e) => {
setPassword(e.target.value);
}}
id="password"
name="password"
type="password"
autoComplete="password"
required
/>
</div>
<div>
<Button
loading={loading}
color="primary"
type="submit"
disabled={loading}
className="w-full justify-center">
{t("reset_password")}
</Button>
</div>
</form>
</>
)}
{!isRequestExpired && success && (
<>
<Success />
</>
)}
</AuthContainer>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;
try {
const resetPasswordRequest = await prisma.resetPasswordRequest.findUniqueOrThrow({
where: {
id,
},
select: {
id: true,
expires: true,
},
});
return {
props: {
resetPasswordRequest: {
...resetPasswordRequest,
expires: resetPasswordRequest.expires.toString(),
},
id,
csrfToken: await getCsrfToken({ req: context.req }),
},
};
} catch (reason) {
return {
notFound: true,
};
}
}

View File

@ -1,145 +0,0 @@
import debounce from "lodash/debounce";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { SyntheticEvent } from "react";
import AuthContainer from "@calcom/features/auth/components/AuthContainer";
import { getSession } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [email, setEmail] = React.useState("");
const router = useRouter();
const handleChange = (e: SyntheticEvent) => {
const target = e.target as typeof e.target & { value: string };
setEmail(target.value);
};
const submitForgotPasswordRequest = async ({ email }: { email: string }) => {
try {
const res = await fetch("/api/auth/forgot-password", {
method: "POST",
body: JSON.stringify({ email: email, language: i18n.language }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else if ("resetLink" in json) {
router.push(json.resetLink);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
};
const debouncedHandleSubmitPasswordRequest = debounce(submitForgotPasswordRequest, 250);
const handleSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
if (!email) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedHandleSubmitPasswordRequest({ email });
};
const Success = () => {
return (
<div className="space-y-6 text-sm leading-normal ">
<p className="">{t("password_reset_email", { email })}</p>
<p className="">{t("password_reset_leading")}</p>
{error && <p className="text-center text-red-600">{error.message}</p>}
<Button color="secondary" className="w-full justify-center" href="/auth/login">
{t("back_to_signin")}
</Button>
</div>
);
};
return (
<AuthContainer
showLogo
title={!success ? t("forgot_password") : t("reset_link_sent")}
heading={!success ? t("forgot_password") : t("reset_link_sent")}
description={t("request_password_reset")}
footerText={
!success && (
<>
<Link href="/auth/login" className="font-medium text-gray-900">
{t("back_to_signin")}
</Link>
</>
)
}>
{success && <Success />}
{!success && (
<>
<div className="space-y-6">{error && <p className="text-red-600">{error.message}</p>}</div>
<form className="space-y-6" onSubmit={handleSubmit} action="#">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<EmailField
onChange={handleChange}
id="email"
name="email"
label={t("email_address")}
placeholder="john.doe@example.com"
required
/>
<div className="space-y-2">
<Button
className="w-full justify-center"
type="submit"
disabled={loading}
aria-label={t("request_password_reset")}
loading={loading}>
{t("request_password_reset")}
</Button>
</div>
</form>
</>
)}
</AuthContainer>
);
}
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const session = await getSession({ req });
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return;
}
return {
csrfToken: await getCsrfToken(context),
};
};

View File

@ -1,239 +0,0 @@
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import AuthContainer from "@calcom/features/auth/components/AuthContainer";
import { SAMLLogin } from "@calcom/features/auth/components/SAMLLogin";
import TwoFactor from "@calcom/features/auth/components/TwoFactor";
import { ErrorCode, getSession } from "@calcom/features/auth/lib";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { ssrInit } from "@calcom/trpc/server/ssr";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AddToHomescreen, Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
// TODO: Fix this import
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
interface LoginValues {
email: string;
password: string;
totpCode: string;
csrfToken: string;
}
export default function Login({
csrfToken,
isGoogleLoginEnabled,
isSAMLLoginEnabled,
samlTenantID,
samlProductID,
}: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const errorMessages: { [key: string]: string } = {
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
[ErrorCode.UserNotFound]: t("no_account_exists"),
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
};
const telemetry = useTelemetry();
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
{t("dont_have_an_account")}
</a>
);
const TwoFactorFooter = (
<Button
onClick={() => {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}}
StartIcon={Icon.FiArrowLeft}
color="minimal">
{t("go_back")}
</Button>
);
const onSubmit = async (values: LoginValues) => {
setErrorMessage(null);
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
redirect: false,
});
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
return (
<>
<AuthContainer
title={t("login")}
description={t("login")}
showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={
twoFactorRequired
? TwoFactorFooter
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
? LoginFooter
: null
}>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div>
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
</div>
<div className="space-y-6">
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
{...register("email")}
/>
<div className="relative">
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
<Link
href="/auth/forgot-password"
tabIndex={-1}
className="text-sm font-medium text-gray-600">
{t("forgot")}
</Link>
</div>
<PasswordField
id="password"
autoComplete="current-password"
required
className="mb-0"
{...register("password")}
/>
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
type="submit"
color="primary"
disabled={formState.isSubmitting}
className="w-full justify-center">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full justify-center"
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
</Button>
)}
{isSAMLLoginEnabled && (
<SAMLLogin
samlTenantID={samlTenantID}
samlProductID={samlProductID}
setErrorMessage={setErrorMessage}
/>
)}
</div>
</>
)}
</FormProvider>
</AuthContainer>
<AddToHomescreen />
</>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req } = context;
const session = await getSession({ req });
const ssr = await ssrInit(context);
if (session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
const userCount = await prisma.user.count();
if (userCount === 0) {
// Proceed to new onboarding to create first admin user
return {
redirect: {
destination: "/auth/setup",
permanent: false,
},
};
}
return {
props: {
csrfToken: await getCsrfToken(context),
trpcState: ssr.dehydrate(),
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
samlTenantID,
samlProductID,
},
};
}

View File

@ -1,64 +0,0 @@
import { GetServerSidePropsContext } from "next";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import AuthContainer from "@calcom/features/auth/components/AuthContainer";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/trpc/server/ssr";
import { Button, Icon } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
type Props = inferSSRProps<typeof getServerSideProps>;
export default function Logout(props: Props) {
const { status } = useSession();
if (status === "authenticated") signOut({ redirect: false });
const router = useRouter();
useEffect(() => {
if (props.query?.survey === "true") {
router.push(`${WEBSITE_URL}/cancellation`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.query?.survey]);
const { t } = useLocale();
return (
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<Icon.FiCheck className="h-6 w-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("youve_been_logged_out")}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{t("hope_to_see_you_soon")}</p>
</div>
</div>
</div>
<Button href="/auth/login" passHref className="flex w-full justify-center">
{t("go_back_login")}
</Button>
</AuthContainer>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
// Deleting old cookie manually, remove this code after all existing cookies have expired
context.res.setHeader(
"Set-Cookie",
"next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
);
return {
props: {
trpcState: ssr.dehydrate(),
query: context.query,
},
};
}

View File

@ -1,6 +0,0 @@
export default function NewUserPage() {
if (typeof window !== "undefined") {
window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
}
return null;
}

View File

@ -1,70 +0,0 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getSession } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import SetupFormStep1 from "../../components/SetupFormStep1";
import StepDone from "../../components/StepDone";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const shouldDisable = props.userCount !== 0;
const steps = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
isLoading: isLoadingStep1,
},
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false,
},
];
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
</>
);
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
redirect: {
destination: `/404`,
permanent: false,
},
};
}
return {
props: {
userCount,
},
};
};

View File

@ -1,42 +0,0 @@
import { GetServerSidePropsContext } from "next";
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
import { Button } from "@calcom/ui";
type Provider = {
name: string;
id: string;
};
function signin({ providers }: { providers: Provider[] }) {
return (
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
{Object.values(providers).map((provider) => {
return (
<div key={provider.name}>
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
</div>
);
})}
</div>
);
}
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,
},
};
}

View File

@ -1,180 +0,0 @@
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<typeof getServerSideProps>;
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<string | null> => {
// @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;
};

View File

@ -1,176 +0,0 @@
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 <Loader />;
}
const { valid, hasPaymentFailed, customer } = data;
if (!valid) {
throw new Error("Invalid session or customer id");
}
if (!stripeCustomerId && !sessionId) {
return <div>Invalid Link</div>;
}
return (
<div className="bg-black bg-opacity-90 text-white backdrop-blur-md backdrop-grayscale backdrop-filter">
<Head>
<title>
{/* @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}
</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center px-6">
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
<div className="rounded-full border border-white p-3">
{hasPaymentFailed ? (
<ExclamationIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
) : sessionId ? (
<CheckIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
) : (
<MailOpenIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
)}
</div>
<h3 className="font-cal my-6 text-3xl font-normal">
{hasPaymentFailed
? "Your payment failed"
: sessionId
? "Payment successful!"
: "Check your Inbox"}
</h3>
{hasPaymentFailed && (
<p className="my-6">Your account has been created, but your premium has not been reserved.</p>
)}
<p>
We have sent an email to <b>{customer?.email} </b>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."}
</p>
<p className="mt-6 text-gray-400">
Don&apos;t see an email? Click the button below to send another email.
</p>
<div className="mt-6 flex space-x-5 text-center">
<Button
color="secondary"
disabled={secondsLeft > 0}
onClick={async (e) => {
if (!customer) {
return;
}
e.preventDefault();
setSecondsLeft(30);
// Update query params with t:timestamp, shallow: true doesn't re-render the page
router.push(
router.asPath,
{
query: {
...router.query,
t: Date.now(),
},
},
{ shallow: true }
);
return await sendVerificationLogin(customer.email, customer.username);
}}>
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}
</Button>
<Button color="primary" href={`${WEBAPP_URL || "https://app.cal.com"}/auth/login`}>
Login using another method
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,14 @@
import z from "zod";
import { InstalledAppVariants } from "../utils";
const InstalledAppVariants = [
"conferencing",
"calendar",
"payment",
"analytics",
"automation",
"other",
"web3",
] as const;
const variantSchema = z.enum(InstalledAppVariants);

View File

@ -1,58 +0,0 @@
import { GetStaticPropsContext } from "next";
import Link from "next/link";
import { useRouter } from "next/router";
import z from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssgInit } from "@calcom/trpc/server/ssg";
import { Button, Icon, SkeletonText } from "@calcom/ui";
import AuthContainer from "../components/AuthContainer";
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 = <SkeletonText />;
if (router.isReady) {
errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
}
return (
<AuthContainer title="" description="">
<div>
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
<Icon.FiX className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{error}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{errorMsg}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-6">
<Link href="/auth/login" passHref legacyBehavior>
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
</Link>
</div>
</AuthContainer>
);
}
export const getStaticProps = async (context: GetStaticPropsContext) => {
const ssr = await ssgInit(context);
return {
props: {
trpcState: ssr.dehydrate(),
},
};
};

View File

@ -1,195 +0,0 @@
import { ResetPasswordRequest } from "@prisma/client";
import debounce from "lodash/debounce";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import Link from "next/link";
import React, { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { Button, TextField } from "@calcom/ui";
import AuthContainer from "../../components/AuthContainer";
type Props = {
id: string;
resetPasswordRequest: ResetPasswordRequest;
csrfToken: string;
};
export default function Page({ resetPasswordRequest, csrfToken }: Props) {
const { t } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = React.useState("");
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
try {
const res = await fetch("/api/auth/reset-password", {
method: "POST",
body: JSON.stringify({ requestId: requestId, password: password }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
};
const debouncedChangePassword = debounce(submitChangePassword, 250);
const Success = () => {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal mt-6 text-center text-3xl font-extrabold text-gray-900">
{t("password_updated")}
</h2>
</div>
<Button href="/auth/login" className="w-full justify-center">
{t("login")}
</Button>
</div>
</>
);
};
const Expired = () => {
return (
<>
<div className="space-y-6">
<div>
<h2 className="font-cal mt-6 text-center text-3xl font-extrabold text-gray-900">{t("whoops")}</h2>
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
</div>
<p>{t("request_is_expired_instructions")}</p>
<Link href="/auth/forgot-password" passHref legacyBehavior>
<button
type="button"
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
{t("try_again")}
</button>
</Link>
</div>
</>
);
};
const isRequestExpired = useMemo(() => {
const now = dayjs();
return dayjs(resetPasswordRequest.expires).isBefore(now);
}, [resetPasswordRequest]);
return (
<AuthContainer
showLogo
title={t("reset_password")}
description={t("change_your_password")}
heading={!success ? t("reset_password") : undefined}>
{isRequestExpired && <Expired />}
{!isRequestExpired && !success && (
<>
<form
className="space-y-6"
onSubmit={async (e) => {
e.preventDefault();
if (!password) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedChangePassword({ password, requestId: resetPasswordRequest.id });
}}
action="#">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<div className="mt-1">
<TextField
label={t("new_password")}
onChange={(e) => {
setPassword(e.target.value);
}}
id="password"
name="password"
type="password"
autoComplete="password"
required
/>
</div>
<div>
<Button
loading={loading}
color="primary"
type="submit"
disabled={loading}
className="w-full justify-center">
{t("reset_password")}
</Button>
</div>
</form>
</>
)}
{!isRequestExpired && success && (
<>
<Success />
</>
)}
</AuthContainer>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const id = context.params?.id as string;
try {
const resetPasswordRequest = await prisma.resetPasswordRequest.findUniqueOrThrow({
where: {
id,
},
select: {
id: true,
expires: true,
},
});
return {
props: {
resetPasswordRequest: {
...resetPasswordRequest,
expires: resetPasswordRequest.expires.toString(),
},
id,
csrfToken: await getCsrfToken({ req: context.req }),
},
};
} catch (reason) {
return {
notFound: true,
};
}
}

View File

@ -1,146 +0,0 @@
import debounce from "lodash/debounce";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import React, { SyntheticEvent } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, EmailField } from "@calcom/ui";
import AuthContainer from "../../components/AuthContainer";
import { getSession } from "../../lib";
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useLocale();
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<{ message: string } | null>(null);
const [success, setSuccess] = React.useState(false);
const [email, setEmail] = React.useState("");
const router = useRouter();
const handleChange = (e: SyntheticEvent) => {
const target = e.target as typeof e.target & { value: string };
setEmail(target.value);
};
const submitForgotPasswordRequest = async ({ email }: { email: string }) => {
try {
const res = await fetch("/api/auth/forgot-password", {
method: "POST",
body: JSON.stringify({ email: email, language: i18n.language }),
headers: {
"Content-Type": "application/json",
},
});
const json = await res.json();
if (!res.ok) {
setError(json);
} else if ("resetLink" in json) {
router.push(json.resetLink);
} else {
setSuccess(true);
}
return json;
} catch (reason) {
setError({ message: t("unexpected_error_try_again") });
} finally {
setLoading(false);
}
};
const debouncedHandleSubmitPasswordRequest = debounce(submitForgotPasswordRequest, 250);
const handleSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
if (!email) {
return;
}
if (loading) {
return;
}
setLoading(true);
setError(null);
setSuccess(false);
await debouncedHandleSubmitPasswordRequest({ email });
};
const Success = () => {
return (
<div className="space-y-6 text-sm leading-normal ">
<p className="">{t("password_reset_email", { email })}</p>
<p className="">{t("password_reset_leading")}</p>
{error && <p className="text-center text-red-600">{error.message}</p>}
<Button color="secondary" className="w-full justify-center" href="/auth/login">
{t("back_to_signin")}
</Button>
</div>
);
};
return (
<AuthContainer
showLogo
title={!success ? t("forgot_password") : t("reset_link_sent")}
heading={!success ? t("forgot_password") : t("reset_link_sent")}
description={t("request_password_reset")}
footerText={
!success && (
<>
<Link href="/auth/login" className="font-medium text-gray-900">
{t("back_to_signin")}
</Link>
</>
)
}>
{success && <Success />}
{!success && (
<>
<div className="space-y-6">{error && <p className="text-red-600">{error.message}</p>}</div>
<form className="space-y-6" onSubmit={handleSubmit} action="#">
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
<EmailField
onChange={handleChange}
id="email"
name="email"
label={t("email_address")}
placeholder="john.doe@example.com"
required
/>
<div className="space-y-2">
<Button
className="w-full justify-center"
type="submit"
disabled={loading}
aria-label={t("request_password_reset")}
loading={loading}>
{t("request_password_reset")}
</Button>
</div>
</form>
</>
)}
</AuthContainer>
);
}
ForgotPassword.getInitialProps = async (context: GetServerSidePropsContext) => {
const { req, res } = context;
const session = await getSession({ req });
if (session) {
res.writeHead(302, { Location: "/" });
res.end();
return;
}
return {
csrfToken: await getCsrfToken(context),
};
};

View File

@ -1,240 +0,0 @@
import classNames from "classnames";
import { GetServerSidePropsContext } from "next";
import { getCsrfToken, signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/components/SAMLLogin";
import { ErrorCode, getSession } from "@calcom/features/auth/lib";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { ssrInit } from "@calcom/trpc/server/ssr";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AddToHomescreen, Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
// TODO: Fix this import
import { IS_GOOGLE_LOGIN_ENABLED } from "@calcom/web/server/lib/constants";
import AuthContainer from "../components/AuthContainer";
import TwoFactor from "../components/TwoFactor";
interface LoginValues {
email: string;
password: string;
totpCode: string;
csrfToken: string;
}
export default function Login({
csrfToken,
isGoogleLoginEnabled,
isSAMLLoginEnabled,
samlTenantID,
samlProductID,
}: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const methods = useForm<LoginValues>();
const { register, formState } = methods;
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const errorMessages: { [key: string]: string } = {
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
[ErrorCode.UserNotFound]: t("no_account_exists"),
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
};
const telemetry = useTelemetry();
let callbackUrl = typeof router.query?.callbackUrl === "string" ? router.query.callbackUrl : "";
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
{t("dont_have_an_account")}
</a>
);
const TwoFactorFooter = (
<Button
onClick={() => {
setTwoFactorRequired(false);
methods.setValue("totpCode", "");
}}
StartIcon={Icon.FiArrowLeft}
color="minimal">
{t("go_back")}
</Button>
);
const onSubmit = async (values: LoginValues) => {
setErrorMessage(null);
telemetry.event(telemetryEventTypes.login, collectPageParameters());
const res = await signIn<"credentials">("credentials", {
...values,
callbackUrl,
redirect: false,
});
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
// we're logged in! let's do a hard refresh to the desired url
else if (!res.error) router.push(callbackUrl);
// reveal two factor input if required
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
// fallback if error not found
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
return (
<>
<AuthContainer
title={t("login")}
description={t("login")}
showLogo
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
footerText={
twoFactorRequired
? TwoFactorFooter
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
? LoginFooter
: null
}>
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
<div>
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
</div>
<div className="space-y-6">
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
<EmailField
id="email"
label={t("email_address")}
defaultValue={router.query.email as string}
placeholder="john.doe@example.com"
required
{...register("email")}
/>
<div className="relative">
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
<Link
href="/auth/forgot-password"
tabIndex={-1}
className="text-sm font-medium text-gray-600">
{t("forgot")}
</Link>
</div>
<PasswordField
id="password"
autoComplete="current-password"
required
className="mb-0"
{...register("password")}
/>
</div>
</div>
{twoFactorRequired && <TwoFactor center />}
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
type="submit"
color="primary"
disabled={formState.isSubmitting}
className="w-full justify-center">
{twoFactorRequired ? t("submit") : t("sign_in")}
</Button>
</div>
</form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full justify-center"
data-testid="google"
StartIcon={FaGoogle}
onClick={async (e) => {
e.preventDefault();
// track Google logins. Without personal data/payload
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
await signIn("google");
}}>
{t("signin_with_google")}
</Button>
)}
{isSAMLLoginEnabled && (
<SAMLLogin
samlTenantID={samlTenantID}
samlProductID={samlProductID}
setErrorMessage={setErrorMessage}
/>
)}
</div>
</>
)}
</FormProvider>
</AuthContainer>
<AddToHomescreen />
</>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { req } = context;
const session = await getSession({ req });
const ssr = await ssrInit(context);
if (session) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
const userCount = await prisma.user.count();
if (userCount === 0) {
// Proceed to new onboarding to create first admin user
return {
redirect: {
destination: "/auth/setup",
permanent: false,
},
};
}
return {
props: {
csrfToken: await getCsrfToken(context),
trpcState: ssr.dehydrate(),
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
isSAMLLoginEnabled,
samlTenantID,
samlProductID,
},
};
}

View File

@ -1,65 +0,0 @@
import { GetServerSidePropsContext } from "next";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { ssrInit } from "@calcom/trpc/server/ssr";
import { Button, Icon } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import AuthContainer from "../components/AuthContainer";
type Props = inferSSRProps<typeof getServerSideProps>;
export default function Logout(props: Props) {
const { status } = useSession();
if (status === "authenticated") signOut({ redirect: false });
const router = useRouter();
useEffect(() => {
if (props.query?.survey === "true") {
router.push(`${WEBSITE_URL}/cancellation`);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.query?.survey]);
const { t } = useLocale();
return (
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
<div className="mb-4">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<Icon.FiCheck className="h-6 w-6 text-green-600" />
</div>
<div className="mt-3 text-center sm:mt-5">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("youve_been_logged_out")}
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">{t("hope_to_see_you_soon")}</p>
</div>
</div>
</div>
<Button href="/auth/login" passHref className="flex w-full justify-center">
{t("go_back_login")}
</Button>
</AuthContainer>
);
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
// Deleting old cookie manually, remove this code after all existing cookies have expired
context.res.setHeader(
"Set-Cookie",
"next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
);
return {
props: {
trpcState: ssr.dehydrate(),
query: context.query,
},
};
}

View File

@ -1,6 +0,0 @@
export default function NewUserPage() {
if (typeof window !== "undefined") {
window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
}
return null;
}

View File

@ -1,70 +0,0 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getSession } from "@calcom/features/auth/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { WizardForm } from "@calcom/ui";
import SetupFormStep1 from "../../components/SetupFormStep1";
import StepDone from "../../components/StepDone";
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
const shouldDisable = props.userCount !== 0;
const steps = [
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
isLoading: isLoadingStep1,
},
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
isLoading: false,
},
];
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
</>
);
}
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
redirect: {
destination: `/404`,
permanent: false,
},
};
}
return {
props: {
userCount,
},
};
};

View File

@ -1,42 +0,0 @@
import { GetServerSidePropsContext } from "next";
import { getProviders, signIn, getSession, getCsrfToken } from "next-auth/react";
import { Button } from "@calcom/ui";
type Provider = {
name: string;
id: string;
};
function signin({ providers }: { providers: Provider[] }) {
return (
<div className="center mt-10 justify-between space-y-5 text-center align-baseline">
{Object.values(providers).map((provider) => {
return (
<div key={provider.name}>
<Button onClick={() => signIn(provider.id)}>Sign in with {provider.name}</Button>
</div>
);
})}
</div>
);
}
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,
},
};
}

View File

@ -1,180 +0,0 @@
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<typeof getServerSideProps>;
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<string | null> => {
// @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;
};

View File

@ -1,176 +0,0 @@
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 <Loader />;
}
const { valid, hasPaymentFailed, customer } = data;
if (!valid) {
throw new Error("Invalid session or customer id");
}
if (!stripeCustomerId && !sessionId) {
return <div>Invalid Link</div>;
}
return (
<div className="bg-black bg-opacity-90 text-white backdrop-blur-md backdrop-grayscale backdrop-filter">
<Head>
<title>
{/* @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}
</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center px-6">
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
<div className="rounded-full border border-white p-3">
{hasPaymentFailed ? (
<ExclamationIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
) : sessionId ? (
<CheckIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
) : (
<MailOpenIcon className="h-12 w-12 flex-shrink-0 p-0.5 font-extralight text-white" />
)}
</div>
<h3 className="font-cal my-6 text-3xl font-normal">
{hasPaymentFailed
? "Your payment failed"
: sessionId
? "Payment successful!"
: "Check your Inbox"}
</h3>
{hasPaymentFailed && (
<p className="my-6">Your account has been created, but your premium has not been reserved.</p>
)}
<p>
We have sent an email to <b>{customer?.email} </b>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."}
</p>
<p className="mt-6 text-gray-400">
Don&apos;t see an email? Click the button below to send another email.
</p>
<div className="mt-6 flex space-x-5 text-center">
<Button
color="secondary"
disabled={secondsLeft > 0}
onClick={async (e) => {
if (!customer) {
return;
}
e.preventDefault();
setSecondsLeft(30);
// Update query params with t:timestamp, shallow: true doesn't re-render the page
router.push(
router.asPath,
{
query: {
...router.query,
t: Date.now(),
},
},
{ shallow: true }
);
return await sendVerificationLogin(customer.email, customer.username);
}}>
{secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Send another mail"}
</Button>
<Button color="primary" href={`${WEBAPP_URL || "https://app.cal.com"}/auth/login`}>
Login using another method
</Button>
</div>
</div>
</div>
</div>
);
}