Merging in the React Refactor #56
39
internal/app/server/auth_handler_v2.go
Normal file
39
internal/app/server/auth_handler_v2.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,8 +8,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func (s *Server) GetRecipeOfTheWeekHandler(ctx *gin.Context) {
|
// GetRecipeOfTheWeekHandler fetchs the current recipe of the week and returns it.
|
||||||
// BUG: This needs to be different
|
// 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
|
userId := 1
|
||||||
|
|
||||||
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(&userId)
|
recipe, err := s.deps.RecipeService.GetRecipeOfTheWeek(&userId)
|
||||||
@ -17,14 +22,14 @@ func (s *Server) GetRecipeOfTheWeekHandler(ctx *gin.Context) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.JSON(http.StatusBadRequest, gin.H{
|
ctx.JSON(http.StatusBadRequest, gin.H{
|
||||||
"status": http.StatusBadRequest,
|
"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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.JSON(http.StatusOK, gin.H{
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
"status": http.StatusOK,
|
"status": http.StatusOK,
|
||||||
"message": "[OK] Successfully retrieved recipe of the week.\n",
|
"message": "[OK] Successfully retrieved recipe of the week.",
|
||||||
"recipe": recipe,
|
"recipe": recipe,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,8 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
// SETUP GOOGLE AUTH
|
// SETUP GOOGLE AUTH
|
||||||
var (
|
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
|
clientId string = cfg.GoogleClientId
|
||||||
clientSecret string = cfg.GoogleClientSecret
|
clientSecret string = cfg.GoogleClientSecret
|
||||||
scope []string = []string{
|
scope []string = []string{
|
||||||
@ -195,7 +196,9 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
// ---- VERSION 2 ROUTES ---- //
|
// ---- VERSION 2 ROUTES ---- //
|
||||||
router_api_v2 := router_v2.Group(domain.API)
|
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
|
return s
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ const WEB_NOT_FOUND = VERSION_1 + WEB + "/404"
|
|||||||
// API prefixed routes
|
// API prefixed routes
|
||||||
const API_AUTH_LOGIN = VERSION_1 + API + "/auth/login"
|
const API_AUTH_LOGIN = VERSION_1 + API + "/auth/login"
|
||||||
const API_AUTH_CALLBACK = VERSION_1 + API + "/auth/callback"
|
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_AUTH_LOGOUT = VERSION_1 + API + "/auth/logout"
|
||||||
const API_CREATE_RECIPE = VERSION_1 + API + "/recipe"
|
const API_CREATE_RECIPE = VERSION_1 + API + "/recipe"
|
||||||
const API_SEARCH_RECIPES = VERSION_1 + API + "/recipe/search"
|
const API_SEARCH_RECIPES = VERSION_1 + API + "/recipe/search"
|
||||||
|
|||||||
@ -8,12 +8,17 @@ import Create from './pages/Create';
|
|||||||
import Favorites from './pages/Favorites';
|
import Favorites from './pages/Favorites';
|
||||||
import Profile from './pages/Profile';
|
import Profile from './pages/Profile';
|
||||||
import ShoppingList from './pages/ShoppingList';
|
import ShoppingList from './pages/ShoppingList';
|
||||||
|
import LoginPage from './pages/Login';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to={ROUTE_CONSTANTS.Home} replace />} />
|
<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 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 />} />
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useEffectEvent, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import SalmonVideo from "../assets/videos/salmon_video.mp4";
|
import SalmonVideo from "../assets/videos/salmon_video.mp4";
|
||||||
import Banner from "../components/Banner";
|
import Banner from "../components/Banner";
|
||||||
import ROUTE_CONSTANTS from "../types/routes";
|
import ROUTE_CONSTANTS from "../types/routes";
|
||||||
@ -8,8 +8,9 @@ import type { Recipe } from "../types/recipe";
|
|||||||
import RecipeCardSmall from "../components/cards/RecipeCardSmall";
|
import RecipeCardSmall from "../components/cards/RecipeCardSmall";
|
||||||
import ContentCardSmall from "../components/cards/ContentCardSmall";
|
import ContentCardSmall from "../components/cards/ContentCardSmall";
|
||||||
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
|
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 { isApiError, type ApiError } from "../types/api/error";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [loggedIn, isLoggedIn] = useState<boolean>(false);
|
const [loggedIn, isLoggedIn] = useState<boolean>(false);
|
||||||
@ -20,6 +21,8 @@ export default function Home() {
|
|||||||
|
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
|
||||||
// BUG: Remove these
|
// BUG: Remove these
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -125,6 +128,14 @@ export default function Home() {
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
}, [error]);
|
}, [error]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchParams.has("token")) {
|
||||||
|
const token: string = searchParams.get("token")!;
|
||||||
|
console.log("@token", token);
|
||||||
|
}
|
||||||
|
console.log(searchParams);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Intro Section */}
|
{/* Intro Section */}
|
||||||
|
|||||||
75
web/src/pages/Login.tsx
Normal file
75
web/src/pages/Login.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
18
web/src/services/AuthService.ts
Normal file
18
web/src/services/AuthService.ts
Normal 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;
|
||||||
|
}
|
||||||
6
web/src/types/api/auth.ts
Normal file
6
web/src/types/api/auth.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
export interface GetGoogleAuthUrlResponse {
|
||||||
|
status: number;
|
||||||
|
message: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user