Compare commits
15 Commits
feature/or
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 4236e44df4 | |||
|
|
9c2c7f976f | ||
| 3662ced22b | |||
| 4645d61d65 | |||
|
|
cb6ac7610c | ||
|
|
57ffa49c5b | ||
| 787fff6bb0 | |||
| 34080466bd | |||
|
|
23d426ad71 | ||
|
|
bacb070e6d | ||
| acb1ed1fd3 | |||
| 8aa748d7ed | |||
|
|
3ad2c93448 | ||
|
|
efeaccc6e3 | ||
| f798ddb74c |
@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@ -32,12 +31,13 @@ func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) {
|
|||||||
domain := s.deps.EnvironmentConfig.FrontendDomain
|
domain := s.deps.EnvironmentConfig.FrontendDomain
|
||||||
|
|
||||||
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
|
||||||
url := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error()))
|
redirectUrl := fmt.Sprintf("%s/v2/web/login?error=%s", domain, url.QueryEscape(err.Error()))
|
||||||
ctx.Redirect(http.StatusSeeOther, url)
|
ctx.Redirect(http.StatusSeeOther, redirectUrl)
|
||||||
} else {
|
} else {
|
||||||
url := fmt.Sprintf("%s/v2/web/home", domain)
|
// Pass JWT via query param - frontend will set the cookie
|
||||||
s.SetCookie(ctx, "jwt_token", jwt, time.Hour*24*7)
|
// This bypasses cross-origin cookie issues with Cloudflare/proxies
|
||||||
ctx.Redirect(http.StatusSeeOther, url)
|
redirectUrl := fmt.Sprintf("%s/v2/web/auth/callback?token=%s", domain, url.QueryEscape(jwt))
|
||||||
|
ctx.Redirect(http.StatusSeeOther, redirectUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -43,8 +43,8 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
|
|||||||
value,
|
value,
|
||||||
maxAge,
|
maxAge,
|
||||||
path,
|
path,
|
||||||
".gophernest.net", // or your backend domain / parent
|
"gophernest.net",
|
||||||
true, // secure
|
true,
|
||||||
httpOnly,
|
httpOnly,
|
||||||
)
|
)
|
||||||
case "dev":
|
case "dev":
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { use, type ReactNode } from 'react';
|
|||||||
import { AuthContext } from './context/AuthContext';
|
import { AuthContext } from './context/AuthContext';
|
||||||
import RecipePage from './pages/Recipe';
|
import RecipePage from './pages/Recipe';
|
||||||
import SearchPage from './pages/Search';
|
import SearchPage from './pages/Search';
|
||||||
|
import AuthCallback from './pages/AuthCallback';
|
||||||
|
|
||||||
function ProtectedRoute({ children }: { children: ReactNode }) {
|
function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||||
const { isLoggedIn } = use(AuthContext)
|
const { isLoggedIn } = use(AuthContext)
|
||||||
@ -37,6 +38,9 @@ function App() {
|
|||||||
{/* Login page does not inherit WebLayout */}
|
{/* Login page does not inherit WebLayout */}
|
||||||
<Route path="/v2/web/login" element={<LoginPage />} />
|
<Route path="/v2/web/login" element={<LoginPage />} />
|
||||||
|
|
||||||
|
{/* Auth callback - handles token from OAuth redirect */}
|
||||||
|
<Route path="/v2/web/auth/callback" element={<AuthCallback />} />
|
||||||
|
|
||||||
<Route path="/v2/web" element={<WebLayout />}>
|
<Route path="/v2/web" element={<WebLayout />}>
|
||||||
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
||||||
<Route path="home" element={<Home />} />
|
<Route path="home" element={<Home />} />
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { SearchFilters } from "../types/search";
|
|||||||
interface FilterContextType {
|
interface FilterContextType {
|
||||||
filters: SearchFilters;
|
filters: SearchFilters;
|
||||||
setFilters: (filters: SearchFilters) => void;
|
setFilters: (filters: SearchFilters) => void;
|
||||||
|
resetFilters: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilterContext = createContext<FilterContextType>({
|
export const FilterContext = createContext<FilterContextType>({
|
||||||
@ -16,4 +17,5 @@ export const FilterContext = createContext<FilterContextType>({
|
|||||||
Favorites: false,
|
Favorites: false,
|
||||||
},
|
},
|
||||||
setFilters: () => { return },
|
setFilters: () => { return },
|
||||||
|
resetFilters: () => { return },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function FilterProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [filters]);
|
}, [filters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FilterContext value={{ filters, setFilters }}>
|
<FilterContext value={{ filters, setFilters, resetFilters: () => setFilters(DEFAULT_FILTERS) }}>
|
||||||
{children}
|
{children}
|
||||||
</FilterContext>
|
</FilterContext>
|
||||||
)
|
)
|
||||||
|
|||||||
30
web/src/pages/AuthCallback.tsx
Normal file
30
web/src/pages/AuthCallback.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSearchParams, useNavigate } from "react-router-dom";
|
||||||
|
import { useCookies } from "react-cookie";
|
||||||
|
|
||||||
|
export default function AuthCallback() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [, setCookie] = useCookies(["jwt_token"]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// Set cookie with 7 day expiration, accessible across all subdomains
|
||||||
|
setCookie("jwt_token", token, {
|
||||||
|
path: "/",
|
||||||
|
domain: "gophernest.net", // shared across all subdomains
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 days in seconds
|
||||||
|
secure: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
});
|
||||||
|
void navigate("/v2/web/home", { replace: true });
|
||||||
|
} else {
|
||||||
|
// No token provided, redirect to login
|
||||||
|
void navigate("/v2/web/login", { replace: true });
|
||||||
|
}
|
||||||
|
}, [searchParams, setCookie, navigate]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -12,10 +12,13 @@ import { GetRecipeOfTheWeek } from "../services/RecipeService";
|
|||||||
import { isApiError, type ApiError } from "../types/api/error";
|
import { isApiError, type ApiError } from "../types/api/error";
|
||||||
import { AuthContext } from "../context/AuthContext";
|
import { AuthContext } from "../context/AuthContext";
|
||||||
import { GetAuthenticatedUserMadeRecipes, GetAuthenticateUserViewedRecipes } from "../services/UserService";
|
import { GetAuthenticatedUserMadeRecipes, GetAuthenticateUserViewedRecipes } from "../services/UserService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FilterContext } from "../context/FilterContext";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
// Context
|
// Context
|
||||||
const { isLoggedIn } = use(AuthContext);
|
const { isLoggedIn } = use(AuthContext);
|
||||||
|
const { resetFilters } = use(FilterContext);
|
||||||
|
|
||||||
// Page state
|
// Page state
|
||||||
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
|
const [recipeOfTheWeek, setRecipeOfTheWeek] = useState<Recipe | null>(null);
|
||||||
@ -24,6 +27,7 @@ export default function Home() {
|
|||||||
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
|
const [viewedRecipes, setViewedRecipes] = useState<Recipe[]>([]);
|
||||||
|
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Fetch the recipe of the week
|
// Fetch the recipe of the week
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -55,6 +59,12 @@ export default function Home() {
|
|||||||
void fetch();
|
void fetch();
|
||||||
}, [isLoggedIn]);
|
}, [isLoggedIn]);
|
||||||
|
|
||||||
|
const viewAllRecipesHandler = () => {
|
||||||
|
// Clear filters
|
||||||
|
resetFilters();
|
||||||
|
void navigate(ROUTE_CONSTANTS.Search);
|
||||||
|
}
|
||||||
|
|
||||||
// BUG: Prob remove
|
// BUG: Prob remove
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error)
|
if (error)
|
||||||
@ -90,8 +100,11 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="leading-relaxed text-gray-800">
|
<p className="leading-relaxed text-gray-800">
|
||||||
Not sure what you want? {" "}
|
Not sure what you want? {" "}
|
||||||
<a href={ROUTE_CONSTANTS.Search} className="text-blue-500 underline hover:text-blue-700 duration-300">
|
<button onClick={viewAllRecipesHandler} className="text-blue-500 underline hover:text-blue-700 duration-300 cursor-pointer">
|
||||||
View all recipes here
|
View all recipes here
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<a href={ROUTE_CONSTANTS.Search} className="text-blue-500 underline hover:text-blue-700 duration-300">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user