(FEAT): JWT auth is coming along so well!

We have it in the UI, just need a way to send it back and handle it in
the backend.
This commit is contained in:
Hayden Hargreaves 2025-11-13 22:57:05 -07:00
parent 7df879b04a
commit 25ea3fcfd7
10 changed files with 171 additions and 8 deletions

View File

@ -0,0 +1,39 @@
package server
import (
"fmt"
"net/http"
"net/url"
"github.com/gin-gonic/gin"
)
// GetGoogleAuthUrlHandlerV2 fetches a Google authentication URl and returns it.
// This function is atomic and cannot fail.
func (s *Server) GetGoogleAuthUrlHandlerV2(ctx *gin.Context) {
url := s.deps.AuthService.GetGoogleAuthUrl()
ctx.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"message": "[OK] Successfully retrieved Google auth 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
// error occurs the user will be directed to the login page with an error query param.
func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) {
var (
state string = ctx.Query("state")
code string = ctx.Query("code")
)
if jwt, err := s.deps.AuthService.GoogleAuthSuccess(state, code); err != nil {
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)
ctx.Redirect(http.StatusSeeOther, url)
}
}

View File

@ -8,8 +8,13 @@ import (
)
func (s *Server) GetRecipeOfTheWeekHandler(ctx *gin.Context) {
// BUG: This needs to be different
// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it.
// If an error occurs, it will be returned and a recipe will not be returned.
//
// Until auth is reimplemented, there is no way to determine what user is making the
// call.
func (s *Server) GetRecipeOfTheWeekHandlerV2(ctx *gin.Context) {
// BUG: This needs to be different, not hard coded
userId := 1
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(&userId)
@ -17,14 +22,14 @@ func (s *Server) GetRecipeOfTheWeekHandler(ctx *gin.Context) {
if err != nil {
ctx.JSON(http.StatusBadRequest, gin.H{
"status": http.StatusBadRequest,
"message": fmt.Sprintf("[ERROR] Failed to get recipe of the week. %s\n", err.Error()),
"message": fmt.Sprintf("[ERROR] Failed to get recipe of the week. %s", err.Error()),
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"message": "[OK] Successfully retrieved recipe of the week.\n",
"message": "[OK] Successfully retrieved recipe of the week.",
"recipe": recipe,
})
}

View File

@ -74,7 +74,8 @@ func (s *Server) Setup() *Server {
// SETUP GOOGLE AUTH
var (
redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK)
// NOTE: USING V2 NOW
redirectUrl string = fmt.Sprintf("%s%s", cfg.Domain, domain.API_AUTH_CALLBACK_V2)
clientId string = cfg.GoogleClientId
clientSecret string = cfg.GoogleClientSecret
scope []string = []string{
@ -195,7 +196,9 @@ func (s *Server) Setup() *Server {
// ---- VERSION 2 ROUTES ---- //
router_api_v2 := router_v2.Group(domain.API)
router_api_v2.GET("/recipe/of-the-week", s.GetRecipeOfTheWeekHandler)
router_api_v2.GET("/recipe/of-the-week", s.GetRecipeOfTheWeekHandlerV2)
router_api_v2.GET("/auth/login", s.GetGoogleAuthUrlHandlerV2)
router_api_v2.GET("/auth/callback", s.GoogleCallbackHandlerV2)
return s
}

View File

@ -22,6 +22,7 @@ const WEB_NOT_FOUND = VERSION_1 + WEB + "/404"
// API prefixed routes
const API_AUTH_LOGIN = VERSION_1 + API + "/auth/login"
const API_AUTH_CALLBACK = VERSION_1 + API + "/auth/callback"
const API_AUTH_CALLBACK_V2 = VERSION_2 + API + "/auth/callback"
const API_AUTH_LOGOUT = VERSION_1 + API + "/auth/logout"
const API_CREATE_RECIPE = VERSION_1 + API + "/recipe"
const API_SEARCH_RECIPES = VERSION_1 + API + "/recipe/search"

View File

@ -8,12 +8,17 @@ import Create from './pages/Create';
import Favorites from './pages/Favorites';
import Profile from './pages/Profile';
import ShoppingList from './pages/ShoppingList';
import LoginPage from './pages/Login';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
{/* Login page does not inherit WebLayout */}
<Route path="/v2/web/login" element={<LoginPage />} />
<Route path="/v2/web" element={<WebLayout />}>
<Route index element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
<Route path="home" element={<Home />} />

View File

@ -1,4 +1,4 @@
import { useEffect, useEffectEvent, useState } from "react";
import { useEffect, useState } from "react";
import SalmonVideo from "../assets/videos/salmon_video.mp4";
import Banner from "../components/Banner";
import ROUTE_CONSTANTS from "../types/routes";
@ -8,8 +8,9 @@ import type { Recipe } from "../types/recipe";
import RecipeCardSmall from "../components/cards/RecipeCardSmall";
import ContentCardSmall from "../components/cards/ContentCardSmall";
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
import { GetRecipeOfTheWeek } from "../services/recipeService";
import { GetRecipeOfTheWeek } from "../services/RecipeService";
import { isApiError, type ApiError } from "../types/api/error";
import { useSearchParams } from "react-router-dom";
export default function Home() {
const [loggedIn, isLoggedIn] = useState<boolean>(false);
@ -20,6 +21,8 @@ export default function Home() {
const [error, setError] = useState<string>("");
const [searchParams, setSearchParams] = useSearchParams();
// BUG: Remove these
useEffect(() => {
@ -125,6 +128,14 @@ export default function Home() {
console.error(error);
}, [error]);
useEffect(() => {
if (searchParams.has("token")) {
const token: string = searchParams.get("token")!;
console.log("@token", token);
}
console.log(searchParams);
}, [searchParams]);
return (
<>
{/* Intro Section */}

75
web/src/pages/Login.tsx Normal file
View File

@ -0,0 +1,75 @@
import { useEffect, useState } from "react";
import { GetGoogleAuthUrl } from "../services/AuthService"
import { isApiError, type ApiError } from "../types/api/error"
import { useSearchParams } from "react-router-dom";
export default function LoginPage() {
const [error, setError] = useState<string>("");
const [searchParams, setSearchParams] = useSearchParams();
const clickHandler = async (): Promise<void> => {
const result: string | ApiError = await GetGoogleAuthUrl();
if (isApiError(result)) {
setError(result.message);
return;
}
window.location.href = result;
}
useEffect(() => {
if (error)
console.error(error);
}, [error]);
useEffect(() => {
if (searchParams.has("error")) {
const error: string = searchParams.get("error")!;
setError(error);
}
}, [searchParams]);
// TODO: Implement an error display!
return <>
<div className="h-screen w-full grid place-items-center bg-gray-100">
<div className="w-3/4 sm:w-3/4 md:w-1/2 lg:w-2/7 bg-white border border-gray-200 rounded-xl shadow-2xs">
<div className="p-4 sm:p-7">
<div className="">
<h1 className="block text-2xl font-bold text-gray-800">
Sign in to Continue
</h1>
<p className="mt-2 text-sm text-gray-600">
You need to sign in to continue. Don't have an account? Signing in will
create one for you!
</p>
</div>
<div className="mt-5">
<button onClick={() => { void clickHandler(); }} className="w-full py-3 px-4 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-800 shadow-2xs hover:bg-gray-50 focus:outline-hidden focus:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none cursor-pointer">
<svg className="w-4 h-auto" width="46" height="47" viewBox="0 0 46 47" fill="none">
<path
d="M46 24.0287C46 22.09 45.8533 20.68 45.5013 19.2112H23.4694V27.9356H36.4069C36.1429 30.1094 34.7347 33.37 31.5957 35.5731L31.5663 35.8669L38.5191 41.2719L38.9885 41.3306C43.4477 37.2181 46 31.1669 46 24.0287Z"
fill="#4285F4"
></path>
<path
d="M23.4694 47C29.8061 47 35.1161 44.9144 39.0179 41.3012L31.625 35.5437C29.6301 36.9244 26.9898 37.8937 23.4987 37.8937C17.2793 37.8937 12.0281 33.7812 10.1505 28.1412L9.88649 28.1706L2.61097 33.7812L2.52296 34.0456C6.36608 41.7125 14.287 47 23.4694 47Z"
fill="#34A853"
></path>
<path
d="M10.1212 28.1413C9.62245 26.6725 9.32908 25.1156 9.32908 23.5C9.32908 21.8844 9.62245 20.3275 10.0918 18.8588V18.5356L2.75765 12.8369L2.52296 12.9544C0.909439 16.1269 0 19.7106 0 23.5C0 27.2894 0.909439 30.8731 2.49362 34.0456L10.1212 28.1413Z"
fill="#FBBC05"
></path>
<path
d="M23.4694 9.07688C27.8699 9.07688 30.8622 10.9863 32.5344 12.5725L39.1645 6.11C35.0867 2.32063 29.8061 0 23.4694 0C14.287 0 6.36607 5.2875 2.49362 12.9544L10.0918 18.8588C11.9987 13.1894 17.25 9.07688 23.4694 9.07688Z"
fill="#EB4335"
></path>
</svg>
Sign in with Google
</button>
</div>
</div>
</div>
</div>
</>
}

View File

@ -0,0 +1,18 @@
import axios from "axios";
import type { GetGoogleAuthUrlResponse } from "../types/api/auth";
import type { ApiError } from "../types/api/error";
export async function GetGoogleAuthUrl (): Promise<string | ApiError> {
const response = await axios.get<GetGoogleAuthUrlResponse>("http://localhost:3000/v2/api/auth/login");
if (response.status !== 200) {
const err: ApiError = {
status: response.status,
message: "[FAIL] Something went wrong."
};
return err;
}
return response.data.url;
}

View File

@ -0,0 +1,6 @@
export interface GetGoogleAuthUrlResponse {
status: number;
message: string;
url: string;
}