From d56582d473fc1992671a2b7dcc66ac59102c671d Mon Sep 17 00:00:00 2001 From: MRSIMM0 Date: Sat, 26 Oct 2024 21:17:55 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Api=20integration=20=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/src/api/api.types.ts | 9 +++---- apps/frontend/src/api/config/axios.config.ts | 12 ++++++---- apps/frontend/src/api/useAuthApi.tsx | 25 +++++++++++++------- apps/frontend/src/auth/Guard.tsx | 5 ++-- apps/frontend/src/auth/auth.utils.ts | 4 ++-- apps/frontend/src/layout/Layout.tsx | 10 ++++++-- apps/frontend/src/layout/navbar/Navbar.tsx | 10 ++++---- apps/frontend/src/pages/login/Login.tsx | 5 ++-- apps/frontend/src/store/auth/auth.store.ts | 2 +- 9 files changed, 49 insertions(+), 33 deletions(-) diff --git a/apps/frontend/src/api/api.types.ts b/apps/frontend/src/api/api.types.ts index e10dd19..25e9aaf 100644 --- a/apps/frontend/src/api/api.types.ts +++ b/apps/frontend/src/api/api.types.ts @@ -1,10 +1,7 @@ -export const API_PORT = 5000; -export const API_ADDRESS = `http://localhost:${API_PORT}`; - export enum AUTH_ENDPOINTS { - LOGIN = '/login', - LOGIN_GOOGLE = '/google/login', - CURRENT_USER = '/currentUser', + LOGIN = '/auth/login', + LOGIN_GOOGLE = '/auth/google/login', + CURRENT_USER = '/users/me', } export interface LoginResponse { diff --git a/apps/frontend/src/api/config/axios.config.ts b/apps/frontend/src/api/config/axios.config.ts index 7f4e685..cd34af3 100644 --- a/apps/frontend/src/api/config/axios.config.ts +++ b/apps/frontend/src/api/config/axios.config.ts @@ -1,7 +1,12 @@ import Axios from 'axios'; import {getAccessToken, removeAccessToken} from "../../auth/auth.utils.ts"; +// CONSTRAINTS MOVE TO ENV +export const API_PORT = 5000; +export const API_ADDRESS = `http://localhost:${API_PORT}`; -const axios = Axios.create(); +const axios = Axios.create({ + baseURL: API_ADDRESS, +}); axios.interceptors.request.use((config) => { config.headers.Authorization = `Bearer ${getAccessToken() ?? ''}`; @@ -12,7 +17,6 @@ axios.interceptors.response.use((res) => res, (err) => { if (err.request.status === 401 && !err.request.responseURL.includes('login')) { removeAccessToken(); delete axios.defaults.headers.common.Authorization; - window.location.reload(); } return err; @@ -20,6 +24,4 @@ axios.interceptors.response.use((res) => res, (err) => { export default axios; -// CONSTRAINTS MOVE TO ENV -export const API_PORT = 5000; -export const API_ADDRESS = `http://localhost:${API_PORT}`; + diff --git a/apps/frontend/src/api/useAuthApi.tsx b/apps/frontend/src/api/useAuthApi.tsx index fd80096..fbb3ba8 100644 --- a/apps/frontend/src/api/useAuthApi.tsx +++ b/apps/frontend/src/api/useAuthApi.tsx @@ -21,15 +21,22 @@ const useAuthApi = () => { } ); - const useCurrentUser = () => - useQuery([ - "user", - async () => { - const response = await axios.get(AUTH_ENDPOINTS.CURRENT_USER); - return await handleResponse(response); - } - ] - ); + const useCurrentUser = () => { + return useQuery({ + queryKey: ['user'], + queryFn: async () => { + const response = await axios.get(AUTH_ENDPOINTS.CURRENT_USER); + if (response.status === 200 && response.data) { + return response.data; + } else { + throw new Error('Failed to fetch user data'); + } + }, + staleTime: 1000 * 60 * 5, // 5 minutes + refetchOnWindowFocus: false, + }); + }; + const handleResponse = (res: AxiosResponse): Promise => { return new Promise((resolve, reject) => { diff --git a/apps/frontend/src/auth/Guard.tsx b/apps/frontend/src/auth/Guard.tsx index 60ba631..5b3b0e5 100644 --- a/apps/frontend/src/auth/Guard.tsx +++ b/apps/frontend/src/auth/Guard.tsx @@ -11,18 +11,19 @@ interface GuardProps { const Guard: FC = ({ children }): ReactElement => { const location = useLocation(); const isLogin = location.pathname === Route.LOGIN; - const { isAuthenticated, setIsAuthenticated } = useAuthStore(); + const {isAuthenticated, setIsAuthenticated } = useAuthStore(); const hasCheckedToken = useRef(false); useEffect(() => { if (!isAuthenticated && !hasCheckedToken.current) { hasCheckedToken.current = true; // to prevent loop const tokenIsValid = verifyToken(); + if (tokenIsValid) { setIsAuthenticated(true); } } - }, [isAuthenticated, setIsAuthenticated]); + }, [isAuthenticated]); if (isLogin && isAuthenticated) { return ; diff --git a/apps/frontend/src/auth/auth.utils.ts b/apps/frontend/src/auth/auth.utils.ts index a0fa89f..396dd60 100644 --- a/apps/frontend/src/auth/auth.utils.ts +++ b/apps/frontend/src/auth/auth.utils.ts @@ -1,7 +1,7 @@ import {LoginForm, LoginValidationErrors} from "./auth.types.ts"; export const verifyToken = (): boolean => { - return false; + return getAccessToken != null; } export const getAccessToken = () => { @@ -25,7 +25,7 @@ export const removeAccessToken = () => { export const loginFormValidator = (form: LoginForm) => { const errors: LoginValidationErrors = {}; - const emailRegex = /^[\w-\.]{1,30}@([\w-]+\.)+[\w-]{2,4}$/g; + const emailRegex = /^[\w-]{1,30}@([\w-]+\.)+[\w-]{2,4}$/g; if (!form.email) { errors.email = 'Email is required'; } else if (!emailRegex.test(form.email)) { diff --git a/apps/frontend/src/layout/Layout.tsx b/apps/frontend/src/layout/Layout.tsx index e420028..55c0706 100644 --- a/apps/frontend/src/layout/Layout.tsx +++ b/apps/frontend/src/layout/Layout.tsx @@ -3,6 +3,8 @@ import ScrollableContainer from "./scrollablecontainer/ScrollableContainer.tsx"; import Navbar from "./navbar/Navbar.tsx"; import useThemeStore from "../store/theme/theme.store.ts"; import {Route} from "../router/router.types.ts"; +import useAuthApi from '../api/useAuthApi.tsx'; +import { User } from '../api/api.types.ts'; interface LayoutProps { children?: ReactElement; @@ -21,11 +23,15 @@ const Layout: FC = ({children}): ReactElement => { const { theme } = useThemeStore(); - + + const { useCurrentUser } = useAuthApi(); + + const user = useCurrentUser() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error return (
- {!DISABLE_NAVBAR.includes(route as Route) && } + {!DISABLE_NAVBAR.includes(route as Route) && } {children} diff --git a/apps/frontend/src/layout/navbar/Navbar.tsx b/apps/frontend/src/layout/navbar/Navbar.tsx index 344d1ef..8847cdb 100644 --- a/apps/frontend/src/layout/navbar/Navbar.tsx +++ b/apps/frontend/src/layout/navbar/Navbar.tsx @@ -10,7 +10,7 @@ import { RiMenuFill } from "react-icons/ri"; import { RiCloseFill } from "react-icons/ri"; import { RiCheckboxBlankCircleLine } from "react-icons/ri"; import {Route} from "../../router/router.types.ts"; -import useAuthApi from "../../api/useAuthApi.tsx"; +import { User } from '../../api/api.types.ts'; @@ -50,10 +50,12 @@ const initializeMenu = (): Mode => { else return translateMenuObject(0)!; }; -const Navbar = () => { - const location = window.location; +export interface NavbarProps { + user: User; +} - const user = useAuthApi().useCurrentUser().data +const Navbar = ({user}: NavbarProps) => { + const location = window.location; const [windowWidth, setWindowWidth] = useState(window.innerWidth); const [mode, setMode] = useState(initializeMenu()); diff --git a/apps/frontend/src/pages/login/Login.tsx b/apps/frontend/src/pages/login/Login.tsx index 12cfc93..01502ff 100644 --- a/apps/frontend/src/pages/login/Login.tsx +++ b/apps/frontend/src/pages/login/Login.tsx @@ -6,6 +6,7 @@ import {loginFormValidator} from "../../auth/auth.utils.ts"; import useAuthApi from "../../api/useAuthApi.tsx"; import {useNavigate} from "react-router-dom"; import {toast} from "react-toastify"; +import { Route } from '../../router/router.types.ts'; const initialFormState = { email: '', @@ -42,7 +43,7 @@ const Login: FC = (): ReactElement => { googleQuery.mutateAsync({token: credential}) .then(() => { toast("Login success", { type: 'success' }); - setTimeout(() => { navigate('/home'); }, 2000); + setTimeout(() => { navigate(Route.HOME); }, 2000); }) .catch(() => { toast("Login failed", { type: 'error' }); @@ -63,7 +64,7 @@ const Login: FC = (): ReactElement => { loginQuery.mutateAsync(form) .then(() => { toast("Login success", { type: 'success' }); - setTimeout(() => { navigate('/home'); }, 2000); + setTimeout(() => { navigate(Route.HOME); }, 2000); }) .catch(() => { toast("Login failed", { type: 'error' }); diff --git a/apps/frontend/src/store/auth/auth.store.ts b/apps/frontend/src/store/auth/auth.store.ts index 0e3f7a0..35f4ffa 100644 --- a/apps/frontend/src/store/auth/auth.store.ts +++ b/apps/frontend/src/store/auth/auth.store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { AuthState, AuthStore } from "./auth.store.types.ts"; const initialValues: AuthState = { - isAuthenticated: false, + isAuthenticated: true, } const useAuthStore = create()((set) =>( { From 5e142644a57ad4945188e4851372e60b32edb8ad Mon Sep 17 00:00:00 2001 From: MRSIMM0 Date: Sat, 26 Oct 2024 21:33:53 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Fix=20=F0=9F=92=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend/package-lock.json | 13 +++++++++++-- apps/frontend/package.json | 1 + apps/frontend/src/auth/auth.utils.ts | 15 ++++++++++++++- pnpm-lock.yaml | 21 +++++++++++++++------ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 2159979..fe278eb 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -1,16 +1,17 @@ { - "name": "frontend", + "name": "dodo-frontend", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frontend", + "name": "dodo-frontend", "version": "0.0.0", "dependencies": { "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^4.36.1", "axios": "^1.7.7", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", @@ -2377,6 +2378,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/apps/frontend/package.json b/apps/frontend/package.json index dd27ce4..6fbebcf 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -13,6 +13,7 @@ "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^4.36.1", "axios": "^1.7.7", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-icons": "^5.3.0", diff --git a/apps/frontend/src/auth/auth.utils.ts b/apps/frontend/src/auth/auth.utils.ts index 396dd60..0e5c269 100644 --- a/apps/frontend/src/auth/auth.utils.ts +++ b/apps/frontend/src/auth/auth.utils.ts @@ -1,7 +1,20 @@ import {LoginForm, LoginValidationErrors} from "./auth.types.ts"; +import { jwtDecode } from 'jwt-decode'; export const verifyToken = (): boolean => { - return getAccessToken != null; + const token = getAccessToken(); + if (!token) { + return false; + } + + try { + const decodedToken = jwtDecode(token); + const currentTimestamp = Math.floor(Date.now() / 1000); + return (decodedToken.exp ?? 0) > currentTimestamp; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return false; + } } export const getAccessToken = () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef4a867..a42648f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,10 +65,10 @@ importers: version: 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5) '@nestjs/swagger': specifier: ^7.4.0 - version: 7.4.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.4.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^10.0.2 - version: 10.0.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@swc/core@1.7.36)(@types/node@20.16.13)(typescript@5.6.3))) + version: 10.0.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@swc/core@1.7.36)(@types/node@20.16.13)(typescript@5.6.3))) '@types/passport-jwt': specifier: ^4.0.1 version: 4.0.1 @@ -117,7 +117,7 @@ importers: version: 10.2.2(chokidar@3.6.0)(typescript@5.6.3) '@nestjs/testing': specifier: ^10.0.0 - version: 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)) + version: 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(@nestjs/platform-express@10.4.5) '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -184,6 +184,9 @@ importers: axios: specifier: ^1.7.7 version: 1.7.7 + jwt-decode: + specifier: ^4.0.0 + version: 4.0.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -2965,6 +2968,10 @@ packages: jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -5047,7 +5054,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@7.4.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@7.4.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.15.0 '@nestjs/common': 10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5062,7 +5069,7 @@ snapshots: class-transformer: 0.5.1 class-validator: 0.14.1 - '@nestjs/testing@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5))': + '@nestjs/testing@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(@nestjs/platform-express@10.4.5)': dependencies: '@nestjs/common': 10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -5070,7 +5077,7 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5) - '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1))(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@swc/core@1.7.36)(@types/node@20.16.13)(typescript@5.6.3)))': + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.3)(ts-node@10.9.2(@swc/core@1.7.36)(@types/node@20.16.13)(typescript@5.6.3)))': dependencies: '@nestjs/common': 10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/core': 10.4.5(@nestjs/common@10.4.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.5)(reflect-metadata@0.2.2)(rxjs@7.8.1) @@ -7582,6 +7589,8 @@ snapshots: jwa: 2.0.0 safe-buffer: 5.2.1 + jwt-decode@4.0.0: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1