Skip to content

Commit

Permalink
Merge pull request #93 from Arquisoft/develop
Browse files Browse the repository at this point in the history
v0.2.0
  • Loading branch information
dariogmori authored Mar 8, 2024
2 parents 0efcfc1 + c81dd57 commit 9b5ddce
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 117 deletions.
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion webapp/.env

This file was deleted.

1 change: 1 addition & 0 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
Expand Down
35 changes: 35 additions & 0 deletions webapp/src/components/auth/AuthUtils.js
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion webapp/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ export default i18n.use(Backend)
.use(initReactI18next)
.init({
fallbackLng: "en",
debug: true
debug: false
})
3 changes: 2 additions & 1 deletion webapp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ChakraProvider>
<React.StrictMode>
Expand Down
147 changes: 76 additions & 71 deletions webapp/src/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Center display={"flex"} flexDirection={"column"} w={"100wh"} h={"100vh"}
bg={"blue.50"} justifyContent={"center"} alignItems={"center"}>
<Stack flexDir={"column"} mb="2" justifyContent="center" alignItems={"center"}>
<Avatar bg="blue.500" />
<Heading as="h2" color="blue.400">{ t("common.login")}</Heading>
{
!hasError ?
<></> :
<Center bgColor={"#FFA98A"} margin={"1vh 0vw"} padding={"1vh 0vw"}
color={"#FF0500"} border={"0.1875em solid #FF0500"}
borderRadius={"0.75em"} maxW={"100%"} minW={"30%"}>
<Text>{t("error.login")}</Text>
</Center>
}
<Box minW={{md: "400px"}}>
<Stack spacing={4} p="1rem" backgroundColor="whiteAlpha.900" boxShadow="md">
<FormControl>
<InputGroup>
<InputLeftElement children={<ChakraFaCardAlt color="gray.300" />}/>
<Input type="text" placeholder={t("session.email")} />
</InputGroup>
</FormControl>
<FormControl>
<InputGroup>
<InputLeftElement children={<ChakraFaLock color="gray.300" />}/>
<Input type={showPassword ? "text" : "password"} placeholder={t("session.password")}/>
<InputRightElement>
<IconButton h="1.75rem" size="sm" onClick={changeShowP} aria-label='Shows or hides the password' icon={showPassword ? <ViewOffIcon/> : <ViewIcon/>} data-testid="togglePasswordButton"/>
</InputRightElement>
</InputGroup>
</FormControl>
<ButtonEf dataTestId={"Login"} variant={"solid"} colorScheme={"blue"} text={t("common.login")} onClick={sendLogin}/>
</Stack>
</Box>
</Stack>
</Center>
);
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 (
<Center display={"flex"} flexDirection={"column"} w={"100wh"} h={"100vh"}
bg={"blue.50"} justifyContent={"center"} alignItems={"center"}>
<Stack flexDir={"column"} mb="2" justifyContent="center" alignItems={"center"}>
<Avatar bg="blue.500" />
<Heading as="h2" color="blue.400">{ t("common.login")}</Heading>
{
!hasError ?
<></> :
<Center bgColor={"#FFA98A"} margin={"1vh 0vw"} padding={"1vh 0vw"}
color={"#FF0500"} border={"0.1875em solid #FF0500"}
borderRadius={"0.75em"} maxW={"100%"} minW={"30%"}>
<Text>{t("error.login")}</Text>
</Center>
}
<Box minW={{md: "400px"}}>
<Stack spacing={4} p="1rem" backgroundColor="whiteAlpha.900" boxShadow="md">
<FormControl>
<InputGroup>
<InputLeftElement children={<ChakraFaCardAlt color="gray.300" />}/>
<Input type="text" id={"user"} placeholder={t("session.email")} />
</InputGroup>
</FormControl>
<FormControl>
<InputGroup>
<InputLeftElement children={<ChakraFaLock color="gray.300" />}/>
<Input type={showPassword ? "text" : "password"} id={"password"} placeholder={t("session.password")}/>
<InputRightElement>
<IconButton h="1.75rem" size="sm" onClick={changeShowP} aria-label='Shows or hides the password' icon={showPassword ? <ViewOffIcon/> : <ViewIcon/>} data-testid="togglePasswordButton"/>
</InputRightElement>
</InputGroup>
</FormControl>
<ButtonEf dataTestId={"Login"} variant={"solid"} colorScheme={"blue"} text={t("common.login")} onClick={sendLogin}/>
</Stack>
</Box>
</Stack>
</Center>
);
}
5 changes: 4 additions & 1 deletion webapp/src/pages/Root.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import ButtonEf from '../components/ButtonEf';
export default function Root() {
const navigate = useNavigate();
const { t } = useTranslation();
const signup = () => {
navigate("/signup");
}

return (
<Center display={"flex"} flexDirection={"column"} w={"100wh"} h={"100vh"}
Expand All @@ -16,7 +19,7 @@ export default function Root() {
<p>{t("session.welcome")}</p>
<Stack spacing={4} p="3rem">
<ButtonEf dataTestId={"Login"} variant={"solid"} colorScheme={"blue"} text={t("common.login")} onClick={() => navigate("/login")}/>
<p onClick={() => navigate("/signup")} style={{ cursor: 'pointer' }}>{t("session.account")}</p>
<p onClick={signup} onKeyDown={signup} style={{ cursor: 'pointer' }}>{t("session.account")}</p>
</Stack>
</Center>
);
Expand Down
76 changes: 76 additions & 0 deletions webapp/src/tests/AuthUtils.test.js
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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);
});
});
});


Loading

0 comments on commit 9b5ddce

Please sign in to comment.