diff --git a/internal/app/server/auth_handler_v2.go b/internal/app/server/auth_handler_v2.go new file mode 100644 index 0000000..5363c2f --- /dev/null +++ b/internal/app/server/auth_handler_v2.go @@ -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) + } +} diff --git a/internal/app/server/recipe_handler_v2.go b/internal/app/server/recipe_handler_v2.go index 986ea2c..d38e553 100644 --- a/internal/app/server/recipe_handler_v2.go +++ b/internal/app/server/recipe_handler_v2.go @@ -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, }) } diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 0e1bcee..ed8e92a 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -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 } diff --git a/internal/domain/server/routes.go b/internal/domain/server/routes.go index 94c4525..c65f1af 100644 --- a/internal/domain/server/routes.go +++ b/internal/domain/server/routes.go @@ -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" diff --git a/web/src/App.tsx b/web/src/App.tsx index 6c7ca47..e5dace2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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 ( } /> + + {/* Login page does not inherit WebLayout */} + } /> + }> } /> } /> diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 576cd14..3738088 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -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(false); @@ -20,6 +21,8 @@ export default function Home() { const [error, setError] = useState(""); + 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 */} diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx new file mode 100644 index 0000000..19d088f --- /dev/null +++ b/web/src/pages/Login.tsx @@ -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(""); + const [searchParams, setSearchParams] = useSearchParams(); + + const clickHandler = async (): Promise => { + 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 <> +
+
+
+
+

+ Sign in to Continue +

+

+ You need to sign in to continue. Don't have an account? Signing in will + create one for you! +

+
+
+ +
+
+
+
+ +} diff --git a/web/src/services/AuthService.ts b/web/src/services/AuthService.ts new file mode 100644 index 0000000..c7afff4 --- /dev/null +++ b/web/src/services/AuthService.ts @@ -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 { + const response = await axios.get("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; +} diff --git a/web/src/services/recipeService.ts b/web/src/services/RecipeService.ts similarity index 100% rename from web/src/services/recipeService.ts rename to web/src/services/RecipeService.ts diff --git a/web/src/types/api/auth.ts b/web/src/types/api/auth.ts new file mode 100644 index 0000000..8acc4c6 --- /dev/null +++ b/web/src/types/api/auth.ts @@ -0,0 +1,6 @@ + +export interface GetGoogleAuthUrlResponse { + status: number; + message: string; + url: string; +}