(FEAT): Working on auth still
This commit is contained in:
parent
1749a91bf9
commit
3177a4d089
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"})
|
||||
|
||||
25
internal/app/server/user_handler_v2.go
Normal file
25
internal/app/server/user_handler_v2.go
Normal 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,
|
||||
})
|
||||
}
|
||||
@ -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.
|
||||
|
||||
@ -21,7 +21,7 @@ function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||
}
|
||||
|
||||
if (isLoggedIn) return children;
|
||||
|
||||
|
||||
// Redirect to login page if not authenicated
|
||||
return <Navigate to="/v2/web/login" replace />
|
||||
}
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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: () => ""
|
||||
});
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
2
web/src/context/ProtectedRoute.tsx
Normal file
2
web/src/context/ProtectedRoute.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
export function ProtectedRoute
|
||||
@ -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 */}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
19
web/src/services/UserService.ts
Normal file
19
web/src/services/UserService.ts
Normal 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;
|
||||
}
|
||||
@ -4,3 +4,7 @@ export interface GetGoogleAuthUrlResponse {
|
||||
message: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface LogoutResponse {
|
||||
stauts: number;
|
||||
}
|
||||
|
||||
7
web/src/types/api/user.ts
Normal file
7
web/src/types/api/user.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { User } from "../user";
|
||||
|
||||
export interface GetAuthenticateUserResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
user?: User;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user