chore: App router migration - /auth/forgot-password/*, /auth/login, /auth/logout (#16467)
* migrate auth/forgot-password/*, auth/login, auth/logout * revert * fix forgot password * fix folder structure * fix logout * fix middleware * remove TODO log * refactor logout
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import SetNewUserPassword from "@pages/auth/forgot-password/[id]";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";
|
||||
import { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/auth/forgot-password/[id]/forgot-password-single-view";
|
||||
import SetNewUserPassword from "~/auth/forgot-password/[id]/forgot-password-single-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
@@ -15,5 +17,5 @@ export const generateMetadata = async () => {
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: SetNewUserPassword,
|
||||
getData: withAppDirSsr(getServerSideProps),
|
||||
getData: withAppDirSsr<PageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import ForgotPassword from "@pages/auth/forgot-password";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";
|
||||
import { getServerSidePropsAppDir } from "@server/lib/auth/forgot-password/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/auth/forgot-password/forgot-password-view";
|
||||
import ForgotPassword from "~/auth/forgot-password/forgot-password-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("reset_password"),
|
||||
(t) => t("change_your_password")
|
||||
(t) => t("forgot_password"),
|
||||
(t) => t("request_password_reset")
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: ForgotPassword,
|
||||
getData: withAppDirSsr(getServerSideProps),
|
||||
getData: withAppDirSsr<PageProps>(getServerSidePropsAppDir),
|
||||
})<"P">;
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import Login from "@pages/auth/login";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { InferGetServerSidePropsType } from "next";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
|
||||
|
||||
import type { PageProps } from "~/auth/login-view";
|
||||
import Login from "~/auth/login-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("login")}`,
|
||||
(t) => t("login"),
|
||||
(t) => t("login")
|
||||
);
|
||||
};
|
||||
@@ -16,5 +17,5 @@ export const generateMetadata = async () => {
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: Login,
|
||||
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||
getData: withAppDirSsr<PageProps>(getServerSideProps),
|
||||
})<"P">;
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
import Logout from "@pages/auth/logout";
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
export default WithLayout({ getLayout: null, Page: Logout, getData: withAppDirSsr(getServerSideProps) })<"P">;
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
import Logout from "~/auth/logout-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("logged_out"),
|
||||
(t) => t("youve_been_logged_out")
|
||||
);
|
||||
};
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
// cookie will be cleared in `/apps/web/middleware.ts`
|
||||
const h = headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
const context = buildLegacyCtx(h, cookies(), params, searchParams);
|
||||
const ssr = await ssrInit(context);
|
||||
const props = {
|
||||
trpcState: ssr.dehydrate(),
|
||||
query: context.query,
|
||||
};
|
||||
|
||||
return (
|
||||
<PageWrapper requiresLicense={false} getLayout={null} nonce={nonce} themeBasis={null}>
|
||||
<Logout {...props} />
|
||||
</PageWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -93,7 +93,10 @@ const middleware = async (req: NextRequest): Promise<NextResponse<unknown>> => {
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/future/auth/logout")) {
|
||||
cookies().delete("next-auth.session-token");
|
||||
cookies().set("next-auth.session-token", "", {
|
||||
path: "/",
|
||||
expires: new Date(0),
|
||||
});
|
||||
}
|
||||
|
||||
requestHeaders.set("x-pathname", url.pathname);
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, PasswordField, Form } from "@calcom/ui";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/getServerSideProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function Page({ requestId, isRequestExpired, csrfToken }: PageProps) {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useForm<{ new_password: string }>();
|
||||
const success = formMethods.formState.isSubmitSuccessful;
|
||||
const loading = formMethods.formState.isSubmitting;
|
||||
const passwordValue = formMethods.watch("new_password");
|
||||
const isEmpty = passwordValue?.length === 0;
|
||||
|
||||
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
|
||||
const res = await fetch("/api/auth/reset-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ requestId, password }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
|
||||
};
|
||||
|
||||
const Success = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">
|
||||
{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 text-emphasis mt-6 text-center text-3xl font-extrabold">{t("whoops")}</h2>
|
||||
<h2 className="text-emphasis text-center text-3xl font-extrabold">{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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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"
|
||||
form={formMethods}
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}
|
||||
handleSubmit={async (values) => {
|
||||
await submitChangePassword({
|
||||
password: values.new_password,
|
||||
requestId,
|
||||
});
|
||||
}}>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<div className="mt-1">
|
||||
<PasswordField
|
||||
{...formMethods.register("new_password", {
|
||||
minLength: {
|
||||
message: t("password_hint_min"),
|
||||
value: 7, // We don't have user here so we can't check if they are admin or not
|
||||
},
|
||||
pattern: {
|
||||
message: "Should contain a number, uppercase and lowercase letters",
|
||||
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
|
||||
},
|
||||
})}
|
||||
label={t("new_password")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
loading={loading}
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={loading || isEmpty}
|
||||
className="w-full justify-center">
|
||||
{t("reset_password")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
{!isRequestExpired && success && (
|
||||
<>
|
||||
<Success />
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { debounce } from "lodash";
|
||||
import Link from "next/link";
|
||||
import type { CSSProperties, SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmailField } from "@calcom/ui";
|
||||
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/forgot-password/getServerSideProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export default function ForgotPassword(props: PageProps) {
|
||||
const csrfToken = "csrfToken" in props ? (props.csrfToken as string) : undefined;
|
||||
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 [email, setEmail] = React.useState("");
|
||||
|
||||
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 }),
|
||||
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 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="text-emphasis font-medium">
|
||||
{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="#"
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<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 dark:bg-white dark:text-black"
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
aria-label={t("request_password_reset")}
|
||||
loading={loading}>
|
||||
{t("request_password_reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import classNames from "classnames";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { WithNonceProps } from "@lib/withNonce";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import BackupCode from "@components/auth/BackupCode";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
|
||||
|
||||
interface LoginValues {
|
||||
email: string;
|
||||
password: string;
|
||||
totpCode: string;
|
||||
backupCode: string;
|
||||
csrfToken: string;
|
||||
}
|
||||
|
||||
const GoogleIcon = () => (
|
||||
<img className="text-subtle mr-2 h-4 w-4 dark:invert" src="/google-icon.svg" alt="" />
|
||||
);
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
export default function Login({
|
||||
csrfToken,
|
||||
isGoogleLoginEnabled,
|
||||
isSAMLLoginEnabled,
|
||||
samlTenantID,
|
||||
samlProductID,
|
||||
totpEmail,
|
||||
}: // eslint-disable-next-line @typescript-eslint/ban-types
|
||||
PageProps & WithNonceProps<{}>) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, `${t("error_required_field")}`)
|
||||
.email(`${t("enter_valid_email")}`),
|
||||
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
|
||||
})
|
||||
// Passthrough other fields like totpCode
|
||||
.passthrough();
|
||||
const methods = useForm<LoginValues>({ resolver: zodResolver(formSchema) });
|
||||
const { register, formState } = methods;
|
||||
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
|
||||
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [lastUsed, setLastUsed] = useLastUsed();
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||
// Don't leak information about whether an email is registered or not
|
||||
[ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"),
|
||||
[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 = searchParams?.get("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 = (
|
||||
<Link href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
|
||||
{t("dont_have_an_account")}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const TwoFactorFooter = (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (twoFactorLostAccess) {
|
||||
setTwoFactorLostAccess(false);
|
||||
methods.setValue("backupCode", "");
|
||||
} else {
|
||||
setTwoFactorRequired(false);
|
||||
methods.setValue("totpCode", "");
|
||||
}
|
||||
setErrorMessage(null);
|
||||
}}
|
||||
StartIcon="arrow-left"
|
||||
color="minimal">
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
{!twoFactorLostAccess ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTwoFactorLostAccess(true);
|
||||
setErrorMessage(null);
|
||||
methods.setValue("totpCode", "");
|
||||
}}
|
||||
StartIcon="lock"
|
||||
color="minimal">
|
||||
{t("lost_access")}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const ExternalTotpFooter = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.replace("/");
|
||||
}}
|
||||
color="minimal">
|
||||
{t("cancel")}
|
||||
</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) {
|
||||
setLastUsed("credentials");
|
||||
router.push(callbackUrl);
|
||||
} else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
|
||||
else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
|
||||
else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
|
||||
// fallback if error not found
|
||||
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
||||
};
|
||||
|
||||
const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery();
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
},
|
||||
[error]
|
||||
);
|
||||
|
||||
const displaySSOLogin = HOSTED_CAL_FEATURES
|
||||
? true
|
||||
: isSAMLLoginEnabled && !isPending && data?.connectionExists;
|
||||
|
||||
return (
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
showLogo
|
||||
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
||||
footerText={
|
||||
twoFactorRequired
|
||||
? !totpEmail
|
||||
? TwoFactorFooter
|
||||
: ExternalTotpFooter
|
||||
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
||||
? LoginFooter
|
||||
: null
|
||||
}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate 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={totpEmail || (searchParams?.get("email") as string)}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="relative">
|
||||
<PasswordField
|
||||
id="password"
|
||||
autoComplete="off"
|
||||
required={!totpEmail}
|
||||
className="mb-0"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
tabIndex={-1}
|
||||
className="text-default text-sm font-medium">
|
||||
{t("forgot")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{twoFactorRequired ? !twoFactorLostAccess ? <TwoFactor center /> : <BackupCode center /> : null}
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
className="w-full justify-center">
|
||||
<span>{twoFactorRequired ? t("submit") : t("sign_in")}</span>
|
||||
{lastUsed === "credentials" && (
|
||||
<span className="absolute right-3 text-xs text-gray-600">{t("last_used")}</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{!twoFactorRequired && (
|
||||
<>
|
||||
{(isGoogleLoginEnabled || displaySSOLogin) && <hr className="border-subtle my-8" />}
|
||||
<div className="space-y-3">
|
||||
{isGoogleLoginEnabled && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="w-full justify-center"
|
||||
disabled={formState.isSubmitting}
|
||||
data-testid="google"
|
||||
CustomStartIcon={<GoogleIcon />}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await signIn("google", {
|
||||
callbackUrl,
|
||||
});
|
||||
if (res && !res.error) {
|
||||
setLastUsed("google");
|
||||
}
|
||||
}}>
|
||||
<span>{t("signin_with_google")}</span>
|
||||
{lastUsed === "google" && <LastUsed />}
|
||||
</Button>
|
||||
)}
|
||||
{displaySSOLogin && (
|
||||
<SAMLLogin
|
||||
samlTenantID={samlTenantID}
|
||||
samlProductID={samlProductID}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FormProvider>
|
||||
</AuthContainer>
|
||||
<AddToHomescreen />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import type { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
|
||||
|
||||
export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
|
||||
export function Logout(props: PageProps) {
|
||||
const [btnLoading, setBtnLoading] = useState<boolean>(false);
|
||||
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();
|
||||
|
||||
const message = () => {
|
||||
if (props.query?.passReset === "true") return "reset_your_password";
|
||||
if (props.query?.emailChange === "true") return "email_change";
|
||||
return "hope_to_see_you_soon";
|
||||
};
|
||||
|
||||
const navigateToLogin = () => {
|
||||
setBtnLoading(true);
|
||||
router.push("/auth/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||
<div className="mb-4">
|
||||
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="check" className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{t("youve_been_logged_out")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-subtle text-sm">{t(message())}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="logout-btn"
|
||||
onClick={navigateToLogin}
|
||||
className="flex w-full justify-center"
|
||||
loading={btnLoading}>
|
||||
{t("go_back_login")}
|
||||
</Button>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Logout;
|
||||
@@ -1,145 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, PasswordField, Form } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
isRequestExpired: boolean;
|
||||
csrfToken: string | undefined;
|
||||
};
|
||||
|
||||
export default function Page({ requestId, isRequestExpired, csrfToken }: Props) {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useForm<{ new_password: string }>();
|
||||
const success = formMethods.formState.isSubmitSuccessful;
|
||||
const loading = formMethods.formState.isSubmitting;
|
||||
const passwordValue = formMethods.watch("new_password");
|
||||
const isEmpty = passwordValue?.length === 0;
|
||||
|
||||
const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
|
||||
const res = await fetch("/api/auth/reset-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ requestId, password }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
|
||||
};
|
||||
|
||||
const Success = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="font-cal text-emphasis mt-6 text-center text-3xl font-extrabold">
|
||||
{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 text-emphasis mt-6 text-center text-3xl font-extrabold">{t("whoops")}</h2>
|
||||
<h2 className="text-emphasis text-center text-3xl font-extrabold">{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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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"
|
||||
form={formMethods}
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}
|
||||
handleSubmit={async (values) => {
|
||||
await submitChangePassword({
|
||||
password: values.new_password,
|
||||
requestId,
|
||||
});
|
||||
}}>
|
||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||
<div className="mt-1">
|
||||
<PasswordField
|
||||
{...formMethods.register("new_password", {
|
||||
minLength: {
|
||||
message: t("password_hint_min"),
|
||||
value: 7, // We don't have user here so we can't check if they are admin or not
|
||||
},
|
||||
pattern: {
|
||||
message: "Should contain a number, uppercase and lowercase letters",
|
||||
value: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).*$/gm,
|
||||
},
|
||||
})}
|
||||
label={t("new_password")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
loading={loading}
|
||||
color="primary"
|
||||
type="submit"
|
||||
disabled={loading || isEmpty}
|
||||
className="w-full justify-center">
|
||||
{t("reset_password")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
{!isRequestExpired && success && (
|
||||
<>
|
||||
<Success />
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
import type { PageProps } from "~/auth/forgot-password/[id]/forgot-password-single-view";
|
||||
import ForgotPassword from "~/auth/forgot-password/[id]/forgot-password-single-view";
|
||||
|
||||
const Page = (props: PageProps) => <ForgotPassword {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
export { getServerSideProps };
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/getServerSideProps";
|
||||
|
||||
@@ -1,150 +1,11 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { debounce } from "lodash";
|
||||
import Link from "next/link";
|
||||
import type { CSSProperties, SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmailField } from "@calcom/ui";
|
||||
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";
|
||||
import type { PageProps } from "~/auth/forgot-password/forgot-password-view";
|
||||
import ForgotPassword from "~/auth/forgot-password/forgot-password-view";
|
||||
|
||||
export default function ForgotPassword(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const csrfToken = "csrfToken" in props ? (props.csrfToken as string) : undefined;
|
||||
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 [email, setEmail] = React.useState("");
|
||||
const Page = (props: PageProps) => <ForgotPassword {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
const handleChange = (e: SyntheticEvent) => {
|
||||
const target = e.target as typeof e.target & { value: string };
|
||||
setEmail(target.value);
|
||||
};
|
||||
export default Page;
|
||||
|
||||
const submitForgotPasswordRequest = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/forgot-password", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email }),
|
||||
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 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="text-emphasis font-medium">
|
||||
{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="#"
|
||||
style={
|
||||
{
|
||||
"--cal-brand": "#111827",
|
||||
"--cal-brand-emphasis": "#101010",
|
||||
"--cal-brand-text": "white",
|
||||
"--cal-brand-subtle": "#9CA3AF",
|
||||
} as CSSProperties
|
||||
}>
|
||||
<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 dark:bg-white dark:text-black"
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={loading}
|
||||
aria-label={t("request_password_reset")}
|
||||
loading={loading}>
|
||||
{t("request_password_reset")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.PageWrapper = PageWrapper;
|
||||
|
||||
export { getServerSideProps };
|
||||
export { getServerSideProps } from "@server/lib/auth/forgot-password/getServerSideProps";
|
||||
|
||||
@@ -1,289 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import classNames from "classnames";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLastUsed, LastUsed } from "@calcom/lib/hooks/useLastUsed";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { WithNonceProps } from "@lib/withNonce";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import BackupCode from "@components/auth/BackupCode";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
|
||||
import type { PageProps } from "~/auth/login-view";
|
||||
import Login from "~/auth/login-view";
|
||||
|
||||
interface LoginValues {
|
||||
email: string;
|
||||
password: string;
|
||||
totpCode: string;
|
||||
backupCode: string;
|
||||
csrfToken: string;
|
||||
}
|
||||
const Page = (props: PageProps) => <Login {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
const GoogleIcon = () => (
|
||||
<img className="text-subtle mr-2 h-4 w-4 dark:invert" src="/google-icon.svg" alt="" />
|
||||
);
|
||||
export default function Login({
|
||||
csrfToken,
|
||||
isGoogleLoginEnabled,
|
||||
isSAMLLoginEnabled,
|
||||
samlTenantID,
|
||||
samlProductID,
|
||||
totpEmail,
|
||||
}: // eslint-disable-next-line @typescript-eslint/ban-types
|
||||
inferSSRProps<typeof getServerSideProps> & WithNonceProps<{}>) {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z
|
||||
.string()
|
||||
.min(1, `${t("error_required_field")}`)
|
||||
.email(`${t("enter_valid_email")}`),
|
||||
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
|
||||
})
|
||||
// Passthrough other fields like totpCode
|
||||
.passthrough();
|
||||
const methods = useForm<LoginValues>({ resolver: zodResolver(formSchema) });
|
||||
const { register, formState } = methods;
|
||||
const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
|
||||
const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [lastUsed, setLastUsed] = useLastUsed();
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||
// Don't leak information about whether an email is registered or not
|
||||
[ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"),
|
||||
[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 = searchParams?.get("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 = (
|
||||
<Link href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
|
||||
{t("dont_have_an_account")}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const TwoFactorFooter = (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (twoFactorLostAccess) {
|
||||
setTwoFactorLostAccess(false);
|
||||
methods.setValue("backupCode", "");
|
||||
} else {
|
||||
setTwoFactorRequired(false);
|
||||
methods.setValue("totpCode", "");
|
||||
}
|
||||
setErrorMessage(null);
|
||||
}}
|
||||
StartIcon="arrow-left"
|
||||
color="minimal">
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
{!twoFactorLostAccess ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setTwoFactorLostAccess(true);
|
||||
setErrorMessage(null);
|
||||
methods.setValue("totpCode", "");
|
||||
}}
|
||||
StartIcon="lock"
|
||||
color="minimal">
|
||||
{t("lost_access")}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
const ExternalTotpFooter = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
window.location.replace("/");
|
||||
}}
|
||||
color="minimal">
|
||||
{t("cancel")}
|
||||
</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) {
|
||||
setLastUsed("credentials");
|
||||
router.push(callbackUrl);
|
||||
} else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
|
||||
else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
|
||||
else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
|
||||
// fallback if error not found
|
||||
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
||||
};
|
||||
|
||||
const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery();
|
||||
|
||||
useEffect(
|
||||
function refactorMeWithoutEffect() {
|
||||
if (error) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
},
|
||||
[error]
|
||||
);
|
||||
|
||||
const displaySSOLogin = HOSTED_CAL_FEATURES
|
||||
? true
|
||||
: isSAMLLoginEnabled && !isPending && data?.connectionExists;
|
||||
|
||||
return (
|
||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||
<AuthContainer
|
||||
title={t("login")}
|
||||
description={t("login")}
|
||||
showLogo
|
||||
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
||||
footerText={
|
||||
twoFactorRequired
|
||||
? !totpEmail
|
||||
? TwoFactorFooter
|
||||
: ExternalTotpFooter
|
||||
: process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== "true"
|
||||
? LoginFooter
|
||||
: null
|
||||
}>
|
||||
<FormProvider {...methods}>
|
||||
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate 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={totpEmail || (searchParams?.get("email") as string)}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="relative">
|
||||
<PasswordField
|
||||
id="password"
|
||||
autoComplete="off"
|
||||
required={!totpEmail}
|
||||
className="mb-0"
|
||||
{...register("password")}
|
||||
/>
|
||||
<div className="absolute -top-[2px] ltr:right-0 rtl:left-0">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
tabIndex={-1}
|
||||
className="text-default text-sm font-medium">
|
||||
{t("forgot")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{twoFactorRequired ? !twoFactorLostAccess ? <TwoFactor center /> : <BackupCode center /> : null}
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
disabled={formState.isSubmitting}
|
||||
className="w-full justify-center">
|
||||
<span>{twoFactorRequired ? t("submit") : t("sign_in")}</span>
|
||||
{lastUsed === "credentials" && (
|
||||
<span className="absolute right-3 text-xs text-gray-600">{t("last_used")}</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{!twoFactorRequired && (
|
||||
<>
|
||||
{(isGoogleLoginEnabled || displaySSOLogin) && <hr className="border-subtle my-8" />}
|
||||
<div className="space-y-3">
|
||||
{isGoogleLoginEnabled && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="w-full justify-center"
|
||||
disabled={formState.isSubmitting}
|
||||
data-testid="google"
|
||||
CustomStartIcon={<GoogleIcon />}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const res = await signIn("google", {
|
||||
callbackUrl,
|
||||
});
|
||||
if (res && !res.error) {
|
||||
setLastUsed("google");
|
||||
}
|
||||
}}>
|
||||
<span>{t("signin_with_google")}</span>
|
||||
{lastUsed === "google" && <LastUsed />}
|
||||
</Button>
|
||||
)}
|
||||
{displaySSOLogin && (
|
||||
<SAMLLogin
|
||||
samlTenantID={samlTenantID}
|
||||
samlProductID={samlProductID}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FormProvider>
|
||||
</AuthContainer>
|
||||
<AddToHomescreen />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
Login.PageWrapper = PageWrapper;
|
||||
export default Page;
|
||||
export { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
|
||||
|
||||
@@ -1,74 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
import { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
|
||||
import type { PageProps } from "~/auth/logout-view";
|
||||
import Logout from "~/auth/logout-view";
|
||||
|
||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||
const Page = (props: PageProps) => <Logout {...props} />;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export function Logout(props: Props) {
|
||||
const [btnLoading, setBtnLoading] = useState<boolean>(false);
|
||||
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();
|
||||
|
||||
const message = () => {
|
||||
if (props.query?.passReset === "true") return "reset_your_password";
|
||||
if (props.query?.emailChange === "true") return "email_change";
|
||||
return "hope_to_see_you_soon";
|
||||
};
|
||||
|
||||
const navigateToLogin = () => {
|
||||
setBtnLoading(true);
|
||||
router.push("/auth/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||
<div className="mb-4">
|
||||
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<Icon name="check" className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
{t("youve_been_logged_out")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-subtle text-sm">{t(message())}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
data-testid="logout-btn"
|
||||
onClick={navigateToLogin}
|
||||
className="flex w-full justify-center"
|
||||
loading={btnLoading}>
|
||||
{t("go_back_login")}
|
||||
</Button>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Logout.PageWrapper = PageWrapper;
|
||||
export default Logout;
|
||||
|
||||
export { getServerSideProps };
|
||||
export default Page;
|
||||
export { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
|
||||
|
||||
+20
-1
@@ -10,7 +10,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
const session = await getServerSession({ req });
|
||||
|
||||
// @TODO res will not be available in future pages (app dir)
|
||||
if (session) {
|
||||
res.writeHead(302, { Location: "/" });
|
||||
res.end();
|
||||
@@ -25,3 +24,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getServerSidePropsAppDir(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
|
||||
const session = await getServerSession({ req });
|
||||
|
||||
if (session) {
|
||||
const redirect = await import("next/navigation").then((mod) => mod.redirect);
|
||||
redirect("/");
|
||||
return { props: {} };
|
||||
}
|
||||
const locale = await getLocale(context.req);
|
||||
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
...(await serverSideTranslations(locale, ["common"])),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req, res, query } = context;
|
||||
const { req, query } = context;
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
const session = await getServerSession({ req });
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const verifyJwt = (jwt: string) => {
|
||||
|
||||
Reference in New Issue
Block a user