From a727f02248b45f7695db684505643519c933f438 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 27 Feb 2024 12:54:37 +0100 Subject: [PATCH 01/19] feat: add initial version of auth in webapp (untested) --- webapp/src/components/auth/Auth.js | 16 ++++++++++++++++ webapp/src/components/auth/AuthUtils.js | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 webapp/src/components/auth/Auth.js create mode 100644 webapp/src/components/auth/AuthUtils.js diff --git a/webapp/src/components/auth/Auth.js b/webapp/src/components/auth/Auth.js new file mode 100644 index 00000000..bf0b2468 --- /dev/null +++ b/webapp/src/components/auth/Auth.js @@ -0,0 +1,16 @@ +import React, { createContext, useState } from "react"; + +export const authContext = createContext({ + jwt: null, + setJwt: (newJwt) => {} +}); + +export function authProvider({children}) { + + const [jwt, setJwt] = useState(null); + + return + {children} + +} + diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js new file mode 100644 index 00000000..6c0e0e43 --- /dev/null +++ b/webapp/src/components/auth/AuthUtils.js @@ -0,0 +1,16 @@ +import { useContext } from "react"; +import { authContext } from "./Auth"; +import axios from "axios"; + +export function isLoggedIn() { + const context = useContext(authContext); + + return context.jwt != null; +} + +export async function logIn(loginData) { + const requestAnswer = await axios.post(process.env.API_URL + + process.env.LOGIN_ENDPOINT, loginData); + + // TODO: Compare response with the status code +} \ No newline at end of file From 95227baa44a03fe1d12f2339434ea32aa3d92c29 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Fri, 1 Mar 2024 20:25:19 +0100 Subject: [PATCH 02/19] chore: updated .gitignore to include .env --- webapp/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/webapp/.gitignore b/webapp/.gitignore index 4d29575d..8692cf66 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -13,6 +13,7 @@ # misc .DS_Store +.env .env.local .env.development.local .env.test.local From a91f8f4ec6e69fccc4e3931ca520cc0affe5f3a4 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Fri, 1 Mar 2024 20:26:07 +0100 Subject: [PATCH 03/19] feat: add tentatively working support for login (cannot continue due to API bug) --- webapp/.env | 1 - webapp/src/components/auth/Auth.js | 2 +- webapp/src/components/auth/AuthUtils.js | 17 +-- webapp/src/index.js | 1 - webapp/src/pages/Login.jsx | 151 +++++++++++++----------- 5 files changed, 90 insertions(+), 82 deletions(-) delete mode 100644 webapp/.env diff --git a/webapp/.env b/webapp/.env deleted file mode 100644 index c810bde7..00000000 --- a/webapp/.env +++ /dev/null @@ -1 +0,0 @@ -REACT_APP_API_ENDPOINT=http://localhost:8000 \ No newline at end of file diff --git a/webapp/src/components/auth/Auth.js b/webapp/src/components/auth/Auth.js index bf0b2468..20a53e9a 100644 --- a/webapp/src/components/auth/Auth.js +++ b/webapp/src/components/auth/Auth.js @@ -5,7 +5,7 @@ export const authContext = createContext({ setJwt: (newJwt) => {} }); -export function authProvider({children}) { +export function AuthProvider({children}) { const [jwt, setJwt] = useState(null); diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index 6c0e0e43..7325ed0d 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -2,15 +2,18 @@ import { useContext } from "react"; import { authContext } from "./Auth"; import axios from "axios"; -export function isLoggedIn() { +export function useLoggedState() { const context = useContext(authContext); - return context.jwt != null; } -export async function logIn(loginData) { - const requestAnswer = await axios.post(process.env.API_URL + - process.env.LOGIN_ENDPOINT, loginData); - - // TODO: Compare response with the status code +export async function logIn(loginData, onSuccess, onError) { + const url = process.env.REACT_APP_API_ENDPOINT + process.env.REACT_APP_LOGIN_ENDPOINT; + const requestAnswer = await axios.post(url, loginData); + + return { + status: requestAnswer.status, + token: requestAnswer.data.token, + refreshToken: requestAnswer.data.refreshToken + }; } \ No newline at end of file diff --git a/webapp/src/index.js b/webapp/src/index.js index 2a34dfff..798dfd4f 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -5,7 +5,6 @@ import reportWebVitals from './reportWebVitals'; import {createBrowserRouter, RouterProvider} from 'react-router-dom'; import router from 'components/Router'; import { ChakraProvider } from '@chakra-ui/react'; - import "./i18n"; const root = ReactDOM.createRoot(document.querySelector("body")); diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index 8850e3e9..23ab5602 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -1,73 +1,80 @@ -import { Center } from "@chakra-ui/layout"; -import { Heading, Input, Button, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, Text } from "@chakra-ui/react"; -import axios, { HttpStatusCode } from "axios"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { FaLock, FaAddressCard } from "react-icons/fa"; -import ButtonEf from '../components/ButtonEf'; -import '../styles/AppView.css'; - -export default function Login() { - - const [hasError, setHasError] = useState(false); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const [showPassword, setShowPassword] = useState(false); - const changeShowP = () => setShowPassword(!showPassword); - - const ChakraFaCardAlt = chakra(FaAddressCard); - const ChakraFaLock = chakra(FaLock); - - const sendLogin = async () => { - let data = {}; - let response = await axios.post(process.env.API_URL, data); - if (response.status === HttpStatusCode.Accepted) { - navigate("/home"); - } else { - setHasError(true); - } - } - - return ( -
- - - { t("common.login")} - { - !hasError ? - <> : -
- Error -
- } - - - - - }/> - - - - - - }/> - - - - - - - - - -
-
- ); +import { Center } from "@chakra-ui/layout"; +import { Heading, Input, Button, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, Text } from "@chakra-ui/react"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { FaLock, FaAddressCard } from "react-icons/fa"; +import ButtonEf from '../components/ButtonEf'; +import '../styles/AppView.css'; +import {logIn} from "../components/auth/AuthUtils"; +import {HttpStatusCode} from "axios"; + +export default function Login() { + + const [hasError, setHasError] = useState(false); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const [showPassword, setShowPassword] = useState(false); + const changeShowP = () => setShowPassword(!showPassword); + + const ChakraFaCardAlt = chakra(FaAddressCard); + const ChakraFaLock = chakra(FaLock); + + const sendLogin = () => { + let data = { + "email": document.getElementById("user").value, + "password": document.getElementById("password").value + }; + logIn(data).then(result => { + if (result.status === HttpStatusCode.Ok) { + localStorage.setItem('token', result.token); + localStorage.setItem('refreshToken', result.refreshToken); + navigate("/dashboard"); + } else { + setHasError(true); + } + }) + } + + return ( +
+ + + { t("common.login")} + { + !hasError ? + <> : +
+ Error +
+ } + + + + + }/> + + + + + + }/> + + + + + + + + + +
+
+ ); } \ No newline at end of file From f3bc9761d4d9faff00e9b8ad63179804951bffd8 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sat, 2 Mar 2024 19:52:55 +0100 Subject: [PATCH 04/19] chore: some refactor, as well as minor error fix --- webapp/src/components/auth/AuthUtils.js | 2 +- webapp/src/i18n.js | 2 +- webapp/src/index.js | 2 ++ webapp/src/pages/Login.jsx | 9 +++++++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index 7325ed0d..a882bfc9 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -14,6 +14,6 @@ export async function logIn(loginData, onSuccess, onError) { return { status: requestAnswer.status, token: requestAnswer.data.token, - refreshToken: requestAnswer.data.refreshToken + refreshToken: requestAnswer.data.refresh_Token }; } \ No newline at end of file diff --git a/webapp/src/i18n.js b/webapp/src/i18n.js index a57b1175..a9a6fdb1 100644 --- a/webapp/src/i18n.js +++ b/webapp/src/i18n.js @@ -9,5 +9,5 @@ export default i18n.use(Backend) .use(initReactI18next) .init({ fallbackLng: "en", - debug: true + debug: false }) \ No newline at end of file diff --git a/webapp/src/index.js b/webapp/src/index.js index 798dfd4f..fd2d1dc6 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -6,9 +6,11 @@ import {createBrowserRouter, RouterProvider} from 'react-router-dom'; import router from 'components/Router'; import { ChakraProvider } from '@chakra-ui/react'; import "./i18n"; +import axios from "axios"; const root = ReactDOM.createRoot(document.querySelector("body")); const browserRouter = createBrowserRouter(router); +axios.defaults.headers.post["Content-Type"] = "application/json"; root.render( diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index 23ab5602..19bbb365 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -6,16 +6,21 @@ import { useNavigate } from "react-router-dom"; import { FaLock, FaAddressCard } from "react-icons/fa"; import ButtonEf from '../components/ButtonEf'; import '../styles/AppView.css'; -import {logIn} from "../components/auth/AuthUtils"; +import {logIn, useLoggedState} from "../components/auth/AuthUtils"; import {HttpStatusCode} from "axios"; export default function Login() { - const [hasError, setHasError] = useState(false); const navigate = useNavigate(); + if (useLoggedState()) { + navigate("/dashboard"); + } + + const [hasError, setHasError] = useState(false); const { t } = useTranslation(); const [showPassword, setShowPassword] = useState(false); + const changeShowP = () => setShowPassword(!showPassword); const ChakraFaCardAlt = chakra(FaAddressCard); From 40090728f5a5fdbff4c0952e11fe25fbb325d0ca Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 3 Mar 2024 14:11:04 +0100 Subject: [PATCH 05/19] feat: add tentative support for JWT auth (routes are yet to be protected though) --- webapp/src/components/auth/Auth.js | 16 ---- webapp/src/components/auth/AuthUtils.js | 23 +++-- webapp/src/pages/Login.jsx | 38 ++++---- webapp/src/pages/Signup.jsx | 110 +++++++++++++----------- 4 files changed, 85 insertions(+), 102 deletions(-) delete mode 100644 webapp/src/components/auth/Auth.js diff --git a/webapp/src/components/auth/Auth.js b/webapp/src/components/auth/Auth.js deleted file mode 100644 index 20a53e9a..00000000 --- a/webapp/src/components/auth/Auth.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { createContext, useState } from "react"; - -export const authContext = createContext({ - jwt: null, - setJwt: (newJwt) => {} -}); - -export function AuthProvider({children}) { - - const [jwt, setJwt] = useState(null); - - return - {children} - -} - diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index a882bfc9..aa4ee624 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -1,19 +1,16 @@ -import { useContext } from "react"; -import { authContext } from "./Auth"; import axios from "axios"; -export function useLoggedState() { - const context = useContext(authContext); - return context.jwt != null; +export function isUserLogged() { + return axios.defaults.headers.common["Authorization"] != null; } -export async function logIn(loginData, onSuccess, onError) { - const url = process.env.REACT_APP_API_ENDPOINT + process.env.REACT_APP_LOGIN_ENDPOINT; - const requestAnswer = await axios.post(url, loginData); +export function saveToken(requestAnswer) { + axios.defaults.headers.common["Authorization"] = "Bearer " + requestAnswer.data.token; + localStorage.setItem("jwtToken", requestAnswer.data.token); + localStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_Token) +} - return { - status: requestAnswer.status, - token: requestAnswer.data.token, - refreshToken: requestAnswer.data.refresh_Token - }; +export function login(loginData) { + return axios.post(process.env.REACT_APP_API_ENDPOINT + + process.env.REACT_APP_LOGIN_ENDPOINT, loginData); } \ No newline at end of file diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index 19bbb365..9979b2dc 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -1,24 +1,36 @@ import { Center } from "@chakra-ui/layout"; import { Heading, Input, Button, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, Text } from "@chakra-ui/react"; -import React, { useState } from "react"; +import React, {useState} from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { FaLock, FaAddressCard } from "react-icons/fa"; import ButtonEf from '../components/ButtonEf'; import '../styles/AppView.css'; -import {logIn, useLoggedState} from "../components/auth/AuthUtils"; -import {HttpStatusCode} from "axios"; +import {isUserLogged, login, saveToken} from "../components/auth/AuthUtils"; export default function Login() { const navigate = useNavigate(); - if (useLoggedState()) { + if (isUserLogged()) { navigate("/dashboard"); } const [hasError, setHasError] = useState(false); const { t } = useTranslation(); + const sendLogin = async () => { + const loginData = { + "email": document.getElementById("user").value, + "password": document.getElementById("password").value + }; + login(loginData).then(requestAnswer => { + saveToken(requestAnswer); + navigate("/dashboard"); + }).catch(err => { + setHasError(true); + }); + } + const [showPassword, setShowPassword] = useState(false); const changeShowP = () => setShowPassword(!showPassword); @@ -26,22 +38,6 @@ export default function Login() { const ChakraFaCardAlt = chakra(FaAddressCard); const ChakraFaLock = chakra(FaLock); - const sendLogin = () => { - let data = { - "email": document.getElementById("user").value, - "password": document.getElementById("password").value - }; - logIn(data).then(result => { - if (result.status === HttpStatusCode.Ok) { - localStorage.setItem('token', result.token); - localStorage.setItem('refreshToken', result.refreshToken); - navigate("/dashboard"); - } else { - setHasError(true); - } - }) - } - return (
@@ -76,7 +72,7 @@ export default function Login() { - + diff --git a/webapp/src/pages/Signup.jsx b/webapp/src/pages/Signup.jsx index 8d3da600..4d1278a5 100644 --- a/webapp/src/pages/Signup.jsx +++ b/webapp/src/pages/Signup.jsx @@ -1,53 +1,59 @@ -import { Center } from "@chakra-ui/layout"; -import { Button, FormControl, FormLabel, Heading, Input, Text, Stack } from "@chakra-ui/react"; -import axios, { HttpStatusCode } from "axios"; -import React, { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; - -export default function Signup() { - - const [hasError, setHasError] = useState(false); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const sendLogin = async () => { - let data = {}; - let response = await axios.post(process.env.API_URL, data); - if (response.status === HttpStatusCode.Accepted) { - navigate("/home"); - } else { - setHasError(true); - } - } - - return ( -
- { t("common.register")} - { - !hasError ? - <> : -
- Error -
- } - - - { t("session.username") } - - - - { t("Correo electrónico") } {/* To be changed */} - - - - {t("session.password")} - - - - -
- ); +import { Center } from "@chakra-ui/layout"; +import { Button, FormControl, FormLabel, Heading, Input, Text, Stack } from "@chakra-ui/react"; +import axios, { HttpStatusCode } from "axios"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import {saveToken} from "../components/auth/AuthUtils"; + +export default function Signup() { + + const [hasError, setHasError] = useState(false); + const navigate = useNavigate(); + const { t } = useTranslation(); + + const sendRegister = async () => { + const petitionData = { + "user": document.getElementById("user").value, + "email": document.getElementById("email").value, + "password": document.getElementById("password").value + }; + + axios.post(process.env.REACT_APP_API_ENDPOINT + + process.env.REACT_APP_LOGIN_ENDPOINT, petitionData).then(() => { + navigate("/login"); + }).catch(err => { + setHasError(true); + }); + } + + return ( +
+ { t("common.register")} + { + !hasError ? + <> : +
+ Error +
+ } + + + { t("session.username") } + + + + { t("Correo electrónico") } {/* To be changed */} + + + + {t("session.password")} + + + + +
+ ); } \ No newline at end of file From f27c266ce817513beea04076e887f17f79b03c71 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Sun, 3 Mar 2024 14:21:53 +0100 Subject: [PATCH 06/19] chore: updated endpoint of signup request --- webapp/src/pages/Signup.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webapp/src/pages/Signup.jsx b/webapp/src/pages/Signup.jsx index 4d1278a5..c4d98d26 100644 --- a/webapp/src/pages/Signup.jsx +++ b/webapp/src/pages/Signup.jsx @@ -1,10 +1,9 @@ import { Center } from "@chakra-ui/layout"; import { Button, FormControl, FormLabel, Heading, Input, Text, Stack } from "@chakra-ui/react"; -import axios, { HttpStatusCode } from "axios"; +import axios from "axios"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import {saveToken} from "../components/auth/AuthUtils"; export default function Signup() { @@ -20,7 +19,7 @@ export default function Signup() { }; axios.post(process.env.REACT_APP_API_ENDPOINT - + process.env.REACT_APP_LOGIN_ENDPOINT, petitionData).then(() => { + + process.env.REACT_APP_SIGNUP_ENDPOINT, petitionData).then(() => { navigate("/login"); }).catch(err => { setHasError(true); From 4b193d30c42e2b369d816ca637b55979e783c9b4 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 12:46:32 +0100 Subject: [PATCH 07/19] chore: rewrite to make use of async/await --- webapp/src/components/auth/AuthUtils.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index aa4ee624..422ef22b 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -1,7 +1,7 @@ -import axios from "axios"; +import axios, { HttpStatusCode } from "axios"; export function isUserLogged() { - return axios.defaults.headers.common["Authorization"] != null; + return localStorage.getItem("jwtToken") != undefined; } export function saveToken(requestAnswer) { @@ -10,7 +10,13 @@ export function saveToken(requestAnswer) { localStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_Token) } -export function login(loginData) { - return axios.post(process.env.REACT_APP_API_ENDPOINT +export async function login(loginData, onSuccess, onError) { + let requestAnswer = await axios.post(process.env.REACT_APP_API_ENDPOINT + process.env.REACT_APP_LOGIN_ENDPOINT, loginData); + if (HttpStatusCode.Ok === requestAnswer.status) { + saveToken(requestAnswer); + onSuccess(); + } else { + onError(); + } } \ No newline at end of file From 4b1be6795d573ddd31c93c47554f33b986d60ed0 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 12:46:51 +0100 Subject: [PATCH 08/19] feat: add AuthUtils tests --- webapp/src/tests/AuthUtils.test.js | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 webapp/src/tests/AuthUtils.test.js diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js new file mode 100644 index 00000000..cad9ac5e --- /dev/null +++ b/webapp/src/tests/AuthUtils.test.js @@ -0,0 +1,53 @@ +import MockAdapter from "axios-mock-adapter"; +import axios, { HttpStatusCode } from "axios"; +import { isUserLogged, login } from "components/auth/AuthUtils"; + +const mockAxios = new MockAdapter(axios); + +describe("Auth Utils tests", () => { + describe("when the user is not authenticated", () => { + + beforeEach(() => { + mockAxios.reset(); + }); + + it("does not have a stored token", () => { + expect(isUserLogged()).not.toBe(true); + }); + + it("when logging in it is possible to do it", async () => { + + // Mock response + mockAxios.onPost().replyOnce(HttpStatusCode.Ok, { + "token": "token", + "refresh_Token": "refreshToken" + }); + + // Test + const loginData = { + "email": "test@email.com", + "password": "test" + }; + + await login(loginData); + + //Check the user is now logged in + expect(isUserLogged()).toBe(true); + expect(localStorage.getItem("jwtToken")).toBe("token"); + expect(localStorage.getItem("jwtRefreshToken")).toBe("refreshToken"); + }); + }); + + describe("when the user is authenticated", () => { + + beforeAll(() => { + localStorage.setItem("jwtToken", "token"); + }) + + it("has a stored token", () => { + expect(isUserLogged()).toBe(true); + }); + }); +}); + + From d73801a20fc545588b894d87388a19201b9da6a9 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 13:44:00 +0100 Subject: [PATCH 09/19] chore: restored some accidentally deleted code --- webapp/src/components/auth/AuthUtils.js | 19 ++++++++++------ webapp/src/pages/Login.jsx | 29 +++++++++++++++---------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index 422ef22b..ffa02d9f 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -1,7 +1,8 @@ import axios, { HttpStatusCode } from "axios"; export function isUserLogged() { - return localStorage.getItem("jwtToken") != undefined; + console.log(localStorage.getItem("jwtToken")) + return localStorage.getItem("jwtToken") !== null; } export function saveToken(requestAnswer) { @@ -11,12 +12,16 @@ export function saveToken(requestAnswer) { } export async function login(loginData, onSuccess, onError) { - let requestAnswer = await axios.post(process.env.REACT_APP_API_ENDPOINT - + process.env.REACT_APP_LOGIN_ENDPOINT, loginData); - if (HttpStatusCode.Ok === requestAnswer.status) { - saveToken(requestAnswer); - onSuccess(); - } else { + try { + let requestAnswer = await axios.post(process.env.REACT_APP_API_ENDPOINT + + process.env.REACT_APP_LOGIN_ENDPOINT, loginData); + if (HttpStatusCode.Ok === requestAnswer.status) { + saveToken(requestAnswer); + onSuccess(); + } else { + onError(); + } + } catch { onError(); } } \ No newline at end of file diff --git a/webapp/src/pages/Login.jsx b/webapp/src/pages/Login.jsx index b6e4f605..7c2f224d 100644 --- a/webapp/src/pages/Login.jsx +++ b/webapp/src/pages/Login.jsx @@ -1,18 +1,25 @@ import { Center } from "@chakra-ui/layout"; import { Heading, Input, InputGroup, Stack, InputLeftElement, chakra, Box, Avatar, FormControl, InputRightElement, Text, IconButton } from "@chakra-ui/react"; import { ViewIcon, ViewOffIcon } from '@chakra-ui/icons' -import axios, { HttpStatusCode } from "axios"; -import React, { useState } from "react"; +import React, {useEffect, useState} from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { FaLock, FaAddressCard } from "react-icons/fa"; import ButtonEf from '../components/ButtonEf'; import '../styles/AppView.css'; +import {isUserLogged, login} from "../components/auth/AuthUtils"; export default function Login() { - const [hasError, setHasError] = useState(false); const navigate = useNavigate(); + const navigateToDashboard = () => { + if (isUserLogged()) { + navigate("/dashboard"); + } + } + + useEffect(navigateToDashboard); + const [hasError, setHasError] = useState(false); const { t } = useTranslation(); const [showPassword, setShowPassword] = useState(false); @@ -22,13 +29,11 @@ export default function Login() { const ChakraFaLock = chakra(FaLock); const sendLogin = async () => { - let data = {}; - let response = await axios.post(process.env.API_URL, data); - if (response.status === HttpStatusCode.Accepted) { - navigate("/home"); - } else { - setHasError(true); - } + const loginData = { + "email": document.getElementById("user").value, + "password": document.getElementById("password").value + }; + await login(loginData, navigateToDashboard, () => setHasError(true)); } return ( @@ -51,13 +56,13 @@ export default function Login() { }/> - + }/> - + : } data-testid="togglePasswordButton"/> From 282e81632312d1d5ad2b5105e6d6d191b44f6fe7 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 13:46:53 +0100 Subject: [PATCH 10/19] feat: solved tests not passing --- webapp/src/tests/AuthUtils.test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index cad9ac5e..118f1aed 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -17,11 +17,13 @@ describe("Auth Utils tests", () => { it("when logging in it is possible to do it", async () => { - // Mock response + // Mock axios and the onSuccess and onError functions mockAxios.onPost().replyOnce(HttpStatusCode.Ok, { "token": "token", "refresh_Token": "refreshToken" }); + const mockOnSucess = jest.fn(); + const mockOnError = jest.fn(); // Test const loginData = { @@ -29,7 +31,7 @@ describe("Auth Utils tests", () => { "password": "test" }; - await login(loginData); + await login(loginData, mockOnSucess, mockOnError); //Check the user is now logged in expect(isUserLogged()).toBe(true); From d6beb384b9de50261e50a955d23c80f473ba17bc Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 13:54:41 +0100 Subject: [PATCH 11/19] chore: rewrote stored data to include reception time --- webapp/src/components/auth/AuthUtils.js | 10 ++++++---- webapp/src/tests/AuthUtils.test.js | 8 +++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index ffa02d9f..1769422f 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -1,14 +1,16 @@ import axios, { HttpStatusCode } from "axios"; export function isUserLogged() { - console.log(localStorage.getItem("jwtToken")) - return localStorage.getItem("jwtToken") !== null; + return localStorage.getItem("authData") !== null; } export function saveToken(requestAnswer) { axios.defaults.headers.common["Authorization"] = "Bearer " + requestAnswer.data.token; - localStorage.setItem("jwtToken", requestAnswer.data.token); - localStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_Token) + localStorage.setItem("authData", { + "jwtToken": requestAnswer.data.token, + "refreshToken": requestAnswer.data.refresh_Token, + "receivedOnUTC": Date.now() + }); } export async function login(loginData, onSuccess, onError) { diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index 118f1aed..604baed5 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -35,15 +35,17 @@ describe("Auth Utils tests", () => { //Check the user is now logged in expect(isUserLogged()).toBe(true); - expect(localStorage.getItem("jwtToken")).toBe("token"); - expect(localStorage.getItem("jwtRefreshToken")).toBe("refreshToken"); + expect(localStorage.getItem("authData").jwtToken).toBe("token"); + expect(localStorage.getItem("authData").jwtRefreshToken).toBe("refreshToken"); }); }); describe("when the user is authenticated", () => { beforeAll(() => { - localStorage.setItem("jwtToken", "token"); + localStorage.setItem("authData", { + "token": "testToken" + }); }) it("has a stored token", () => { From 072f9438f28a720378ad94d53edbaac84ef0a7de Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 15:20:44 +0100 Subject: [PATCH 12/19] fix: fix tests not passing --- webapp/src/components/auth/AuthUtils.js | 18 ++++++++++++------ webapp/src/tests/AuthUtils.test.js | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js index 1769422f..296fb582 100644 --- a/webapp/src/components/auth/AuthUtils.js +++ b/webapp/src/components/auth/AuthUtils.js @@ -1,16 +1,22 @@ import axios, { HttpStatusCode } from "axios"; export function isUserLogged() { - return localStorage.getItem("authData") !== null; + return getLoginData().jwtToken !== null; } export function saveToken(requestAnswer) { axios.defaults.headers.common["Authorization"] = "Bearer " + requestAnswer.data.token; - localStorage.setItem("authData", { - "jwtToken": requestAnswer.data.token, - "refreshToken": requestAnswer.data.refresh_Token, - "receivedOnUTC": Date.now() - }); + sessionStorage.setItem("jwtToken", requestAnswer.data.token); + sessionStorage.setItem("jwtRefreshToken", requestAnswer.data.refresh_Token); + sessionStorage.setItem("jwtReceptionMillis", Date.now().toString()); +} + +export function getLoginData() { + return { + "jwtToken": sessionStorage.getItem("jwtToken"), + "jwtRefreshToken": sessionStorage.getItem("jwtRefreshToken"), + "jwtReceptionDate": new Date(sessionStorage.getItem("jwtReceptionMillis")) + }; } export async function login(loginData, onSuccess, onError) { diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index 604baed5..c92c8371 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -1,6 +1,6 @@ import MockAdapter from "axios-mock-adapter"; import axios, { HttpStatusCode } from "axios"; -import { isUserLogged, login } from "components/auth/AuthUtils"; +import { isUserLogged, login} from "components/auth/AuthUtils"; const mockAxios = new MockAdapter(axios); @@ -35,15 +35,15 @@ describe("Auth Utils tests", () => { //Check the user is now logged in expect(isUserLogged()).toBe(true); - expect(localStorage.getItem("authData").jwtToken).toBe("token"); - expect(localStorage.getItem("authData").jwtRefreshToken).toBe("refreshToken"); + expect(sessionStorage.getItem("jwtToken")).toBe("token"); + expect(sessionStorage.getItem("jwtRefreshToken")).toBe("refreshToken"); }); }); describe("when the user is authenticated", () => { beforeAll(() => { - localStorage.setItem("authData", { + sessionStorage.setItem("authData", { "token": "testToken" }); }) From eac244f85c8fabbcf357d4037fdb5b389d003a1b Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 16:16:09 +0100 Subject: [PATCH 13/19] chore: add new test that checks the token is saved --- webapp/src/tests/AuthUtils.test.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index c92c8371..7053b41c 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -1,6 +1,6 @@ import MockAdapter from "axios-mock-adapter"; import axios, { HttpStatusCode } from "axios"; -import { isUserLogged, login} from "components/auth/AuthUtils"; +import {isUserLogged, login, saveToken} from "components/auth/AuthUtils"; const mockAxios = new MockAdapter(axios); @@ -15,7 +15,7 @@ describe("Auth Utils tests", () => { expect(isUserLogged()).not.toBe(true); }); - it("when logging in it is possible to do it", async () => { + it("can log in", async () => { // Mock axios and the onSuccess and onError functions mockAxios.onPost().replyOnce(HttpStatusCode.Ok, { @@ -35,8 +35,6 @@ describe("Auth Utils tests", () => { //Check the user is now logged in expect(isUserLogged()).toBe(true); - expect(sessionStorage.getItem("jwtToken")).toBe("token"); - expect(sessionStorage.getItem("jwtRefreshToken")).toBe("refreshToken"); }); }); @@ -52,6 +50,24 @@ describe("Auth Utils tests", () => { expect(isUserLogged()).toBe(true); }); }); + + describe("saving the token", () => { + beforeAll(() => { + sessionStorage.clear(); + }); + + it ("is saved", () => { + let mockResponse = { + "data": { + "token": "token", + "refresh_Token": "refreshToken" + } + }; + saveToken(mockResponse); + expect(sessionStorage.getItem("jwtToken")).toBe(mockResponse.data.token); + expect(sessionStorage.getItem("jwtRefreshToken")).toBe(mockResponse.data.refresh_Token); + }); + }); }); From 7a22e7f3df1d18708809a9fbe9ab4326e1df2b73 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 16:51:13 +0100 Subject: [PATCH 14/19] chore: update sonar config --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 097e51c4..d274caae 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,7 +11,7 @@ sonar.language=js,java sonar.projectName=wiq_en2b sonar.coverage.exclusions=**/*.test.js,**/*.test.jsx -sonar.sources=webapp/src/components,api/src/main/java +sonar.sources=webapp/src/components,api/src/main/java,webapp/src/components/auth/,webapp/src/pages/ sonar.sourceEncoding=UTF-8 sonar.exclusions=node_modules/** sonar.javascript.lcov.reportPaths=**/coverage/lcov.info From 079cb0ae4b32df1af36faea1b431afd5a315f729 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 16:56:52 +0100 Subject: [PATCH 15/19] chore: update sonar config again --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index d274caae..7fe945d9 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,7 +11,7 @@ sonar.language=js,java sonar.projectName=wiq_en2b sonar.coverage.exclusions=**/*.test.js,**/*.test.jsx -sonar.sources=webapp/src/components,api/src/main/java,webapp/src/components/auth/,webapp/src/pages/ +sonar.sources=webapp/src/components,api/src/main/java,webapp/src/pages/ sonar.sourceEncoding=UTF-8 sonar.exclusions=node_modules/** sonar.javascript.lcov.reportPaths=**/coverage/lcov.info From 420ea0d406fe10e6f520cfd642a85051e1189063 Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Tue, 5 Mar 2024 17:07:30 +0100 Subject: [PATCH 16/19] chore: minor rewrite due to old test code not being replaced --- webapp/src/tests/AuthUtils.test.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js index 7053b41c..e98e5309 100644 --- a/webapp/src/tests/AuthUtils.test.js +++ b/webapp/src/tests/AuthUtils.test.js @@ -8,6 +8,7 @@ describe("Auth Utils tests", () => { describe("when the user is not authenticated", () => { beforeEach(() => { + sessionStorage.clear(); mockAxios.reset(); }); @@ -41,9 +42,11 @@ describe("Auth Utils tests", () => { describe("when the user is authenticated", () => { beforeAll(() => { - sessionStorage.setItem("authData", { - "token": "testToken" - }); + sessionStorage.setItem("jwtToken", "token"); + }) + + afterEach(() => { + sessionStorage.clear(); }) it("has a stored token", () => { @@ -52,7 +55,7 @@ describe("Auth Utils tests", () => { }); describe("saving the token", () => { - beforeAll(() => { + beforeEach(() => { sessionStorage.clear(); }); From 75946c1b26fd713b8d2faa16ac9795c9939f083d Mon Sep 17 00:00:00 2001 From: jjgancfer Date: Wed, 6 Mar 2024 19:21:54 +0100 Subject: [PATCH 17/19] chore: add key down listener to root page --- webapp/src/pages/Root.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/src/pages/Root.jsx b/webapp/src/pages/Root.jsx index df44626d..5459fef1 100644 --- a/webapp/src/pages/Root.jsx +++ b/webapp/src/pages/Root.jsx @@ -8,6 +8,9 @@ import ButtonEf from '../components/ButtonEf'; export default function Root() { const navigate = useNavigate(); const { t } = useTranslation(); + const signup = () => { + navigate("/signup"); + } return (
{t("session.welcome")}

navigate("/login")}/> -

navigate("/signup")} style={{ cursor: 'pointer' }}>{t("session.account")}

+

{t("session.account")}

); From a7504699e77180c7ab296467cb35d132199ac789 Mon Sep 17 00:00:00 2001 From: "Dario G. Mori" Date: Thu, 7 Mar 2024 11:24:45 +0100 Subject: [PATCH 18/19] chore: add webapp to deployment --- docker-compose.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 600412c7..a7a27429 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,16 @@ services: ports: - "8080:8080" + webapp: + container_name: webapp-${teamname:-defaultASW} + image: ghcr.io/arquisoft/wiq_en2a/webapp:latest + profiles: [ "dev", "prod" ] + build: ./webapp + networks: + - mynetwork + ports: + - "3000:3000" + volumes: postgres_data: From ba8819c275f6b5ebc0aa9231c6c2ba5c4faaf86b Mon Sep 17 00:00:00 2001 From: Gonzalo Alonso Fernandez Date: Fri, 8 Mar 2024 11:03:44 +0100 Subject: [PATCH 19/19] fix: fixing the loginTests to work correctly. --- webapp/src/tests/Login.test.js | 74 +++++++++++++++------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/webapp/src/tests/Login.test.js b/webapp/src/tests/Login.test.js index 1a20e7fc..0be238e5 100644 --- a/webapp/src/tests/Login.test.js +++ b/webapp/src/tests/Login.test.js @@ -1,64 +1,56 @@ import React from 'react'; -import { render, fireEvent, waitFor, getByTestId, getAllByTestId } from '@testing-library/react'; -import axios from 'axios'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; import { MemoryRouter } from 'react-router'; -import Signup from '../pages/Signup'; +import Login from '../pages/Login'; +import { login as mockLogin } from '../components/auth/AuthUtils'; -describe('Signup Component', () => { +jest.mock('../components/auth/AuthUtils', () => ({ + isUserLogged: jest.fn(), + login: jest.fn(), +})); +describe('Login Component', () => { it('renders form elements correctly', () => { - const { getByPlaceholderText } = render(); + const { getByPlaceholderText, getByTestId } = render(); expect(getByPlaceholderText('session.email')).toBeInTheDocument(); - expect(getByPlaceholderText('session.username')).toBeInTheDocument(); expect(getByPlaceholderText('session.password')).toBeInTheDocument(); - expect(getByPlaceholderText('session.confirm_password')).toBeInTheDocument(); - expect(getByTestId(document.body, 'Sign up')).toBeInTheDocument(); + expect(getByTestId('Login')).toBeInTheDocument(); }); - it('toggles password visibility', () => { - const { getByPlaceholderText } = render(); + it('toggles password visibility', () => { + const { getByLabelText, getByPlaceholderText } = render(); + + // Initially password should be hidden const passwordInput = getByPlaceholderText('session.password'); - const confirmPasswordInput = getByPlaceholderText('session.confirm_password'); - const showPasswordButtons = getAllByTestId(document.body, 'show-confirm-password-button'); - - fireEvent.click(showPasswordButtons[0]); - fireEvent.click(showPasswordButtons[1]); - - expect(passwordInput.getAttribute('type')).toBe('text'); - expect(confirmPasswordInput.getAttribute('type')).toBe('text'); + expect(passwordInput).toHaveAttribute('type', 'password'); + + // Click on the toggle password button + const toggleButton = getByLabelText('Shows or hides the password'); + fireEvent.click(toggleButton); + + // Password should now be visible + expect(passwordInput).toHaveAttribute('type', 'text'); }); - it('submits form data correctly', async () => { - const axiosMock = jest.spyOn(axios, 'post'); - axiosMock.mockResolvedValueOnce({ status: 202 }); // Accepted status code - - // Render the Signup component - const { getByPlaceholderText } = render(); - - // Get form elements and submit button by their text and placeholder values + it('calls login function with correct credentials on submit', async () => { + const { getByPlaceholderText, getByTestId } = render(, { wrapper: MemoryRouter }); const emailInput = getByPlaceholderText('session.email'); - const usernameInput = getByPlaceholderText('session.username'); const passwordInput = getByPlaceholderText('session.password'); - const signUpButton = getByTestId(document.body, 'Sign up'); + const loginButton = getByTestId('Login'); - // Fill out the form with valid data and submit it fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); - fireEvent.change(usernameInput, { target: { value: 'testuser' } }); - fireEvent.change(passwordInput, { target: { value: 'password' } }); - fireEvent.click(signUpButton); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + fireEvent.click(loginButton); - // Check if the form data was sent correctly await waitFor(() => { - expect(axiosMock).toHaveBeenCalledWith(process.env.API_URL, { - email: 'test@example.com', - username: 'testuser', - password: 'password' - }); - expect(axiosMock).toHaveBeenCalledTimes(1); + expect(mockLogin).toHaveBeenCalledWith( + { email: 'test@example.com', password: 'password123' }, + expect.any(Function), + expect.any(Function) + ); }); - - axiosMock.mockRestore(); }); }); \ No newline at end of file