DONE: first init
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function AuthGuard({ children, roles }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const user = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNecessaryRole = roles.some(role => user.roles.find(userRole => userRole === role));
|
||||
|
||||
if (!hasNecessaryRole) {
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ children }
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function CanAccess( {children, roles} ){
|
||||
|
||||
const {user, setUser} = useContext(UserContext);
|
||||
const hasRole = user && user.roles && user.roles.some(userRole => roles.find(role => role === userRole));
|
||||
return hasRole ? children : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
header.main-header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 0 0 0 -2%;
|
||||
background-color: #d3e2f8;
|
||||
width: 101vw;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.header-logo-png {
|
||||
width: 200px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-logout-btn {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
justify-content: center; /* Center content horizontally */
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
box-shadow: 3px 8px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: 130px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.header-logout-btn:hover {
|
||||
background-color: var(--light-blue);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.header-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 0.5%;
|
||||
|
||||
}
|
||||
|
||||
.header-profile-png {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { lastPathPart } from '../../utils/StringUtils.js';
|
||||
import profileImage from '../../../assets/profile-user.png'
|
||||
|
||||
import './Header.css';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('userData');
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const path = window.location.pathname;
|
||||
const showBackButton = !!user
|
||||
&& '/' !== path
|
||||
&& '/login' !== path
|
||||
&& 'home' !== lastPathPart(window.location.href);
|
||||
|
||||
const goHome = () => {
|
||||
if (user?.tenantCode) {
|
||||
navigate(`/${user.tenantCode}/home`);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="main-header">
|
||||
<h2 className="header-logo" onClick={ goHome }>BBQMS</h2>
|
||||
|
||||
<div className="header-logout">
|
||||
{ !!user && (
|
||||
<button className="header-logout-btn" onClick={ handleLogout }>
|
||||
Logout
|
||||
</button>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<div className="header-profile" onClick={ () => navigate('/profile') }>
|
||||
<img src={ profileImage } className="header-profile-png" alt="Profile image" />
|
||||
</div>
|
||||
</header>
|
||||
{ showBackButton && (
|
||||
<Button variant="secondary"
|
||||
className="mt-2 px-4"
|
||||
onClick={ goHome }>
|
||||
Back
|
||||
</Button>
|
||||
) }
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.button-hp{
|
||||
height: 80%;
|
||||
display: inline-block;
|
||||
margin-top: 50%;
|
||||
box-shadow: 3px 6px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-hp{
|
||||
width: 14%;
|
||||
border-radius: 10px;
|
||||
float: left;
|
||||
margin-right: 4%;
|
||||
margin-left: 6%;
|
||||
margin-top: 6%;
|
||||
}
|
||||
.card-title-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
height: 10%;
|
||||
max-height: 10%;
|
||||
}
|
||||
.card-text-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
}
|
||||
.button-container-hp{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCard( {title, backgroundColor, buttonColor, url} ) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: backgroundColor, height: '200px'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>{title}</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}></p>
|
||||
<div className="button-container-hp">
|
||||
<button className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: buttonColor}} onClick ={ () => navigate(url) } >Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCardLight() {
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: '#548CA8'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>Card title</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}>Some quick example text to build on
|
||||
the card title and make up the bulk of
|
||||
the card's content.</p>
|
||||
<div className="button-container-hp">
|
||||
<a href="#" className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: '#476072', border: '1px solid #548CA8'}}>Go somewhere</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#auth-container *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#auth-container{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#authForm {
|
||||
border: 1px solid black;
|
||||
padding: 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.button-auth {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button-auth:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useRef, useReducer, useContext } from "react";
|
||||
import "./LoginAuth.css";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function LoginAuth() {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
|
||||
//e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
|
||||
try {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const userData = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
if (!userData) {
|
||||
throw new Error("Email not found in localStorage");
|
||||
}
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
|
||||
const { data, success } = await fetchData(url, 'POST', {
|
||||
code: inputValues.join(""),
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
if (success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.userData);
|
||||
navigate(`/${ data.userData.tenantCode }/home`);
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect or expired.");
|
||||
resetInputs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
let navigate = useNavigate();
|
||||
function routeChange() {
|
||||
handleSubmit({});
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="auth-container">
|
||||
<form id="authForm" onSubmit={handleSubmit}>
|
||||
<div className="inputs">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button className="button-auth" disabled={status === "pending"} onClick={routeChange}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#registration{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { GoogleLogin } from '@react-oauth/google';
|
||||
import React, { useState } from "react";
|
||||
import validator from "validator";
|
||||
import "./LoginForm.css";
|
||||
import LoginAuth from "../LoginAuth/LoginAuth";
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
|
||||
const LoginForm = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function handleGoogleLogin(credentialResponse) {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login/oauth2/google`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
googleToken: credentialResponse.credential
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
setError("Error while trying to log in with Google.");
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log("username: " + username);
|
||||
console.log("password: " + password);
|
||||
|
||||
if (!username.trim()) {
|
||||
setError("Username is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(username) && !validator.isMobilePhone(username, "any")) {
|
||||
setError("Invalid username format. Please enter a valid email or phone number.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setError("Password is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: username,
|
||||
password: password
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
|
||||
if (response.ok) {
|
||||
setIsSubmitted(true);
|
||||
navigate('/');
|
||||
} else if (response.status === 403) {
|
||||
setError("Your credentials are incorrect.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setError("An error occurred. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
setUsername(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
setPassword(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/' element={<LoginAuth />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>LOGIN</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Email or phone number:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
{error && (error.includes("Username") || error.includes("Invalid")) && <p className="error">{error}</p>}
|
||||
{error && (error.includes("credentials")) && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
{error && error.includes("Password") && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<input type="submit" value="Submit" />
|
||||
<p id="registration">
|
||||
Not registered? <Link to="/registration">Create an account</Link>
|
||||
</p>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -0,0 +1,66 @@
|
||||
.auth-container-reg *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-container-reg{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.authForm-reg {
|
||||
border: 1px solid black;
|
||||
padding: 0px 40px 40px 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.QRCodeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.QRCodeContainer h1 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.inputs-reg {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs-reg > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useEffect, useRef, useReducer, useState } from "react";
|
||||
import "./RegistrationAuth.css";
|
||||
import { SERVER_URL } from "../../constants";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function RefistrationAuth( {qrCode, email} ) {
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(inputValues).then(() => dispatch({ type: "VERIFY_SUCCESS" }));
|
||||
}
|
||||
|
||||
const checkInputValues = async () =>{
|
||||
const enteredValues = inputValues.join("");
|
||||
const requestBody = {
|
||||
code: enteredValues,
|
||||
email: email
|
||||
}
|
||||
try{
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/tfa', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
if (response.ok) {
|
||||
dispatch({ type: "VERIFY_SUCCESS" });
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect.");
|
||||
resetInputs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-container-reg">
|
||||
<form className="authForm-reg" onSubmit={handleSubmit}>
|
||||
<div className="QRCodeContainer">
|
||||
<h1>Scan QR code</h1>
|
||||
<img src={qrCode} height='300px'></img>
|
||||
</div>
|
||||
<div className="inputs-reg">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button onClick={checkInputValues} disabled={status === "pending"}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.login-form-reg {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-form-reg form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="text"],
|
||||
.login-form-reg form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.login{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-reg{
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import "./RegistrationForm.css";
|
||||
import RegistrationAuth from '../RegistrationAuth/RegistrationAuth';
|
||||
import { SERVER_URL } from '../../constants';
|
||||
|
||||
const userSchema = yup.object().shape({
|
||||
email: yup.string().email("Please enter a valid email").required("Email is required"),
|
||||
password: yup.string().min(4).max(10).required("Password is required")
|
||||
});
|
||||
|
||||
export default function RegistrationForm() {
|
||||
const navigate = useNavigate();
|
||||
const [nextPage, setNextPage] = useState(false);
|
||||
const [qrCode, setQrCode] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: userSchema,
|
||||
onSubmit: async (values, { setFieldError }) => {
|
||||
try {
|
||||
const isValid = await userSchema.isValid(values);
|
||||
if (isValid) {
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
phone_number: "00000"
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
const responseBody = await fetch(SERVER_URL + `/api/v1/auth/tfa?email=${body.email}`);
|
||||
const responseData = await responseBody.json();
|
||||
setQrCode(responseData.qrCode);
|
||||
setEmail(body.email);
|
||||
setNextPage(true);
|
||||
} else if (response.status === 400) {
|
||||
setFieldError('email', 'Email already in use');
|
||||
} else {
|
||||
console.error('Failed to register:', response.statusText);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nextPage) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='*' element={<RegistrationAuth qrCode={qrCode} email={email} />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-form-reg">
|
||||
<h1>REGISTER ACCOUNT</h1>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<label htmlFor="email">Email or phone number:</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
{formik.touched.email && formik.errors.email ? (
|
||||
<p className="error-reg">{formik.errors.email}</p>
|
||||
) : null}
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
{formik.touched.password && formik.errors.password ? (
|
||||
<p className="error-reg">{formik.errors.password}</p>
|
||||
) : null}
|
||||
<input type="submit" value="Create new account"/>
|
||||
<p className="login">
|
||||
Already have an account? <Link to="/login">Login</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSortContext } from '../../context/SortContext.jsx';
|
||||
import { useSortSearchState } from '../../hooks/sortParamsHooks.jsx';
|
||||
|
||||
export function SortableHeader({ columnName, children }) {
|
||||
const { sort, updateSort } = useSortSearchState(columnName);
|
||||
const sortContext = useSortContext();
|
||||
|
||||
function handleSort() {
|
||||
const newSort = updateSort();
|
||||
sortContext.onSort(newSort);
|
||||
}
|
||||
|
||||
return (
|
||||
<th style={{ cursor: 'pointer' }}
|
||||
onClick={ handleSort }>
|
||||
{ children }
|
||||
</th>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react';
|
||||
import { createClassName } from '../../utils/StringUtils.js'
|
||||
|
||||
export function TimePicker({ title, onChange, className, defaultHour, defaultMinute }) {
|
||||
const [ hours, setHours ] = useState(defaultHour ?? '');
|
||||
const [ minutes, setMinutes ] = useState(defaultMinute ?? '');
|
||||
|
||||
function handleChange(hours, minutes) {
|
||||
onChange({
|
||||
hour: hours && hours !== '' ? parseInt(hours) : defaultHour ?? 0,
|
||||
minutes: minutes && minutes !== '' ? parseInt(minutes) : defaultMinute ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
function handleHourChange(value) {
|
||||
setHours(value);
|
||||
handleChange(value, minutes);
|
||||
}
|
||||
|
||||
function handleMinuteChange(value) {
|
||||
setMinutes(value);
|
||||
handleChange(hours, value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ createClassName([ 'd-inline-flex flex-column gap-1', className ]) }>
|
||||
<div>{ title }</div>
|
||||
<div className="d-inline-flex gap-2 align-items-center">
|
||||
<TimePart value={ hours }
|
||||
placeholder="HH"
|
||||
min={ 0 }
|
||||
max={ 23 }
|
||||
onChange={ handleHourChange } />
|
||||
:
|
||||
<TimePart value={ minutes }
|
||||
placeholder="MM"
|
||||
min={ 0 }
|
||||
max={ 59 }
|
||||
onChange={ handleMinuteChange } />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimePart({ value, placeholder, onChange, min, max }) {
|
||||
function handleChange(newValueString) {
|
||||
if (newValueString !== '') {
|
||||
if (isNaN(newValueString)) {
|
||||
onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = parseInt(newValueString);
|
||||
|
||||
if (newValue <= max && newValue >= min) {
|
||||
onChange(newValue)
|
||||
}
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input className="border py-1 px-2 text-center rounded-1"
|
||||
value={ value && value !== '' && parseInt(value) }
|
||||
placeholder={ placeholder }
|
||||
onChange={ e => handleChange(e.target.value) }
|
||||
size={ 3 }
|
||||
min={ min }
|
||||
max={ max } />
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user