From 1749a91bf9466a889afe30b6ad58bd80ff1072df Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 14 Nov 2025 13:08:05 -0700 Subject: [PATCH] (FEAT): Context is somewhat working It works okay, feels a bit slugish, but that might just be the environments fault. --- internal/app/server/auth_handler_v2.go | 11 ++--- internal/app/server/cookies.go | 7 +-- internal/app/server/middleware_v2.go | 59 ++++++++++++++++++++++++++ internal/app/server/server.go | 13 +++++- web/package-lock.json | 53 ++++++++++++++++++++++- web/package.json | 1 + web/src/App.tsx | 24 +++++++++-- web/src/context/AuthContext.tsx | 8 ++++ web/src/context/AuthProvider.tsx | 25 +++++++++++ web/src/main.tsx | 8 +++- web/src/pages/Home.tsx | 32 +++++++------- 11 files changed, 209 insertions(+), 32 deletions(-) create mode 100644 internal/app/server/middleware_v2.go create mode 100644 web/src/context/AuthContext.tsx create mode 100644 web/src/context/AuthProvider.tsx diff --git a/internal/app/server/auth_handler_v2.go b/internal/app/server/auth_handler_v2.go index 5363c2f..13f44cf 100644 --- a/internal/app/server/auth_handler_v2.go +++ b/internal/app/server/auth_handler_v2.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "net/url" + "time" "github.com/gin-gonic/gin" ) @@ -13,15 +14,14 @@ import ( func (s *Server) GetGoogleAuthUrlHandlerV2(ctx *gin.Context) { url := s.deps.AuthService.GetGoogleAuthUrl() ctx.JSON(http.StatusOK, gin.H{ - "status": http.StatusOK, + "status": http.StatusOK, "message": "[OK] Successfully retrieved Google auth URL.", - "url": url, + "url": url, }) } - // GoogleCallbackHandlerV2 reads the data from the Google redirection and uses it -// to generate a JWT which is sent back to the UI via a URL query parameter. If an +// to generate a JWT which is sent back to the UI via a URL query parameter. If an // error occurs the user will be directed to the login page with an error query param. func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) { var ( @@ -33,7 +33,8 @@ func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) { url := fmt.Sprintf("http://localhost:5173/v2/web/login?error=%s", url.QueryEscape(err.Error())) ctx.Redirect(http.StatusSeeOther, url) } else { - url := fmt.Sprintf("http://localhost:5173/v2/web/home?token=%s", jwt) + url := "http://localhost:5173/v2/web/home" + s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7) ctx.Redirect(http.StatusSeeOther, url) } } diff --git a/internal/app/server/cookies.go b/internal/app/server/cookies.go index 8cb46b3..3297c3f 100644 --- a/internal/app/server/cookies.go +++ b/internal/app/server/cookies.go @@ -18,7 +18,7 @@ import ( func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.Duration) { var ( path string = "/" - httpOnly bool = true + httpOnly bool = false // NOTE: Should use false so React can see it! maxAge int secure bool domain string @@ -32,7 +32,7 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D maxAge = 0 } else { // Normal calculation - maxAge = int(time.Now().Add(duration).Sub(time.Now()).Seconds()) + maxAge = int(time.Until(time.Now().Add(duration)).Seconds()) } if s.deps.EnvironmentConfig.Environment == "prod" { @@ -41,7 +41,8 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D } else if s.deps.EnvironmentConfig.Environment == "dev" { secure = false - domain = s.deps.EnvironmentConfig.Domain + // domain = s.deps.EnvironmentConfig.Domain + domain = "localhost" } else { // Defaults diff --git a/internal/app/server/middleware_v2.go b/internal/app/server/middleware_v2.go new file mode 100644 index 0000000..a9e6168 --- /dev/null +++ b/internal/app/server/middleware_v2.go @@ -0,0 +1,59 @@ +package server + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + domain "github.com/haydenhargreaves/Potion/internal/domain/server" +) + +func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { + return func(ctx *gin.Context) { + tokenString, err := ctx.Cookie("jwt_token") + fmt.Println(tokenString) + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": fmt.Sprintf("[UNAUTHORIZED] Failed to get token from cookie. %s", err.Error()), + }) + ctx.Abort() + return + } + + claims := &domain.JwtClaims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return jwtSecretKey, nil + }) + + // Error occurred when parsing + if err != nil { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": fmt.Sprintf("[UNAUTHORIZED] Error parsing cooking. %s", err.Error()), + }) + ctx.Abort() + return + } + + // Token is invalid + if !token.Valid { + ctx.JSON(http.StatusUnauthorized, gin.H{ + "status": http.StatusUnauthorized, + "message": "[UNAUTHORIZED] Token is invalid.", + }) + ctx.Abort() + return + } + + // Found: Set the values + ctx.Set("userId", claims.UserId) + ctx.Set("userEmail", claims.Email) + ctx.Next() + } +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index ed8e92a..db97f95 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -42,7 +42,11 @@ func Init(port int) *Server { server.Router.SetTrustedProxies(nil) // Setup the CORS settings and active them - server.config.AllowAllOrigins = true + // server.config.AllowAllOrigins = true + server.config.AllowOrigins = []string{"http://localhost:5173"} + server.config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} + server.config.AllowHeaders = []string{"Origin", "Content-Type", "Authorization"} + server.config.AllowCredentials = true server.Router.Use(cors.New(server.config)) return server @@ -121,7 +125,8 @@ func (s *Server) Setup() *Server { // Apply middleware s.Router.Use(RecoveryMiddleware()) - s.Router.Use(JwtAuthMiddleWare(jwtSecret)) + // NOTE: No longer running on every connection! + // s.Router.Use(JwtAuthMiddleWare(jwtSecret)) // Redirect index to home page: Update this as needed s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) @@ -200,5 +205,9 @@ func (s *Server) Setup() *Server { router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2) router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2) + router_api_v2.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) { + ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"}) + }) + return s } diff --git a/web/package-lock.json b/web/package-lock.json index aeabb28..b688f04 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,6 +13,7 @@ "eslint-plugin-react-dom": "^2.2.4", "eslint-plugin-react-x": "^2.2.4", "react": "^19.1.1", + "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.5", "tailwindcss": "^4.1.16" @@ -1605,6 +1606,18 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1625,7 +1638,6 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -2122,7 +2134,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/debug": { @@ -2823,6 +2834,15 @@ "node": ">= 0.4" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3518,6 +3538,20 @@ "node": ">=0.10.0" } }, + "node_modules/react-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", + "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.6", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^8.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -3530,6 +3564,12 @@ "react": "^19.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react-router": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", @@ -3919,6 +3959,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/universal-cookie": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-8.0.1.tgz", + "integrity": "sha512-B6ks9FLLnP1UbPPcveOidfvB9pHjP+wekP2uRYB9YDfKVpvcjKgy1W5Zj+cEXJ9KTPnqOKGfVDQBmn8/YCQfRg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", diff --git a/web/package.json b/web/package.json index 407b256..437610c 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "eslint-plugin-react-dom": "^2.2.4", "eslint-plugin-react-x": "^2.2.4", "react": "^19.1.1", + "react-cookie": "^8.0.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.5", "tailwindcss": "^4.1.16" diff --git a/web/src/App.tsx b/web/src/App.tsx index e5dace2..aaf1f10 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -9,6 +9,22 @@ import Favorites from './pages/Favorites'; import Profile from './pages/Profile'; import ShoppingList from './pages/ShoppingList'; import LoginPage from './pages/Login'; +import { use, type ReactNode } from 'react'; +import { AuthContext } from './context/AuthContext'; + +function ProtectedRoute({ children }: { children: ReactNode }) { + const { isLoggedIn } = use(AuthContext) + // Wait until the value is set + if (isLoggedIn === undefined) { + // Still checking auth state: don't render anything yet, or show a spinner if desired + return null; // or + } + + if (isLoggedIn) return children; + + // Redirect to login page if not authenicated + return +} function App() { return ( @@ -22,10 +38,10 @@ function App() { }> } /> } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> {/* } /> */} diff --git a/web/src/context/AuthContext.tsx b/web/src/context/AuthContext.tsx new file mode 100644 index 0000000..9f312ad --- /dev/null +++ b/web/src/context/AuthContext.tsx @@ -0,0 +1,8 @@ +import { createContext } from "react"; + +interface AuthContextType { + isLoggedIn: boolean | undefined; + setIsLoggedIn: (state: boolean) => void; +} + +export const AuthContext = createContext({ isLoggedIn: undefined, setIsLoggedIn: () => { return } }); diff --git a/web/src/context/AuthProvider.tsx b/web/src/context/AuthProvider.tsx new file mode 100644 index 0000000..17922be --- /dev/null +++ b/web/src/context/AuthProvider.tsx @@ -0,0 +1,25 @@ +import { useEffect, useState, type ReactNode } from "react"; +import { AuthContext } from "./AuthContext"; +import { useCookies } from 'react-cookie'; + +// BUG: The rerender issue is ridiclious, and needs to be updated. Maybe using a global +// state management tool instead of a context +export function AuthProvider({ children }: { children: ReactNode }) { + const [cookies] = useCookies(["jwt_token"]); + const [isLoggedIn, setIsLoggedIn] = useState(cookies.jwt_token !== undefined); + + useEffect(() => { + setIsLoggedIn(cookies.jwt_token !== undefined); + }, [cookies]); + + // NOTE: Display some loading page, maybe... + // if (isLoggedIn === undefined) { + // return
Loading authentication...
; // or null for no flicker + // } + + return ( + + {children} + + ); +} diff --git a/web/src/main.tsx b/web/src/main.tsx index bef5202..d6a1d3d 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -2,9 +2,15 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { AuthProvider } from './context/AuthProvider.tsx' +import { CookiesProvider } from 'react-cookie' createRoot(document.getElementById('root')!).render( - + + + + + , ) diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 3738088..9f10c80 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { use, useEffect, useState } from "react"; import SalmonVideo from "../assets/videos/salmon_video.mp4"; import Banner from "../components/Banner"; import ROUTE_CONSTANTS from "../types/routes"; @@ -10,10 +10,14 @@ import ContentCardSmall from "../components/cards/ContentCardSmall"; import RecipeSearchBar from "../components/inputs/RecipeSearchBar"; import { GetRecipeOfTheWeek } from "../services/RecipeService"; import { isApiError, type ApiError } from "../types/api/error"; -import { useSearchParams } from "react-router-dom"; +import axios from "axios"; +import { AuthContext } from "../context/AuthContext"; export default function Home() { - const [loggedIn, isLoggedIn] = useState(false); + // Context + const { isLoggedIn } = use(AuthContext); + + // Page state const [recipeOfTheWeek, setRecipeOfTheWeek] = useState(null); const [madeRecipes, setMadeRecipes] = useState([]); @@ -21,8 +25,6 @@ export default function Home() { const [error, setError] = useState(""); - const [searchParams, setSearchParams] = useSearchParams(); - // BUG: Remove these useEffect(() => { @@ -104,7 +106,6 @@ export default function Home() { Favorite: true }; - isLoggedIn(true); setRecipeOfTheWeek(recipe); const recipes: Recipe[] = [recipe, recipe2]; @@ -124,17 +125,18 @@ export default function Home() { void fetch(); }, []); + // BUG: Prob remove useEffect(() => { - console.error(error); + if (error) + console.error(error); }, [error]); + // BUG: This is useless, just for testing useEffect(() => { - if (searchParams.has("token")) { - const token: string = searchParams.get("token")!; - console.log("@token", token); - } - console.log(searchParams); - }, [searchParams]); + // NOTE: Be sure to call this WITH CREDENTIALS + void axios.get("http://localhost:3000/v2/api/protected", { withCredentials: true }); + }, []); + return ( <> @@ -186,7 +188,7 @@ export default function Home() {

Recently viewed

- {loggedIn ? + {isLoggedIn ?
{viewedRecipes && viewedRecipes.length > 0 ? ( <> @@ -207,7 +209,7 @@ export default function Home() {
}

Make again

- {loggedIn ? + {isLoggedIn ?
{madeRecipes && madeRecipes.length > 0 ? ( <>