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:
diff --git a/sonar-project.properties b/sonar-project.properties
index 1634853f..afef61b3 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/pages/
sonar.sourceEncoding=UTF-8
sonar.exclusions=node_modules/**,**/quizapi/commons/utils/**,**/quizapi/commons/exceptions/**,**/quizapi/auth/jwt/**,**/quizapi/**/dtos/**
sonar.javascript.lcov.reportPaths=**/coverage/lcov.info
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/.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
diff --git a/webapp/src/components/auth/AuthUtils.js b/webapp/src/components/auth/AuthUtils.js
new file mode 100644
index 00000000..296fb582
--- /dev/null
+++ b/webapp/src/components/auth/AuthUtils.js
@@ -0,0 +1,35 @@
+import axios, { HttpStatusCode } from "axios";
+
+export function isUserLogged() {
+ return getLoginData().jwtToken !== null;
+}
+
+export function saveToken(requestAnswer) {
+ axios.defaults.headers.common["Authorization"] = "Bearer " + requestAnswer.data.token;
+ 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) {
+ 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/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 2a34dfff..fd2d1dc6 100644
--- a/webapp/src/index.js
+++ b/webapp/src/index.js
@@ -5,11 +5,12 @@ import reportWebVitals from './reportWebVitals';
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 5ae7e1c1..7c2f224d 100644
--- a/webapp/src/pages/Login.jsx
+++ b/webapp/src/pages/Login.jsx
@@ -1,72 +1,77 @@
-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 { 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 ?
- <>> :
-
- {t("error.login")}
-
- }
-
-
-
-
- }/>
-
-
-
-
-
- }/>
-
-
- : } data-testid="togglePasswordButton"/>
-
-
-
-
-
-
-
-
- );
+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 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 navigate = useNavigate();
+ const navigateToDashboard = () => {
+ if (isUserLogged()) {
+ navigate("/dashboard");
+ }
+ }
+
+ useEffect(navigateToDashboard);
+ const [hasError, setHasError] = useState(false);
+ const { t } = useTranslation();
+
+ const [showPassword, setShowPassword] = useState(false);
+ const changeShowP = () => setShowPassword(!showPassword);
+
+ const ChakraFaCardAlt = chakra(FaAddressCard);
+ const ChakraFaLock = chakra(FaLock);
+
+ const sendLogin = async () => {
+ const loginData = {
+ "email": document.getElementById("user").value,
+ "password": document.getElementById("password").value
+ };
+ await login(loginData, navigateToDashboard, () => setHasError(true));
+ }
+
+ return (
+
+
+
+ { t("common.login")}
+ {
+ !hasError ?
+ <>> :
+
+ {t("error.login")}
+
+ }
+
+
+
+
+ }/>
+
+
+
+
+
+ }/>
+
+
+ : } data-testid="togglePasswordButton"/>
+
+
+
+
+
+
+
+
+ );
}
\ No newline at end of file
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")}
);
diff --git a/webapp/src/tests/AuthUtils.test.js b/webapp/src/tests/AuthUtils.test.js
new file mode 100644
index 00000000..e98e5309
--- /dev/null
+++ b/webapp/src/tests/AuthUtils.test.js
@@ -0,0 +1,76 @@
+import MockAdapter from "axios-mock-adapter";
+import axios, { HttpStatusCode } from "axios";
+import {isUserLogged, login, saveToken} from "components/auth/AuthUtils";
+
+const mockAxios = new MockAdapter(axios);
+
+describe("Auth Utils tests", () => {
+ describe("when the user is not authenticated", () => {
+
+ beforeEach(() => {
+ sessionStorage.clear();
+ mockAxios.reset();
+ });
+
+ it("does not have a stored token", () => {
+ expect(isUserLogged()).not.toBe(true);
+ });
+
+ it("can log in", async () => {
+
+ // 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 = {
+ "email": "test@email.com",
+ "password": "test"
+ };
+
+ await login(loginData, mockOnSucess, mockOnError);
+
+ //Check the user is now logged in
+ expect(isUserLogged()).toBe(true);
+ });
+ });
+
+ describe("when the user is authenticated", () => {
+
+ beforeAll(() => {
+ sessionStorage.setItem("jwtToken", "token");
+ })
+
+ afterEach(() => {
+ sessionStorage.clear();
+ })
+
+ it("has a stored token", () => {
+ expect(isUserLogged()).toBe(true);
+ });
+ });
+
+ describe("saving the token", () => {
+ beforeEach(() => {
+ 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);
+ });
+ });
+});
+
+
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