diff --git a/apps/web/src/app/[locale]/logout/page.tsx b/apps/web/src/app/[locale]/logout/page.tsx
deleted file mode 100644
index e622c0651..000000000
--- a/apps/web/src/app/[locale]/logout/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-"use client";
-import { signOut } from "next-auth/react";
-import React from "react";
-
-export default function Page() {
- React.useEffect(() => {
- signOut({ callbackUrl: "/login" });
- });
- return null;
-}
diff --git a/apps/web/src/app/[locale]/logout/route.ts b/apps/web/src/app/[locale]/logout/route.ts
new file mode 100644
index 000000000..aec0ccd4f
--- /dev/null
+++ b/apps/web/src/app/[locale]/logout/route.ts
@@ -0,0 +1,10 @@
+import { NextResponse } from "next/server";
+
+import { resetUser } from "@/app/guest";
+import { absoluteUrl } from "@/utils/absolute-url";
+
+export async function GET() {
+ const res = NextResponse.redirect(absoluteUrl());
+ await resetUser(res);
+ return res;
+}
diff --git a/apps/web/src/app/guest.ts b/apps/web/src/app/guest.ts
new file mode 100644
index 000000000..3797feb0a
--- /dev/null
+++ b/apps/web/src/app/guest.ts
@@ -0,0 +1,107 @@
+import { randomid } from "@rallly/backend/utils/nanoid";
+import type { TimeFormat } from "@rallly/database";
+import { unsealData } from "iron-session/edge";
+import { NextRequest, NextResponse } from "next/server";
+import { encode, JWT } from "next-auth/jwt";
+
+import { absoluteUrl } from "@/utils/absolute-url";
+
+function getCookieSettings() {
+ const secure = absoluteUrl().startsWith("https://");
+ const prefix = secure ? "__Secure-" : "";
+ const name = `${prefix}next-auth.session-token`;
+ return {
+ secure,
+ name,
+ };
+}
+
+async function setCookie(res: NextResponse, jwt: JWT) {
+ const { name, secure } = getCookieSettings();
+
+ const token = await encode({
+ token: jwt,
+ secret: process.env.SECRET_PASSWORD,
+ });
+
+ res.cookies.set({
+ name,
+ value: token,
+ httpOnly: true,
+ secure,
+ sameSite: "lax",
+ path: "/",
+ });
+}
+
+export async function resetUser(res: NextResponse) {
+ // resets to a new guest user
+ const jwt: JWT = {
+ sub: `user-${randomid()}`,
+ email: null,
+ };
+
+ await setCookie(res, jwt);
+}
+
+export async function initGuest(req: NextRequest, res: NextResponse) {
+ const { name } = getCookieSettings();
+
+ if (req.cookies.has(name)) {
+ // already has a session token
+ return;
+ }
+
+ // TODO (Luke Vella) [2023-11-07]: Remove this after 30 days (Date: 2023-12-07)
+ const legacyJwt = await getLegacyToken(req, res);
+
+ const jwt: JWT = legacyJwt || {
+ sub: `user-${randomid()}`,
+ email: null,
+ };
+
+ await setCookie(res, jwt);
+
+ return jwt;
+}
+
+async function getLegacyToken(req: NextRequest, res: NextResponse) {
+ /**
+ * 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) {
+ // delete old cookie
+ res.cookies.delete("rallly-session");
+
+ // make sure old cookie isn't expired
+ const payload = await unsealData<{
+ user: {
+ id: string;
+ isGuest: boolean;
+ preferences?: {
+ weekStart?: number;
+ timeZone?: string;
+ timeFormat?: TimeFormat;
+ };
+ };
+ }>(legacyToken.value, {
+ password: process.env.SECRET_PASSWORD,
+ });
+ // if it's not expired, write it to a new cookie that we
+ // can read from the client
+ if (Object.keys(payload).length > 0 && payload?.user?.isGuest) {
+ const jwt: JWT = {
+ sub: payload.user.id,
+ email: null,
+ ...payload.user.preferences,
+ };
+
+ return jwt;
+ }
+ }
+ return null;
+}
diff --git a/apps/web/src/components/user-dropdown.tsx b/apps/web/src/components/user-dropdown.tsx
index bdf9ad947..d8866738c 100644
--- a/apps/web/src/components/user-dropdown.tsx
+++ b/apps/web/src/components/user-dropdown.tsx
@@ -22,7 +22,6 @@ import {
UserPlusIcon,
} from "lucide-react";
import Link from "next/link";
-import { signOut } from "next-auth/react";
import { LoginLink } from "@/components/login-link";
import { RegisterLink } from "@/components/register-link";
@@ -147,25 +146,25 @@ export const UserDropdown = () => {
+
- signOut({
- redirect: false,
- })
- }
+ asChild
+ className="flex text-destructive items-center gap-x-2"
>
-
-
+ {/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
+
+
+
+
- signOut()}
- >
-
-
+
+ {/* Don't use signOut() from next-auth. It doesn't work in vercel-production env. I don't know why. */}
+
+
+
+
diff --git a/apps/web/src/components/user-provider.tsx b/apps/web/src/components/user-provider.tsx
index dbd018a37..d6684ecf6 100644
--- a/apps/web/src/components/user-provider.tsx
+++ b/apps/web/src/components/user-provider.tsx
@@ -1,7 +1,6 @@
"use client";
-import Cookies from "js-cookie";
import { Session } from "next-auth";
-import { signIn, useSession } from "next-auth/react";
+import { useSession } from "next-auth/react";
import React from "react";
import { z } from "zod";
@@ -59,27 +58,7 @@ export const IfGuest = (props: { children?: React.ReactNode }) => {
};
export const UserProvider = (props: { children?: React.ReactNode }) => {
- const session = useSession({
- required: true,
- onUnauthenticated() {
- // 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,
- redirect: false,
- });
- } else {
- // End: Legacy token migration
- signIn("guest", {
- redirect: false,
- });
- }
- },
- });
+ const session = useSession();
const user = session.data?.user;
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
index de148d4e0..de0ffe402 100644
--- a/apps/web/src/middleware.ts
+++ b/apps/web/src/middleware.ts
@@ -1,14 +1,13 @@
-import { randomid } from "@rallly/backend/utils/nanoid";
import languages from "@rallly/languages";
import languageParser from "accept-language-parser";
-import { unsealData } from "iron-session/edge";
import { NextResponse } from "next/server";
-import { encode } from "next-auth/jwt";
import withAuth from "next-auth/middleware";
+import { initGuest } from "@/app/guest";
+
const supportedLocales = Object.keys(languages);
-export default withAuth(
+export const middleware = withAuth(
async function middleware(req) {
const { headers, nextUrl } = req;
const newUrl = nextUrl.clone();
@@ -39,54 +38,7 @@ export default withAuth(
const res = NextResponse.rewrite(newUrl);
- if (!req.nextauth.token) {
- /**
- * 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) {
- // delete old cookie
- res.cookies.delete("rallly-session");
- // make sure old cookie isn't expired
- const payload = await unsealData(legacyToken.value, {
- password: process.env.SECRET_PASSWORD,
- });
- // if it's not expired, write it to a new cookie that we
- // can read from the client
- if (Object.keys(payload).length > 0) {
- res.cookies.set({
- name: "legacy-token",
- value: legacyToken.value,
- httpOnly: false,
- });
- } else {
- // Create new guest user
- const newUser = `user-${randomid()}`;
- const token = await encode({
- token: {
- sub: newUser,
- email: null,
- },
- secret: process.env.SECRET_PASSWORD,
- });
- const secure = process.env.NODE_ENV === "production";
- const prefix = secure ? "__Secure-" : "";
- const name = `${prefix}next-auth.session-token`;
-
- res.cookies.set({
- name,
- value: token,
- httpOnly: true,
- secure,
- sameSite: "lax",
- path: "/",
- });
- }
- }
- }
+ await initGuest(req, res);
return res;
},
diff --git a/apps/web/src/utils/auth.ts b/apps/web/src/utils/auth.ts
index cb997e4e1..fb40c4569 100644
--- a/apps/web/src/utils/auth.ts
+++ b/apps/web/src/utils/auth.ts
@@ -18,7 +18,6 @@ import CredentialsProvider from "next-auth/providers/credentials";
import EmailProvider from "next-auth/providers/email";
import { absoluteUrl } from "@/utils/absolute-url";
-import { LegacyTokenProvider } from "@/utils/auth/legacy-token-provider";
import { mergeGuestsIntoUser } from "@/utils/auth/merge-user";
import { emailClient } from "@/utils/emails";
@@ -30,7 +29,6 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
strategy: "jwt",
},
providers: [
- LegacyTokenProvider,
// When a user registers, we don't want to go through the email verification process
// so this providers allows us exchange the registration token for a session token
CredentialsProvider({
@@ -139,7 +137,8 @@ const getAuthOptions = (...args: GetServerSessionParams) =>
return false;
}
}
- } else {
+ } else if (user.email) {
+ // merge guest user into newly logged in user
const session = await getServerSession(...args);
if (session && session.user.email === null) {
await mergeGuestsIntoUser(user.id, [session.user.id]);
@@ -198,8 +197,8 @@ type GetServerSessionParams =
| [NextApiRequest, NextApiResponse]
| [];
-export function getServerSession(...args: GetServerSessionParams) {
- return getServerSessionWithOptions(...args, getAuthOptions(...args));
+export async function getServerSession(...args: GetServerSessionParams) {
+ return await getServerSessionWithOptions(...args, getAuthOptions(...args));
}
export async function AuthApiRoute(req: NextApiRequest, res: NextApiResponse) {
diff --git a/apps/web/src/utils/auth/legacy-token-provider.ts b/apps/web/src/utils/auth/legacy-token-provider.ts
deleted file mode 100644
index bde7ff264..000000000
--- a/apps/web/src/utils/auth/legacy-token-provider.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { decryptToken } from "@rallly/backend/session";
-import { prisma, TimeFormat } from "@rallly/database";
-import CredentialsProvider from "next-auth/providers/credentials";
-
-/**
- * This provider allows us to login with a token from an older session created with
- * iron-session.
- *
- * We should keep this provider available for at least 30 days in production to allow returning
- * users to keep their existing sessions.
- *
- * @deprecated
- */
-export const LegacyTokenProvider = CredentialsProvider({
- id: "legacy-token",
- name: "Legacy Token",
- credentials: {
- token: {
- label: "Token",
- type: "text",
- },
- },
- async authorize(credentials) {
- if (credentials?.token) {
- const session = await decryptToken<{
- user: {
- id: string;
- isGuest: boolean;
- preferences?: {
- weekStart?: number;
- timeZone?: string;
- timeFormat?: TimeFormat;
- };
- };
- }>(credentials.token);
-
- if (session?.user) {
- if (session.user.isGuest) {
- return {
- id: session.user.id,
- email: null,
- weekStart: session.user.preferences?.weekStart,
- timeZone: session.user.preferences?.timeZone,
- timeFormat: session.user.preferences?.timeFormat,
- };
- } else {
- const user = await prisma.user.findUnique({
- where: {
- id: session.user.id,
- },
- select: {
- id: true,
- email: true,
- name: true,
- },
- });
-
- if (user) {
- return {
- id: user.id,
- name: user.name,
- email: user.email,
- };
- }
- }
- }
- }
-
- return null;
- },
-});