mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-06 20:51:48 +02:00
♻️ Avoid getServerSideProps for login/register (#755)
This commit is contained in:
parent
becc7a7930
commit
438c4ec35b
12 changed files with 98 additions and 197 deletions
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
6
apps/web/src/contexts/whoami.ts
Normal file
6
apps/web/src/contexts/whoami.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
|
||||
export const useWhoAmI = () => {
|
||||
const { data: whoAmI } = trpc.whoami.get.useQuery();
|
||||
return whoAmI;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue