From e0fdf0b064882dcb9241b82714ed693dd15930a4 Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:01:28 -0500 Subject: [PATCH 1/7] Add a middleware layer, handle optimistic auth check on basic routes --- src/lib/api/user-session.ts | 8 +++---- src/middleware.ts | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 src/middleware.ts diff --git a/src/lib/api/user-session.ts b/src/lib/api/user-session.ts index e79a3657..910c118e 100644 --- a/src/lib/api/user-session.ts +++ b/src/lib/api/user-session.ts @@ -20,7 +20,7 @@ export const loginUser = async ( var userSession: UserSession = defaultUserSession(); var err: string = ""; - const data = await axios + await axios .post( `${siteConfig.env.backendServiceURL}/login`, { @@ -45,8 +45,8 @@ export const loginUser = async ( // deserialize successfully, then downstream operations will see the default // userSessionData in state and we will experience subtle Bugs. We should consider // how best we want to handle this. Ex. clear auth cookie? - console.debug("userSessionData: ", userSessionData) - throw { message: 'Login Failed to produce valid User Session data' } + console.debug("userSessionData: ", userSessionData); + throw { message: "Login Failed to produce valid User Session data" }; } }) .catch(function (error: AxiosError) { @@ -73,7 +73,7 @@ export const logoutUser = async (): Promise => { const data = await axios .get(`${siteConfig.env.backendServiceURL}/logout`, { withCredentials: true, - setTimeout: 5000, // 5 seconds before timing out trying to log in with the backend + setTimeout: 5000, // 5 seconds before timing out trying to log out with the backend }) .then(function (response: AxiosResponse) { // handle success diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 00000000..7e8dae1d --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,42 @@ +import { NextRequest, NextResponse } from "next/server"; + +// 1. Specify protected and public routes +const protectedRoutes = [ + "/dashboard", + "/coaching-sessions", + "/settings", + "/profile", +]; +const publicRoutes = ["/"]; + +export default async function middleware(req: NextRequest) { + // 2. Check if the current route is protected or public + const path = req.nextUrl.pathname; + const isProtectedRoute = protectedRoutes.includes(path); + const isPublicRoute = publicRoutes.includes(path); + + // 3. Decrypt the session from the cookie + const sessionCookie = req.cookies.get("id"); + let session = sessionCookie?.value; + + // 4. Redirect to / if the user is not authenticated + if (isProtectedRoute && !session) { + return NextResponse.redirect(new URL("/", req.nextUrl)); + } + + // 5. Redirect to /dashboard if the user is authenticated + if ( + isPublicRoute && + session && + !req.nextUrl.pathname.startsWith("/dashboard") + ) { + return NextResponse.redirect(new URL("/dashboard", req.nextUrl)); + } + + return NextResponse.next(); +} + +// Routes Middleware should not run on +export const config = { + matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], +}; From 105ec9fdb6101dfa6cfb1a0cfbdcb2493da88a1f Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Wed, 12 Mar 2025 13:59:32 -0500 Subject: [PATCH 2/7] fix variable declaration src/middleware.ts Co-authored-by: Jim Hodapp --- src/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware.ts b/src/middleware.ts index 7e8dae1d..d83af709 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -17,7 +17,7 @@ export default async function middleware(req: NextRequest) { // 3. Decrypt the session from the cookie const sessionCookie = req.cookies.get("id"); - let session = sessionCookie?.value; + const session = sessionCookie?.value; // 4. Redirect to / if the user is not authenticated if (isProtectedRoute && !session) { From acd057c3504555cc7efd61ce24cc69634ad0e89a Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Thu, 13 Mar 2025 13:48:53 -0500 Subject: [PATCH 3/7] Adjust middleware approach, using localstorage and reinitializing auth-store from localStorage --- src/lib/providers/auth-store-provider.tsx | 7 ++++++- src/lib/stores/auth-store.ts | 2 +- src/middleware.ts | 11 +++++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/lib/providers/auth-store-provider.tsx b/src/lib/providers/auth-store-provider.tsx index 7aa4d1d6..caf6aefd 100644 --- a/src/lib/providers/auth-store-provider.tsx +++ b/src/lib/providers/auth-store-provider.tsx @@ -16,8 +16,13 @@ export interface AuthStoreProviderProps { export const AuthStoreProvider = ({ children }: AuthStoreProviderProps) => { const storeRef = useRef>(undefined); + const authStore = localStorage.getItem("auth-store"); + + const persistedStoreOrNull = authStore ? JSON.parse(authStore).state : null; + if (!storeRef.current) { - storeRef.current = createAuthStore(); + // storeRef.current = createAuthStore(authStore); + storeRef.current = createAuthStore(persistedStoreOrNull); } return ( diff --git a/src/lib/stores/auth-store.ts b/src/lib/stores/auth-store.ts index cb0471b0..68797c40 100644 --- a/src/lib/stores/auth-store.ts +++ b/src/lib/stores/auth-store.ts @@ -50,7 +50,7 @@ export const createAuthStore = (initState: AuthState = defaultInitState) => { }), { name: "auth-store", - storage: createJSONStorage(() => sessionStorage), + storage: createJSONStorage(() => localStorage), } ) ) diff --git a/src/middleware.ts b/src/middleware.ts index d83af709..983f80b7 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -12,14 +12,17 @@ const publicRoutes = ["/"]; export default async function middleware(req: NextRequest) { // 2. Check if the current route is protected or public const path = req.nextUrl.pathname; - const isProtectedRoute = protectedRoutes.includes(path); - const isPublicRoute = publicRoutes.includes(path); + const isProtectedRoute = protectedRoutes.some((route) => + path.startsWith(route) + ); + const isPublicRoute = publicRoutes.some((route) => path === route); - // 3. Decrypt the session from the cookie + // 3. Get the session from the cookie const sessionCookie = req.cookies.get("id"); const session = sessionCookie?.value; // 4. Redirect to / if the user is not authenticated + // 4b. TODO: Check session validity/expiration? if (isProtectedRoute && !session) { return NextResponse.redirect(new URL("/", req.nextUrl)); } @@ -38,5 +41,5 @@ export default async function middleware(req: NextRequest) { // Routes Middleware should not run on export const config = { - matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], + matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$).*)"], }; From a148ed0645e399f57837d675eadbc4fb18f1e359 Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:30:05 -0500 Subject: [PATCH 4/7] Remove comment --- src/lib/providers/auth-store-provider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/providers/auth-store-provider.tsx b/src/lib/providers/auth-store-provider.tsx index caf6aefd..472820fe 100644 --- a/src/lib/providers/auth-store-provider.tsx +++ b/src/lib/providers/auth-store-provider.tsx @@ -21,7 +21,6 @@ export const AuthStoreProvider = ({ children }: AuthStoreProviderProps) => { const persistedStoreOrNull = authStore ? JSON.parse(authStore).state : null; if (!storeRef.current) { - // storeRef.current = createAuthStore(authStore); storeRef.current = createAuthStore(persistedStoreOrNull); } From 37109a141b7c2903b22fefda24e71077ea0eddce Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Fri, 14 Mar 2025 11:31:26 -0500 Subject: [PATCH 5/7] fix lint error --- src/lib/providers/auth-store-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/providers/auth-store-provider.tsx b/src/lib/providers/auth-store-provider.tsx index 472820fe..316c640b 100644 --- a/src/lib/providers/auth-store-provider.tsx +++ b/src/lib/providers/auth-store-provider.tsx @@ -16,7 +16,7 @@ export interface AuthStoreProviderProps { export const AuthStoreProvider = ({ children }: AuthStoreProviderProps) => { const storeRef = useRef>(undefined); - const authStore = localStorage.getItem("auth-store"); + const authStore = window.localStorage.getItem("auth-store"); const persistedStoreOrNull = authStore ? JSON.parse(authStore).state : null; From 466e086d2813fbc8a08884feff12cc309f9e798f Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Fri, 14 Mar 2025 12:54:30 -0500 Subject: [PATCH 6/7] remove window reference --- src/lib/providers/auth-store-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/providers/auth-store-provider.tsx b/src/lib/providers/auth-store-provider.tsx index 316c640b..472820fe 100644 --- a/src/lib/providers/auth-store-provider.tsx +++ b/src/lib/providers/auth-store-provider.tsx @@ -16,7 +16,7 @@ export interface AuthStoreProviderProps { export const AuthStoreProvider = ({ children }: AuthStoreProviderProps) => { const storeRef = useRef>(undefined); - const authStore = window.localStorage.getItem("auth-store"); + const authStore = localStorage.getItem("auth-store"); const persistedStoreOrNull = authStore ? JSON.parse(authStore).state : null; From fee8f3a311e37538aa257f6fbe1a377f0f157e2d Mon Sep 17 00:00:00 2001 From: Zach Gavin <9595970+zgavin1@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:11:10 -0500 Subject: [PATCH 7/7] Add check for window before accessing localstorage in authstoreprovider initiatlization --- src/lib/providers/auth-store-provider.tsx | 35 +++++++++++++++++------ 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/lib/providers/auth-store-provider.tsx b/src/lib/providers/auth-store-provider.tsx index 472820fe..84917048 100644 --- a/src/lib/providers/auth-store-provider.tsx +++ b/src/lib/providers/auth-store-provider.tsx @@ -1,8 +1,16 @@ +"use client"; // The purpose of this provider is to provide compatibility with // Next.js re-rendering and component caching -"use client"; -import { type ReactNode, createContext, useRef, useContext } from "react"; +import { + type ReactNode, + createContext, + useRef, + useContext, + useMemo, + useEffect, + useState, +} from "react"; import { type StoreApi, useStore } from "zustand"; import { type AuthStore, createAuthStore } from "@/lib/stores/auth-store"; @@ -15,13 +23,22 @@ export interface AuthStoreProviderProps { } export const AuthStoreProvider = ({ children }: AuthStoreProviderProps) => { - const storeRef = useRef>(undefined); - const authStore = localStorage.getItem("auth-store"); - - const persistedStoreOrNull = authStore ? JSON.parse(authStore).state : null; - - if (!storeRef.current) { - storeRef.current = createAuthStore(persistedStoreOrNull); + const storeRef = useRef | null>(null); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") { + // Now safe to access localStorage + const storedValue = localStorage.getItem("auth-store"); + const initialState = storedValue ? JSON.parse(storedValue).state : null; + storeRef.current = createAuthStore(initialState); + setIsInitialized(true); + } + }, []); + + // Ensure store is initialized before rendering the provider + if (!isInitialized) { + return null; // or return a loading component } return (