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_NETWORK_RESURRECTOR_API_URL=http://localhost:5064
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api
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_SERVER_URL=http://localhost:5062
#REACT_APP_NETWORK_RESURRECTOR_SERVER_URL=https://lab.code-rove.com/network-resurrector-server-api
#600000 milliseconds = 10 minutes
REACT_APP_MACHINE_PING_INTERVAL=600000

View File

@ -1,5 +1,5 @@
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
#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,
"dependencies": {
"@flare/tuitio-client-react": "^1.0.0",
"@flare/js-utils": "^1.0.2",
"@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@testing-library/jest-dom": "^5.11.6",

View File

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

View File

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

View File

@ -1,49 +1,59 @@
import React, { useState } from "react";
import React, { useContext } from "react";
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 LoggedInComponent from "./LoggedInComponent";
import { useTuitioClient, useTuitioToken } from "@flare/tuitio-client-react";
const LoginContainer = () => {
const [credentials, setCredentials] = useState({
userName: "",
password: ""
});
const state = useContext(ApplicationStateContext);
const dispatchActions = useContext(ApplicationDispatchContext);
const { error } = useToast();
const { t } = useTranslation();
const { login, logout } = useTuitioClient({
onLoginFailed: response => error(t("Login.IncorrectCredentials")),
onLoginError: err => error(err.message)
});
const { valid: tokenIsValid } = useTuitioToken();
const { tokenIsValid, invalidateToken, getToken } = useAuthorizationToken();
const handleChange = prop => event => {
setCredentials(prev => ({ ...prev, [prop]: event.target.value }));
dispatchActions.onCredentialsChange(prop, event.target.value);
};
const handleLogin = () => {
const { userName, password } = credentials;
return login(userName, password);
const handleLogin = async () => {
const { userName, password } = state.credentials;
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 = () => {
logout();
invalidate();
invalidateToken();
};
return (
<>
{tokenIsValid ? (
<LoggedInComponent
credentials={credentials}
credentials={state.credentials}
token={getToken()}
onChange={handleChange}
onLogin={handleLogin}
onLogout={handleLogout}
/>
) : (
<LoginCard
credentials={credentials}
credentials={state.credentials}
onChange={handleChange}
onLogin={handleLogin}
/>

View File

@ -1 +1,2 @@
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 App from "./components/App";
import { BrowserRouter as Router } from "react-router-dom";
import { TuitioProvider } from "@flare/tuitio-client-react";
ReactDOM.render(
<Router basename={process.env.PUBLIC_URL || ""}>
<Suspense fallback={<div>Loading...</div>}>
<TuitioProvider tuitioUrl={process.env.REACT_APP_TUITIO_URL}>
<App />
</TuitioProvider>
<App />
</Suspense>
</Router>,
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 = {
credentials: {
userName: userName || "",
password: ""
},
security: {
authorization: {
token
}
},
network: {
machines: Object.assign([], { loaded: false }),
test: ""

View File

@ -1,5 +1,15 @@
export function reducer(state, action) {
switch (action.type) {
case "onCredentialsChange": {
const { prop, value } = action.payload;
return {
...state,
credentials: {
...state.credentials,
[prop]: value
}
};
}
case "onNetworkChange": {
const { prop, value } = action.payload;
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: {
return state;
}
@ -17,6 +40,10 @@ export function reducer(state, action) {
}
export const dispatchActions = dispatch => ({
onCredentialsChange: (prop, value) =>
dispatch({ type: "onCredentialsChange", payload: { 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 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() {
const { token } = fetchTuitioData();
const token = getItem(storageKeys.TOKEN);
const language = i18next.language;
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 };