♻️ Avoid getServerSideProps for login/register (#755)

This commit is contained in:
Luke Vella 2023-07-14 18:09:14 +01:00 committed by GitHub
parent becc7a7930
commit 438c4ec35b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 197 deletions

View file

@ -68,7 +68,6 @@
"no": "No",
"noDatesSelected": "No dates selected",
"notificationsDisabled": "Notifications have been disabled for <b>{title}</b>",
"notRegistered": "Create a new account →",
"noVotes": "No one has voted for this option",
"ok": "Ok",
"optional": "optional",

View file

@ -1,4 +1,5 @@
import { trpc } from "@rallly/backend";
import { ArrowRightIcon } from "@rallly/icons";
import { Button } from "@rallly/ui/button";
import Link from "next/link";
import { Trans, useTranslation } from "next-i18next";
@ -98,7 +99,7 @@ const VerifyCode: React.FunctionComponent<{
{t("verificationCodeHelp")}
</p>
</fieldset>
<div className="mt-6 space-y-4 sm:flex sm:space-x-3 sm:space-y-0">
<div className="mt-6 flex flex-col gap-2 sm:flex-row">
<Button
loading={formState.isSubmitting || formState.isSubmitSuccessful}
type="submit"
@ -128,7 +129,7 @@ type RegisterFormData = {
export const RegisterForm: React.FunctionComponent<{
onClickLogin?: React.MouseEventHandler;
onRegistered: () => void;
onRegistered?: () => void;
defaultValues?: Partial<RegisterFormData>;
}> = ({ onClickLogin, onRegistered, defaultValues }) => {
const { t } = useTranslation();
@ -157,7 +158,7 @@ export const RegisterForm: React.FunctionComponent<{
queryClient.invalidate();
onRegistered();
onRegistered?.();
posthog?.identify(res.user.id, {
email: res.user.email,
name: res.user.name,
@ -252,7 +253,7 @@ export const RegisterForm: React.FunctionComponent<{
loading={formState.isSubmitting}
type="submit"
variant="primary"
className="h-12 px-6"
size="lg"
>
{t("continue")}
</Button>
@ -280,7 +281,7 @@ export const LoginForm: React.FunctionComponent<{
e: React.MouseEvent<HTMLAnchorElement>,
email: string,
) => void;
onAuthenticated: () => void;
onAuthenticated?: () => void;
}> = ({ onAuthenticated, onClickRegister }) => {
const { t } = useTranslation();
const { register, handleSubmit, getValues, formState, setError } = useForm<{
@ -305,7 +306,7 @@ export const LoginForm: React.FunctionComponent<{
if (!res.user) {
throw new Error("Failed to authenticate user");
} else {
onAuthenticated();
onAuthenticated?.();
queryClient.invalidate();
posthog?.identify(res.user.id, {
email: res.user.email,
@ -395,25 +396,27 @@ export const LoginForm: React.FunctionComponent<{
</div>
) : null}
</fieldset>
<div className="space-y-3">
<div className="flex flex-col gap-2">
<Button
loading={formState.isSubmitting}
type="submit"
size="lg"
variant="primary"
className="h-12 w-full px-6"
className=""
>
{t("continue")}
</Button>
<Link
href="/register"
className="btn-default h-12 w-full px-6"
onClick={(e) => {
onClickRegister?.(e, getValues("email"));
}}
>
{t("notRegistered")}
</Link>
<Button size="lg" asChild>
<Link
href="/register"
onClick={(e) => {
onClickRegister?.(e, getValues("email"));
}}
>
{t("createAnAccount")}
<ArrowRightIcon className="h-4 w-4" />
</Link>
</Button>
</div>
</form>
);

View file

@ -1,92 +0,0 @@
import { Dialog, DialogContent } from "@rallly/ui/dialog";
import Image from "next/image";
import React from "react";
import { LoginForm, RegisterForm } from "./login-form";
export const LoginModal: React.FunctionComponent<{
open: boolean;
defaultView?: "login" | "register";
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange, defaultView = "login" }) => {
const [newAccount, setNewAccount] = React.useState(
defaultView === "register",
);
const [defaultEmail, setDefaultEmail] = React.useState("");
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0">
<div
data-testid="login-modal"
className="border-t-primary max-w-full overflow-hidden border-t-4 shadow-sm"
>
<div className="bg-pattern flex justify-center py-8">
<Image
src="/static/logo.svg"
width={140}
height={30}
alt="Rallly"
/>
</div>
<div className="p-4 sm:p-6">
{newAccount ? (
<RegisterForm
defaultValues={{ email: defaultEmail }}
onRegistered={() => onOpenChange(false)}
onClickLogin={(e) => {
e.preventDefault();
setNewAccount(false);
}}
/>
) : (
<LoginForm
onAuthenticated={() => onOpenChange(false)}
onClickRegister={(e, email) => {
e.preventDefault();
setDefaultEmail(email);
setNewAccount(true);
}}
/>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};
export const LoginModalProvider = ({ children }: React.PropsWithChildren) => {
const [open, setOpen] = React.useState(false);
const [view, setView] = React.useState<"login" | "register">("login");
React.useEffect(() => {
const handleClick = (event: MouseEvent) => {
const target = event.target as HTMLElement;
const href = target.getAttribute("href");
if (
target.tagName === "A" &&
(href === "/login" || href === "/register")
) {
// Handle the click event here
event.preventDefault();
setView(href === "/login" ? "login" : "register");
setOpen(true);
}
};
document.addEventListener("click", handleClick, { capture: true });
return () => {
document.removeEventListener("click", handleClick, { capture: true });
};
}, []);
return (
<>
{open ? (
<LoginModal open={open} defaultView={view} onOpenChange={setOpen} />
) : null}
{children}
</>
);
};

View file

@ -1,24 +0,0 @@
import { SpinnerIcon } from "@rallly/icons";
import clsx from "clsx";
import * as React from "react";
interface FullPageLoaderProps {
className?: string;
children?: React.ReactNode;
}
const FullPageLoader: React.FunctionComponent<FullPageLoaderProps> = ({
children,
className,
}) => {
return (
<div className={clsx("flex h-full items-center justify-center", className)}>
<div className="bg-primary-600 flex items-center rounded-lg px-4 py-3 text-sm text-white shadow-sm">
<SpinnerIcon className="mr-3 h-5 animate-spin" />
{children}
</div>
</div>
);
};
export default FullPageLoader;

View file

@ -11,7 +11,6 @@ import { useInterval } from "react-use";
import spacetime from "spacetime";
import soft from "timezone-soft";
import { LoginModalProvider } from "@/components/auth/login-modal";
import { Container } from "@/components/container";
import FeedbackButton from "@/components/feedback";
import { Spinner } from "@/components/spinner";
@ -154,13 +153,11 @@ export const StandardLayout: React.FunctionComponent<{
<UserProvider>
<Toaster />
<ModalProvider>
<LoginModalProvider>
<div className="flex min-h-screen flex-col" {...rest}>
<MainNav />
<div>{children}</div>
</div>
{process.env.NEXT_PUBLIC_FEEDBACK_EMAIL ? <FeedbackButton /> : null}
</LoginModalProvider>
<div className="flex min-h-screen flex-col" {...rest}>
<MainNav />
<div>{children}</div>
</div>
{process.env.NEXT_PUBLIC_FEEDBACK_EMAIL ? <FeedbackButton /> : null}
</ModalProvider>
</UserProvider>
);

View file

@ -1,9 +1,9 @@
import { SpinnerIcon } from "@rallly/icons";
import { Loader2Icon } from "@rallly/icons";
import clsx from "clsx";
export const Spinner = (props: { className?: string }) => {
return (
<SpinnerIcon
<Loader2Icon
className={clsx("inline-block h-5 animate-spin", props.className)}
/>
);

View file

@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next";
import React from "react";
import { PostHogProvider } from "@/contexts/posthog";
import { useWhoAmI } from "@/contexts/whoami";
import { useRequiredContext } from "./use-required-context";
@ -48,7 +49,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
const queryClient = trpc.useContext();
const { data: user } = trpc.whoami.get.useQuery();
const user = useWhoAmI();
const billingQuery = trpc.user.getBilling.useQuery();
const { data: userPreferences } = trpc.userPreferences.get.useQuery();

View file

@ -0,0 +1,6 @@
import { trpc } from "@rallly/backend";
export const useWhoAmI = () => {
const { data: whoAmI } = trpc.whoami.get.useQuery();
return whoAmI;
};

View file

@ -15,24 +15,23 @@ export async function middleware(req: NextRequest) {
const newUrl = nextUrl.clone();
const res = NextResponse.next();
const session = await getSession(req, res);
if (
// a protected path is one that requires to be logged in
const isProtectedPath = protectedPaths.some((protectedPath) =>
req.nextUrl.pathname.includes(protectedPath),
);
const isProtectedPathDueToRequiredAuth =
process.env.AUTH_REQUIRED &&
session.user?.isGuest !== false &&
!publicPaths.some((publicPath) =>
req.nextUrl.pathname.startsWith(publicPath),
)
) {
newUrl.pathname = "/login";
return NextResponse.redirect(newUrl);
}
);
if (
session.user?.isGuest !== false &&
protectedPaths.some((protectedPath) =>
req.nextUrl.pathname.includes(protectedPath),
)
) {
const isGuest = session.user?.isGuest !== false;
if (isGuest && (isProtectedPathDueToRequiredAuth || isProtectedPath)) {
newUrl.pathname = "/login";
newUrl.searchParams.set("redirect", req.nextUrl.pathname);
return NextResponse.redirect(newUrl);
}

View file

@ -1,8 +1,5 @@
import {
composeGetServerSideProps,
withSessionSsr,
} from "@rallly/backend/next";
import { GetServerSideProps, NextPage } from "next";
import { Loader2Icon } from "@rallly/icons";
import { NextPage } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
@ -10,37 +7,45 @@ import React from "react";
import { AuthLayout } from "@/components/auth/auth-layout";
import { LoginForm } from "@/components/auth/login-form";
import { PageDialog } from "@/components/page-dialog";
import { useWhoAmI } from "@/contexts/whoami";
import { withPageTranslations } from "../utils/with-page-translations";
import { getStaticTranslations } from "../utils/with-page-translations";
const Page: NextPage<{ referer: string | null }> = () => {
const { t } = useTranslation();
const Redirect = () => {
const router = useRouter();
React.useEffect(() => {
router.replace((router.query.redirect as string) ?? "/");
});
return (
<AuthLayout>
<Head>
<title>{t("login")}</title>
</Head>
<LoginForm
onAuthenticated={async () => {
router.replace("/polls");
}}
/>
</AuthLayout>
<PageDialog>
<Loader2Icon className="h-10 w-10 animate-spin text-gray-400" />
</PageDialog>
);
};
export const getServerSideProps: GetServerSideProps = withSessionSsr(
composeGetServerSideProps(async (ctx) => {
if (ctx.req.session.user?.isGuest === false) {
return {
redirect: { destination: "/polls" },
props: {},
};
}
return { props: {} };
}, withPageTranslations()),
);
const Page: NextPage<{ referer: string | null }> = () => {
const { t } = useTranslation();
const whoami = useWhoAmI();
if (whoami?.isGuest === false) {
return <Redirect />;
}
return (
<>
<Head>
<title>{t("login")}</title>
</Head>
<AuthLayout>
<LoginForm />
</AuthLayout>
</>
);
};
export default Page;
export const getStaticProps = getStaticTranslations;

View file

@ -1,4 +1,3 @@
import { withSessionSsr } from "@rallly/backend/next";
import { NextPage } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
@ -6,11 +5,11 @@ import { useTranslation } from "next-i18next";
import { AuthLayout } from "../components/auth/auth-layout";
import { RegisterForm } from "../components/auth/login-form";
import { withSession } from "../components/user-provider";
import { withPageTranslations } from "../utils/with-page-translations";
import { getStaticTranslations } from "../utils/with-page-translations";
const Page: NextPage = () => {
const { t } = useTranslation();
const router = useRouter();
return (
<AuthLayout>
@ -19,13 +18,13 @@ const Page: NextPage = () => {
</Head>
<RegisterForm
onRegistered={() => {
router.replace("/polls");
router.replace("/");
}}
/>
</AuthLayout>
);
};
export const getServerSideProps = withSessionSsr(withPageTranslations());
export const getStaticProps = getStaticTranslations;
export default withSession(Page);
export default Page;

View file

@ -6,17 +6,25 @@ const getVercelUrl = () => {
: null;
};
function joinPath(baseUrl: string, subpath = "") {
if (subpath) {
const url = new URL(subpath, baseUrl);
return url.href;
}
return baseUrl;
}
export function absoluteUrl(subpath = "") {
const baseUrl =
process.env.NEXT_PUBLIC_BASE_URL ??
getVercelUrl() ??
`http://localhost:${port}`;
const url = new URL(subpath, baseUrl);
return url.href;
return joinPath(baseUrl, subpath);
}
export function shortUrl(subpath = "") {
const baseUrl = process.env.NEXT_PUBLIC_SHORT_BASE_URL ?? absoluteUrl();
const url = new URL(subpath, baseUrl);
return url.href;
return joinPath(baseUrl, subpath);
}