diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1975f68ea..ed498a482 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,9 +8,11 @@ "name": "argentbank", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.1.0", "proptype": "^1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^9.1.0", "react-router-dom": "^6.22.0" }, "devDependencies": { @@ -525,6 +527,29 @@ "node": ">= 8" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz", + "integrity": "sha512-nfJ/b4ZhzUevQ1ZPKjlDL6CMYxO4o7ZL7OSsvSOxzT/EN11LsBDgTqP7aedHtBrFSVoK7oTP1SbMWUwGb30NLg==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.0.tgz", @@ -922,13 +947,13 @@ "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true + "devOptional": true }, "node_modules/@types/react": { "version": "18.2.55", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz", "integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -948,7 +973,12 @@ "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true + "devOptional": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", @@ -1279,7 +1309,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "devOptional": true }, "node_modules/debug": { "version": "4.3.4", @@ -2072,6 +2102,15 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -2932,6 +2971,32 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/react-redux": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.0.tgz", + "integrity": "sha512-6qoDzIO+gbrza8h3hjMA9aq4nwVFCKFtY2iLxCtVT38Swyy2C/dJCGBXHeHLtx6qlg/8qzc2MrhOeduf5K32wQ==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "react-native": ">=0.69", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-native": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.0.tgz", @@ -2962,6 +3027,19 @@ "react-dom": ">=16.8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz", @@ -3005,6 +3083,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", + "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -3474,6 +3557,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/vite": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index 97729f9cd..cbc30d807 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,9 +10,11 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.1.0", "proptype": "^1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-redux": "^9.1.0", "react-router-dom": "^6.22.0" }, "devDependencies": { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7fcfab372..83299b142 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import Navbar from './layout/NavBar'; import Home from './pages/Home'; import Login from './pages/Login'; import Profile from './pages/Profile'; +import PrivateRoute from './components/PrivateRoute'; const router = createBrowserRouter([ { @@ -19,7 +20,7 @@ const router = createBrowserRouter([ children: [ { path: '/', element: }, { path: '/login', element: }, - { path: '/profile', element: }, + { path: '/profile', element: }, // { path: '/transactions', element: }, ], }, diff --git a/frontend/src/components/PrivateRoute.jsx b/frontend/src/components/PrivateRoute.jsx new file mode 100644 index 000000000..c9c9af02f --- /dev/null +++ b/frontend/src/components/PrivateRoute.jsx @@ -0,0 +1,16 @@ +import { useSelector } from 'react-redux'; +import { Navigate } from 'react-router-dom'; +import PropTypes from 'prop-types'; + +// PrivateRoute component used to protect routes that require authentication +const PrivateRoute = ({ component: Component }) => { + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); + + return isAuthenticated ? : ; +}; + +PrivateRoute.propTypes = { + component: PropTypes.elementType.isRequired, +}; + +export default PrivateRoute; diff --git a/frontend/src/layout/NavBar.jsx b/frontend/src/layout/NavBar.jsx index b6ac8afda..fdec75448 100644 --- a/frontend/src/layout/NavBar.jsx +++ b/frontend/src/layout/NavBar.jsx @@ -1,7 +1,19 @@ import { NavLink } from 'react-router-dom'; import argBankLogo from '../assets/img/argentBankLogo.png'; +import { useDispatch, useSelector } from 'react-redux'; +import { authenticate } from '../service/user/userApi'; const Navbar = () => { + const dispatch = useDispatch(); + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); + + const handleAuth = () => { + if (isAuthenticated) { + localStorage.removeItem('token'); // Remove the token from local storage + dispatch(authenticate.rejected()); // Dispatch the authSlice action + } + }; + return ( diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 51a8c5825..2f6065579 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,9 +1,13 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App.jsx' +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.jsx'; +import { Provider } from 'react-redux'; +import { store } from './service/store.js'; ReactDOM.createRoot(document.getElementById('root')).render( - - , -) + + + + +); diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 3f94114f6..98366039f 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,30 +1,25 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { useNavigate } from 'react-router-dom'; +import { authenticate } from '../service/user/userApi'; const Login = () => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); + const dispatch = useDispatch(); const navigate = useNavigate(); + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); const handleSubmit = async (e) => { e.preventDefault(); + dispatch(authenticate({ email, password })); + }; - const response = await fetch('http://localhost:3001/api/v1/user/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }); - - const data = await response.json(); - - if (response.ok) { - localStorage.setItem('token', data.body.token); - console.log(data.message); + useEffect(() => { + if (isAuthenticated) { navigate('/profile'); - } else { - window.alert(data.message); } - }; + }, [isAuthenticated, navigate]); return (
diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index 3157e0416..ba8c58a59 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -1,5 +1,4 @@ const Profile = () => { - // const { user } = useAuth(); return (
diff --git a/frontend/src/service/store.js b/frontend/src/service/store.js new file mode 100644 index 000000000..fbd47fbc8 --- /dev/null +++ b/frontend/src/service/store.js @@ -0,0 +1,17 @@ +import { configureStore } from '@reduxjs/toolkit'; +import authSlice from '../service/user/userSlice'; + +// The value of isAuthenticated is determined by +// whether there is a 'token' item in the local storage. +const preloadedState = { + auth: { + isAuthenticated: !!localStorage.getItem('token'), + }, +}; + +export const store = configureStore({ + reducer: { + auth: authSlice, + }, + preloadedState, +}); diff --git a/frontend/src/service/user/userApi.js b/frontend/src/service/user/userApi.js new file mode 100644 index 000000000..459fa2f79 --- /dev/null +++ b/frontend/src/service/user/userApi.js @@ -0,0 +1,24 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; + +// This async thunk dispatches actions to authenticate a user. +export const authenticate = createAsyncThunk( + 'user/authenticate', + async ({ email, password }) => { + const response = await fetch('http://localhost:3001/api/v1/user/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + if (response.ok) { + localStorage.setItem('token', data.body.token); + console.log(data.message); + return data; + } else { + window.alert(data.message); + throw new Error(data.message); + } + } +); diff --git a/frontend/src/service/user/userSlice.js b/frontend/src/service/user/userSlice.js new file mode 100644 index 000000000..254e2dbe3 --- /dev/null +++ b/frontend/src/service/user/userSlice.js @@ -0,0 +1,21 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { authenticate } from './userApi'; + +// The authSlice reducer manages the state of the user's authentication status. +const authSlice = createSlice({ + name: 'auth', + initialState: { isAuthenticated: false }, + reducers: {}, + // The authenticate.fulfilled action changes the value of isAuthenticated to true. + extraReducers: (builder) => { + builder + .addCase(authenticate.fulfilled, (state) => { + state.isAuthenticated = true; + }) + .addCase(authenticate.rejected, (state) => { + state.isAuthenticated = false; + }); + }, +}); + +export default authSlice.reducer;