Compare commits

..

No commits in common. "f6fbdd94f73c92f453c291f82bae3f6f323dda30" and "8856f25eacf4d555d6fbd6a568c1fe25acee29ce" have entirely different histories.

16 changed files with 507 additions and 434 deletions

9
.env
View File

@ -1,8 +1,9 @@
#REACT_APP_TUITIO_URL=http://localhost:5063/identity/authenticate?UserName={username}&Password={password} #REACT_APP_IDENTITY_AUTHENTICATION_URL=http://localhost:5063/identity/authenticate?UserName={username}&Password={password}
REACT_APP_TUITIO_URL=https://lab.code-rove.com/tuitio REACT_APP_IDENTITY_AUTHENTICATION_URL=https://lab.code-rove.com/identity-server-api/identity/authenticate?UserName={username}&Password={password}
#REACT_APP_NETWORK_RESURRECTOR_API_URL=http://localhost:5064 REACT_APP_NETWORK_RESURRECTOR_API_URL=http://localhost:5064
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api #REACT_APP_NETWORK_RESURRECTOR_SERVER_URL=http://localhost:5062
#REACT_APP_NETWORK_RESURRECTOR_SERVER_URL=https://lab.code-rove.com/network-resurrector-server-api
#600000 milliseconds = 10 minutes #600000 milliseconds = 10 minutes
REACT_APP_MACHINE_PING_INTERVAL=600000 REACT_APP_MACHINE_PING_INTERVAL=600000

View File

@ -1,5 +1,5 @@
PUBLIC_URL=/network-resurrector/ PUBLIC_URL=/network-resurrector/
REACT_APP_TUITIO_URL=https://lab.code-rove.com/tuitio REACT_APP_IDENTITY_AUTHENTICATION_URL=https://lab.code-rove.com/tuitio/identity/authenticate?UserName={username}&Password={password}
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api
#900000 milliseconds = 15 minutes #900000 milliseconds = 15 minutes

22
LICENSE
View File

@ -1,22 +0,0 @@
MIT License
Copyright (c) 2020 Tudor Stanciu
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

694
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@flare/tuitio-client-react": "^1.0.0", "@flare/js-utils": "^1.0.2",
"@material-ui/core": "^4.11.2", "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.6", "@testing-library/jest-dom": "^5.11.6",

View File

@ -2,7 +2,7 @@ import React from "react";
import ApplicationStepper from "./stepper/ApplicationStepper"; import ApplicationStepper from "./stepper/ApplicationStepper";
import Switcher from "./Switcher"; import Switcher from "./Switcher";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { useTuitioToken } from "@flare/tuitio-client-react"; import { useAuthorizationToken } from "../../hooks";
import LoginContainer from "../../features/login/components/LoginContainer"; import LoginContainer from "../../features/login/components/LoginContainer";
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
@ -16,7 +16,7 @@ const useStyles = makeStyles(() => ({
const Main = () => { const Main = () => {
const classes = useStyles(); const classes = useStyles();
const { validate: validateToken } = useTuitioToken(); const { validateToken } = useAuthorizationToken();
const tokenIsValid = validateToken(); const tokenIsValid = validateToken();
return ( return (

View File

@ -1,4 +1,4 @@
import React, { useState, useMemo } from "react"; import React, { useState, useCallback } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import { import {
@ -18,29 +18,35 @@ import LoginComponent from "./LoginComponent";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useToast } from "../../../hooks"; import { useToast } from "../../../hooks";
import styles from "../styles"; import styles from "../styles";
import { useTuitioUser } from "@flare/tuitio-client-react";
const useStyles = makeStyles(styles); const useStyles = makeStyles(styles);
const LoggedInComponent = ({ credentials, onChange, onLogin, onLogout }) => { const LoggedInComponent = ({
credentials,
token,
onChange,
onLogin,
onLogout
}) => {
const classes = useStyles(); const classes = useStyles();
const { t } = useTranslation(); const { t } = useTranslation();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const { info } = useToast(); const { info } = useToast();
const { lastLoginDate, userName } = useTuitioUser();
const handleExpandLogin = () => { const handleExpandLogin = () => {
setExpanded(!expanded); setExpanded(!expanded);
}; };
const loginDate = useMemo(() => { const getTokenValidFrom = useCallback(() => {
if (lastLoginDate) { const tokenValidFrom = token?.validFrom;
const valueForDisplay = t("LONG_DATE", { date: lastLoginDate });
if (tokenValidFrom) {
const valueForDisplay = t("LONG_DATE", { date: tokenValidFrom });
return valueForDisplay; return valueForDisplay;
} }
return "N/A"; return "N/A";
}, [lastLoginDate, t]); }, [token, t]);
const handleLogin = async () => { const handleLogin = async () => {
const result = await onLogin(); const result = await onLogin();
@ -59,11 +65,15 @@ const LoggedInComponent = ({ credentials, onChange, onLogin, onLogout }) => {
<AccountBox /> <AccountBox />
</Avatar> </Avatar>
} }
title={<strong>{t("Login.Hello", { username: userName })}</strong>} title={
<strong>
{t("Login.Hello", { username: credentials.userName })}
</strong>
}
subheader={ subheader={
<Tooltip title={t("Login.AuthenticationDate")}> <Tooltip title={t("Login.AuthenticationDate")}>
<Typography variant="caption" display="block"> <Typography variant="caption" display="block">
{loginDate} {getTokenValidFrom()}
</Typography> </Typography>
</Tooltip> </Tooltip>
} }
@ -107,6 +117,7 @@ const LoggedInComponent = ({ credentials, onChange, onLogin, onLogout }) => {
LoggedInComponent.propTypes = { LoggedInComponent.propTypes = {
credentials: PropTypes.object.isRequired, credentials: PropTypes.object.isRequired,
token: PropTypes.object,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired, onLogin: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired onLogout: PropTypes.func.isRequired

View File

@ -43,9 +43,6 @@ const LoginComponent = ({ credentials, onChange, onLogin }) => {
className={classes.field} className={classes.field}
onChange={onChange("password")} onChange={onChange("password")}
value={credentials.password} value={credentials.password}
onKeyDown={e => {
if (e.key === "Enter") onLogin();
}}
/> />
</CardContent> </CardContent>
<CardActions> <CardActions>

View File

@ -1,49 +1,59 @@
import React, { useState } from "react"; import React, { useContext } from "react";
import LoginCard from "./LoginCard"; import LoginCard from "./LoginCard";
import { useToast } from "../../../hooks"; import { authenticate, invalidate } from "../../../utils/identity";
import {
ApplicationStateContext,
ApplicationDispatchContext
} from "../../../state/ApplicationContexts";
import { useToast, useAuthorizationToken } from "../../../hooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LoggedInComponent from "./LoggedInComponent"; import LoggedInComponent from "./LoggedInComponent";
import { useTuitioClient, useTuitioToken } from "@flare/tuitio-client-react";
const LoginContainer = () => { const LoginContainer = () => {
const [credentials, setCredentials] = useState({ const state = useContext(ApplicationStateContext);
userName: "", const dispatchActions = useContext(ApplicationDispatchContext);
password: ""
});
const { error } = useToast(); const { error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { login, logout } = useTuitioClient({ const { tokenIsValid, invalidateToken, getToken } = useAuthorizationToken();
onLoginFailed: response => error(t("Login.IncorrectCredentials")),
onLoginError: err => error(err.message)
});
const { valid: tokenIsValid } = useTuitioToken();
const handleChange = prop => event => { const handleChange = prop => event => {
setCredentials(prev => ({ ...prev, [prop]: event.target.value })); dispatchActions.onCredentialsChange(prop, event.target.value);
}; };
const handleLogin = () => { const handleLogin = async () => {
const { userName, password } = credentials; const { userName, password } = state.credentials;
return login(userName, password);
try {
const response = await authenticate(userName, password);
if (response.status === "SUCCESS") {
dispatchActions.onAuthorizationTokenChange(response.token);
return response.token;
} else if (response.status === "BAD_CREDENTIALS") {
error(t("Login.IncorrectCredentials"));
}
} catch (err) {
error(err.message);
}
}; };
const handleLogout = () => { const handleLogout = () => {
logout(); invalidate();
invalidateToken();
}; };
return ( return (
<> <>
{tokenIsValid ? ( {tokenIsValid ? (
<LoggedInComponent <LoggedInComponent
credentials={credentials} credentials={state.credentials}
token={getToken()}
onChange={handleChange} onChange={handleChange}
onLogin={handleLogin} onLogin={handleLogin}
onLogout={handleLogout} onLogout={handleLogout}
/> />
) : ( ) : (
<LoginCard <LoginCard
credentials={credentials} credentials={state.credentials}
onChange={handleChange} onChange={handleChange}
onLogin={handleLogin} onLogin={handleLogin}
/> />

View File

@ -1 +1,2 @@
export { useToast } from "./useToast"; export { useToast } from "./useToast";
export { useAuthorizationToken } from "./useAuthorizationToken";

View File

@ -0,0 +1,29 @@
import { useContext } from "react";
import {
ApplicationStateContext,
ApplicationDispatchContext
} from "../state/ApplicationContexts";
export const useAuthorizationToken = () => {
const state = useContext(ApplicationStateContext);
const dispatchActions = useContext(ApplicationDispatchContext);
const getToken = () => state.security.authorization.token;
const validateToken = () => {
const token = getToken();
if (!token) {
return false;
}
const valid = new Date(token.validUntil) >= new Date();
return valid;
};
const tokenIsValid = validateToken();
const invalidateToken = () => {
dispatchActions.onAuthorizationTokenChange(null);
};
return { getToken, validateToken, tokenIsValid, invalidateToken };
};

View File

@ -4,14 +4,11 @@ import "./index.css";
import "./utils/i18n"; import "./utils/i18n";
import App from "./components/App"; import App from "./components/App";
import { BrowserRouter as Router } from "react-router-dom"; import { BrowserRouter as Router } from "react-router-dom";
import { TuitioProvider } from "@flare/tuitio-client-react";
ReactDOM.render( ReactDOM.render(
<Router basename={process.env.PUBLIC_URL || ""}> <Router basename={process.env.PUBLIC_URL || ""}>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<TuitioProvider tuitioUrl={process.env.REACT_APP_TUITIO_URL}> <App />
<App />
</TuitioProvider>
</Suspense> </Suspense>
</Router>, </Router>,
document.getElementById("root") document.getElementById("root")

View File

@ -1,4 +1,20 @@
import { localStorage } from "@flare/js-utils";
import { storageKeys } from "../utils/identity";
const { getItem } = localStorage;
const token = getItem(storageKeys.TOKEN);
const userName = getItem(storageKeys.USER);
export const initialState = { export const initialState = {
credentials: {
userName: userName || "",
password: ""
},
security: {
authorization: {
token
}
},
network: { network: {
machines: Object.assign([], { loaded: false }), machines: Object.assign([], { loaded: false }),
test: "" test: ""

View File

@ -1,5 +1,15 @@
export function reducer(state, action) { export function reducer(state, action) {
switch (action.type) { switch (action.type) {
case "onCredentialsChange": {
const { prop, value } = action.payload;
return {
...state,
credentials: {
...state.credentials,
[prop]: value
}
};
}
case "onNetworkChange": { case "onNetworkChange": {
const { prop, value } = action.payload; const { prop, value } = action.payload;
return { return {
@ -10,6 +20,19 @@ export function reducer(state, action) {
} }
}; };
} }
case "onAuthorizationTokenChange": {
const { token } = action.payload;
return {
...state,
security: {
...state.security,
authorization: {
...state.security.authorization,
token
}
}
};
}
default: { default: {
return state; return state;
} }
@ -17,6 +40,10 @@ export function reducer(state, action) {
} }
export const dispatchActions = dispatch => ({ export const dispatchActions = dispatch => ({
onCredentialsChange: (prop, value) =>
dispatch({ type: "onCredentialsChange", payload: { prop, value } }),
onNetworkChange: (prop, value) => onNetworkChange: (prop, value) =>
dispatch({ type: "onNetworkChange", payload: { prop, value } }) dispatch({ type: "onNetworkChange", payload: { prop, value } }),
onAuthorizationTokenChange: token =>
dispatch({ type: "onAuthorizationTokenChange", payload: { token } })
}); });

View File

@ -1,9 +1,12 @@
import axios from "axios"; import axios from "axios";
import i18next from "i18next"; import i18next from "i18next";
import { fetch as fetchTuitioData } from "@flare/tuitio-client"; import { localStorage } from "@flare/js-utils";
import { storageKeys } from "./identity";
const { getItem } = localStorage;
function getHeaders() { function getHeaders() {
const { token } = fetchTuitioData(); const token = getItem(storageKeys.TOKEN);
const language = i18next.language; const language = i18next.language;
return { return {

37
src/utils/identity.js Normal file
View File

@ -0,0 +1,37 @@
import { request } from "./axios";
import { localStorage } from "@flare/js-utils";
const { setItem, getItem, removeItem } = localStorage;
const storageKeys = {
TOKEN: "AUTHORIZATION_TOKEN",
USER: "USER_NAME"
};
const authenticate = async (userName, password) => {
const urlTemplate = process.env.REACT_APP_IDENTITY_AUTHENTICATION_URL;
const url = urlTemplate
.replace("{username}", userName)
.replace("{password}", password);
const options = {
method: "post"
};
const response = await request(url, options);
if (response.status === "SUCCESS") {
setItem(storageKeys.TOKEN, response.token);
setItem(storageKeys.USER, userName);
}
return response;
};
const invalidate = () => {
const token = getItem(storageKeys.TOKEN);
if (token) {
removeItem(storageKeys.TOKEN);
removeItem(storageKeys.USER);
}
};
export { storageKeys, authenticate, invalidate };