mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-02 03:36:33 +02:00
♻️ Handle self-hosting environment with updated authentication (#918)
This commit is contained in:
parent
221ae62d8e
commit
3e90c302d6
12 changed files with 133 additions and 222 deletions
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -71,6 +71,7 @@ const Page: NextPageWithLayout = () => {
|
|||
};
|
||||
|
||||
Page.getLayout = getProfileLayout;
|
||||
Page.isAuthRequired = true;
|
||||
|
||||
export const getStaticProps = getStaticTranslations;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}),
|
||||
});
|
Loading…
Add table
Reference in a new issue