mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-10 07:26:48 +02:00
♻️ Create backend package (#643)
This commit is contained in:
parent
7fc08c6736
commit
05fe2edaea
68 changed files with 476 additions and 391 deletions
8
apps/web/declarations/i18next.d.ts
vendored
8
apps/web/declarations/i18next.d.ts
vendored
|
@ -1,9 +1,9 @@
|
|||
import "react-i18next";
|
||||
|
||||
import app from "~/public/locales/en/app.json";
|
||||
import common from "~/public/locales/en/common.json";
|
||||
import errors from "~/public/locales/en/errors.json";
|
||||
import homepage from "~/public/locales/en/homepage.json";
|
||||
import app from "../public/locales/en/app.json";
|
||||
import common from "../public/locales/en/common.json";
|
||||
import errors from "../public/locales/en/errors.json";
|
||||
import homepage from "../public/locales/en/homepage.json";
|
||||
|
||||
interface I18nNamespaces {
|
||||
homepage: typeof homepage;
|
||||
|
|
10
apps/web/declarations/iron-session.d.ts
vendored
10
apps/web/declarations/iron-session.d.ts
vendored
|
@ -1,10 +0,0 @@
|
|||
import "iron-session";
|
||||
|
||||
declare module "iron-session" {
|
||||
export interface IronSessionData {
|
||||
user: {
|
||||
id: string;
|
||||
isGuest: boolean;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ const nextConfig = {
|
|||
i18n: i18n,
|
||||
productionBrowserSourceMaps: true,
|
||||
output: "standalone",
|
||||
transpilePackages: ["@rallly/emails", "@rallly/database"],
|
||||
transpilePackages: ["@rallly/backend"],
|
||||
webpack(config) {
|
||||
config.module.rules.push({
|
||||
test: /\.svg$/,
|
||||
|
|
|
@ -16,21 +16,16 @@
|
|||
"docker:start": "./scripts/docker-start.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@rallly/backend": "*",
|
||||
"@rallly/database": "*",
|
||||
"@rallly/tailwind-config": "*",
|
||||
"@floating-ui/react-dom-interactions": "^0.13.3",
|
||||
"@headlessui/react": "^1.7.7",
|
||||
"@next/bundle-analyzer": "^12.3.4",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@rallly/database": "*",
|
||||
"@rallly/emails": "*",
|
||||
"@rallly/tailwind-config": "*",
|
||||
"@sentry/nextjs": "^7.33.0",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tanstack/react-query": "^4.22.0",
|
||||
"@trpc/client": "^10.13.0",
|
||||
"@trpc/next": "^10.13.0",
|
||||
"@trpc/react-query": "^10.13.0",
|
||||
"@trpc/server": "^10.13.0",
|
||||
"@vercel/analytics": "^0.1.8",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
|
@ -42,7 +37,6 @@
|
|||
"js-cookie": "^3.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "^4.0.0",
|
||||
"next": "^13.2.4",
|
||||
"next-i18next": "^13.0.3",
|
||||
"next-seo": "^5.15.0",
|
||||
"postcss": "^8.4.21",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from "react";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
import Logo from "~//logo.svg";
|
||||
|
||||
export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import Link from "next/link";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
@ -6,7 +7,6 @@ import { useForm } from "react-hook-form";
|
|||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { requiredString, validEmail } from "../../utils/form-validation";
|
||||
import { trpc } from "../../utils/trpc";
|
||||
import { Button } from "../button";
|
||||
import { TextInput } from "../text-input";
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { Button } from "./button";
|
||||
import {
|
||||
NewEventData,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
@ -7,7 +8,6 @@ import { usePostHog } from "@/utils/posthog";
|
|||
|
||||
import { useDayjs } from "../../utils/dayjs";
|
||||
import { requiredString } from "../../utils/form-validation";
|
||||
import { trpc } from "../../utils/trpc";
|
||||
import { Button } from "../button";
|
||||
import CompactButton from "../compact-button";
|
||||
import Dropdown, { DropdownItem } from "../dropdown";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import Link from "next/link";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
@ -8,7 +9,6 @@ import Speakerphone from "@/components/icons/speakerphone.svg";
|
|||
import { Logo } from "@/components/logo";
|
||||
import { useModalState } from "@/components/modal/use-modal";
|
||||
import Tooltip from "@/components/tooltip";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
|
||||
const FeedbackForm = (props: { onClose: () => void }) => {
|
||||
const { t } = useTranslation("app");
|
||||
|
|
|
@ -7,7 +7,7 @@ import * as React from "react";
|
|||
|
||||
import DotsVertical from "@/components/icons/dots-vertical.svg";
|
||||
import Github from "@/components/icons/github.svg";
|
||||
import Logo from "~/public/logo.svg";
|
||||
import Logo from "~//logo.svg";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
import Footer from "./page-layout/footer";
|
||||
|
|
|
@ -7,10 +7,10 @@ import Discord from "@/components/icons/discord.svg";
|
|||
import Star from "@/components/icons/star.svg";
|
||||
import Translate from "@/components/icons/translate.svg";
|
||||
import Twitter from "@/components/icons/twitter.svg";
|
||||
import DigitalOcean from "~/public/digitalocean.svg";
|
||||
import Logo from "~/public/logo.svg";
|
||||
import Sentry from "~/public/sentry.svg";
|
||||
import Vercel from "~/public/vercel-logotype-dark.svg";
|
||||
import DigitalOcean from "~//digitalocean.svg";
|
||||
import Logo from "~//logo.svg";
|
||||
import Sentry from "~//sentry.svg";
|
||||
import Vercel from "~//vercel-logotype-dark.svg";
|
||||
|
||||
import { LanguageSelect } from "../../poll/language-selector";
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { domMax, LazyMotion } from "framer-motion";
|
|||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import { DayjsProvider } from "@/utils/dayjs";
|
||||
|
||||
|
@ -34,6 +35,7 @@ const StandardLayout: React.FunctionComponent<{
|
|||
}> = ({ children, ...rest }) => {
|
||||
return (
|
||||
<LazyMotion features={domMax}>
|
||||
<Toaster />
|
||||
<UserProvider>
|
||||
<DayjsProvider>
|
||||
<ModalProvider>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import Logo from "~/public/logo.svg";
|
||||
import Logo from "~//logo.svg";
|
||||
|
||||
export const HomeLink = (props: { className?: string }) => {
|
||||
return (
|
||||
|
|
|
@ -23,7 +23,7 @@ import OpenBeta from "../../open-beta-modal";
|
|||
import { UserDropdown } from "./user-dropdown";
|
||||
|
||||
export const MobileNavigation = (props: { className?: string }) => {
|
||||
const { user, isUpdating } = useUser();
|
||||
const { user } = useUser();
|
||||
const { t } = useTranslation(["common", "app"]);
|
||||
|
||||
const [isPinned, setIsPinned] = React.useState(false);
|
||||
|
@ -113,9 +113,6 @@ export const MobileNavigation = (props: { className?: string }) => {
|
|||
data-testid="user"
|
||||
className={clsx(
|
||||
"group inline-flex w-full items-center space-x-2 rounded px-2 py-1 text-left transition-colors hover:bg-slate-500/10 active:bg-slate-500/20",
|
||||
{
|
||||
"opacity-50": isUpdating,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="relative shrink-0">
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import Head from "next/head";
|
||||
|
||||
import Clock from "@/components/icons/clock.svg";
|
||||
import Logo from "~/public/logo.svg";
|
||||
|
||||
import Logo from "../../public/logo.svg";
|
||||
|
||||
const Maintenance: React.FunctionComponent = () => {
|
||||
return (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
@ -15,7 +16,6 @@ import { useDeleteParticipantModal } from "@/components/poll/use-delete-particip
|
|||
import { TextInput } from "@/components/text-input";
|
||||
import { useFormValidation } from "@/utils/form-validation";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
|
||||
import { Participant } from ".prisma/client";
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { Participant, Vote, VoteType } from "@rallly/database";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
import FullPageLoader from "./full-page-loader";
|
||||
import { useRequiredContext } from "./use-required-context";
|
||||
|
||||
|
|
|
@ -3,7 +3,8 @@ import Cookies from "js-cookie";
|
|||
import { useTranslation } from "next-i18next";
|
||||
|
||||
import ChevronDown from "@/components/icons/chevron-down.svg";
|
||||
import languages from "~/languages.json";
|
||||
|
||||
import languages from "../../../languages.json";
|
||||
|
||||
export const LanguageSelect: React.FunctionComponent<{
|
||||
className?: string;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import clsx from "clsx";
|
||||
import { Trans, useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
@ -7,8 +8,6 @@ import { Button } from "@/components/button";
|
|||
import Exclamation from "@/components/icons/exclamation.svg";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { trpc } from "../../../utils/trpc";
|
||||
|
||||
const confirmText = "delete-me";
|
||||
|
||||
export const DeletePollForm: React.FunctionComponent<{
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { trpc } from "../../utils/trpc";
|
||||
import { ParticipantForm } from "./types";
|
||||
|
||||
export const normalizeVotes = (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
||||
|
@ -7,7 +8,6 @@ import Bell from "@/components/icons/bell.svg";
|
|||
import BellCrossed from "@/components/icons/bell-crossed.svg";
|
||||
import { useUser } from "@/components/user-provider";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
import { usePollByAdmin } from "@/utils/trpc/hooks";
|
||||
|
||||
import { usePoll } from "../poll-context";
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { useMount } from "react-use";
|
||||
|
||||
import { trpc } from "../../utils/trpc";
|
||||
|
||||
/**
|
||||
* Touching a poll updates a column with the current date. This information is used to
|
||||
* find polls that haven't been accessed for some time so that they can be deleted by house keeping.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -12,7 +13,6 @@ import User from "@/components/icons/user.svg";
|
|||
import Tooltip from "@/components/tooltip";
|
||||
|
||||
import { useDayjs } from "../utils/dayjs";
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { EmptyState } from "./empty-state";
|
||||
import { UserDetails } from "./profile/user-details";
|
||||
import { useUser } from "./user-provider";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { m } from "framer-motion";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
|
@ -6,7 +7,6 @@ import { useForm } from "react-hook-form";
|
|||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { requiredString, validEmail } from "../../utils/form-validation";
|
||||
import { trpc } from "../../utils/trpc";
|
||||
import { Button } from "../button";
|
||||
import { TextInput } from "../text-input";
|
||||
|
||||
|
|
|
@ -1,16 +1,15 @@
|
|||
import { trpc, UserSession } from "@rallly/backend";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import React from "react";
|
||||
|
||||
import { UserSession } from "@/utils/auth";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { useRequiredContext } from "./use-required-context";
|
||||
|
||||
export const UserContext = React.createContext<{
|
||||
user: UserSession & { shortName: string };
|
||||
refresh: () => void;
|
||||
isUpdating: boolean;
|
||||
logout: () => void;
|
||||
ownsObject: (obj: { userId: string | null }) => boolean;
|
||||
} | null>(null);
|
||||
|
@ -55,13 +54,10 @@ export const UserProvider = (props: {
|
|||
const queryClient = trpc.useContext();
|
||||
const { data: user } = trpc.whoami.get.useQuery();
|
||||
|
||||
const [isUpdating, setIsUpdating] = React.useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const logout = trpc.whoami.destroy.useMutation({
|
||||
onSuccess: async () => {
|
||||
setIsUpdating(true);
|
||||
await queryClient.whoami.invalidate();
|
||||
setIsUpdating(false);
|
||||
router.push("/logout");
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -91,7 +87,6 @@ export const UserProvider = (props: {
|
|||
return (
|
||||
<UserContext.Provider
|
||||
value={{
|
||||
isUpdating,
|
||||
user: { ...user, shortName },
|
||||
refresh: () => {
|
||||
return queryClient.whoami.invalidate();
|
||||
|
|
|
@ -25,8 +25,10 @@ const supportedLocales = [
|
|||
"zh",
|
||||
];
|
||||
|
||||
export function middleware({ headers, cookies, nextUrl }: NextRequest) {
|
||||
export async function middleware(req: NextRequest) {
|
||||
const { headers, cookies, nextUrl } = req;
|
||||
const newUrl = nextUrl.clone();
|
||||
const res = NextResponse.next();
|
||||
|
||||
// Check if locale is specified in cookie
|
||||
const localeCookie = cookies.get("NEXT_LOCALE");
|
||||
|
@ -50,7 +52,7 @@ export function middleware({ headers, cookies, nextUrl }: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
return res;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import "react-big-calendar/lib/css/react-big-calendar.css";
|
||||
import "tailwindcss/tailwind.css";
|
||||
import "~/style.css";
|
||||
import "../style.css";
|
||||
|
||||
import { trpc, UserSession } from "@rallly/backend/next/trpc/client";
|
||||
import { inject } from "@vercel/analytics";
|
||||
import { NextPage } from "next";
|
||||
import { AppProps } from "next/app";
|
||||
|
@ -12,15 +13,12 @@ import { DefaultSeo } from "next-seo";
|
|||
import posthog from "posthog-js";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import React from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
|
||||
import Maintenance from "@/components/maintenance";
|
||||
|
||||
import { useCrispChat } from "../components/crisp-chat";
|
||||
import { NextPageWithLayout } from "../types";
|
||||
import { absoluteUrl } from "../utils/absolute-url";
|
||||
import { UserSession } from "../utils/auth";
|
||||
import { trpc } from "../utils/trpc";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
|
@ -94,7 +92,6 @@ const MyApp: NextPage<AppPropsWithLayout> = ({ Component, pageProps }) => {
|
|||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, user-scalable=yes"
|
||||
/>
|
||||
</Head>
|
||||
<Toaster />
|
||||
<style jsx global>{`
|
||||
html {
|
||||
--font-inter: ${inter.style.fontFamily};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
@ -7,7 +8,6 @@ import { getStandardLayout } from "@/components/layouts/standard-layout";
|
|||
import { ParticipantsProvider } from "@/components/participants-provider";
|
||||
import { Poll } from "@/components/poll";
|
||||
import { PollContextProvider } from "@/components/poll-context";
|
||||
import { withAuthIfRequired, withSessionSsr } from "@/utils/auth";
|
||||
import { usePollByAdmin } from "@/utils/trpc/hooks";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
|
||||
import { createContext } from "../../../server/context";
|
||||
import { appRouter } from "../../../server/routers/_app";
|
||||
import { withSessionRoute } from "../../../utils/auth";
|
||||
import { trpcNextApiHandler } from "@rallly/backend/next/trpc/server";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
|
@ -10,9 +6,4 @@ export const config = {
|
|||
},
|
||||
};
|
||||
// export API handler
|
||||
export default withSessionRoute(
|
||||
trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
}),
|
||||
);
|
||||
export default trpcNextApiHandler;
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import { DisableNotificationsPayload } from "@rallly/backend";
|
||||
import {
|
||||
composeGetServerSideProps,
|
||||
withSessionSsr,
|
||||
} from "@rallly/backend/next";
|
||||
import { decryptToken } from "@rallly/backend/session";
|
||||
import { prisma } from "@rallly/database";
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
|
@ -9,12 +15,6 @@ import { useMount } from "react-use";
|
|||
import Bell from "@/components/icons/bell-crossed.svg";
|
||||
import { AuthLayout } from "@/components/layouts/auth-layout";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import {
|
||||
composeGetServerSideProps,
|
||||
decryptToken,
|
||||
DisableNotificationsPayload,
|
||||
withSessionSsr,
|
||||
} from "@/utils/auth";
|
||||
import { usePostHog } from "@/utils/posthog";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
import { LoginTokenPayload } from "@rallly/backend";
|
||||
import {
|
||||
composeGetServerSideProps,
|
||||
withSessionSsr,
|
||||
} from "@rallly/backend/next";
|
||||
import { decryptToken } from "@rallly/backend/session";
|
||||
import { prisma } from "@rallly/database";
|
||||
import clsx from "clsx";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
@ -9,12 +15,6 @@ import React from "react";
|
|||
import CheckCircle from "@/components/icons/check-circle.svg";
|
||||
import { AuthLayout } from "@/components/layouts/auth-layout";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import {
|
||||
composeGetServerSideProps,
|
||||
decryptToken,
|
||||
LoginTokenPayload,
|
||||
withSessionSsr,
|
||||
} from "@/utils/auth";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
const defaultRedirectPath = "/profile";
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
|
||||
import { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useTranslation } from "next-i18next";
|
||||
|
@ -8,8 +10,6 @@ import { usePostHog } from "@/utils/posthog";
|
|||
|
||||
import FullPageLoader from "../components/full-page-loader";
|
||||
import { withSession } from "../components/user-provider";
|
||||
import { withAuthIfRequired, withSessionSsr } from "../utils/auth";
|
||||
import { trpc } from "../utils/trpc";
|
||||
import { withPageTranslations } from "../utils/with-page-translations";
|
||||
|
||||
const Demo: NextPage = () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { composeGetServerSideProps } from "@rallly/backend/next";
|
||||
import { GetServerSideProps } from "next";
|
||||
|
||||
import Home from "@/components/home";
|
||||
import { composeGetServerSideProps } from "@/utils/auth";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
export default function Page() {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { withSessionSsr } from "@rallly/backend/next";
|
||||
import { GetServerSideProps, NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -8,7 +9,6 @@ import { AuthLayout } from "@/components/auth/auth-layout";
|
|||
import { LoginForm } from "@/components/auth/login-form";
|
||||
import { useUser, withSession } from "@/components/user-provider";
|
||||
|
||||
import { withSessionSsr } from "../utils/auth";
|
||||
import { withPageTranslations } from "../utils/with-page-translations";
|
||||
|
||||
const Page: NextPage<{ referer: string | null }> = () => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { withSessionSsr } from "@rallly/backend/next";
|
||||
import { NextPage } from "next";
|
||||
|
||||
import { withSessionSsr } from "../utils/auth";
|
||||
|
||||
const Page: NextPage = () => {
|
||||
return null;
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
@ -6,7 +7,6 @@ import CreatePoll from "@/components/create-poll";
|
|||
|
||||
import StandardLayout from "../components/layouts/standard-layout";
|
||||
import { NextPageWithLayout } from "../types";
|
||||
import { withAuthIfRequired, withSessionSsr } from "../utils/auth";
|
||||
import { withPageTranslations } from "../utils/with-page-translations";
|
||||
|
||||
const Page: NextPageWithLayout = () => {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { withSessionSsr } from "@rallly/backend/next";
|
||||
import { decryptToken } from "@rallly/backend/session";
|
||||
import { GetServerSideProps } from "next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
@ -7,8 +10,6 @@ import { ParticipantsProvider } from "@/components/participants-provider";
|
|||
import { Poll } from "@/components/poll";
|
||||
import { PollContextProvider } from "@/components/poll-context";
|
||||
import { UserProvider, useUser } from "@/components/user-provider";
|
||||
import { decryptToken, withSessionSsr } from "@/utils/auth";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
import { withPageTranslations } from "@/utils/with-page-translations";
|
||||
|
||||
import StandardLayout from "../../components/layouts/standard-layout";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { withAuth, withSessionSsr } from "@/utils/auth";
|
||||
import { withAuth, withSessionSsr } from "@rallly/backend/next";
|
||||
|
||||
import { getStandardLayout } from "../components/layouts/standard-layout";
|
||||
import { Profile } from "../components/profile";
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { withSessionSsr } from "@rallly/backend/next";
|
||||
import { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -6,7 +7,6 @@ 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 { withSessionSsr } from "../utils/auth";
|
||||
import { withPageTranslations } from "../utils/with-page-translations";
|
||||
|
||||
const Page: NextPage = () => {
|
||||
|
|
|
@ -1,244 +0,0 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import {
|
||||
IronSession,
|
||||
IronSessionOptions,
|
||||
sealData,
|
||||
unsealData,
|
||||
} from "iron-session";
|
||||
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
|
||||
import {
|
||||
GetServerSideProps,
|
||||
GetServerSidePropsContext,
|
||||
NextApiHandler,
|
||||
} from "next";
|
||||
|
||||
import { createSSGHelperFromContext } from "../server/context";
|
||||
import { randomid } from "./nanoid";
|
||||
|
||||
const sessionOptions: IronSessionOptions = {
|
||||
password: process.env.SECRET_PASSWORD,
|
||||
cookieName: "rallly-session",
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
ttl: 0, // basically forever
|
||||
};
|
||||
|
||||
export type RegistrationTokenPayload = {
|
||||
name: string;
|
||||
email: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type LoginTokenPayload = {
|
||||
userId: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type RegisteredUserSession = {
|
||||
isGuest: false;
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type GuestUserSession = {
|
||||
isGuest: true;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type DisableNotificationsPayload = {
|
||||
pollId: string;
|
||||
watcherId: number;
|
||||
};
|
||||
|
||||
export type UserSession = GuestUserSession | RegisteredUserSession;
|
||||
|
||||
const setUser = async (session: IronSession) => {
|
||||
if (!session.user) {
|
||||
session.user = await createGuestUser();
|
||||
await session.save();
|
||||
}
|
||||
|
||||
if (!session.user.isGuest) {
|
||||
// Check registered user still exists
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
session.user = await createGuestUser();
|
||||
await session.save();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function withSessionRoute(handler: NextApiHandler) {
|
||||
return withIronSessionApiRoute(async (req, res) => {
|
||||
await setUser(req.session);
|
||||
return await handler(req, res);
|
||||
}, sessionOptions);
|
||||
}
|
||||
|
||||
export const composeGetServerSideProps = (
|
||||
...fns: GetServerSideProps[]
|
||||
): GetServerSideProps => {
|
||||
return async (ctx) => {
|
||||
const res = { props: {} };
|
||||
for (const getServerSideProps of fns) {
|
||||
const fnRes = await getServerSideProps(ctx);
|
||||
|
||||
if ("props" in fnRes) {
|
||||
res.props = {
|
||||
...res.props,
|
||||
...fnRes.props,
|
||||
};
|
||||
} else {
|
||||
return fnRes;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Require user to be logged in
|
||||
* @returns
|
||||
*/
|
||||
export const withAuth: GetServerSideProps = async (ctx) => {
|
||||
if (!ctx.req.session.user || ctx.req.session.user.isGuest) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
/**
|
||||
* Require user to be logged in if AUTH_REQUIRED is true
|
||||
* @returns
|
||||
*/
|
||||
export const withAuthIfRequired: GetServerSideProps = async (ctx) => {
|
||||
if (process.env.AUTH_REQUIRED === "true") {
|
||||
return await withAuth(ctx);
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
export function withSessionSsr(
|
||||
handler: GetServerSideProps | GetServerSideProps[],
|
||||
options?: {
|
||||
onPrefetch?: (
|
||||
ssg: Awaited<ReturnType<typeof createSSGHelperFromContext>>,
|
||||
ctx: GetServerSidePropsContext,
|
||||
) => Promise<void>;
|
||||
},
|
||||
): GetServerSideProps {
|
||||
const composedHandler = Array.isArray(handler)
|
||||
? composeGetServerSideProps(...handler)
|
||||
: handler;
|
||||
|
||||
return withIronSessionSsr(async (ctx) => {
|
||||
const ssg = await createSSGHelperFromContext(ctx);
|
||||
await ssg.whoami.get.prefetch(); // always prefetch user
|
||||
if (options?.onPrefetch) {
|
||||
try {
|
||||
await options.onPrefetch(ssg, ctx);
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
const res = await composedHandler(ctx);
|
||||
if ("props" in res) {
|
||||
return {
|
||||
...res,
|
||||
props: {
|
||||
...res.props,
|
||||
trpcState: ssg.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}, sessionOptions);
|
||||
}
|
||||
|
||||
export const decryptToken = async <P extends Record<string, unknown>>(
|
||||
token: string,
|
||||
): Promise<P | null> => {
|
||||
const payload = await unsealData(token, {
|
||||
password: sessionOptions.password,
|
||||
});
|
||||
if (Object.keys(payload).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload as P;
|
||||
};
|
||||
|
||||
export const createToken = async <T extends Record<string, unknown>>(
|
||||
payload: T,
|
||||
options?: {
|
||||
ttl?: number;
|
||||
},
|
||||
) => {
|
||||
return await sealData(payload, {
|
||||
password: sessionOptions.password,
|
||||
ttl: options?.ttl ?? 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
|
||||
export const createGuestUser = async (): Promise<{
|
||||
isGuest: true;
|
||||
id: string;
|
||||
}> => {
|
||||
return {
|
||||
id: `user-${await randomid()}`,
|
||||
isGuest: true,
|
||||
};
|
||||
};
|
||||
|
||||
// assigns participants and comments created by guests to a user
|
||||
// we could have multiple guests because a login might be triggered from one device
|
||||
// and opened in another one.
|
||||
export const mergeGuestsIntoUser = async (
|
||||
userId: string,
|
||||
guestIds: string[],
|
||||
) => {
|
||||
await prisma.participant.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: guestIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.comment.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: guestIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const getCurrentUser = async (
|
||||
session: IronSession,
|
||||
): Promise<{ isGuest: boolean; id: string }> => {
|
||||
await setUser(session);
|
||||
|
||||
return session.user;
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { trpc } from "@/utils/trpc";
|
||||
|
||||
export const usePollByAdmin = () => {
|
||||
const router = useRouter();
|
||||
const adminUrlId = router.query.urlId as string;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"~/*": ["./*"]
|
||||
"~/*": ["public/*"]
|
||||
},
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
|
|
1
packages/backend/index.ts
Normal file
1
packages/backend/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./next/trpc/client";
|
8
packages/backend/next/edge.ts
Normal file
8
packages/backend/next/edge.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { getIronSession } from "iron-session/edge";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
import { sessionConfig } from "../session-config";
|
||||
|
||||
export const getSession = async (req: NextRequest, res: NextResponse) => {
|
||||
return getIronSession(req, res, sessionConfig);
|
||||
};
|
2
packages/backend/next/index.ts
Normal file
2
packages/backend/next/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from "./session";
|
||||
export * from "./utils";
|
89
packages/backend/next/session.ts
Normal file
89
packages/backend/next/session.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { withIronSessionApiRoute, withIronSessionSsr } from "iron-session/next";
|
||||
import {
|
||||
GetServerSideProps,
|
||||
GetServerSidePropsContext,
|
||||
NextApiHandler,
|
||||
} from "next";
|
||||
|
||||
import { sessionConfig } from "../session-config";
|
||||
import { createSSGHelperFromContext } from "../trpc/context";
|
||||
import { composeGetServerSideProps } from "./utils";
|
||||
|
||||
export function withSessionRoute(handler: NextApiHandler) {
|
||||
return withIronSessionApiRoute(handler, sessionConfig);
|
||||
}
|
||||
|
||||
export function withSessionSsr(
|
||||
handler: GetServerSideProps | GetServerSideProps[],
|
||||
options?: {
|
||||
onPrefetch?: (
|
||||
ssg: Awaited<ReturnType<typeof createSSGHelperFromContext>>,
|
||||
ctx: GetServerSidePropsContext,
|
||||
) => Promise<void>;
|
||||
},
|
||||
): GetServerSideProps {
|
||||
const composedHandler = Array.isArray(handler)
|
||||
? composeGetServerSideProps(...handler)
|
||||
: handler;
|
||||
|
||||
return withIronSessionSsr(async (ctx) => {
|
||||
const ssg = await createSSGHelperFromContext(ctx);
|
||||
await ssg.whoami.get.prefetch(); // always prefetch user
|
||||
if (options?.onPrefetch) {
|
||||
try {
|
||||
await options.onPrefetch(ssg, ctx);
|
||||
} catch {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
const res = await composedHandler(ctx);
|
||||
if ("props" in res) {
|
||||
return {
|
||||
...res,
|
||||
props: {
|
||||
...res.props,
|
||||
trpcState: ssg.dehydrate(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return res;
|
||||
}, sessionConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Require user to be logged in
|
||||
* @returns
|
||||
*/
|
||||
export const withAuth: GetServerSideProps = async (ctx) => {
|
||||
if (!ctx.req.session.user || ctx.req.session.user.isGuest) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { props: {} };
|
||||
};
|
||||
|
||||
/**
|
||||
* Require user to be logged in if AUTH_REQUIRED is true
|
||||
* @returns
|
||||
*/
|
||||
export const withAuthIfRequired: GetServerSideProps = async (ctx) => {
|
||||
if (process.env.AUTH_REQUIRED === "true") {
|
||||
if (!ctx.req.session.user || ctx.req.session.user.isGuest) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return { props: {} };
|
||||
};
|
|
@ -4,7 +4,9 @@ import { createTRPCNext } from "@trpc/next";
|
|||
import toast from "react-hot-toast";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { AppRouter } from "../server/routers/_app";
|
||||
import { AppRouter } from "../../trpc/routers";
|
||||
|
||||
export * from "../../trpc/types";
|
||||
|
||||
export const trpc = createTRPCNext<AppRouter>({
|
||||
unstable_overrides: {
|
12
packages/backend/next/trpc/server.ts
Normal file
12
packages/backend/next/trpc/server.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
|
||||
import { createContext } from "../../trpc/context";
|
||||
import { appRouter } from "../../trpc/routers";
|
||||
import { withSessionRoute } from "../session";
|
||||
|
||||
export const trpcNextApiHandler = withSessionRoute(
|
||||
trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
}),
|
||||
);
|
23
packages/backend/next/utils.ts
Normal file
23
packages/backend/next/utils.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { GetServerSideProps } from "next";
|
||||
|
||||
export function composeGetServerSideProps(
|
||||
...fns: GetServerSideProps[]
|
||||
): GetServerSideProps {
|
||||
return async (ctx) => {
|
||||
const res = { props: {} };
|
||||
for (const getServerSideProps of fns) {
|
||||
const fnRes = await getServerSideProps(ctx);
|
||||
|
||||
if ("props" in fnRes) {
|
||||
res.props = {
|
||||
...res.props,
|
||||
...fnRes.props,
|
||||
};
|
||||
} else {
|
||||
return fnRes;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
}
|
20
packages/backend/package.json
Normal file
20
packages/backend/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@rallly/backend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"dependencies": {
|
||||
"@rallly/database": "*",
|
||||
"@rallly/emails": "*",
|
||||
"@rallly/utils": "*",
|
||||
"@tanstack/react-query": "^4.22.0",
|
||||
"@trpc/client": "^10.13.0",
|
||||
"@trpc/next": "^10.13.0",
|
||||
"@trpc/react-query": "^10.13.0",
|
||||
"@trpc/server": "^10.13.0",
|
||||
"iron-session": "^6.3.1",
|
||||
"next": "^13.2.4",
|
||||
"superjson": "^1.12.2"
|
||||
}
|
||||
}
|
8
packages/backend/session-config.ts
Normal file
8
packages/backend/session-config.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const sessionConfig = {
|
||||
password: process.env.SECRET_PASSWORD ?? "",
|
||||
cookieName: "rallly-session",
|
||||
cookieOptions: {
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
ttl: 60 * 60 * 24 * 30, // 30 days
|
||||
};
|
36
packages/backend/session.ts
Normal file
36
packages/backend/session.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { sealData, unsealData } from "iron-session";
|
||||
|
||||
import { sessionConfig } from "./session-config";
|
||||
|
||||
type UserSessionData = { id: string; isGuest: boolean };
|
||||
|
||||
declare module "iron-session" {
|
||||
export interface IronSessionData {
|
||||
user: UserSessionData;
|
||||
}
|
||||
}
|
||||
|
||||
export const decryptToken = async <P = UserSessionData>(
|
||||
token: string,
|
||||
): Promise<P | null> => {
|
||||
const payload = await unsealData(token, {
|
||||
password: sessionConfig.password,
|
||||
});
|
||||
if (Object.keys(payload).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload as P;
|
||||
};
|
||||
|
||||
export const createToken = async <T = UserSessionData>(
|
||||
payload: T,
|
||||
options?: {
|
||||
ttl?: number;
|
||||
},
|
||||
) => {
|
||||
return await sealData(payload, {
|
||||
password: sessionConfig.password,
|
||||
ttl: options?.ttl ?? 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
|
@ -4,14 +4,21 @@ import * as trpcNext from "@trpc/server/adapters/next";
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { getCurrentUser } from "../utils/auth";
|
||||
import { appRouter } from "./routers/_app";
|
||||
import { randomid } from "../utils/nanoid";
|
||||
import { appRouter } from "./routers";
|
||||
|
||||
export async function createContext(
|
||||
opts: trpcNext.CreateNextContextOptions | GetServerSidePropsContext,
|
||||
) {
|
||||
const user = await getCurrentUser(opts.req.session);
|
||||
|
||||
let user = opts.req.session.user;
|
||||
if (!user) {
|
||||
user = {
|
||||
id: `user-${randomid()}`,
|
||||
isGuest: true,
|
||||
};
|
||||
opts.req.session.user = user;
|
||||
await opts.req.session.save();
|
||||
}
|
||||
return { user, session: opts.req.session };
|
||||
}
|
||||
|
|
@ -1,19 +1,40 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { sendEmail } from "@rallly/emails";
|
||||
import { absoluteUrl } from "@rallly/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
|
||||
import {
|
||||
createToken,
|
||||
decryptToken,
|
||||
LoginTokenPayload,
|
||||
mergeGuestsIntoUser,
|
||||
RegistrationTokenPayload,
|
||||
} from "../../utils/auth";
|
||||
import { createToken, decryptToken } from "../../session";
|
||||
import { generateOtp } from "../../utils/nanoid";
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
import { LoginTokenPayload, RegistrationTokenPayload } from "../types";
|
||||
|
||||
// assigns participants and comments created by guests to a user
|
||||
// we could have multiple guests because a login might be triggered from one device
|
||||
// and opened in another one.
|
||||
const mergeGuestsIntoUser = async (userId: string, guestIds: string[]) => {
|
||||
await prisma.participant.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: guestIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.comment.updateMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: guestIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isEmailBlocked = (email: string) => {
|
||||
if (process.env.ALLOWED_EMAILS) {
|
|
@ -2,7 +2,7 @@ import { prisma } from "@rallly/database";
|
|||
import { sendRawEmail } from "@rallly/emails";
|
||||
import { z } from "zod";
|
||||
|
||||
import { publicProcedure, router } from "@/server/trpc";
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
|
||||
export const feedback = router({
|
||||
send: publicProcedure
|
||||
|
@ -27,7 +27,7 @@ export const feedback = router({
|
|||
to: process.env.NEXT_PUBLIC_FEEDBACK_EMAIL,
|
||||
from: {
|
||||
name: "Rallly Feedback Form",
|
||||
address: process.env.SUPPORT_EMAIL,
|
||||
address: process.env.SUPPORT_EMAIL ?? "",
|
||||
},
|
||||
subject: "Feedback",
|
||||
replyTo,
|
|
@ -1,10 +1,10 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { sendEmail } from "@rallly/emails";
|
||||
import { absoluteUrl } from "@rallly/utils";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "../../utils/absolute-url";
|
||||
import { nanoid } from "../../utils/nanoid";
|
||||
import { possiblyPublicProcedure, publicProcedure, router } from "../trpc";
|
||||
import { comments } from "./polls/comments";
|
|
@ -1,11 +1,11 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { sendEmail } from "@rallly/emails";
|
||||
import { absoluteUrl } from "@rallly/utils";
|
||||
import { z } from "zod";
|
||||
|
||||
import { absoluteUrl } from "@/utils/absolute-url";
|
||||
import { createToken, DisableNotificationsPayload } from "@/utils/auth";
|
||||
|
||||
import { createToken } from "../../../session";
|
||||
import { publicProcedure, router } from "../../trpc";
|
||||
import { DisableNotificationsPayload } from "../../types";
|
||||
|
||||
export const comments = router({
|
||||
list: publicProcedure
|
|
@ -4,8 +4,9 @@ import { absoluteUrl } from "@rallly/utils";
|
|||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createToken, DisableNotificationsPayload } from "../../../utils/auth";
|
||||
import { createToken } from "../../../session";
|
||||
import { publicProcedure, router } from "../../trpc";
|
||||
import { DisableNotificationsPayload } from "../../types";
|
||||
|
||||
export const participants = router({
|
||||
list: publicProcedure
|
|
@ -1,26 +1,13 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { IronSessionData } from "iron-session";
|
||||
import { z } from "zod";
|
||||
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
|
||||
const requireUser = (user: IronSessionData["user"]) => {
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Tried to access user route without a session",
|
||||
});
|
||||
}
|
||||
return user;
|
||||
};
|
||||
|
||||
export const user = router({
|
||||
getPolls: publicProcedure.query(async ({ ctx }) => {
|
||||
const user = requireUser(ctx.session.user);
|
||||
const userPolls = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: user.id,
|
||||
id: ctx.user.id,
|
||||
},
|
||||
select: {
|
||||
polls: {
|
|
@ -1,7 +1,7 @@
|
|||
import { prisma } from "@rallly/database";
|
||||
|
||||
import { createGuestUser, UserSession } from "../../utils/auth";
|
||||
import { publicProcedure, router } from "../trpc";
|
||||
import { UserSession } from "../types";
|
||||
|
||||
export const whoami = router({
|
||||
get: publicProcedure.query(async ({ ctx }): Promise<UserSession> => {
|
||||
|
@ -15,11 +15,8 @@ export const whoami = router({
|
|||
});
|
||||
|
||||
if (user === null) {
|
||||
const guestUser = await createGuestUser();
|
||||
ctx.session.user = guestUser;
|
||||
await ctx.session.save();
|
||||
|
||||
return guestUser;
|
||||
ctx.session.destroy();
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
return { isGuest: false, ...user };
|
|
@ -17,7 +17,7 @@ export const publicProcedure = t.procedure;
|
|||
export const middleware = t.middleware;
|
||||
|
||||
const checkAuthIfRequired = middleware(async ({ ctx, next }) => {
|
||||
if (process.env.AUTH_REQUIRED === "true" && ctx.session.user.isGuest) {
|
||||
if (process.env.AUTH_REQUIRED === "true" && ctx.session) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required" });
|
||||
}
|
||||
return next();
|
29
packages/backend/trpc/types.ts
Normal file
29
packages/backend/trpc/types.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
export type RegistrationTokenPayload = {
|
||||
name: string;
|
||||
email: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type LoginTokenPayload = {
|
||||
userId: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type DisableNotificationsPayload = {
|
||||
pollId: string;
|
||||
watcherId: number;
|
||||
};
|
||||
|
||||
export type RegisteredUserSession = {
|
||||
isGuest: false;
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type GuestUserSession = {
|
||||
isGuest: true;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type UserSession = GuestUserSession | RegisteredUserSession;
|
6
packages/backend/tsconfig.json
Normal file
6
packages/backend/tsconfig.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"extends": "@rallly/tsconfig/react-library.json",
|
||||
"include": ["**/*.ts", "**/*.tsx", "iron-session.d.t.s"],
|
||||
|
||||
"exclude": ["node_modules"]
|
||||
}
|
13
packages/backend/utils/nanoid.ts
Normal file
13
packages/backend/utils/nanoid.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { customAlphabet } from "nanoid/async";
|
||||
|
||||
export const nanoid = customAlphabet(
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
|
||||
12,
|
||||
);
|
||||
|
||||
export const randomid = customAlphabet(
|
||||
"0123456789abcdefghijklmnopqrstuvwxyz",
|
||||
12,
|
||||
);
|
||||
|
||||
export const generateOtp = customAlphabet("0123456789", 6);
|
99
packages/tsconfig/environment.d.ts
vendored
Normal file
99
packages/tsconfig/environment.d.ts
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* Full database connection string
|
||||
*/
|
||||
DATABASE_URL: string;
|
||||
/**
|
||||
* "development" or "production"
|
||||
*/
|
||||
NODE_ENV: "development" | "production";
|
||||
/**
|
||||
* Set to "true" to take users straight to app instead of landing page
|
||||
*/
|
||||
DISABLE_LANDING_PAGE?: string;
|
||||
/**
|
||||
* Must be 32 characters long
|
||||
*/
|
||||
SECRET_PASSWORD: string;
|
||||
/**
|
||||
* "1" to turn on maintenance mode
|
||||
*/
|
||||
NEXT_PUBLIC_MAINTENANCE_MODE?: string;
|
||||
/**
|
||||
* Posthog API key
|
||||
*/
|
||||
NEXT_PUBLIC_POSTHOG_API_KEY?: string;
|
||||
/**
|
||||
* Posthog API host
|
||||
*/
|
||||
NEXT_PUBLIC_POSTHOG_API_HOST?: string;
|
||||
/**
|
||||
* Crisp website ID
|
||||
*/
|
||||
NEXT_PUBLIC_CRISP_WEBSITE_ID?: string;
|
||||
/**
|
||||
* When defined users will be able to send feedback to this email address
|
||||
*/
|
||||
NEXT_PUBLIC_FEEDBACK_EMAIL?: string;
|
||||
/**
|
||||
* Users of your instance will see this as their support email
|
||||
*/
|
||||
SUPPORT_EMAIL: string;
|
||||
/**
|
||||
* Host address of the SMTP server
|
||||
*/
|
||||
SMTP_HOST: string;
|
||||
/**
|
||||
* Email address or user if authentication is required
|
||||
*/
|
||||
SMTP_USER: string;
|
||||
/**
|
||||
* Password if authentication is required
|
||||
*/
|
||||
SMTP_PWD: string;
|
||||
/**
|
||||
* "true" to use SSL
|
||||
*/
|
||||
SMTP_SECURE: string;
|
||||
/**
|
||||
* Port number of the SMTP server
|
||||
*/
|
||||
SMTP_PORT: string;
|
||||
/**
|
||||
* Comma separated list of email addresses that are allowed to register and login.
|
||||
* If not set, all emails are allowed. Wildcard characters are supported.
|
||||
*
|
||||
* Example: "user@example.com, *@example.com, *@*.example.com"
|
||||
*/
|
||||
ALLOWED_EMAILS?: string;
|
||||
/**
|
||||
* "true" to require authentication for creating new polls and accessing admin pages
|
||||
*/
|
||||
AUTH_REQUIRED?: string;
|
||||
/**
|
||||
* Determines what email provider to use. "smtp" or "ses"
|
||||
*/
|
||||
EMAIL_PROVIDER?: "smtp" | "ses";
|
||||
/**
|
||||
* AWS access key ID
|
||||
*/
|
||||
AWS_ACCESS_KEY_ID?: string;
|
||||
/**
|
||||
* AWS secret access key
|
||||
*/
|
||||
AWS_SECRET_ACCESS_KEY?: string;
|
||||
/**
|
||||
* AWS region
|
||||
*/
|
||||
AWS_REGION?: string;
|
||||
/**
|
||||
* The app version just for reference
|
||||
*/
|
||||
NEXT_PUBLIC_APP_VERSION?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
Loading…
Add table
Add a link
Reference in a new issue