♻️ Handle self-hosting environment with updated authentication (#918)

This commit is contained in:
Luke Vella 2023-10-30 10:23:43 +00:00 committed by GitHub
parent 221ae62d8e
commit 3e90c302d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 133 additions and 222 deletions

View file

@ -15,8 +15,6 @@ import { useCopyToClipboard } from "react-use";
import { useParticipants } from "@/components/participants-provider";
import { Trans } from "@/components/trans";
import { usePoll } from "@/contexts/poll";
import { shortUrl } from "@/utils/absolute-url";
import { isSelfHosted } from "@/utils/constants";
export const InviteDialog = () => {
const { participants } = useParticipants();
@ -31,10 +29,6 @@ export const InviteDialog = () => {
}
}, [state]);
const inviteLink = isSelfHosted
? window.location.origin + `/invite/${poll?.id}`
: shortUrl(`/invite/${poll?.id}`);
const [didCopy, setDidCopy] = React.useState(false);
return (
@ -72,7 +66,7 @@ export const InviteDialog = () => {
<Button
className="w-full min-w-0 bg-gray-50 px-2.5"
onClick={() => {
copyToClipboard(inviteLink);
copyToClipboard(poll.inviteLink);
setDidCopy(true);
setTimeout(() => {
setDidCopy(false);
@ -82,7 +76,7 @@ export const InviteDialog = () => {
{didCopy ? (
<Trans i18nKey="copied" />
) : (
<span className="flex truncate">{inviteLink}</span>
<span className="flex truncate">{poll.inviteLink}</span>
)}
</Button>
<div className="shrink-0">

View file

@ -23,11 +23,10 @@ import { UserDropdown } from "@/components/user-dropdown";
import { IfCloudHosted } from "@/contexts/environment";
import { IfFreeUser } from "@/contexts/plan";
import { appVersion, isFeedbackEnabled } from "@/utils/constants";
import { ConnectedDayjsProvider } from "@/utils/dayjs";
import { IconComponent, NextPageWithLayout } from "../../types";
import ModalProvider from "../modal/modal-provider";
import { IfGuest, UserProvider } from "../user-provider";
import { IfGuest } from "../user-provider";
const NavMenuItem = ({
href,
@ -181,49 +180,45 @@ export const StandardLayout: React.FunctionComponent<{
}> = ({ children, hideNav, ...rest }) => {
const key = hideNav ? "no-nav" : "nav";
return (
<UserProvider>
<ConnectedDayjsProvider>
<Toaster />
<ModalProvider>
<div className="flex min-h-screen flex-col" {...rest}>
<AnimatePresence initial={false}>
{!hideNav ? <MainNav /> : null}
</AnimatePresence>
<AnimatePresence mode="wait" initial={false}>
<m.div
key={key}
variants={{
hidden: { opacity: 0, y: -56 },
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate="visible"
exit={{ opacity: 0, y: 56 }}
>
{children}
</m.div>
</AnimatePresence>
{appVersion ? (
<div className="fixed bottom-0 right-0 z-50 rounded-tl-md bg-gray-200/90">
<Link
className="px-2 py-1 text-xs tabular-nums tracking-tight"
target="_blank"
href={`https://github.com/lukevella/rallly/releases/${appVersion}`}
>
{`${appVersion}`}
</Link>
</div>
) : null}
<ModalProvider>
<Toaster />
<div className="flex min-h-screen flex-col" {...rest}>
<AnimatePresence initial={false}>
{!hideNav ? <MainNav /> : null}
</AnimatePresence>
<AnimatePresence mode="wait" initial={false}>
<m.div
key={key}
variants={{
hidden: { opacity: 0, y: -56 },
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate="visible"
exit={{ opacity: 0, y: 56 }}
>
{children}
</m.div>
</AnimatePresence>
{appVersion ? (
<div className="fixed bottom-0 right-0 z-50 rounded-tl-md bg-gray-200/90">
<Link
className="px-2 py-1 text-xs tabular-nums tracking-tight"
target="_blank"
href={`https://github.com/lukevella/rallly/releases/${appVersion}`}
>
{`${appVersion}`}
</Link>
</div>
{isFeedbackEnabled ? (
<>
<FeaturebaseIdentify />
<FeedbackButton />
</>
) : null}
</ModalProvider>
</ConnectedDayjsProvider>
</UserProvider>
) : null}
</div>
{isFeedbackEnabled ? (
<>
<FeaturebaseIdentify />
<FeedbackButton />
</>
) : null}
</ModalProvider>
);
};

View file

@ -9,11 +9,14 @@ import { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import Script from "next/script";
import { SessionProvider } from "next-auth/react";
import { SessionProvider, signIn, useSession } from "next-auth/react";
import { appWithTranslation } from "next-i18next";
import { DefaultSeo } from "next-seo";
import React from "react";
import Maintenance from "@/components/maintenance";
import { UserProvider } from "@/components/user-provider";
import { ConnectedDayjsProvider } from "@/utils/dayjs";
import { trpc } from "@/utils/trpc/client";
import * as nextI18nNextConfig from "../../next-i18next.config.js";
@ -29,12 +32,30 @@ type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
const Auth = ({ children }: { children: React.ReactNode }) => {
const session = useSession();
const isAuthenticated = !!session.data?.user.email;
React.useEffect(() => {
if (!isAuthenticated) {
signIn();
}
}, [isAuthenticated]);
if (isAuthenticated) {
return <>{children}</>;
}
return null;
};
const MyApp: NextPage<AppPropsWithLayout> = ({ Component, pageProps }) => {
if (process.env.NEXT_PUBLIC_MAINTENANCE_MODE === "1") {
return <Maintenance />;
}
const getLayout = Component.getLayout ?? ((page) => page);
const children = <Component {...pageProps} />;
return (
<SessionProvider>
@ -83,7 +104,15 @@ const MyApp: NextPage<AppPropsWithLayout> = ({ Component, pageProps }) => {
}
`}</style>
<TooltipProvider delayDuration={200}>
{getLayout(<Component {...pageProps} />)}
<UserProvider>
<ConnectedDayjsProvider>
{Component.isAuthRequired ? (
<Auth>{getLayout(children)}</Auth>
) : (
getLayout(children)
)}
</ConnectedDayjsProvider>
</UserProvider>
</TooltipProvider>
</LazyMotion>
</SessionProvider>

View file

@ -13,12 +13,11 @@ import { Poll } from "@/components/poll";
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { Trans } from "@/components/trans";
import { UserDropdown } from "@/components/user-dropdown";
import { UserProvider, useUser } from "@/components/user-provider";
import { useUser } from "@/components/user-provider";
import { VisibilityProvider } from "@/components/visibility";
import { PermissionsContext } from "@/contexts/permissions";
import { usePoll } from "@/contexts/poll";
import { absoluteUrl } from "@/utils/absolute-url";
import { ConnectedDayjsProvider } from "@/utils/dayjs";
import { trpc } from "@/utils/trpc/client";
import { getStaticTranslations } from "@/utils/with-page-translations";
@ -118,62 +117,58 @@ const Page = ({ id, title, user }: PageProps) => {
],
}}
/>
<UserProvider>
<ConnectedDayjsProvider>
<Prefetch>
<LegacyPollContextProvider>
<VisibilityProvider>
<div>
<svg
className="absolute inset-x-0 top-0 -z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
aria-hidden="true"
<Prefetch>
<LegacyPollContextProvider>
<VisibilityProvider>
<div>
<svg
className="absolute inset-x-0 top-0 -z-10 hidden h-[64rem] w-full stroke-gray-300/75 [mask-image:radial-gradient(800px_800px_at_center,white,transparent)] sm:block"
aria-hidden="true"
>
<defs>
<pattern
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
width={240}
height={240}
x="50%"
y={-1}
patternUnits="userSpaceOnUse"
>
<defs>
<pattern
id="1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84"
width={240}
height={240}
x="50%"
y={-1}
patternUnits="userSpaceOnUse"
>
<path d="M.5 240V.5H240" fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
<path d="M.5 240V.5H240" fill="none" />
</pattern>
</defs>
<rect
width="100%"
height="100%"
strokeWidth={0}
fill="url(#1f932ae7-37de-4c0a-a8b0-a6e3b4d44b84)"
/>
</svg>
<GoToApp />
<div className="mx-auto max-w-4xl space-y-4 px-3 sm:py-8">
<Poll />
<div className="mt-4 space-y-4 text-center text-gray-500">
<div className="py-8">
<Trans
defaults="Powered by <a>{name}</a>"
i18nKey="poweredByRallly"
values={{ name: "rallly.co" }}
components={{
a: (
<Link
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
href="https://rallly.co"
/>
),
}}
/>
</svg>
<GoToApp />
<div className="mx-auto max-w-4xl space-y-4 px-3 sm:py-8">
<Poll />
<div className="mt-4 space-y-4 text-center text-gray-500">
<div className="py-8">
<Trans
defaults="Powered by <a>{name}</a>"
i18nKey="poweredByRallly"
values={{ name: "rallly.co" }}
components={{
a: (
<Link
className="hover:text-primary-600 rounded-none border-b border-b-gray-500 font-semibold"
href="https://rallly.co"
/>
),
}}
/>
</div>
</div>
</div>
</div>
</VisibilityProvider>
</LegacyPollContextProvider>
</Prefetch>
</ConnectedDayjsProvider>
</UserProvider>
</div>
</div>
</VisibilityProvider>
</LegacyPollContextProvider>
</Prefetch>
</>
);
};

View file

@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next";
import { CreatePoll } from "@/components/create-poll";
import { getStandardLayout } from "@/components/layouts/standard-layout";
import { isSelfHosted } from "@/utils/constants";
import { NextPageWithLayout } from "../types";
import { getStaticTranslations } from "../utils/with-page-translations";
@ -95,6 +96,7 @@ const Page: NextPageWithLayout = () => {
};
Page.getLayout = getStandardLayout;
Page.isAuthRequired = isSelfHosted;
export default Page;

View file

@ -10,6 +10,7 @@ import { RegisterLink } from "@/components/register-link";
import { useUser } from "@/components/user-provider";
import { usePoll } from "@/contexts/poll";
import { NextPageWithLayout } from "@/types";
import { isSelfHosted } from "@/utils/constants";
import { getStaticTranslations } from "@/utils/with-page-translations";
const GuestPollAlert = () => {
@ -58,6 +59,7 @@ const Page: NextPageWithLayout = () => {
};
Page.getLayout = getPollLayout;
Page.isAuthRequired = isSelfHosted;
export const getStaticPaths = async () => {
return {

View file

@ -23,6 +23,7 @@ import { PollStatusBadge } from "@/components/poll-status";
import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { NextPageWithLayout } from "@/types";
import { isSelfHosted } from "@/utils/constants";
import { useDayjs } from "@/utils/dayjs";
import { trpc } from "@/utils/trpc/client";
import { getStaticTranslations } from "@/utils/with-page-translations";
@ -187,6 +188,7 @@ const Page: NextPageWithLayout = () => {
};
Page.getLayout = getStandardLayout;
Page.isAuthRequired = isSelfHosted;
export default Page;

View file

@ -282,6 +282,7 @@ const Page: NextPageWithLayout = () => {
Page.getLayout = getProfileLayout;
export const getStaticProps: GetStaticProps = async (ctx) => {
// This page is only available on the hosted version
if (isSelfHosted) {
return {
notFound: true,

View file

@ -71,6 +71,7 @@ const Page: NextPageWithLayout = () => {
};
Page.getLayout = getProfileLayout;
Page.isAuthRequired = true;
export const getStaticProps = getStaticTranslations;

View file

@ -10,6 +10,7 @@ export type PropsOf<TTag extends ReactTag> = TTag extends React.ElementType
// eslint-disable-next-line @typescript-eslint/ban-types
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
isAuthRequired?: boolean;
};
// eslint-disable-next-line @typescript-eslint/ban-types

View file

@ -16,7 +16,6 @@ import {
router,
} from "../trpc";
import { comments } from "./polls/comments";
import { demo } from "./polls/demo";
import { options } from "./polls/options";
import { participants } from "./polls/participants";
@ -42,7 +41,6 @@ const getPollIdFromAdminUrlId = async (urlId: string) => {
};
export const polls = router({
demo,
participants,
comments,
options,
@ -390,11 +388,12 @@ export const polls = router({
message: "Poll not found",
});
}
const inviteLink = ctx.shortUrl(`/invite/${res.id}`);
if (ctx.user.id === res.userId || res.adminUrlId === input.adminToken) {
return res;
return { ...res, inviteLink };
} else {
return { ...res, adminUrlId: "" };
return { ...res, adminUrlId: "", inviteLink };
}
}),
transfer: possiblyPublicProcedure

View file

@ -1,110 +0,0 @@
import { prisma, VoteType } from "@rallly/database";
import dayjs from "dayjs";
import { nanoid } from "../../../utils/nanoid";
import { possiblyPublicProcedure, router } from "../../trpc";
const participantData: Array<{ name: string; votes: VoteType[] }> = [
{
name: "Reed",
votes: ["yes", "no", "yes", "no"],
},
{
name: "Susan",
votes: ["yes", "yes", "yes", "no"],
},
{
name: "Johnny",
votes: ["no", "no", "yes", "yes"],
},
{
name: "Ben",
votes: ["yes", "yes", "yes", "yes"],
},
];
const optionValues = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
export const demo = router({
create: possiblyPublicProcedure.mutation(async () => {
const adminUrlId = nanoid();
const demoUser = { name: "John Example", email: "noreply@rallly.co" };
const options: Array<{ start: Date; id: string }> = [];
for (let i = 0; i < optionValues.length; i++) {
options.push({ id: await nanoid(), start: new Date(optionValues[i]) });
}
const participants: Array<{
name: string;
id: string;
userId: string;
createdAt: Date;
}> = [];
const votes: Array<{
optionId: string;
participantId: string;
type: VoteType;
}> = [];
for (let i = 0; i < participantData.length; i++) {
const { name, votes: participantVotes } = participantData[i];
const participantId = await nanoid();
participants.push({
id: participantId,
name,
userId: "user-demo",
createdAt: dayjs()
.add(i * -1, "minutes")
.toDate(),
});
options.forEach((option, index) => {
votes.push({
optionId: option.id,
participantId,
type: participantVotes[index],
});
});
}
await prisma.poll.create({
data: {
id: nanoid(),
title: "Lunch Meeting",
location: "Starbucks, 901 New York Avenue",
description: `Hey everyone, please choose the dates when you are available to meet for our monthly get together. Looking forward to see you all!`,
demo: true,
adminUrlId,
participantUrlId: nanoid(),
user: {
connectOrCreate: {
where: {
email: demoUser.email,
},
create: demoUser,
},
},
options: {
createMany: {
data: options,
},
},
participants: {
createMany: {
data: participants,
},
},
votes: {
createMany: {
data: votes,
},
},
},
});
return adminUrlId;
}),
});