From d059b1399bfd5b07c1537e63cb9c0a0b4b55520e Mon Sep 17 00:00:00 2001 From: Benny Joo Date: Tue, 3 Sep 2024 05:53:16 -0400 Subject: [PATCH] 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 --- .../future/auth/forgot-password/[id]/page.tsx | 8 +- .../app/future/auth/forgot-password/page.tsx | 12 +- apps/web/app/future/auth/login/page.tsx | 9 +- apps/web/app/future/auth/logout/page.tsx | 41 ++- apps/web/middleware.ts | 5 +- .../[id]/forgot-password-single-view.tsx | 138 +++++++++ .../forgot-password/forgot-password-view.tsx | 147 +++++++++ apps/web/modules/auth/login-view.tsx | 285 +++++++++++++++++ apps/web/modules/auth/logout-view.tsx | 70 +++++ apps/web/pages/auth/forgot-password/[id].tsx | 148 +-------- apps/web/pages/auth/forgot-password/index.tsx | 151 +-------- apps/web/pages/auth/login.tsx | 291 +----------------- apps/web/pages/auth/logout.tsx | 76 +---- .../[id]/getServerSideProps.tsx | 0 .../forgot-password/getServerSideProps.tsx | 21 +- .../lib/auth/login/getServerSideProps.tsx | 4 +- 16 files changed, 744 insertions(+), 662 deletions(-) create mode 100644 apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx create mode 100644 apps/web/modules/auth/forgot-password/forgot-password-view.tsx create mode 100644 apps/web/modules/auth/login-view.tsx create mode 100644 apps/web/modules/auth/logout-view.tsx rename apps/web/server/lib/{ => auth}/forgot-password/[id]/getServerSideProps.tsx (100%) rename apps/web/server/lib/{ => auth}/forgot-password/getServerSideProps.tsx (60%) diff --git a/apps/web/app/future/auth/forgot-password/[id]/page.tsx b/apps/web/app/future/auth/forgot-password/[id]/page.tsx index 126faa3a7c..3fe21ff685 100644 --- a/apps/web/app/future/auth/forgot-password/[id]/page.tsx +++ b/apps/web/app/future/auth/forgot-password/[id]/page.tsx @@ -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(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/auth/forgot-password/page.tsx b/apps/web/app/future/auth/forgot-password/page.tsx index 5b3bc27871..b66348f5e2 100644 --- a/apps/web/app/future/auth/forgot-password/page.tsx +++ b/apps/web/app/future/auth/forgot-password/page.tsx @@ -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(getServerSidePropsAppDir), })<"P">; diff --git a/apps/web/app/future/auth/login/page.tsx b/apps/web/app/future/auth/login/page.tsx index 31309f48ff..1117171c93 100644 --- a/apps/web/app/future/auth/login/page.tsx +++ b/apps/web/app/future/auth/login/page.tsx @@ -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>(getServerSideProps), + getData: withAppDirSsr(getServerSideProps), })<"P">; diff --git a/apps/web/app/future/auth/logout/page.tsx b/apps/web/app/future/auth/logout/page.tsx index 87da33148d..0c7a19efd5 100644 --- a/apps/web/app/future/auth/logout/page.tsx +++ b/apps/web/app/future/auth/logout/page.tsx @@ -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 ( + + + + ); +}; + +export default Page; diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts index 84269aea4a..ed749dbe5d 100644 --- a/apps/web/middleware.ts +++ b/apps/web/middleware.ts @@ -93,7 +93,10 @@ const middleware = async (req: NextRequest): Promise> => { } 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); diff --git a/apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx b/apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx new file mode 100644 index 0000000000..22913533a8 --- /dev/null +++ b/apps/web/modules/auth/forgot-password/[id]/forgot-password-single-view.tsx @@ -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; + +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 ( + <> +
+
+

+ {t("password_updated")} +

+
+ +
+ + ); + }; + + const Expired = () => { + return ( + <> +
+
+

{t("whoops")}

+

{t("request_is_expired")}

+
+

{t("request_is_expired_instructions")}

+ + + +
+ + ); + }; + + return ( + + {isRequestExpired && } + {!isRequestExpired && !success && ( + <> +
{ + await submitChangePassword({ + password: values.new_password, + requestId, + }); + }}> + +
+ +
+ +
+ +
+
+ + )} + {!isRequestExpired && success && ( + <> + + + )} +
+ ); +} diff --git a/apps/web/modules/auth/forgot-password/forgot-password-view.tsx b/apps/web/modules/auth/forgot-password/forgot-password-view.tsx new file mode 100644 index 0000000000..d8587cd114 --- /dev/null +++ b/apps/web/modules/auth/forgot-password/forgot-password-view.tsx @@ -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; + +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 ( +
+

{t("password_reset_email", { email })}

+

{t("password_reset_leading")}

+ {error &&

{error.message}

} + +
+ ); + }; + + return ( + + + {t("back_to_signin")} + + + ) + }> + {success && } + {!success && ( + <> +
{error &&

{error.message}

}
+
+ + +
+ +
+ + + )} +
+ ); +} diff --git a/apps/web/modules/auth/login-view.tsx b/apps/web/modules/auth/login-view.tsx new file mode 100644 index 0000000000..80f81741ab --- /dev/null +++ b/apps/web/modules/auth/login-view.tsx @@ -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 = () => ( + +); +export type PageProps = inferSSRProps; +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({ resolver: zodResolver(formSchema) }); + const { register, formState } = methods; + const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false); + const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false); + const [errorMessage, setErrorMessage] = useState(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 = ( + + {t("dont_have_an_account")} + + ); + + const TwoFactorFooter = ( + <> + + {!twoFactorLostAccess ? ( + + ) : null} + + ); + + const ExternalTotpFooter = ( + + ); + + 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 ( +
+ + +
+
+ +
+
+
+ +
+ +
+ + {t("forgot")} + +
+
+
+ + {twoFactorRequired ? !twoFactorLostAccess ? : : null} + + {errorMessage && } + +
+
+ {!twoFactorRequired && ( + <> + {(isGoogleLoginEnabled || displaySSOLogin) &&
} +
+ {isGoogleLoginEnabled && ( + + )} + {displaySSOLogin && ( + + )} +
+ + )} +
+
+ +
+ ); +} diff --git a/apps/web/modules/auth/logout-view.tsx b/apps/web/modules/auth/logout-view.tsx new file mode 100644 index 0000000000..459844169a --- /dev/null +++ b/apps/web/modules/auth/logout-view.tsx @@ -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; + +export function Logout(props: PageProps) { + const [btnLoading, setBtnLoading] = useState(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 ( + +
+
+ +
+
+ +
+

{t(message())}

+
+
+
+ +
+ ); +} + +export default Logout; diff --git a/apps/web/pages/auth/forgot-password/[id].tsx b/apps/web/pages/auth/forgot-password/[id].tsx index cbc7e4ad29..07861ce4c6 100644 --- a/apps/web/pages/auth/forgot-password/[id].tsx +++ b/apps/web/pages/auth/forgot-password/[id].tsx @@ -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 ( - <> -
-
-

- {t("password_updated")} -

-
- -
- - ); - }; - - const Expired = () => { - return ( - <> -
-
-

{t("whoops")}

-

{t("request_is_expired")}

-
-

{t("request_is_expired_instructions")}

- - - -
- - ); - }; - - return ( - - {isRequestExpired && } - {!isRequestExpired && !success && ( - <> -
{ - await submitChangePassword({ - password: values.new_password, - requestId, - }); - }}> - -
- -
- -
- -
-
- - )} - {!isRequestExpired && success && ( - <> - - - )} -
- ); -} +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) => ; Page.PageWrapper = PageWrapper; -export { getServerSideProps }; + +export default Page; + +export { getServerSideProps } from "@server/lib/auth/forgot-password/[id]/getServerSideProps"; diff --git a/apps/web/pages/auth/forgot-password/index.tsx b/apps/web/pages/auth/forgot-password/index.tsx index 8c473d404b..56bbf7b149 100644 --- a/apps/web/pages/auth/forgot-password/index.tsx +++ b/apps/web/pages/auth/forgot-password/index.tsx @@ -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) { - 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) => ; +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 ( -
-

{t("password_reset_email", { email })}

-

{t("password_reset_leading")}

- {error &&

{error.message}

} - -
- ); - }; - - return ( - - - {t("back_to_signin")} - - - ) - }> - {success && } - {!success && ( - <> -
{error &&

{error.message}

}
-
- - -
- -
- - - )} -
- ); -} - -ForgotPassword.PageWrapper = PageWrapper; - -export { getServerSideProps }; +export { getServerSideProps } from "@server/lib/auth/forgot-password/getServerSideProps"; diff --git a/apps/web/pages/auth/login.tsx b/apps/web/pages/auth/login.tsx index 9781dc8212..c86b8c2033 100644 --- a/apps/web/pages/auth/login.tsx +++ b/apps/web/pages/auth/login.tsx @@ -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) => ; +Page.PageWrapper = PageWrapper; -const GoogleIcon = () => ( - -); -export default function Login({ - csrfToken, - isGoogleLoginEnabled, - isSAMLLoginEnabled, - samlTenantID, - samlProductID, - totpEmail, -}: // eslint-disable-next-line @typescript-eslint/ban-types -inferSSRProps & 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({ resolver: zodResolver(formSchema) }); - const { register, formState } = methods; - const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false); - const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false); - const [errorMessage, setErrorMessage] = useState(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 = ( - - {t("dont_have_an_account")} - - ); - - const TwoFactorFooter = ( - <> - - {!twoFactorLostAccess ? ( - - ) : null} - - ); - - const ExternalTotpFooter = ( - - ); - - 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 ( -
- - -
-
- -
-
-
- -
- -
- - {t("forgot")} - -
-
-
- - {twoFactorRequired ? !twoFactorLostAccess ? : : null} - - {errorMessage && } - -
-
- {!twoFactorRequired && ( - <> - {(isGoogleLoginEnabled || displaySSOLogin) &&
} -
- {isGoogleLoginEnabled && ( - - )} - {displaySSOLogin && ( - - )} -
- - )} -
-
- -
- ); -} - -export { getServerSideProps }; - -Login.PageWrapper = PageWrapper; +export default Page; +export { getServerSideProps } from "@server/lib/auth/login/getServerSideProps"; diff --git a/apps/web/pages/auth/logout.tsx b/apps/web/pages/auth/logout.tsx index 0ba2466df5..892db3ec68 100644 --- a/apps/web/pages/auth/logout.tsx +++ b/apps/web/pages/auth/logout.tsx @@ -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; +const Page = (props: PageProps) => ; +Page.PageWrapper = PageWrapper; -export function Logout(props: Props) { - const [btnLoading, setBtnLoading] = useState(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 ( - -
-
- -
-
- -
-

{t(message())}

-
-
-
- -
- ); -} - -Logout.PageWrapper = PageWrapper; -export default Logout; - -export { getServerSideProps }; +export default Page; +export { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps"; diff --git a/apps/web/server/lib/forgot-password/[id]/getServerSideProps.tsx b/apps/web/server/lib/auth/forgot-password/[id]/getServerSideProps.tsx similarity index 100% rename from apps/web/server/lib/forgot-password/[id]/getServerSideProps.tsx rename to apps/web/server/lib/auth/forgot-password/[id]/getServerSideProps.tsx diff --git a/apps/web/server/lib/forgot-password/getServerSideProps.tsx b/apps/web/server/lib/auth/forgot-password/getServerSideProps.tsx similarity index 60% rename from apps/web/server/lib/forgot-password/getServerSideProps.tsx rename to apps/web/server/lib/auth/forgot-password/getServerSideProps.tsx index 5c7faca873..919fd8c03e 100644 --- a/apps/web/server/lib/forgot-password/getServerSideProps.tsx +++ b/apps/web/server/lib/auth/forgot-password/getServerSideProps.tsx @@ -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"])), + }, + }; +} diff --git a/apps/web/server/lib/auth/login/getServerSideProps.tsx b/apps/web/server/lib/auth/login/getServerSideProps.tsx index 64144dec37..24175c8a51 100644 --- a/apps/web/server/lib/auth/login/getServerSideProps.tsx +++ b/apps/web/server/lib/auth/login/getServerSideProps.tsx @@ -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) => {