(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:
parent
25ea3fcfd7
commit
1749a91bf9
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
59
internal/app/server/middleware_v2.go
Normal file
59
internal/app/server/middleware_v2.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@ -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
53
web/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 />} /> */}
|
||||
|
||||
|
||||
8
web/src/context/AuthContext.tsx
Normal file
8
web/src/context/AuthContext.tsx
Normal 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 } });
|
||||
25
web/src/context/AuthProvider.tsx
Normal file
25
web/src/context/AuthProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>,
|
||||
)
|
||||
|
||||
@ -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 ? (
|
||||
<>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user