♻️ Create backend package (#643)

This commit is contained in:
Luke Vella 2023-04-03 10:41:19 +01:00 committed by GitHub
parent 7fc08c6736
commit 05fe2edaea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 476 additions and 391 deletions

View file

@ -1,9 +1,9 @@
import "react-i18next"; import "react-i18next";
import app from "~/public/locales/en/app.json"; import app from "../public/locales/en/app.json";
import common from "~/public/locales/en/common.json"; import common from "../public/locales/en/common.json";
import errors from "~/public/locales/en/errors.json"; import errors from "../public/locales/en/errors.json";
import homepage from "~/public/locales/en/homepage.json"; import homepage from "../public/locales/en/homepage.json";
interface I18nNamespaces { interface I18nNamespaces {
homepage: typeof homepage; homepage: typeof homepage;

View file

@ -1,10 +0,0 @@
import "iron-session";
declare module "iron-session" {
export interface IronSessionData {
user: {
id: string;
isGuest: boolean;
};
}
}

View file

@ -13,7 +13,7 @@ const nextConfig = {
i18n: i18n, i18n: i18n,
productionBrowserSourceMaps: true, productionBrowserSourceMaps: true,
output: "standalone", output: "standalone",
transpilePackages: ["@rallly/emails", "@rallly/database"], transpilePackages: ["@rallly/backend"],
webpack(config) { webpack(config) {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,

View file

@ -16,21 +16,16 @@
"docker:start": "./scripts/docker-start.sh" "docker:start": "./scripts/docker-start.sh"
}, },
"dependencies": { "dependencies": {
"@rallly/backend": "*",
"@rallly/database": "*",
"@rallly/tailwind-config": "*",
"@floating-ui/react-dom-interactions": "^0.13.3", "@floating-ui/react-dom-interactions": "^0.13.3",
"@headlessui/react": "^1.7.7", "@headlessui/react": "^1.7.7",
"@next/bundle-analyzer": "^12.3.4", "@next/bundle-analyzer": "^12.3.4",
"@radix-ui/react-popover": "^1.0.3", "@radix-ui/react-popover": "^1.0.3",
"@rallly/database": "*",
"@rallly/emails": "*",
"@rallly/tailwind-config": "*",
"@sentry/nextjs": "^7.33.0", "@sentry/nextjs": "^7.33.0",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@tailwindcss/typography": "^0.5.9", "@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", "@vercel/analytics": "^0.1.8",
"accept-language-parser": "^1.5.0", "accept-language-parser": "^1.5.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
@ -42,7 +37,6 @@
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nanoid": "^4.0.0", "nanoid": "^4.0.0",
"next": "^13.2.4",
"next-i18next": "^13.0.3", "next-i18next": "^13.0.3",
"next-seo": "^5.15.0", "next-seo": "^5.15.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",

View file

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Logo from "~/public/logo.svg"; import Logo from "~//logo.svg";
export const AuthLayout = ({ children }: { children?: React.ReactNode }) => { export const AuthLayout = ({ children }: { children?: React.ReactNode }) => {
return ( return (

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import Link from "next/link"; import Link from "next/link";
import { Trans, useTranslation } from "next-i18next"; import { Trans, useTranslation } from "next-i18next";
import React from "react"; import React from "react";
@ -6,7 +7,6 @@ import { useForm } from "react-hook-form";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { requiredString, validEmail } from "../../utils/form-validation"; import { requiredString, validEmail } from "../../utils/form-validation";
import { trpc } from "../../utils/trpc";
import { Button } from "../button"; import { Button } from "../button";
import { TextInput } from "../text-input"; import { TextInput } from "../text-input";

View file

@ -1,10 +1,10 @@
import { trpc } from "@rallly/backend";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "../utils/trpc";
import { Button } from "./button"; import { Button } from "./button";
import { import {
NewEventData, NewEventData,

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import clsx from "clsx"; import clsx from "clsx";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
@ -7,7 +8,6 @@ import { usePostHog } from "@/utils/posthog";
import { useDayjs } from "../../utils/dayjs"; import { useDayjs } from "../../utils/dayjs";
import { requiredString } from "../../utils/form-validation"; import { requiredString } from "../../utils/form-validation";
import { trpc } from "../../utils/trpc";
import { Button } from "../button"; import { Button } from "../button";
import CompactButton from "../compact-button"; import CompactButton from "../compact-button";
import Dropdown, { DropdownItem } from "../dropdown"; import Dropdown, { DropdownItem } from "../dropdown";

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import Link from "next/link"; import Link from "next/link";
import { Trans, useTranslation } from "next-i18next"; import { Trans, useTranslation } from "next-i18next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -8,7 +9,6 @@ import Speakerphone from "@/components/icons/speakerphone.svg";
import { Logo } from "@/components/logo"; import { Logo } from "@/components/logo";
import { useModalState } from "@/components/modal/use-modal"; import { useModalState } from "@/components/modal/use-modal";
import Tooltip from "@/components/tooltip"; import Tooltip from "@/components/tooltip";
import { trpc } from "@/utils/trpc";
const FeedbackForm = (props: { onClose: () => void }) => { const FeedbackForm = (props: { onClose: () => void }) => {
const { t } = useTranslation("app"); const { t } = useTranslation("app");

View file

@ -7,7 +7,7 @@ import * as React from "react";
import DotsVertical from "@/components/icons/dots-vertical.svg"; import DotsVertical from "@/components/icons/dots-vertical.svg";
import Github from "@/components/icons/github.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 { Popover, PopoverContent, PopoverTrigger } from "../popover";
import Footer from "./page-layout/footer"; import Footer from "./page-layout/footer";

View file

@ -7,10 +7,10 @@ import Discord from "@/components/icons/discord.svg";
import Star from "@/components/icons/star.svg"; import Star from "@/components/icons/star.svg";
import Translate from "@/components/icons/translate.svg"; import Translate from "@/components/icons/translate.svg";
import Twitter from "@/components/icons/twitter.svg"; import Twitter from "@/components/icons/twitter.svg";
import DigitalOcean from "~/public/digitalocean.svg"; import DigitalOcean from "~//digitalocean.svg";
import Logo from "~/public/logo.svg"; import Logo from "~//logo.svg";
import Sentry from "~/public/sentry.svg"; import Sentry from "~//sentry.svg";
import Vercel from "~/public/vercel-logotype-dark.svg"; import Vercel from "~//vercel-logotype-dark.svg";
import { LanguageSelect } from "../../poll/language-selector"; import { LanguageSelect } from "../../poll/language-selector";

View file

@ -2,6 +2,7 @@ import { domMax, LazyMotion } from "framer-motion";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import { Toaster } from "react-hot-toast";
import { DayjsProvider } from "@/utils/dayjs"; import { DayjsProvider } from "@/utils/dayjs";
@ -34,6 +35,7 @@ const StandardLayout: React.FunctionComponent<{
}> = ({ children, ...rest }) => { }> = ({ children, ...rest }) => {
return ( return (
<LazyMotion features={domMax}> <LazyMotion features={domMax}>
<Toaster />
<UserProvider> <UserProvider>
<DayjsProvider> <DayjsProvider>
<ModalProvider> <ModalProvider>

View file

@ -1,6 +1,6 @@
import Link from "next/link"; import Link from "next/link";
import Logo from "~/public/logo.svg"; import Logo from "~//logo.svg";
export const HomeLink = (props: { className?: string }) => { export const HomeLink = (props: { className?: string }) => {
return ( return (

View file

@ -23,7 +23,7 @@ import OpenBeta from "../../open-beta-modal";
import { UserDropdown } from "./user-dropdown"; import { UserDropdown } from "./user-dropdown";
export const MobileNavigation = (props: { className?: string }) => { export const MobileNavigation = (props: { className?: string }) => {
const { user, isUpdating } = useUser(); const { user } = useUser();
const { t } = useTranslation(["common", "app"]); const { t } = useTranslation(["common", "app"]);
const [isPinned, setIsPinned] = React.useState(false); const [isPinned, setIsPinned] = React.useState(false);
@ -113,9 +113,6 @@ export const MobileNavigation = (props: { className?: string }) => {
data-testid="user" data-testid="user"
className={clsx( 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", "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"> <div className="relative shrink-0">

View file

@ -1,7 +1,8 @@
import Head from "next/head"; import Head from "next/head";
import Clock from "@/components/icons/clock.svg"; import Clock from "@/components/icons/clock.svg";
import Logo from "~/public/logo.svg";
import Logo from "../../public/logo.svg";
const Maintenance: React.FunctionComponent = () => { const Maintenance: React.FunctionComponent = () => {
return ( return (

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import { SubmitHandler, useForm } from "react-hook-form"; 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 { TextInput } from "@/components/text-input";
import { useFormValidation } from "@/utils/form-validation"; import { useFormValidation } from "@/utils/form-validation";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc";
import { Participant } from ".prisma/client"; import { Participant } from ".prisma/client";

View file

@ -1,8 +1,8 @@
import { trpc } from "@rallly/backend";
import { Participant, Vote, VoteType } from "@rallly/database"; import { Participant, Vote, VoteType } from "@rallly/database";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
import { trpc } from "../utils/trpc";
import FullPageLoader from "./full-page-loader"; import FullPageLoader from "./full-page-loader";
import { useRequiredContext } from "./use-required-context"; import { useRequiredContext } from "./use-required-context";

View file

@ -3,7 +3,8 @@ import Cookies from "js-cookie";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import ChevronDown from "@/components/icons/chevron-down.svg"; import ChevronDown from "@/components/icons/chevron-down.svg";
import languages from "~/languages.json";
import languages from "../../../languages.json";
export const LanguageSelect: React.FunctionComponent<{ export const LanguageSelect: React.FunctionComponent<{
className?: string; className?: string;

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import clsx from "clsx"; import clsx from "clsx";
import { Trans, useTranslation } from "next-i18next"; import { Trans, useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
@ -7,8 +8,6 @@ import { Button } from "@/components/button";
import Exclamation from "@/components/icons/exclamation.svg"; import Exclamation from "@/components/icons/exclamation.svg";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "../../../utils/trpc";
const confirmText = "delete-me"; const confirmText = "delete-me";
export const DeletePollForm: React.FunctionComponent<{ export const DeletePollForm: React.FunctionComponent<{

View file

@ -1,6 +1,7 @@
import { trpc } from "@rallly/backend";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "../../utils/trpc";
import { ParticipantForm } from "./types"; import { ParticipantForm } from "./types";
export const normalizeVotes = ( export const normalizeVotes = (

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import * as React from "react"; 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 BellCrossed from "@/components/icons/bell-crossed.svg";
import { useUser } from "@/components/user-provider"; import { useUser } from "@/components/user-provider";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "@/utils/trpc";
import { usePollByAdmin } from "@/utils/trpc/hooks"; import { usePollByAdmin } from "@/utils/trpc/hooks";
import { usePoll } from "../poll-context"; import { usePoll } from "../poll-context";

View file

@ -1,7 +1,6 @@
import { trpc } from "@rallly/backend";
import { useMount } from "react-use"; 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 * 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. * find polls that haven't been accessed for some time so that they can be deleted by house keeping.

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -12,7 +13,6 @@ import User from "@/components/icons/user.svg";
import Tooltip from "@/components/tooltip"; import Tooltip from "@/components/tooltip";
import { useDayjs } from "../utils/dayjs"; import { useDayjs } from "../utils/dayjs";
import { trpc } from "../utils/trpc";
import { EmptyState } from "./empty-state"; import { EmptyState } from "./empty-state";
import { UserDetails } from "./profile/user-details"; import { UserDetails } from "./profile/user-details";
import { useUser } from "./user-provider"; import { useUser } from "./user-provider";

View file

@ -1,3 +1,4 @@
import { trpc } from "@rallly/backend";
import { m } from "framer-motion"; import { m } from "framer-motion";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import * as React from "react"; import * as React from "react";
@ -6,7 +7,6 @@ import { useForm } from "react-hook-form";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { requiredString, validEmail } from "../../utils/form-validation"; import { requiredString, validEmail } from "../../utils/form-validation";
import { trpc } from "../../utils/trpc";
import { Button } from "../button"; import { Button } from "../button";
import { TextInput } from "../text-input"; import { TextInput } from "../text-input";

View file

@ -1,16 +1,15 @@
import { trpc, UserSession } from "@rallly/backend";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
import React from "react"; import React from "react";
import { UserSession } from "@/utils/auth";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { trpc } from "../utils/trpc";
import { useRequiredContext } from "./use-required-context"; import { useRequiredContext } from "./use-required-context";
export const UserContext = React.createContext<{ export const UserContext = React.createContext<{
user: UserSession & { shortName: string }; user: UserSession & { shortName: string };
refresh: () => void; refresh: () => void;
isUpdating: boolean;
logout: () => void; logout: () => void;
ownsObject: (obj: { userId: string | null }) => boolean; ownsObject: (obj: { userId: string | null }) => boolean;
} | null>(null); } | null>(null);
@ -55,13 +54,10 @@ export const UserProvider = (props: {
const queryClient = trpc.useContext(); const queryClient = trpc.useContext();
const { data: user } = trpc.whoami.get.useQuery(); const { data: user } = trpc.whoami.get.useQuery();
const [isUpdating, setIsUpdating] = React.useState(false); const router = useRouter();
const logout = trpc.whoami.destroy.useMutation({ const logout = trpc.whoami.destroy.useMutation({
onSuccess: async () => { onSuccess: async () => {
setIsUpdating(true); router.push("/logout");
await queryClient.whoami.invalidate();
setIsUpdating(false);
}, },
}); });
@ -91,7 +87,6 @@ export const UserProvider = (props: {
return ( return (
<UserContext.Provider <UserContext.Provider
value={{ value={{
isUpdating,
user: { ...user, shortName }, user: { ...user, shortName },
refresh: () => { refresh: () => {
return queryClient.whoami.invalidate(); return queryClient.whoami.invalidate();

View file

@ -25,8 +25,10 @@ const supportedLocales = [
"zh", "zh",
]; ];
export function middleware({ headers, cookies, nextUrl }: NextRequest) { export async function middleware(req: NextRequest) {
const { headers, cookies, nextUrl } = req;
const newUrl = nextUrl.clone(); const newUrl = nextUrl.clone();
const res = NextResponse.next();
// Check if locale is specified in cookie // Check if locale is specified in cookie
const localeCookie = cookies.get("NEXT_LOCALE"); 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 = { export const config = {

View file

@ -1,7 +1,8 @@
import "react-big-calendar/lib/css/react-big-calendar.css"; import "react-big-calendar/lib/css/react-big-calendar.css";
import "tailwindcss/tailwind.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 { inject } from "@vercel/analytics";
import { NextPage } from "next"; import { NextPage } from "next";
import { AppProps } from "next/app"; import { AppProps } from "next/app";
@ -12,15 +13,12 @@ import { DefaultSeo } from "next-seo";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react"; import { PostHogProvider } from "posthog-js/react";
import React from "react"; import React from "react";
import { Toaster } from "react-hot-toast";
import Maintenance from "@/components/maintenance"; import Maintenance from "@/components/maintenance";
import { useCrispChat } from "../components/crisp-chat"; import { useCrispChat } from "../components/crisp-chat";
import { NextPageWithLayout } from "../types"; import { NextPageWithLayout } from "../types";
import { absoluteUrl } from "../utils/absolute-url"; import { absoluteUrl } from "../utils/absolute-url";
import { UserSession } from "../utils/auth";
import { trpc } from "../utils/trpc";
const inter = Inter({ const inter = Inter({
subsets: ["latin"], 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" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=5, user-scalable=yes"
/> />
</Head> </Head>
<Toaster />
<style jsx global>{` <style jsx global>{`
html { html {
--font-inter: ${inter.style.fontFamily}; --font-inter: ${inter.style.fontFamily};

View file

@ -1,3 +1,4 @@
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
import { GetServerSideProps } from "next"; import { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
@ -7,7 +8,6 @@ import { getStandardLayout } from "@/components/layouts/standard-layout";
import { ParticipantsProvider } from "@/components/participants-provider"; import { ParticipantsProvider } from "@/components/participants-provider";
import { Poll } from "@/components/poll"; import { Poll } from "@/components/poll";
import { PollContextProvider } from "@/components/poll-context"; import { PollContextProvider } from "@/components/poll-context";
import { withAuthIfRequired, withSessionSsr } from "@/utils/auth";
import { usePollByAdmin } from "@/utils/trpc/hooks"; import { usePollByAdmin } from "@/utils/trpc/hooks";
import { withPageTranslations } from "@/utils/with-page-translations"; import { withPageTranslations } from "@/utils/with-page-translations";

View file

@ -1,8 +1,4 @@
import * as trpcNext from "@trpc/server/adapters/next"; import { trpcNextApiHandler } from "@rallly/backend/next/trpc/server";
import { createContext } from "../../../server/context";
import { appRouter } from "../../../server/routers/_app";
import { withSessionRoute } from "../../../utils/auth";
export const config = { export const config = {
api: { api: {
@ -10,9 +6,4 @@ export const config = {
}, },
}; };
// export API handler // export API handler
export default withSessionRoute( export default trpcNextApiHandler;
trpcNext.createNextApiHandler({
router: appRouter,
createContext,
}),
);

View file

@ -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 { prisma } from "@rallly/database";
import clsx from "clsx"; import clsx from "clsx";
import Link from "next/link"; import Link from "next/link";
@ -9,12 +15,6 @@ import { useMount } from "react-use";
import Bell from "@/components/icons/bell-crossed.svg"; import Bell from "@/components/icons/bell-crossed.svg";
import { AuthLayout } from "@/components/layouts/auth-layout"; import { AuthLayout } from "@/components/layouts/auth-layout";
import { Spinner } from "@/components/spinner"; import { Spinner } from "@/components/spinner";
import {
composeGetServerSideProps,
decryptToken,
DisableNotificationsPayload,
withSessionSsr,
} from "@/utils/auth";
import { usePostHog } from "@/utils/posthog"; import { usePostHog } from "@/utils/posthog";
import { withPageTranslations } from "@/utils/with-page-translations"; import { withPageTranslations } from "@/utils/with-page-translations";

View file

@ -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 { prisma } from "@rallly/database";
import clsx from "clsx"; import clsx from "clsx";
import { GetServerSideProps } from "next"; import { GetServerSideProps } from "next";
@ -9,12 +15,6 @@ import React from "react";
import CheckCircle from "@/components/icons/check-circle.svg"; import CheckCircle from "@/components/icons/check-circle.svg";
import { AuthLayout } from "@/components/layouts/auth-layout"; import { AuthLayout } from "@/components/layouts/auth-layout";
import { Spinner } from "@/components/spinner"; import { Spinner } from "@/components/spinner";
import {
composeGetServerSideProps,
decryptToken,
LoginTokenPayload,
withSessionSsr,
} from "@/utils/auth";
import { withPageTranslations } from "@/utils/with-page-translations"; import { withPageTranslations } from "@/utils/with-page-translations";
const defaultRedirectPath = "/profile"; const defaultRedirectPath = "/profile";

View file

@ -1,3 +1,5 @@
import { trpc } from "@rallly/backend";
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
import { NextPage } from "next"; import { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
@ -8,8 +10,6 @@ import { usePostHog } from "@/utils/posthog";
import FullPageLoader from "../components/full-page-loader"; import FullPageLoader from "../components/full-page-loader";
import { withSession } from "../components/user-provider"; import { withSession } from "../components/user-provider";
import { withAuthIfRequired, withSessionSsr } from "../utils/auth";
import { trpc } from "../utils/trpc";
import { withPageTranslations } from "../utils/with-page-translations"; import { withPageTranslations } from "../utils/with-page-translations";
const Demo: NextPage = () => { const Demo: NextPage = () => {

View file

@ -1,7 +1,7 @@
import { composeGetServerSideProps } from "@rallly/backend/next";
import { GetServerSideProps } from "next"; import { GetServerSideProps } from "next";
import Home from "@/components/home"; import Home from "@/components/home";
import { composeGetServerSideProps } from "@/utils/auth";
import { withPageTranslations } from "@/utils/with-page-translations"; import { withPageTranslations } from "@/utils/with-page-translations";
export default function Page() { export default function Page() {

View file

@ -1,3 +1,4 @@
import { withSessionSsr } from "@rallly/backend/next";
import { GetServerSideProps, NextPage } from "next"; import { GetServerSideProps, NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -8,7 +9,6 @@ import { AuthLayout } from "@/components/auth/auth-layout";
import { LoginForm } from "@/components/auth/login-form"; import { LoginForm } from "@/components/auth/login-form";
import { useUser, withSession } from "@/components/user-provider"; import { useUser, withSession } from "@/components/user-provider";
import { withSessionSsr } from "../utils/auth";
import { withPageTranslations } from "../utils/with-page-translations"; import { withPageTranslations } from "../utils/with-page-translations";
const Page: NextPage<{ referer: string | null }> = () => { const Page: NextPage<{ referer: string | null }> = () => {

View file

@ -1,7 +1,6 @@
import { withSessionSsr } from "@rallly/backend/next";
import { NextPage } from "next"; import { NextPage } from "next";
import { withSessionSsr } from "../utils/auth";
const Page: NextPage = () => { const Page: NextPage = () => {
return null; return null;
}; };

View file

@ -1,3 +1,4 @@
import { withAuthIfRequired, withSessionSsr } from "@rallly/backend/next";
import { GetServerSideProps } from "next"; import { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -6,7 +7,6 @@ import CreatePoll from "@/components/create-poll";
import StandardLayout from "../components/layouts/standard-layout"; import StandardLayout from "../components/layouts/standard-layout";
import { NextPageWithLayout } from "../types"; import { NextPageWithLayout } from "../types";
import { withAuthIfRequired, withSessionSsr } from "../utils/auth";
import { withPageTranslations } from "../utils/with-page-translations"; import { withPageTranslations } from "../utils/with-page-translations";
const Page: NextPageWithLayout = () => { const Page: NextPageWithLayout = () => {

View file

@ -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 { GetServerSideProps } from "next";
import Head from "next/head"; import Head from "next/head";
import Link from "next/link"; import Link from "next/link";
@ -7,8 +10,6 @@ import { ParticipantsProvider } from "@/components/participants-provider";
import { Poll } from "@/components/poll"; import { Poll } from "@/components/poll";
import { PollContextProvider } from "@/components/poll-context"; import { PollContextProvider } from "@/components/poll-context";
import { UserProvider, useUser } from "@/components/user-provider"; 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 { withPageTranslations } from "@/utils/with-page-translations";
import StandardLayout from "../../components/layouts/standard-layout"; import StandardLayout from "../../components/layouts/standard-layout";

View file

@ -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 { getStandardLayout } from "../components/layouts/standard-layout";
import { Profile } from "../components/profile"; import { Profile } from "../components/profile";

View file

@ -1,3 +1,4 @@
import { withSessionSsr } from "@rallly/backend/next";
import { NextPage } from "next"; import { NextPage } from "next";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -6,7 +7,6 @@ import { useTranslation } from "next-i18next";
import { AuthLayout } from "../components/auth/auth-layout"; import { AuthLayout } from "../components/auth/auth-layout";
import { RegisterForm } from "../components/auth/login-form"; import { RegisterForm } from "../components/auth/login-form";
import { withSession } from "../components/user-provider"; import { withSession } from "../components/user-provider";
import { withSessionSsr } from "../utils/auth";
import { withPageTranslations } from "../utils/with-page-translations"; import { withPageTranslations } from "../utils/with-page-translations";
const Page: NextPage = () => { const Page: NextPage = () => {

View file

@ -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;
};

View file

@ -1,7 +1,6 @@
import { trpc } from "@rallly/backend";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { trpc } from "@/utils/trpc";
export const usePollByAdmin = () => { export const usePollByAdmin = () => {
const router = useRouter(); const router = useRouter();
const adminUrlId = router.query.urlId as string; const adminUrlId = router.query.urlId as string;

View file

@ -3,7 +3,7 @@
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["src/*"],
"~/*": ["./*"] "~/*": ["public/*"]
}, },
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,

View file

@ -0,0 +1 @@
export * from "./next/trpc/client";

View 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);
};

View file

@ -0,0 +1,2 @@
export * from "./session";
export * from "./utils";

View 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: {} };
};

View file

@ -4,7 +4,9 @@ import { createTRPCNext } from "@trpc/next";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import superjson from "superjson"; import superjson from "superjson";
import { AppRouter } from "../server/routers/_app"; import { AppRouter } from "../../trpc/routers";
export * from "../../trpc/types";
export const trpc = createTRPCNext<AppRouter>({ export const trpc = createTRPCNext<AppRouter>({
unstable_overrides: { unstable_overrides: {

View 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,
}),
);

View 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;
};
}

View 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"
}
}

View 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
};

View 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
});
};

View file

@ -4,14 +4,21 @@ import * as trpcNext from "@trpc/server/adapters/next";
import { GetServerSidePropsContext } from "next"; import { GetServerSidePropsContext } from "next";
import superjson from "superjson"; import superjson from "superjson";
import { getCurrentUser } from "../utils/auth"; import { randomid } from "../utils/nanoid";
import { appRouter } from "./routers/_app"; import { appRouter } from "./routers";
export async function createContext( export async function createContext(
opts: trpcNext.CreateNextContextOptions | GetServerSidePropsContext, 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 }; return { user, session: opts.req.session };
} }

View file

@ -1,19 +1,40 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails"; import { sendEmail } from "@rallly/emails";
import { absoluteUrl } from "@rallly/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { absoluteUrl } from "@/utils/absolute-url"; import { createToken, decryptToken } from "../../session";
import {
createToken,
decryptToken,
LoginTokenPayload,
mergeGuestsIntoUser,
RegistrationTokenPayload,
} from "../../utils/auth";
import { generateOtp } from "../../utils/nanoid"; import { generateOtp } from "../../utils/nanoid";
import { publicProcedure, router } from "../trpc"; 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) => { const isEmailBlocked = (email: string) => {
if (process.env.ALLOWED_EMAILS) { if (process.env.ALLOWED_EMAILS) {

View file

@ -2,7 +2,7 @@ import { prisma } from "@rallly/database";
import { sendRawEmail } from "@rallly/emails"; import { sendRawEmail } from "@rallly/emails";
import { z } from "zod"; import { z } from "zod";
import { publicProcedure, router } from "@/server/trpc"; import { publicProcedure, router } from "../trpc";
export const feedback = router({ export const feedback = router({
send: publicProcedure send: publicProcedure
@ -27,7 +27,7 @@ export const feedback = router({
to: process.env.NEXT_PUBLIC_FEEDBACK_EMAIL, to: process.env.NEXT_PUBLIC_FEEDBACK_EMAIL,
from: { from: {
name: "Rallly Feedback Form", name: "Rallly Feedback Form",
address: process.env.SUPPORT_EMAIL, address: process.env.SUPPORT_EMAIL ?? "",
}, },
subject: "Feedback", subject: "Feedback",
replyTo, replyTo,

View file

@ -1,10 +1,10 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails"; import { sendEmail } from "@rallly/emails";
import { absoluteUrl } from "@rallly/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { z } from "zod"; import { z } from "zod";
import { absoluteUrl } from "../../utils/absolute-url";
import { nanoid } from "../../utils/nanoid"; import { nanoid } from "../../utils/nanoid";
import { possiblyPublicProcedure, publicProcedure, router } from "../trpc"; import { possiblyPublicProcedure, publicProcedure, router } from "../trpc";
import { comments } from "./polls/comments"; import { comments } from "./polls/comments";

View file

@ -1,11 +1,11 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { sendEmail } from "@rallly/emails"; import { sendEmail } from "@rallly/emails";
import { absoluteUrl } from "@rallly/utils";
import { z } from "zod"; import { z } from "zod";
import { absoluteUrl } from "@/utils/absolute-url"; import { createToken } from "../../../session";
import { createToken, DisableNotificationsPayload } from "@/utils/auth";
import { publicProcedure, router } from "../../trpc"; import { publicProcedure, router } from "../../trpc";
import { DisableNotificationsPayload } from "../../types";
export const comments = router({ export const comments = router({
list: publicProcedure list: publicProcedure

View file

@ -4,8 +4,9 @@ import { absoluteUrl } from "@rallly/utils";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { createToken, DisableNotificationsPayload } from "../../../utils/auth"; import { createToken } from "../../../session";
import { publicProcedure, router } from "../../trpc"; import { publicProcedure, router } from "../../trpc";
import { DisableNotificationsPayload } from "../../types";
export const participants = router({ export const participants = router({
list: publicProcedure list: publicProcedure

View file

@ -1,26 +1,13 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { TRPCError } from "@trpc/server";
import { IronSessionData } from "iron-session";
import { z } from "zod"; import { z } from "zod";
import { publicProcedure, router } from "../trpc"; 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({ export const user = router({
getPolls: publicProcedure.query(async ({ ctx }) => { getPolls: publicProcedure.query(async ({ ctx }) => {
const user = requireUser(ctx.session.user);
const userPolls = await prisma.user.findUnique({ const userPolls = await prisma.user.findUnique({
where: { where: {
id: user.id, id: ctx.user.id,
}, },
select: { select: {
polls: { polls: {

View file

@ -1,7 +1,7 @@
import { prisma } from "@rallly/database"; import { prisma } from "@rallly/database";
import { createGuestUser, UserSession } from "../../utils/auth";
import { publicProcedure, router } from "../trpc"; import { publicProcedure, router } from "../trpc";
import { UserSession } from "../types";
export const whoami = router({ export const whoami = router({
get: publicProcedure.query(async ({ ctx }): Promise<UserSession> => { get: publicProcedure.query(async ({ ctx }): Promise<UserSession> => {
@ -15,11 +15,8 @@ export const whoami = router({
}); });
if (user === null) { if (user === null) {
const guestUser = await createGuestUser(); ctx.session.destroy();
ctx.session.user = guestUser; throw new Error("User not found");
await ctx.session.save();
return guestUser;
} }
return { isGuest: false, ...user }; return { isGuest: false, ...user };

View file

@ -17,7 +17,7 @@ export const publicProcedure = t.procedure;
export const middleware = t.middleware; export const middleware = t.middleware;
const checkAuthIfRequired = middleware(async ({ ctx, next }) => { 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" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Login is required" });
} }
return next(); return next();

View 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;

View file

@ -0,0 +1,6 @@
{
"extends": "@rallly/tsconfig/react-library.json",
"include": ["**/*.ts", "**/*.tsx", "iron-session.d.t.s"],
"exclude": ["node_modules"]
}

View 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
View 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 {};