(FEAT): Context is somewhat working

It works okay, feels a bit slugish, but that might just be the
environments fault.
This commit is contained in:
Hayden Hargreaves 2025-11-14 13:08:05 -07:00
parent 25ea3fcfd7
commit 1749a91bf9
11 changed files with 209 additions and 32 deletions

View File

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

View File

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

View File

@ -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()
}
}

View File

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

53
web/package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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 <Loading />
}
if (isLoggedIn) return children;
// Redirect to login page if not authenicated
return <Navigate to="/v2/web/login" replace />
}
function App() {
return (
@ -22,10 +38,10 @@ function App() {
<Route path="/v2/web" element={<WebLayout />}>
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
<Route path="home" element={<Home />} />
<Route path="favorites" element={<Favorites />} />
<Route path="create" element={<Create />} />
<Route path="profile" element={<Profile />} />
<Route path="list" element={<ShoppingList />} />
<Route path="favorites" element={<ProtectedRoute><Favorites /></ProtectedRoute>} />
<Route path="create" element={<ProtectedRoute><Create /></ProtectedRoute>} />
<Route path="profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="list" element={<ProtectedRoute><ShoppingList /></ProtectedRoute>} />
{/* <Route path="recipe/:id" element={<Home />} /> */}

View File

@ -0,0 +1,8 @@
import { createContext } from "react";
interface AuthContextType {
isLoggedIn: boolean | undefined;
setIsLoggedIn: (state: boolean) => void;
}
export const AuthContext = createContext<AuthContextType>({ isLoggedIn: undefined, setIsLoggedIn: () => { return } });

View File

@ -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<boolean | undefined>(cookies.jwt_token !== undefined);
useEffect(() => {
setIsLoggedIn(cookies.jwt_token !== undefined);
}, [cookies]);
// NOTE: Display some loading page, maybe...
// if (isLoggedIn === undefined) {
// return <div>Loading authentication...</div>; // or null for no flicker
// }
return (
<AuthContext value={{ isLoggedIn, setIsLoggedIn }}>
{children}
</AuthContext>
);
}

View File

@ -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(
<StrictMode>
<App />
<CookiesProvider>
<AuthProvider>
<App />
</AuthProvider>
</CookiesProvider>
</StrictMode>,
)

View File

@ -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<boolean>(false);
// Context
const { isLoggedIn } = use(AuthContext);
// Page state
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
const [madeRecipes, setMadeRecipes] = useState<Recipe[]>([]);
@ -21,8 +25,6 @@ export default function Home() {
const [error, setError] = useState<string>("");
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() {
<Banner content="Take Another Look." />
<div className="w-full">
<h3 className="text-lg mt-8 mx-4">Recently viewed</h3>
{loggedIn ?
{isLoggedIn ?
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
{viewedRecipes && viewedRecipes.length > 0 ? (
<>
@ -207,7 +209,7 @@ export default function Home() {
</div>
}
<h3 className="text-lg mt-8 mx-4">Make again</h3>
{loggedIn ?
{isLoggedIn ?
<div className="flex overflow-x-auto gap-x-4 mx-4 my-4">
{madeRecipes && madeRecipes.length > 0 ? (
<>