;
ownsObject: (obj: { userId: string | null }) => boolean;
} | null>(null);
@@ -46,66 +60,67 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
};
export const UserProvider = (props: { children?: React.ReactNode }) => {
+ const session = useSession();
+
+ const user = session.data?.user;
+
const { t } = useTranslation();
- const queryClient = trpc.useContext();
-
- const user = useWhoAmI();
- const { data: userPreferences } = trpc.userPreferences.get.useQuery();
+ React.useEffect(() => {
+ if (session.status === "unauthenticated") {
+ // Begin: Legacy token migration
+ const legacyToken = Cookies.get("legacy-token");
+ // It's important to remove the token from the cookies,
+ // otherwise when the user signs out.
+ if (legacyToken) {
+ Cookies.remove("legacy-token");
+ signIn("legacy-token", {
+ token: legacyToken,
+ });
+ return;
+ }
+ // End: Legacy token migration
+ signIn("guest");
+ }
+ }, [session.status]);
// TODO (Luke Vella) [2023-09-19]: Remove this when we have a better way to query for an active subscription
trpc.user.subscription.useQuery(undefined, {
enabled: !isSelfHosted,
});
- const name = user
- ? user.isGuest === false
- ? user.name
- : user.id.substring(0, 10)
- : t("guest");
-
- if (!user || userPreferences === undefined) {
+ if (!user || !session.data) {
return null;
}
return (
{
- return queryClient.whoami.invalidate();
+ user: {
+ id: user.id as string,
+ name: user.name ?? t("guest"),
+ email: user.email || null,
+ isGuest: user.email === null,
},
+ refresh: session.update,
ownsObject: ({ userId }) => {
return userId ? [user.id].includes(userId) : false;
},
}}
>
- {props.children}
+ {
+ await session.update(newPreferences);
+ }}
+ >
+ {props.children}
+
);
};
-
-type ParticipantOrComment = {
- userId: string | null;
-};
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-export const withSession = (
- component: React.ComponentType
,
-) => {
- const ComposedComponent: React.FunctionComponent
= (props: P) => {
- const Component = component;
- return (
-
-
-
- );
- };
- ComposedComponent.displayName = `withUser(${component.displayName})`;
- return ComposedComponent;
-};
-
-/**
- * @deprecated Stop using this function. All object
- */
-export const isUnclaimed = (obj: ParticipantOrComment) => !obj.userId;
diff --git a/apps/web/src/contexts/locale.tsx b/apps/web/src/contexts/locale.tsx
new file mode 100644
index 000000000..28fbc3c20
--- /dev/null
+++ b/apps/web/src/contexts/locale.tsx
@@ -0,0 +1,16 @@
+import { useRouter } from "next/router";
+import { useSession } from "next-auth/react";
+
+export const useLocale = () => {
+ const { locale, reload } = useRouter();
+ const session = useSession();
+
+ return {
+ locale: locale ?? "en",
+ updateLocale: (locale: string) => {
+ session.update({ locale });
+
+ reload();
+ },
+ };
+};
diff --git a/apps/web/src/contexts/preferences.tsx b/apps/web/src/contexts/preferences.tsx
index 7f6c6ceb8..e87eac79a 100644
--- a/apps/web/src/contexts/preferences.tsx
+++ b/apps/web/src/contexts/preferences.tsx
@@ -1,45 +1,50 @@
-import { trpc } from "@rallly/backend";
-import Cookies from "js-cookie";
-import { useTranslation } from "next-i18next";
+import { TimeFormat } from "@rallly/database";
+import React from "react";
+import { useSetState } from "react-use";
-import { getBrowserTimeZone } from "@/utils/date-time-utils";
-import { useDayjs } from "@/utils/dayjs";
-
-export const useSystemPreferences = () => {
- const { i18n } = useTranslation();
- const { timeFormat: localeTimeFormat, weekStart: localeTimeFormatWeekStart } =
- useDayjs();
-
- return {
- language: i18n.language, // this should be the value detected in
- timeFormat: localeTimeFormat,
- weekStart: localeTimeFormatWeekStart,
- timeZone: getBrowserTimeZone(),
- } as const;
+type Preferences = {
+ timeZone?: string | null;
+ locale?: string | null;
+ timeFormat?: TimeFormat | null;
+ weekStart?: number | null;
};
-export const updateLanguage = (language: string) => {
- Cookies.set("NEXT_LOCALE", language, {
- expires: 30,
- });
+type PreferencesContextValue = {
+ preferences: Preferences;
+ updatePreferences: (preferences: Partial) => void;
};
-export const useUserPreferences = () => {
- const { data, isFetched } = trpc.userPreferences.get.useQuery(undefined, {
- staleTime: Infinity,
- cacheTime: Infinity,
- });
+const PreferencesContext = React.createContext({
+ preferences: {},
+ updatePreferences: () => {},
+});
- const sytemPreferences = useSystemPreferences();
+export const PreferencesProvider = ({
+ children,
+ initialValue,
+ onUpdate,
+}: {
+ children?: React.ReactNode;
+ initialValue: Partial;
+ onUpdate?: (preferences: Partial) => void;
+}) => {
+ const [preferences, setPreferences] = useSetState(initialValue);
- // We decide the defaults by detecting the user's preferred locale from their browser
- // by looking at the accept-language header.
- if (isFetched) {
- return {
- automatic: data === null,
- timeFormat: data?.timeFormat ?? sytemPreferences.timeFormat,
- weekStart: data?.weekStart ?? sytemPreferences.weekStart,
- timeZone: data?.timeZone ?? sytemPreferences.timeZone,
- } as const;
- }
+ return (
+ {
+ setPreferences(newPreferences);
+ onUpdate?.(newPreferences);
+ },
+ }}
+ >
+ {children}
+
+ );
+};
+
+export const usePreferences = () => {
+ return React.useContext(PreferencesContext);
};
diff --git a/apps/web/src/contexts/whoami.ts b/apps/web/src/contexts/whoami.ts
deleted file mode 100644
index 33dbf7f90..000000000
--- a/apps/web/src/contexts/whoami.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { trpc } from "@rallly/backend";
-
-export const useWhoAmI = () => {
- const { data: whoAmI } = trpc.whoami.get.useQuery();
- return whoAmI;
-};
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
index 6e4f71357..36304080e 100644
--- a/apps/web/src/middleware.ts
+++ b/apps/web/src/middleware.ts
@@ -1,76 +1,70 @@
-import { getSession } from "@rallly/backend/next/edge";
import languages from "@rallly/languages";
import languageParser from "accept-language-parser";
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
+import withAuth from "next-auth/middleware";
const supportedLocales = Object.keys(languages);
-// these paths are always public
-const publicPaths = ["/login", "/register", "/invite", "/auth"];
-// these paths always require authentication
-const protectedPaths = ["/settings/profile"];
+export default withAuth(
+ function middleware(req) {
+ const { headers, nextUrl } = req;
+ const newUrl = nextUrl.clone();
-const checkLoginRequirements = async (req: NextRequest, res: NextResponse) => {
- const session = await getSession(req, res);
- const isGuest = session.user?.isGuest !== false;
+ // if the user is already logged in, don't let them access the login page
+ if (
+ /^\/(login|register)/.test(newUrl.pathname) &&
+ req.nextauth.token?.email
+ ) {
+ newUrl.pathname = "/";
+ return NextResponse.redirect(newUrl);
+ }
- if (!isGuest) {
- // already logged in
- return false;
- }
+ // Check if locale is specified in cookie
+ const preferredLocale = req.nextauth.token?.locale;
+ if (preferredLocale && supportedLocales.includes(preferredLocale)) {
+ newUrl.pathname = `/${preferredLocale}${newUrl.pathname}`;
+ } else {
+ // Check if locale is specified in header
+ const acceptLanguageHeader = headers.get("accept-language");
- // TODO (Luke Vella) [2023-09-11]: We should handle this on the client-side
- if (process.env.NEXT_PUBLIC_SELF_HOSTED === "true") {
- // when self-hosting, only public paths don't require login
- return !publicPaths.some((publicPath) =>
- req.nextUrl.pathname.startsWith(publicPath),
- );
- } else {
- // when using the hosted version, only protected paths require login
- return protectedPaths.some((protectedPath) =>
- req.nextUrl.pathname.includes(protectedPath),
- );
- }
-};
+ if (acceptLanguageHeader) {
+ const locale = languageParser.pick(
+ supportedLocales,
+ acceptLanguageHeader,
+ );
-export async function middleware(req: NextRequest) {
- const { headers, cookies, nextUrl } = req;
- const newUrl = nextUrl.clone();
- const res = NextResponse.next();
-
- const isLoginRequired = await checkLoginRequirements(req, res);
-
- if (isLoginRequired) {
- newUrl.pathname = "/login";
- newUrl.searchParams.set("redirect", req.nextUrl.pathname);
- return NextResponse.redirect(newUrl);
- }
-
- // Check if locale is specified in cookie
- const localeCookie = cookies.get("NEXT_LOCALE");
- const preferredLocale = localeCookie && localeCookie.value;
- if (preferredLocale && supportedLocales.includes(preferredLocale)) {
- newUrl.pathname = `/${preferredLocale}${newUrl.pathname}`;
- return NextResponse.rewrite(newUrl);
- } else {
- // Check if locale is specified in header
- const acceptLanguageHeader = headers.get("accept-language");
-
- if (acceptLanguageHeader) {
- const locale = languageParser.pick(
- supportedLocales,
- acceptLanguageHeader,
- );
-
- if (locale) {
- newUrl.pathname = `/${locale}${newUrl.pathname}`;
- return NextResponse.rewrite(newUrl);
+ if (locale) {
+ newUrl.pathname = `/${locale}${newUrl.pathname}`;
+ }
}
}
- }
- return res;
-}
+ const res = NextResponse.rewrite(newUrl);
+
+ /**
+ * We moved from a bespoke session implementation to next-auth.
+ * This middleware looks for the old session cookie and moves it to
+ * a temporary cookie accessible to the client which will exchange it
+ * for a new session token with the legacy-token provider.
+ */
+ const legacyToken = req.cookies.get("rallly-session");
+ if (legacyToken) {
+ res.cookies.set({
+ name: "legacy-token",
+ value: legacyToken.value,
+ });
+ res.cookies.delete("rallly-session");
+ }
+
+ return res;
+ },
+ {
+ secret: process.env.SECRET_PASSWORD,
+ callbacks: {
+ authorized: () => true, // needs to be true to allow access to all pages
+ },
+ },
+);
export const config = {
matcher: ["/((?!api|_next/static|_next/image|static|.*\\.).*)"],
diff --git a/apps/web/src/pages/404.tsx b/apps/web/src/pages/404.tsx
index 23dab11b0..4663af4af 100644
--- a/apps/web/src/pages/404.tsx
+++ b/apps/web/src/pages/404.tsx
@@ -5,7 +5,7 @@ import React from "react";
import ErrorPage from "@/components/error-page";
import { getStandardLayout } from "@/components/layouts/standard-layout";
import { NextPageWithLayout } from "@/types";
-import { withPageTranslations } from "@/utils/with-page-translations";
+import { getStaticTranslations } from "@/utils/with-page-translations";
const Custom404: NextPageWithLayout = () => {
const { t } = useTranslation();
@@ -20,6 +20,6 @@ const Custom404: NextPageWithLayout = () => {
Custom404.getLayout = getStandardLayout;
-export const getStaticProps = withPageTranslations();
+export const getStaticProps = getStaticTranslations;
export default Custom404;
diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx
index 25ce2b5a2..2f33e34ec 100644
--- a/apps/web/src/pages/_app.tsx
+++ b/apps/web/src/pages/_app.tsx
@@ -2,7 +2,7 @@ import "react-big-calendar/lib/css/react-big-calendar.css";
import "tailwindcss/tailwind.css";
import "../style.css";
-import { trpc, UserSession } from "@rallly/backend/next/trpc/client";
+import { trpc } from "@rallly/backend/next/trpc/client";
import { TooltipProvider } from "@rallly/ui/tooltip";
import { domMax, LazyMotion } from "framer-motion";
import { NextPage } from "next";
@@ -10,9 +10,9 @@ import { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import Script from "next/script";
+import { SessionProvider } from "next-auth/react";
import { appWithTranslation } from "next-i18next";
import { DefaultSeo } from "next-seo";
-import React from "react";
import Maintenance from "@/components/maintenance";
@@ -25,12 +25,8 @@ const inter = Inter({
display: "swap",
});
-type PageProps = {
- user?: UserSession;
-};
-
-type AppPropsWithLayout = AppProps & {
- Component: NextPageWithLayout;
+type AppPropsWithLayout = AppProps & {
+ Component: NextPageWithLayout;
};
const MyApp: NextPage = ({ Component, pageProps }) => {
@@ -41,54 +37,56 @@ const MyApp: NextPage = ({ Component, pageProps }) => {
const getLayout = Component.getLayout ?? ((page) => page);
return (
-
-
-
-
-
- {process.env.NEXT_PUBLIC_PADDLE_VENDOR_ID ? (
-