(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:
parent
7df879b04a
commit
25ea3fcfd7
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) {
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 />} />
|
||||
|
||||
@ -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
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