Revert more stuff
This commit is contained in:
parent
2bba63f9f0
commit
213489c202
|
@ -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",
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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'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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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'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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user