(FEAT): Working on auth still

This commit is contained in:
Hayden Hargreaves 2025-11-14 22:33:54 -07:00
parent 1749a91bf9
commit 3177a4d089
17 changed files with 156 additions and 107 deletions

View File

@ -38,3 +38,10 @@ func (s *Server) GoogleCallbackHandlerV2(ctx *gin.Context) {
ctx.Redirect(http.StatusSeeOther, url)
}
}
// BUG: This is not working, not yet sure why
func (s *Server) LogoutHandlerV2(ctx *gin.Context) {
s.SetCookie(ctx, "jwt_token", "", -1)
// s.SetCookie(ctx, "search-filters", "", -1) // TODO: This was copied, might function differently now
ctx.Status(http.StatusNoContent)
}

View File

@ -20,8 +20,8 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
path string = "/"
httpOnly bool = false // NOTE: Should use false so React can see it!
maxAge int
secure bool
domain string
secure bool = false
domain string = ""
)
if duration < 0 {
@ -43,11 +43,6 @@ func (s *Server) SetCookie(ctx *gin.Context, name, value string, duration time.D
secure = false
// domain = s.deps.EnvironmentConfig.Domain
domain = "localhost"
} else {
// Defaults
secure = false
domain = ""
}
ctx.SetCookie(name, value, maxAge, path, domain, secure, httpOnly)

View File

@ -9,6 +9,13 @@ import (
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
)
// JwtAuthMiddlewareV2 is responsible to protecting routes. Anything that may go wrong
// will be returned via JSON with a 'message' field and a 401 error code. When this
// middleware is successful, it will set the 'userId' and 'userEmail' fields and pass
// to the next function in the chain.
//
// Functions that are called after this can assume that those values defined are always
// set.
func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString, err := ctx.Cookie("jwt_token")

View File

@ -204,6 +204,9 @@ func (s *Server) Setup() *Server {
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)
router_api_v2.GET("/auth/logout", s.LogoutHandlerV2)
router_api_v2.GET("/user", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenticatedUserHandlerV2)
router_api_v2.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"})

View File

@ -0,0 +1,25 @@
package server
import (
"net/http"
"github.com/gin-gonic/gin"
)
func (s *Server) GetAuthenticatedUserHandlerV2(ctx *gin.Context) {
user := s.deps.UserService.GetAuthenicatedUser(ctx)
if user == nil {
ctx.JSON(http.StatusUnauthorized, gin.H{
"status": http.StatusUnauthorized,
"message": "[UNAUTHORIZED] Could not fetch authenticated user.",
})
return
}
ctx.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"message": "[OK] Successfully retrieved authenticated user.",
"user": user,
})
}

View File

@ -1,73 +1,4 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
# IF BACKEND CANNOT GET COOKIE
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is currently not compatible with SWC. See [this issue](https://github.com/vitejs/vite-plugin-react/issues/428) for tracking the progress.
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
Do not forget to send the axios request with the `{ withCredentials: true }` flags.

View File

@ -11,7 +11,7 @@ export default function Navigation() {
return (
<>
<nav className="block md:fixed w-full z-10">
<nav className="block md:fixed w-full z-20">
<div
className="relative w-full px-8 md:px-44 p-4 border-b border-gray-300 shadow-sm shadow-gray-300 bg-white flex justify-between items-center"
>

View File

@ -3,6 +3,11 @@ import { createContext } from "react";
interface AuthContextType {
isLoggedIn: boolean | undefined;
setIsLoggedIn: (state: boolean) => void;
getJwt: () => string;
}
export const AuthContext = createContext<AuthContextType>({ isLoggedIn: undefined, setIsLoggedIn: () => { return } });
export const AuthContext = createContext<AuthContextType>({
isLoggedIn: undefined,
setIsLoggedIn: () => { return },
getJwt: () => ""
});

View File

@ -4,10 +4,19 @@ import { useCookies } from 'react-cookie';
// BUG: The rerender issue is ridiclious, and needs to be updated. Maybe using a global
// state management tool instead of a context
//
// BUG: We do not want to have access to these cookies in the UI, for security reasons.
// Instead, we should implement an api `/auth/status` can be requested to validate
// a token. Not sure how often this should get called, but it should be implemented.
export function AuthProvider({ children }: { children: ReactNode }) {
const [cookies] = useCookies(["jwt_token"]);
const [isLoggedIn, setIsLoggedIn] = useState<boolean | undefined>(cookies.jwt_token !== undefined);
const getJwt = (): string => {
if (!cookies.jwt_token) return "";
return cookies.jwt_token as string;
}
useEffect(() => {
setIsLoggedIn(cookies.jwt_token !== undefined);
}, [cookies]);
@ -18,7 +27,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
// }
return (
<AuthContext value={{ isLoggedIn, setIsLoggedIn }}>
<AuthContext value={{ isLoggedIn, setIsLoggedIn, getJwt }}>
{children}
</AuthContext>
);

View File

@ -0,0 +1,2 @@
export function ProtectedRoute

View File

@ -10,7 +10,6 @@ import ContentCardSmall from "../components/cards/ContentCardSmall";
import RecipeSearchBar from "../components/inputs/RecipeSearchBar";
import { GetRecipeOfTheWeek } from "../services/RecipeService";
import { isApiError, type ApiError } from "../types/api/error";
import axios from "axios";
import { AuthContext } from "../context/AuthContext";
export default function Home() {
@ -113,6 +112,8 @@ export default function Home() {
setViewedRecipes(recipes);
}, []);
// TODO: Fetch other items when needed
// Fetch the recipe of the week
useEffect(() => {
async function fetch() {
const result: Recipe | ApiError = await GetRecipeOfTheWeek();
@ -131,13 +132,6 @@ export default function Home() {
console.error(error);
}, [error]);
// BUG: This is useless, just for testing
useEffect(() => {
// NOTE: Be sure to call this WITH CREDENTIALS
void axios.get("http://localhost:3000/v2/api/protected", { withCredentials: true });
}, []);
return (
<>
{/* Intro Section */}

View File

@ -1,16 +1,29 @@
import { useEffect, useState } from "react";
import { use, useEffect, useState } from "react";
import type { User } from "../types/user";
import type { Recipe } from "../types/recipe";
import RecipeListItem from "../components/results/RecipeListItem";
import type { Engagement } from "../types/engagement";
import ActivityListItem from "../components/results/ActivityListItem";
import { AuthContext } from "../context/AuthContext";
import { GetAuthenticatedUser } from "../services/UserService";
import { isApiError, type ApiError } from "../types/api/error";
import { Logout } from "../services/AuthService";
import { useNavigate } from "react-router-dom";
export default function Profile() {
// Context
const { getJwt } = use(AuthContext);
const navigate = useNavigate();
// Page state
const [error, setError] = useState<string>("");
const [user, setUser] = useState<User | null>(null);
const [recipes, setRecipes] = useState<Recipe[]>([]);
const [favorites, setFavorites] = useState<Recipe[]>([]);
const [activity, setActivity] = useState<Engagement[]>([]);
const [jwt, setJwt] = useState<string>("");
// BUG: Remove this, used for testing
useEffect(() => {
const recipe: Recipe = {
Id: 1,
@ -99,23 +112,43 @@ export default function Profile() {
Created: new Date(),
};
const user: User = {
Id: 1,
GoogleId: "a",
Name: "Hayden Hargreaves",
Email: "hhargreaves2006@gmail.com",
ImageUrl: "https://lh3.googleusercontent.com/a/ACg8ocLeT6ltjQIkiBy1MgMJDbQxtBfMVfn8sP4e1t7d0bCJeHFdpcea=s96-c",
GoogleRefreshToken: "a",
Created: new Date(),
};
setUser(user);
setRecipes([recipe, recipe2, recipe, recipe, recipe, recipe, recipe]);
setRecipes([recipe, recipe2]);
setFavorites([recipe, recipe2]);
setActivity([eng]);
}, []);
// Log the user out and direct to the home page
const logoutHandler = (): void => {
void Logout();
void navigate("/v2/web/home");
}
// Get the JWT from the cookies
useEffect(() => {
setJwt(getJwt());
}, [getJwt]);
// Get the user when the JWTS change
useEffect(() => {
// No jwt, we can't get a user
if (!jwt) return;
async function fetch() {
const result: User | ApiError = await GetAuthenticatedUser();
if (isApiError(result)) {
setError(result.message);
return;
}
setUser(result);
}
void fetch();
}, [jwt]);
useEffect(() => {
if (error)
console.log("@error", error);
}, [error]);
return (
<>
{/* User Details Section */}
@ -195,9 +228,9 @@ export default function Profile() {
{/* Logout Section TODO: Click event*/}
<section className="w-full flex flex-col justify-center items-center py-8 border-t border-gray-300 mt-auto">
<a href="" className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
<button onClick={logoutHandler} className="text-center border border-red-500 text-red-500 w-9/10 md:w-1/3 py-2 rounded-lg hover:cursor-pointer hover:bg-red-100 duration-300">
Logout
</a>
</button>
</section>
</>
);

View File

@ -1,5 +1,5 @@
import axios from "axios";
import type { GetGoogleAuthUrlResponse } from "../types/api/auth";
import type { GetGoogleAuthUrlResponse, LogoutResponse } from "../types/api/auth";
import type { ApiError } from "../types/api/error";
@ -16,3 +16,11 @@ export async function GetGoogleAuthUrl (): Promise<string | ApiError> {
return response.data.url;
}
export async function Logout (): Promise<void> {
const response = await axios.get<LogoutResponse>("http://localhost:3000/v2/api/auth/logout");
// This should never happen
if (response.status !== 204)
console.error("LOGOUT FAILED");
}

View File

@ -0,0 +1,19 @@
import axios from "axios";
import type { ApiError } from "../types/api/error";
import type { User } from "../types/user";
import type { GetAuthenticateUserResponse } from "../types/api/user";
export async function GetAuthenticatedUser(): Promise<User | ApiError> {
const response = await axios.get<GetAuthenticateUserResponse>("http://localhost:3000/v2/api/user", { withCredentials: true });
if (response.data.status !== 200 || response.data.user === undefined){
const err: ApiError = {
status: response.data.status,
message: response.data.message
};
return err;
}
return response.data.user;
}

View File

@ -4,3 +4,7 @@ export interface GetGoogleAuthUrlResponse {
message: string;
url: string;
}
export interface LogoutResponse {
stauts: number;
}

View File

@ -0,0 +1,7 @@
import type { User } from "../user";
export interface GetAuthenticateUserResponse {
status: number;
message: string;
user?: User;
}