[CI] Add linting option for frontend and enforce styling (#1565)
We now extend the default Eslint configuration and enforce styling with prettier for all of our frontend code.pull/1557/head
parent
a5ea31e482
commit
689fd7cb95
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
set +u
|
set +u
|
||||||
|
|
||||||
|
if [[ $BUILDKITE_LABEL == ":service_dog: Linting" ]]; then
|
||||||
|
cd web && yarn install && cd ../
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $BUILDKITE_LABEL =~ ":selenium:" ]]; then
|
if [[ $BUILDKITE_LABEL =~ ":selenium:" ]]; then
|
||||||
DEFAULT_ARCH=coverage
|
DEFAULT_ARCH=coverage
|
||||||
echo "--- :docker: Extract, load and tag build container"
|
echo "--- :docker: Extract, load and tag build container"
|
||||||
|
|
|
@ -5,4 +5,8 @@ runner:
|
||||||
- '%E%f:%l:%c: %m'
|
- '%E%f:%l:%c: %m'
|
||||||
- '%E%f:%l: %m'
|
- '%E%f:%l: %m'
|
||||||
- '%C%.%#'
|
- '%C%.%#'
|
||||||
|
level: error
|
||||||
|
eslint:
|
||||||
|
cmd: cd web && eslint -f rdjson '*/**/*.{js,ts,tsx}'
|
||||||
|
format: rdjson
|
||||||
level: error
|
level: error
|
|
@ -0,0 +1,46 @@
|
||||||
|
module.exports = {
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "tsconfig.json"
|
||||||
|
},
|
||||||
|
"ignorePatterns": "build/*",
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"typescript": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"prettier/@typescript-eslint",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"groups": [
|
||||||
|
"builtin",
|
||||||
|
"external",
|
||||||
|
"internal"
|
||||||
|
],
|
||||||
|
"pathGroups": [
|
||||||
|
{
|
||||||
|
"pattern": "react",
|
||||||
|
"group": "external",
|
||||||
|
"position": "before"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pathGroupsExcludedImportTypes": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"newlines-between": "always",
|
||||||
|
"alphabetize": {
|
||||||
|
"order": "asc",
|
||||||
|
"caseInsensitive": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 120,
|
||||||
|
tabWidth: 4,
|
||||||
|
bracketSpacing: true,
|
||||||
|
jsxBracketSameLine: false,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: "all"
|
||||||
|
};
|
|
@ -27,6 +27,10 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.5",
|
"enzyme-adapter-react-16": "^1.15.5",
|
||||||
|
"eslint-config-prettier": "^7.1.0",
|
||||||
|
"eslint-import-resolver-typescript": "^2.3.0",
|
||||||
|
"eslint-plugin-prettier": "^3.3.0",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
"qrcode.react": "^1.0.1",
|
"qrcode.react": "^1.0.1",
|
||||||
"query-string": "^6.13.8",
|
"query-string": "^6.13.8",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
|
@ -43,6 +47,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
|
"lint": "eslint '*/**/*.{js,ts,tsx}' --fix",
|
||||||
"coverage": "craco build",
|
"coverage": "craco build",
|
||||||
"test": "react-scripts test --coverage --no-cache",
|
"test": "react-scripts test --coverage --no-cache",
|
||||||
"report": "nyc report -r clover -r json -r lcov -r text",
|
"report": "nyc report -r clover -r json -r lcov -r text",
|
||||||
|
@ -62,5 +67,8 @@
|
||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint-formatter-rdjson": "^1.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import { shallow } from "enzyme";
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
import { shallow } from "enzyme";
|
||||||
shallow(<App />);
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
shallow(<App />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { config as faConfig } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { BrowserRouter as Router, Route, Switch, Redirect } from "react-router-dom";
|
||||||
|
|
||||||
|
import NotificationBar from "./components/NotificationBar";
|
||||||
|
import NotificationsContext from "./hooks/NotificationsContext";
|
||||||
|
import { Notification } from "./models/Notifications";
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router, Route, Switch, Redirect
|
FirstFactorRoute,
|
||||||
} from "react-router-dom";
|
ResetPasswordStep2Route,
|
||||||
import ResetPasswordStep1 from './views/ResetPassword/ResetPasswordStep1';
|
ResetPasswordStep1Route,
|
||||||
import ResetPasswordStep2 from './views/ResetPassword/ResetPasswordStep2';
|
RegisterSecurityKeyRoute,
|
||||||
import RegisterSecurityKey from './views/DeviceRegistration/RegisterSecurityKey';
|
|
||||||
import RegisterOneTimePassword from './views/DeviceRegistration/RegisterOneTimePassword';
|
|
||||||
import {
|
|
||||||
FirstFactorRoute, ResetPasswordStep2Route,
|
|
||||||
ResetPasswordStep1Route, RegisterSecurityKeyRoute,
|
|
||||||
RegisterOneTimePasswordRoute,
|
RegisterOneTimePasswordRoute,
|
||||||
LogoutRoute,
|
LogoutRoute,
|
||||||
} from "./Routes";
|
} from "./Routes";
|
||||||
import LoginPortal from './views/LoginPortal/LoginPortal';
|
import { getBasePath } from "./utils/BasePath";
|
||||||
import NotificationsContext from './hooks/NotificationsContext';
|
import { getRememberMe, getResetPassword } from "./utils/Configuration";
|
||||||
import { Notification } from './models/Notifications';
|
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
|
||||||
import NotificationBar from './components/NotificationBar';
|
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
|
||||||
import SignOut from './views/LoginPortal/SignOut/SignOut';
|
import LoginPortal from "./views/LoginPortal/LoginPortal";
|
||||||
import { getRememberMe, getResetPassword } from './utils/Configuration';
|
import SignOut from "./views/LoginPortal/SignOut/SignOut";
|
||||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
import ResetPasswordStep1 from "./views/ResetPassword/ResetPasswordStep1";
|
||||||
import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
|
import ResetPasswordStep2 from "./views/ResetPassword/ResetPasswordStep2";
|
||||||
import { getBasePath } from './utils/BasePath';
|
|
||||||
|
import "@fortawesome/fontawesome-svg-core/styles.css";
|
||||||
|
|
||||||
faConfig.autoAddCss = false;
|
faConfig.autoAddCss = false;
|
||||||
|
|
||||||
|
@ -28,7 +31,7 @@ const App: React.FC = () => {
|
||||||
const [notification, setNotification] = useState(null as Notification | null);
|
const [notification, setNotification] = useState(null as Notification | null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationsContext.Provider value={{ notification, setNotification }} >
|
<NotificationsContext.Provider value={{ notification, setNotification }}>
|
||||||
<Router basename={getBasePath()}>
|
<Router basename={getBasePath()}>
|
||||||
<NotificationBar onClose={() => setNotification(null)} />
|
<NotificationBar onClose={() => setNotification(null)} />
|
||||||
<Switch>
|
<Switch>
|
||||||
|
@ -48,9 +51,7 @@ const App: React.FC = () => {
|
||||||
<SignOut />
|
<SignOut />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={FirstFactorRoute}>
|
<Route path={FirstFactorRoute}>
|
||||||
<LoginPortal
|
<LoginPortal rememberMe={getRememberMe()} resetPassword={getResetPassword()} />
|
||||||
rememberMe={getRememberMe()}
|
|
||||||
resetPassword={getResetPassword()} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Redirect to={FirstFactorRoute} />
|
<Redirect to={FirstFactorRoute} />
|
||||||
|
@ -59,6 +60,6 @@ const App: React.FC = () => {
|
||||||
</Router>
|
</Router>
|
||||||
</NotificationsContext.Provider>
|
</NotificationsContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export const FirstFactorRoute = "/";
|
export const FirstFactorRoute = "/";
|
||||||
export const AuthenticatedRoute = "/authenticated";
|
export const AuthenticatedRoute = "/authenticated";
|
||||||
|
|
||||||
|
@ -11,4 +10,4 @@ export const ResetPasswordStep1Route = "/reset-password/step1";
|
||||||
export const ResetPasswordStep2Route = "/reset-password/step2";
|
export const ResetPasswordStep2Route = "/reset-password/step2";
|
||||||
export const RegisterSecurityKeyRoute = "/security-key/register";
|
export const RegisterSecurityKeyRoute = "/security-key/register";
|
||||||
export const RegisterOneTimePasswordRoute = "/one-time-password/register";
|
export const RegisterOneTimePasswordRoute = "/one-time-password/register";
|
||||||
export const LogoutRoute = "/logout";
|
export const LogoutRoute = "/logout";
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import AppStoreBadges from "./AppStoreBadges";
|
import AppStoreBadges from "./AppStoreBadges";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement("div");
|
||||||
ReactDOM.render(<AppStoreBadges
|
ReactDOM.render(<AppStoreBadges iconSize={32} appleStoreLink="http://apple" googlePlayLink="http://google" />, div);
|
||||||
iconSize={32}
|
|
||||||
appleStoreLink="http://apple"
|
|
||||||
googlePlayLink="http://google" />, div);
|
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import GooglePlay from "../assets/images/googleplay-badge.svg";
|
|
||||||
import AppleStore from "../assets/images/applestore-badge.svg";
|
|
||||||
import { Link } from "@material-ui/core";
|
import { Link } from "@material-ui/core";
|
||||||
|
|
||||||
|
import AppleStore from "../assets/images/applestore-badge.svg";
|
||||||
|
import GooglePlay from "../assets/images/googleplay-badge.svg";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
iconSize: number;
|
iconSize: number;
|
||||||
googlePlayLink: string;
|
googlePlayLink: string;
|
||||||
|
@ -25,8 +27,8 @@ const AppStoreBadges = function (props: Props) {
|
||||||
<Link href={props.appleStoreLink} target={target}>
|
<Link href={props.appleStoreLink} target={target}>
|
||||||
<img src={AppleStore} alt="apple store" style={{ width }} />
|
<img src={AppleStore} alt="apple store" style={{ width }} />
|
||||||
</Link>
|
</Link>
|
||||||
</div >
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AppStoreBadges
|
export default AppStoreBadges;
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import { mount, shallow } from "enzyme";
|
|
||||||
import { expect } from "chai";
|
|
||||||
import ColoredSnackbarContent from "./ColoredSnackbarContent";
|
|
||||||
import { SnackbarContent } from '@material-ui/core';
|
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
import { SnackbarContent } from "@material-ui/core";
|
||||||
const div = document.createElement('div');
|
import { expect } from "chai";
|
||||||
|
import { mount, shallow } from "enzyme";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import ColoredSnackbarContent from "./ColoredSnackbarContent";
|
||||||
|
|
||||||
|
it("renders without crashing", () => {
|
||||||
|
const div = document.createElement("div");
|
||||||
ReactDOM.render(<ColoredSnackbarContent level="success" message="this is a success" />, div);
|
ReactDOM.render(<ColoredSnackbarContent level="success" message="this is a success" />, div);
|
||||||
ReactDOM.unmountComponentAtNode(div);
|
ReactDOM.unmountComponentAtNode(div);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should contain the message', () => {
|
it("should contain the message", () => {
|
||||||
const el = mount(<ColoredSnackbarContent level="success" message="this is a success" />);
|
const el = mount(<ColoredSnackbarContent level="success" message="this is a success" />);
|
||||||
expect(el.text()).to.contain("this is a success");
|
expect(el.text()).to.contain("this is a success");
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
/* eslint-disable @typescript-eslint/no-unused-expressions */
|
||||||
it('should have correct color', () => {
|
it("should have correct color", () => {
|
||||||
let el = shallow(<ColoredSnackbarContent level="success" message="this is a success" />);
|
let el = shallow(<ColoredSnackbarContent level="success" message="this is a success" />);
|
||||||
expect(el.find(SnackbarContent).props().className!.indexOf("success") > -1).to.be.true;
|
expect(el.find(SnackbarContent).props().className!.indexOf("success") > -1).to.be.true;
|
||||||
|
|
||||||
|
@ -30,4 +32,4 @@ it('should have correct color', () => {
|
||||||
el = shallow(<ColoredSnackbarContent level="warning" message="this is an warning" />);
|
el = shallow(<ColoredSnackbarContent level="warning" message="this is an warning" />);
|
||||||
expect(el.find(SnackbarContent).props().className!.indexOf("warning") > -1).to.be.true;
|
expect(el.find(SnackbarContent).props().className!.indexOf("warning") > -1).to.be.true;
|
||||||
});
|
});
|
||||||
/* eslint-enable @typescript-eslint/no-unused-expressions */
|
/* eslint-enable @typescript-eslint/no-unused-expressions */
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import CheckCircleIcon from '@material-ui/icons/CheckCircle';
|
|
||||||
import ErrorIcon from '@material-ui/icons/Error';
|
|
||||||
import InfoIcon from '@material-ui/icons/Info';
|
|
||||||
import WarningIcon from '@material-ui/icons/Warning';
|
|
||||||
import { makeStyles, SnackbarContent } from "@material-ui/core";
|
import { makeStyles, SnackbarContent } from "@material-ui/core";
|
||||||
import { amber, green } from '@material-ui/core/colors';
|
import { amber, green } from "@material-ui/core/colors";
|
||||||
import classnames from "classnames";
|
|
||||||
import { SnackbarContentProps } from "@material-ui/core/SnackbarContent";
|
import { SnackbarContentProps } from "@material-ui/core/SnackbarContent";
|
||||||
|
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
|
||||||
|
import ErrorIcon from "@material-ui/icons/Error";
|
||||||
|
import InfoIcon from "@material-ui/icons/Info";
|
||||||
|
import WarningIcon from "@material-ui/icons/Warning";
|
||||||
|
import classnames from "classnames";
|
||||||
|
|
||||||
const variantIcon = {
|
const variantIcon = {
|
||||||
success: CheckCircleIcon,
|
success: CheckCircleIcon,
|
||||||
|
@ -39,13 +39,14 @@ const ColoredSnackbarContent = function (props: Props) {
|
||||||
{message}
|
{message}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
{...others} />
|
{...others}
|
||||||
)
|
/>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default ColoredSnackbarContent
|
export default ColoredSnackbarContent;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
success: {
|
success: {
|
||||||
backgroundColor: green[600],
|
backgroundColor: green[600],
|
||||||
},
|
},
|
||||||
|
@ -66,7 +67,7 @@ const useStyles = makeStyles(theme => ({
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import FailureIcon from "./FailureIcon";
|
import FailureIcon from "./FailureIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<FailureIcon />);
|
mount(<FailureIcon />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
|
|
||||||
|
|
||||||
export interface Props { }
|
import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
const FailureIcon = function (props: Props) {
|
const FailureIcon = function (props: Props) {
|
||||||
return (
|
return <FontAwesomeIcon icon={faTimesCircle} size="4x" color="red" className="failure-icon" />;
|
||||||
<FontAwesomeIcon icon={faTimesCircle} size="4x" color="red" className="failure-icon" />
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FailureIcon
|
export default FailureIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import FingerTouchIcon from "./FingerTouchIcon";
|
import FingerTouchIcon from "./FingerTouchIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<FingerTouchIcon size={32} />);
|
mount(<FingerTouchIcon size={32} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,21 +1,32 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import style from "./FingerTouchIcon.module.css";
|
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
export interface Props {
|
import style from "./FingerTouchIcon.module.css";
|
||||||
size: number;
|
|
||||||
|
|
||||||
animated?: boolean;
|
export interface Props {
|
||||||
strong?: boolean;
|
size: number;
|
||||||
|
|
||||||
|
animated?: boolean;
|
||||||
|
strong?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FingerTouchIcon = function (props: Props) {
|
const FingerTouchIcon = function (props: Props) {
|
||||||
const shakingClass = (props.animated) ? style.shaking : undefined;
|
const shakingClass = props.animated ? style.shaking : undefined;
|
||||||
const strong = (props.strong) ? style.strong : undefined;
|
const strong = props.strong ? style.strong : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg x="0px" y="0px" viewBox="0 0 500 500" width={props.size} height={props.size} className={classnames(style.hand, strong)}>
|
<svg
|
||||||
<path className={shakingClass} d="M438.827,186.347l-80.213-88.149c-15.872-15.872-41.728-15.893-57.749,0.128c-5.077,5.077-8.533,11.157-10.325,17.643
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
width={props.size}
|
||||||
|
height={props.size}
|
||||||
|
className={classnames(style.hand, strong)}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
className={shakingClass}
|
||||||
|
d="M438.827,186.347l-80.213-88.149c-15.872-15.872-41.728-15.893-57.749,0.128c-5.077,5.077-8.533,11.157-10.325,17.643
|
||||||
c-15.957-12.224-38.976-11.008-53.675,3.691c-5.056,5.077-8.512,11.157-10.347,17.621c-15.957-12.181-38.976-10.987-53.653,3.712
|
c-15.957-12.224-38.976-11.008-53.675,3.691c-5.056,5.077-8.512,11.157-10.347,17.621c-15.957-12.181-38.976-10.987-53.653,3.712
|
||||||
c-4.971,4.971-8.384,10.901-10.24,17.216l-37.803-37.803c-15.872-15.872-41.728-15.893-57.749,0.128
|
c-4.971,4.971-8.384,10.901-10.24,17.216l-37.803-37.803c-15.872-15.872-41.728-15.893-57.749,0.128
|
||||||
c-15.893,15.872-15.893,41.728,0,57.621l145.237,145.237l-86.144,13.525c-23.275,3.328-40.832,23.552-40.832,47.083
|
c-15.893,15.872-15.893,41.728,0,57.621l145.237,145.237l-86.144,13.525c-23.275,3.328-40.832,23.552-40.832,47.083
|
||||||
|
@ -31,14 +42,17 @@ const FingerTouchIcon = function (props: Props) {
|
||||||
c0.021,0.021,0.021,0.021,0.021,0.021h0.021c0.021,0,0.021,0.021,0.021,0.021c4.181,3.968,10.795,3.883,14.869-0.213
|
c0.021,0.021,0.021,0.021,0.021,0.021h0.021c0.021,0,0.021,0.021,0.021,0.021c4.181,3.968,10.795,3.883,14.869-0.213
|
||||||
c4.16-4.16,4.16-10.923,0-15.083l-0.917-0.917c-3.669-3.669-5.696-8.555-5.696-13.739s2.005-10.048,5.803-13.845
|
c4.16-4.16,4.16-10.923,0-15.083l-0.917-0.917c-3.669-3.669-5.696-8.555-5.696-13.739s2.005-10.048,5.803-13.845
|
||||||
c7.595-7.552,19.883-7.531,27.115-0.363l79.872,87.787C439.125,218.389,448,241.301,448,265.216
|
c7.595-7.552,19.883-7.531,27.115-0.363l79.872,87.787C439.125,218.389,448,241.301,448,265.216
|
||||||
C448,290.816,438.037,314.88,419.925,332.992z"/>
|
C448,290.816,438.037,314.88,419.925,332.992z"
|
||||||
<path className={style.wave} d="M183.381,109.931C167.851,75.563,133.547,53.333,96,53.333c-52.928,0-96,43.072-96,96
|
/>
|
||||||
|
<path
|
||||||
|
className={style.wave}
|
||||||
|
d="M183.381,109.931C167.851,75.563,133.547,53.333,96,53.333c-52.928,0-96,43.072-96,96
|
||||||
c0,37.547,22.229,71.851,56.597,87.403c1.429,0.64,2.923,0.939,4.395,0.939c4.053,0,7.936-2.347,9.728-6.272
|
c0,37.547,22.229,71.851,56.597,87.403c1.429,0.64,2.923,0.939,4.395,0.939c4.053,0,7.936-2.347,9.728-6.272
|
||||||
c2.411-5.376,0.021-11.691-5.333-14.123c-26.752-12.096-44.053-38.763-44.053-67.947c0-41.173,33.493-74.667,74.667-74.667
|
c2.411-5.376,0.021-11.691-5.333-14.123c-26.752-12.096-44.053-38.763-44.053-67.947c0-41.173,33.493-74.667,74.667-74.667
|
||||||
c29.184,0,55.851,17.301,67.947,44.053c2.411,5.376,8.747,7.787,14.101,5.333C183.424,121.621,185.813,115.307,183.381,109.931z"
|
c29.184,0,55.851,17.301,67.947,44.053c2.411,5.376,8.747,7.787,14.101,5.333C183.424,121.621,185.813,115.307,183.381,109.931z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FingerTouchIcon
|
export default FingerTouchIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import FixedTextField from "./FixedTextField";
|
import FixedTextField from "./FixedTextField";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<FixedTextField />);
|
mount(<FixedTextField />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,34 +1,37 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
|
|
||||||
import { makeStyles } from "@material-ui/core";
|
import { makeStyles } from "@material-ui/core";
|
||||||
|
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component fixes outlined TextField
|
* This component fixes outlined TextField
|
||||||
* https://github.com/mui-org/material-ui/issues/14530#issuecomment-463576879
|
* https://github.com/mui-org/material-ui/issues/14530#issuecomment-463576879
|
||||||
*
|
*
|
||||||
* @param props the TextField props
|
* @param props the TextField props
|
||||||
*/
|
*/
|
||||||
const FixedTextField = function (props: TextFieldProps) {
|
const FixedTextField = function (props: TextFieldProps) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
return (
|
return (
|
||||||
<TextField {...props}
|
<TextField
|
||||||
|
{...props}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
classes: {
|
classes: {
|
||||||
root: style.label
|
root: style.label,
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
inputProps={{autoCapitalize: props.autoCapitalize}}>
|
inputProps={{ autoCapitalize: props.autoCapitalize }}
|
||||||
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</TextField>
|
</TextField>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FixedTextField
|
export default FixedTextField;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
label: {
|
label: {
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
paddingLeft: theme.spacing(0.1),
|
paddingLeft: theme.spacing(0.1),
|
||||||
paddingRight: theme.spacing(0.1),
|
paddingRight: theme.spacing(0.1),
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import InformationIcon from "./InformationIcon";
|
import InformationIcon from "./InformationIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<InformationIcon />);
|
mount(<InformationIcon />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
export interface Props { }
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
const InformationIcon = function (props: Props) {
|
const InformationIcon = function (props: Props) {
|
||||||
return (
|
return <FontAwesomeIcon icon={faInfoCircle} size="4x" color="#5858ff" className="information-icon" />;
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="4x" color="#5858ff" className="information-icon" />
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InformationIcon
|
export default InformationIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import LinearProgressBar from "./LinearProgressBar";
|
import LinearProgressBar from "./LinearProgressBar";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<LinearProgressBar value={40} />);
|
mount(<LinearProgressBar value={40} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { makeStyles, LinearProgress } from "@material-ui/core";
|
import { makeStyles, LinearProgress } from "@material-ui/core";
|
||||||
import { CSSProperties } from "@material-ui/styles";
|
import { CSSProperties } from "@material-ui/styles";
|
||||||
|
|
||||||
|
@ -10,13 +11,13 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
const LinearProgressBar = function (props: Props) {
|
const LinearProgressBar = function (props: Props) {
|
||||||
const style = makeStyles(theme => ({
|
const style = makeStyles((theme) => ({
|
||||||
progressRoot: {
|
progressRoot: {
|
||||||
height: props.height ? props.height : theme.spacing(),
|
height: props.height ? props.height : theme.spacing(),
|
||||||
},
|
},
|
||||||
transition: {
|
transition: {
|
||||||
transition: "transform .2s linear",
|
transition: "transform .2s linear",
|
||||||
}
|
},
|
||||||
}))();
|
}))();
|
||||||
return (
|
return (
|
||||||
<LinearProgress
|
<LinearProgress
|
||||||
|
@ -24,11 +25,12 @@ const LinearProgressBar = function (props: Props) {
|
||||||
variant="determinate"
|
variant="determinate"
|
||||||
classes={{
|
classes={{
|
||||||
root: style.progressRoot,
|
root: style.progressRoot,
|
||||||
bar1Determinate: style.transition
|
bar1Determinate: style.transition,
|
||||||
}}
|
}}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
className={props.className} />
|
className={props.className}
|
||||||
)
|
/>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default LinearProgressBar
|
export default LinearProgressBar;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import NotificationBar from "./NotificationBar";
|
import NotificationBar from "./NotificationBar";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<NotificationBar onClose={() => { }} />);
|
mount(<NotificationBar onClose={() => {}} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Snackbar } from "@material-ui/core";
|
import { Snackbar } from "@material-ui/core";
|
||||||
import ColoredSnackbarContent from "./ColoredSnackbarContent";
|
|
||||||
import { useNotifications } from "../hooks/NotificationsContext";
|
import { useNotifications } from "../hooks/NotificationsContext";
|
||||||
import { Notification } from "../models/Notifications";
|
import { Notification } from "../models/Notifications";
|
||||||
|
import ColoredSnackbarContent from "./ColoredSnackbarContent";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
@ -26,13 +28,15 @@ const NotificationBar = function (props: Props) {
|
||||||
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
anchorOrigin={{ vertical: "top", horizontal: "right" }}
|
||||||
autoHideDuration={tmpNotification ? tmpNotification.timeout * 1000 : 10000}
|
autoHideDuration={tmpNotification ? tmpNotification.timeout * 1000 : 10000}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
onExited={() => setTmpNotification(null)}>
|
onExited={() => setTmpNotification(null)}
|
||||||
|
>
|
||||||
<ColoredSnackbarContent
|
<ColoredSnackbarContent
|
||||||
className="notification"
|
className="notification"
|
||||||
level={tmpNotification ? tmpNotification.level : "info"}
|
level={tmpNotification ? tmpNotification.level : "info"}
|
||||||
message={tmpNotification ? tmpNotification.message : ""} />
|
message={tmpNotification ? tmpNotification.message : ""}
|
||||||
|
/>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default NotificationBar
|
export default NotificationBar;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import PieChartIcon from "./PieChartIcon";
|
import PieChartIcon from "./PieChartIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<PieChartIcon progress={40} />);
|
mount(<PieChartIcon progress={40} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,13 +23,18 @@ const PieChartIcon = function (props: Props) {
|
||||||
<svg height={`${width}`} width={`${height}`} viewBox="0 0 26 26">
|
<svg height={`${width}`} width={`${height}`} viewBox="0 0 26 26">
|
||||||
<circle r="12" cx="13" cy="13" fill="none" stroke={backgroundColor} strokeWidth="2" />
|
<circle r="12" cx="13" cy="13" fill="none" stroke={backgroundColor} strokeWidth="2" />
|
||||||
<circle r="9" cx="13" cy="13" fill={backgroundColor} stroke="transparent" />
|
<circle r="9" cx="13" cy="13" fill={backgroundColor} stroke="transparent" />
|
||||||
<circle r="5" cx="13" cy="13" fill="none"
|
<circle
|
||||||
|
r="5"
|
||||||
|
cx="13"
|
||||||
|
cy="13"
|
||||||
|
fill="none"
|
||||||
stroke={color}
|
stroke={color}
|
||||||
strokeWidth="10"
|
strokeWidth="10"
|
||||||
strokeDasharray={`${props.progress} ${maxProgress}`}
|
strokeDasharray={`${props.progress} ${maxProgress}`}
|
||||||
transform="rotate(-90) translate(-26)" />
|
transform="rotate(-90) translate(-26)"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PieChartIcon
|
export default PieChartIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import PushNotificationIcon from "./PushNotificationIcon";
|
import PushNotificationIcon from "./PushNotificationIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<PushNotificationIcon width={32} height={32} />);
|
mount(<PushNotificationIcon width={32} height={32} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { useIntermittentClass } from "../hooks/IntermittentClass";
|
||||||
import style from "./PushNotificationIcon.module.css";
|
import style from "./PushNotificationIcon.module.css";
|
||||||
import {useIntermittentClass} from "../hooks/IntermittentClass";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -13,34 +14,59 @@ const PushNotificationIcon = function (props: Props) {
|
||||||
const idleMilliseconds = 2500;
|
const idleMilliseconds = 2500;
|
||||||
const wiggleMilliseconds = 500;
|
const wiggleMilliseconds = 500;
|
||||||
const startMilliseconds = 500;
|
const startMilliseconds = 500;
|
||||||
const wiggleClass = useIntermittentClass((props.animated) ? style.wiggle : "", wiggleMilliseconds, idleMilliseconds, startMilliseconds);
|
const wiggleClass = useIntermittentClass(
|
||||||
|
props.animated ? style.wiggle : "",
|
||||||
|
wiggleMilliseconds,
|
||||||
|
idleMilliseconds,
|
||||||
|
startMilliseconds,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg x="0px" y="0px" viewBox="0 0 60 60" width={props.width} height={props.height} className={wiggleClass}>
|
<svg x="0px" y="0px" viewBox="0 0 60 60" width={props.width} height={props.height} className={wiggleClass}>
|
||||||
<g>
|
<g>
|
||||||
<path className="case" d="M42.595,0H17.405C14.977,0,13,1.977,13,4.405v51.189C13,58.023,14.977,60,17.405,60h25.189C45.023,60,47,58.023,47,55.595
|
<path
|
||||||
|
className="case"
|
||||||
|
d="M42.595,0H17.405C14.977,0,13,1.977,13,4.405v51.189C13,58.023,14.977,60,17.405,60h25.189C45.023,60,47,58.023,47,55.595
|
||||||
V4.405C47,1.977,45.023,0,42.595,0z M15,8h30v38H15V8z M17.405,2h25.189C43.921,2,45,3.079,45,4.405V6H15V4.405
|
V4.405C47,1.977,45.023,0,42.595,0z M15,8h30v38H15V8z M17.405,2h25.189C43.921,2,45,3.079,45,4.405V6H15V4.405
|
||||||
C15,3.079,16.079,2,17.405,2z M42.595,58H17.405C16.079,58,15,56.921,15,55.595V48h30v7.595C45,56.921,43.921,58,42.595,58z"/>
|
C15,3.079,16.079,2,17.405,2z M42.595,58H17.405C16.079,58,15,56.921,15,55.595V48h30v7.595C45,56.921,43.921,58,42.595,58z"
|
||||||
<path className="button" d="M30,49c-2.206,0-4,1.794-4,4s1.794,4,4,4s4-1.794,4-4S32.206,49,30,49z M30,55c-1.103,0-2-0.897-2-2s0.897-2,2-2
|
/>
|
||||||
s2,0.897,2,2S31.103,55,30,55z"/>
|
<path
|
||||||
<path className="speaker" d="M26,5h4c0.553,0,1-0.447,1-1s-0.447-1-1-1h-4c-0.553,0-1,0.447-1,1S25.447,5,26,5z"/>
|
className="button"
|
||||||
<path className="camera" d="M33,5h1c0.553,0,1-0.447,1-1s-0.447-1-1-1h-1c-0.553,0-1,0.447-1,1S32.447,5,33,5z"/>
|
d="M30,49c-2.206,0-4,1.794-4,4s1.794,4,4,4s4-1.794,4-4S32.206,49,30,49z M30,55c-1.103,0-2-0.897-2-2s0.897-2,2-2
|
||||||
|
s2,0.897,2,2S31.103,55,30,55z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="speaker"
|
||||||
|
d="M26,5h4c0.553,0,1-0.447,1-1s-0.447-1-1-1h-4c-0.553,0-1,0.447-1,1S25.447,5,26,5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="camera"
|
||||||
|
d="M33,5h1c0.553,0,1-0.447,1-1s-0.447-1-1-1h-1c-0.553,0-1,0.447-1,1S32.447,5,33,5z"
|
||||||
|
/>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<path d="M56.612,4.569c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c3.736,3.736,3.736,9.815,0,13.552
|
|
||||||
c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
|
|
||||||
C61.128,16.434,61.128,9.085,56.612,4.569z"/>
|
|
||||||
<path d="M52.401,6.845c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c1.237,1.237,1.918,2.885,1.918,4.639
|
|
||||||
s-0.681,3.401-1.918,4.638c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
|
|
||||||
c1.615-1.614,2.504-3.764,2.504-6.052S54.017,8.459,52.401,6.845z"/>
|
|
||||||
<path d="M4.802,5.983c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0c-4.516,4.516-4.516,11.864,0,16.38
|
|
||||||
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
|
|
||||||
C1.065,15.799,1.065,9.72,4.802,5.983z"/>
|
|
||||||
<path d="M9.013,6.569c-0.391-0.391-1.023-0.391-1.414,0c-1.615,1.614-2.504,3.764-2.504,6.052s0.889,4.438,2.504,6.053
|
|
||||||
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
|
|
||||||
c-1.237-1.237-1.918-2.885-1.918-4.639S7.775,9.22,9.013,7.983C9.403,7.593,9.403,6.96,9.013,6.569z"/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PushNotificationIcon
|
<path
|
||||||
|
d="M56.612,4.569c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c3.736,3.736,3.736,9.815,0,13.552
|
||||||
|
c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
|
||||||
|
C61.128,16.434,61.128,9.085,56.612,4.569z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M52.401,6.845c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c1.237,1.237,1.918,2.885,1.918,4.639
|
||||||
|
s-0.681,3.401-1.918,4.638c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
|
||||||
|
c1.615-1.614,2.504-3.764,2.504-6.052S54.017,8.459,52.401,6.845z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.802,5.983c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0c-4.516,4.516-4.516,11.864,0,16.38
|
||||||
|
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
|
||||||
|
C1.065,15.799,1.065,9.72,4.802,5.983z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.013,6.569c-0.391-0.391-1.023-0.391-1.414,0c-1.615,1.614-2.504,3.764-2.504,6.052s0.889,4.438,2.504,6.053
|
||||||
|
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
|
||||||
|
c-1.237-1.237-1.918-2.885-1.918-4.639S7.775,9.22,9.013,7.983C9.403,7.593,9.403,6.96,9.013,6.569z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PushNotificationIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import SuccessIcon from "./SuccessIcon";
|
import SuccessIcon from "./SuccessIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<SuccessIcon />);
|
mount(<SuccessIcon />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
const SuccessIcon = function () {
|
const SuccessIcon = function () {
|
||||||
return (
|
return <FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />;
|
||||||
<FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />
|
};
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SuccessIcon
|
export default SuccessIcon;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import TimerIcon from "./TimerIcon";
|
import TimerIcon from "./TimerIcon";
|
||||||
|
|
||||||
it('renders without crashing', () => {
|
it("renders without crashing", () => {
|
||||||
mount(<TimerIcon width={32} height={32} />);
|
mount(<TimerIcon width={32} height={32} />);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import PieChartIcon from "./PieChartIcon";
|
import PieChartIcon from "./PieChartIcon";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -16,21 +17,26 @@ const TimerIcon = function (props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get the current number of seconds to initialize timer.
|
// Get the current number of seconds to initialize timer.
|
||||||
const initialValue = (new Date().getTime() / 1000) % props.period / props.period * radius;
|
const initialValue = (((new Date().getTime() / 1000) % props.period) / props.period) * radius;
|
||||||
setTimeProgress(initialValue);
|
setTimeProgress(initialValue);
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
const value = (new Date().getTime() / 1000) % props.period / props.period * radius;
|
const value = (((new Date().getTime() / 1000) % props.period) / props.period) * radius;
|
||||||
setTimeProgress(value);
|
setTimeProgress(value);
|
||||||
}, 100);
|
}, 100);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PieChartIcon width={props.width} height={props.height}
|
<PieChartIcon
|
||||||
progress={timeProgress} maxProgress={radius}
|
width={props.width}
|
||||||
backgroundColor={props.backgroundColor} color={props.color} />
|
height={props.height}
|
||||||
)
|
progress={timeProgress}
|
||||||
}
|
maxProgress={radius}
|
||||||
|
backgroundColor={props.backgroundColor}
|
||||||
|
color={props.color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default TimerIcon
|
export default TimerIcon;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
|
||||||
export const GoogleAuthenticator = {
|
export const GoogleAuthenticator = {
|
||||||
googlePlay: "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_us",
|
googlePlay: "https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en_us",
|
||||||
appleStore: "https://apps.apple.com/us/app/google-authenticator/id388497605",
|
appleStore: "https://apps.apple.com/us/app/google-authenticator/id388497605",
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRemoteCall } from "./RemoteCall";
|
|
||||||
import { getConfiguration } from "../services/Configuration";
|
import { getConfiguration } from "../services/Configuration";
|
||||||
|
import { useRemoteCall } from "./RemoteCall";
|
||||||
|
|
||||||
export function useConfiguration() {
|
export function useConfiguration() {
|
||||||
return useRemoteCall(getConfiguration, []);
|
return useRemoteCall(getConfiguration, []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ export function useIntermittentClass(
|
||||||
classname: string,
|
classname: string,
|
||||||
activeMilliseconds: number,
|
activeMilliseconds: number,
|
||||||
inactiveMillisecond: number,
|
inactiveMillisecond: number,
|
||||||
startMillisecond?: number) {
|
startMillisecond?: number,
|
||||||
|
) {
|
||||||
const [currentClass, setCurrentClass] = useState("");
|
const [currentClass, setCurrentClass] = useState("");
|
||||||
const [firstTime, setFirstTime] = useState(true);
|
const [firstTime, setFirstTime] = useState(true);
|
||||||
|
|
||||||
|
@ -34,4 +35,4 @@ export function useIntermittentClass(
|
||||||
}, [currentClass, classname, activeMilliseconds, inactiveMillisecond, startMillisecond, firstTime]);
|
}, [currentClass, classname, activeMilliseconds, inactiveMillisecond, startMillisecond, firstTime]);
|
||||||
|
|
||||||
return currentClass;
|
return currentClass;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,9 @@ export function useIsMountedRef() {
|
||||||
const isMountedRef = useRef(false);
|
const isMountedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isMountedRef.current = true;
|
isMountedRef.current = true;
|
||||||
return () => { isMountedRef.current = false };
|
return () => {
|
||||||
|
isMountedRef.current = false;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
return isMountedRef;
|
return isMountedRef;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import { Level } from "../components/ColoredSnackbarContent";
|
|
||||||
import { useCallback, createContext, useContext } from "react";
|
import { useCallback, createContext, useContext } from "react";
|
||||||
|
|
||||||
|
import { Level } from "../components/ColoredSnackbarContent";
|
||||||
import { Notification } from "../models/Notifications";
|
import { Notification } from "../models/Notifications";
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
timeout: 5,
|
timeout: 5,
|
||||||
}
|
};
|
||||||
|
|
||||||
interface NotificationContextProps {
|
interface NotificationContextProps {
|
||||||
notification: Notification | null;
|
notification: Notification | null;
|
||||||
setNotification: (n: Notification | null) => void;
|
setNotification: (n: Notification | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NotificationsContext = createContext<NotificationContextProps>(
|
const NotificationsContext = createContext<NotificationContextProps>({ notification: null, setNotification: () => {} });
|
||||||
{ notification: null, setNotification: () => { } });
|
|
||||||
|
|
||||||
export default NotificationsContext;
|
export default NotificationsContext;
|
||||||
|
|
||||||
|
|
||||||
export function useNotifications() {
|
export function useNotifications() {
|
||||||
let useNotificationsProps = useContext(NotificationsContext);
|
let useNotificationsProps = useContext(NotificationsContext);
|
||||||
|
|
||||||
const notificationBuilder = (level: Level) => {
|
const notificationBuilder = (level: Level) => {
|
||||||
return (message: string, timeout?: number) => {
|
return (message: string, timeout?: number) => {
|
||||||
useNotificationsProps.setNotification({
|
useNotificationsProps.setNotification({
|
||||||
level, message,
|
level,
|
||||||
timeout: timeout ? timeout : defaultOptions.timeout
|
message,
|
||||||
|
timeout: timeout ? timeout : defaultOptions.timeout,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const resetNotification = () => useNotificationsProps.setNotification(null);
|
const resetNotification = () => useNotificationsProps.setNotification(null);
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
@ -38,7 +38,6 @@ export function useNotifications() {
|
||||||
/* eslint-enable react-hooks/exhaustive-deps */
|
/* eslint-enable react-hooks/exhaustive-deps */
|
||||||
const isActive = useNotificationsProps.notification !== null;
|
const isActive = useNotificationsProps.notification !== null;
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notification: useNotificationsProps.notification,
|
notification: useNotificationsProps.notification,
|
||||||
resetNotification,
|
resetNotification,
|
||||||
|
@ -46,6 +45,6 @@ export function useNotifications() {
|
||||||
createSuccessNotification,
|
createSuccessNotification,
|
||||||
createWarnNotification,
|
createWarnNotification,
|
||||||
createErrorNotification,
|
createErrorNotification,
|
||||||
isActive
|
isActive,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,5 @@ import { useLocation } from "react-router";
|
||||||
export function useRedirectionURL() {
|
export function useRedirectionURL() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const queryParams = queryString.parse(location.search);
|
const queryParams = queryString.parse(location.search);
|
||||||
return (queryParams && "rd" in queryParams)
|
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
|
||||||
? queryParams["rd"] as string
|
}
|
||||||
: undefined;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useState, useCallback, DependencyList } from "react";
|
import { useState, useCallback, DependencyList } from "react";
|
||||||
|
|
||||||
type PromisifiedFunction<Ret> = (...args: any) => Promise<Ret>
|
type PromisifiedFunction<Ret> = (...args: any) => Promise<Ret>;
|
||||||
|
|
||||||
export function useRemoteCall<Ret>(fn: PromisifiedFunction<Ret>, deps: DependencyList)
|
export function useRemoteCall<Ret>(
|
||||||
: [Ret | undefined, PromisifiedFunction<void>, boolean, Error | undefined] {
|
fn: PromisifiedFunction<Ret>,
|
||||||
|
deps: DependencyList,
|
||||||
|
): [Ret | undefined, PromisifiedFunction<void>, boolean, Error | undefined] {
|
||||||
const [data, setData] = useState(undefined as Ret | undefined);
|
const [data, setData] = useState(undefined as Ret | undefined);
|
||||||
const [inProgress, setInProgress] = useState(false);
|
const [inProgress, setInProgress] = useState(false);
|
||||||
const [error, setError] = useState(undefined as Error | undefined);
|
const [error, setError] = useState(undefined as Error | undefined);
|
||||||
|
@ -22,10 +24,5 @@ export function useRemoteCall<Ret>(fn: PromisifiedFunction<Ret>, deps: Dependenc
|
||||||
}
|
}
|
||||||
}, [setInProgress, setError, fnCallback]);
|
}, [setInProgress, setError, fnCallback]);
|
||||||
|
|
||||||
return [
|
return [data, triggerCallback, inProgress, error];
|
||||||
data,
|
}
|
||||||
triggerCallback,
|
|
||||||
inProgress,
|
|
||||||
error,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,4 +3,4 @@ import { useRemoteCall } from "./RemoteCall";
|
||||||
|
|
||||||
export function useAutheliaState() {
|
export function useAutheliaState() {
|
||||||
return useRemoteCall(getState, []);
|
return useRemoteCall(getState, []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,8 @@ export function useTimer(timeoutMs: number): [number, () => void, () => void] {
|
||||||
}
|
}
|
||||||
|
|
||||||
const intervalNode = setInterval(() => {
|
const intervalNode = setInterval(() => {
|
||||||
const elapsedMs = (startDate) ? new Date().getTime() - startDate.getTime() : 0;
|
const elapsedMs = startDate ? new Date().getTime() - startDate.getTime() : 0;
|
||||||
let p = elapsedMs / timeoutMs * 100.0;
|
let p = (elapsedMs / timeoutMs) * 100.0;
|
||||||
if (p >= 100) {
|
if (p >= 100) {
|
||||||
p = 100;
|
p = 100;
|
||||||
setStartDate(undefined);
|
setStartDate(undefined);
|
||||||
|
@ -33,9 +33,5 @@ export function useTimer(timeoutMs: number): [number, () => void, () => void] {
|
||||||
return () => clearInterval(intervalNode);
|
return () => clearInterval(intervalNode);
|
||||||
}, [startDate, setPercent, setStartDate, timeoutMs]);
|
}, [startDate, setPercent, setStartDate, timeoutMs]);
|
||||||
|
|
||||||
return [
|
return [percent, trigger, clear];
|
||||||
percent,
|
}
|
||||||
trigger,
|
|
||||||
clear,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,4 +3,4 @@ import { useRemoteCall } from "./RemoteCall";
|
||||||
|
|
||||||
export function useUserPreferences() {
|
export function useUserPreferences() {
|
||||||
return useRemoteCall(getUserPreferences, []);
|
return useRemoteCall(getUserPreferences, []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import './utils/AssetPath';
|
import "./utils/AssetPath";
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
import * as serviceWorker from './serviceWorker';
|
|
||||||
|
|
||||||
ReactDOM.render(<App />, document.getElementById('root'));
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
import * as serviceWorker from "./serviceWorker";
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById("root"));
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { Grid, makeStyles, Container, Typography, Link } from "@material-ui/core";
|
import { Grid, makeStyles, Container, Typography, Link } from "@material-ui/core";
|
||||||
import { ReactComponent as UserSvg } from "../assets/images/user.svg";
|
|
||||||
import { grey } from "@material-ui/core/colors";
|
import { grey } from "@material-ui/core/colors";
|
||||||
|
|
||||||
|
import { ReactComponent as UserSvg } from "../assets/images/user.svg";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -14,13 +15,7 @@ export interface Props {
|
||||||
const LoginLayout = function (props: Props) {
|
const LoginLayout = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justify="center">
|
||||||
id={props.id}
|
|
||||||
className={style.root}
|
|
||||||
container
|
|
||||||
spacing={0}
|
|
||||||
alignItems="center"
|
|
||||||
justify="center">
|
|
||||||
<Container maxWidth="xs" className={style.rootContainer}>
|
<Container maxWidth="xs" className={style.rootContainer}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
@ -34,27 +29,28 @@ const LoginLayout = function (props: Props) {
|
||||||
<Grid item xs={12} className={style.body}>
|
<Grid item xs={12} className={style.body}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.showBrand ? <Grid item xs={12}>
|
{props.showBrand ? (
|
||||||
<Link
|
<Grid item xs={12}>
|
||||||
href="https://github.com/authelia/authelia"
|
<Link
|
||||||
target="_blank"
|
href="https://github.com/authelia/authelia"
|
||||||
className={style.poweredBy}>
|
target="_blank"
|
||||||
Powered by Authelia
|
className={style.poweredBy}
|
||||||
</Link>
|
>
|
||||||
</Grid>
|
Powered by Authelia
|
||||||
: null
|
</Link>
|
||||||
}
|
</Grid>
|
||||||
|
) : null}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoginLayout
|
export default LoginLayout;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
minHeight: '90vh',
|
minHeight: "90vh",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
// marginTop: theme.spacing(10),
|
// marginTop: theme.spacing(10),
|
||||||
},
|
},
|
||||||
|
@ -71,5 +67,5 @@ const useStyles = makeStyles(theme => ({
|
||||||
poweredBy: {
|
poweredBy: {
|
||||||
fontSize: "0.7em",
|
fontSize: "0.7em",
|
||||||
color: grey[500],
|
color: grey[500],
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -4,4 +4,4 @@ export interface Configuration {
|
||||||
available_methods: Set<SecondFactorMethod>;
|
available_methods: Set<SecondFactorMethod>;
|
||||||
second_factor_enabled: boolean;
|
second_factor_enabled: boolean;
|
||||||
totp_period: number;
|
totp_period: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
export enum SecondFactorMethod {
|
export enum SecondFactorMethod {
|
||||||
TOTP = 1,
|
TOTP = 1,
|
||||||
U2F = 2,
|
U2F = 2,
|
||||||
MobilePush = 3
|
MobilePush = 3,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,4 +4,4 @@ export interface Notification {
|
||||||
message: string;
|
message: string;
|
||||||
level: Level;
|
level: Level;
|
||||||
timeout: number;
|
timeout: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
/// <reference types="react-scripts" />
|
/// <reference types="react-scripts" />
|
||||||
declare var __webpack_public_path__: string;
|
declare var __webpack_public_path__: string;
|
||||||
|
|
|
@ -11,133 +11,123 @@
|
||||||
// opt-in, read https://bit.ly/CRA-PWA
|
// opt-in, read https://bit.ly/CRA-PWA
|
||||||
|
|
||||||
const isLocalhost = Boolean(
|
const isLocalhost = Boolean(
|
||||||
window.location.hostname === 'localhost' ||
|
window.location.hostname === "localhost" ||
|
||||||
// [::1] is the IPv6 localhost address.
|
// [::1] is the IPv6 localhost address.
|
||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === "[::1]" ||
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
type Config = {
|
type Config = {
|
||||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function register(config?: Config) {
|
export function register(config?: Config) {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(
|
const publicUrl = new URL((process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href);
|
||||||
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
window.location.href
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
);
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
return;
|
||||||
// from what our page is served on. This might happen if a CDN is used to
|
}
|
||||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener("load", () => {
|
||||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
if (isLocalhost) {
|
if (isLocalhost) {
|
||||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
checkValidServiceWorker(swUrl, config);
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
// Add some additional logging to localhost, pointing developers to the
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
// service worker/PWA documentation.
|
// service worker/PWA documentation.
|
||||||
navigator.serviceWorker.ready.then(() => {
|
navigator.serviceWorker.ready.then(() => {
|
||||||
console.log(
|
console.log(
|
||||||
'This web app is being served cache-first by a service ' +
|
"This web app is being served cache-first by a service " +
|
||||||
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
"worker. To learn more, visit https://bit.ly/CRA-PWA",
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
// Is not localhost. Just register service worker
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerValidSW(swUrl: string, config?: Config) {
|
function registerValidSW(swUrl: string, config?: Config) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing;
|
||||||
if (installingWorker == null) {
|
if (installingWorker == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
installingWorker.onstatechange = () => {
|
installingWorker.onstatechange = () => {
|
||||||
if (installingWorker.state === 'installed') {
|
if (installingWorker.state === "installed") {
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
// At this point, the updated precached content has been fetched,
|
// At this point, the updated precached content has been fetched,
|
||||||
// but the previous service worker will still serve the older
|
// but the previous service worker will still serve the older
|
||||||
// content until all client tabs are closed.
|
// content until all client tabs are closed.
|
||||||
console.log(
|
console.log(
|
||||||
'New content is available and will be used when all ' +
|
"New content is available and will be used when all " +
|
||||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
"tabs for this page are closed. See https://bit.ly/CRA-PWA.",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onUpdate) {
|
if (config && config.onUpdate) {
|
||||||
config.onUpdate(registration);
|
config.onUpdate(registration);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// At this point, everything has been precached.
|
// At this point, everything has been precached.
|
||||||
// It's the perfect time to display a
|
// It's the perfect time to display a
|
||||||
// "Content is cached for offline use." message.
|
// "Content is cached for offline use." message.
|
||||||
console.log('Content is cached for offline use.');
|
console.log("Content is cached for offline use.");
|
||||||
|
|
||||||
// Execute callback
|
// Execute callback
|
||||||
if (config && config.onSuccess) {
|
if (config && config.onSuccess) {
|
||||||
config.onSuccess(registration);
|
config.onSuccess(registration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error("Error during service worker registration:", error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl)
|
fetch(swUrl)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get("content-type");
|
||||||
if (
|
if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) {
|
||||||
response.status === 404 ||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
) {
|
registration.unregister().then(() => {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
window.location.reload();
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
});
|
||||||
registration.unregister().then(() => {
|
});
|
||||||
window.location.reload();
|
} else {
|
||||||
});
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log("No internet connection found. App is running in offline mode.");
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Service worker found. Proceed as normal.
|
|
||||||
registerValidSW(swUrl, config);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.log(
|
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
|
|
||||||
import { getBasePath } from "../utils/BasePath";
|
import { getBasePath } from "../utils/BasePath";
|
||||||
|
|
||||||
const basePath = getBasePath();
|
const basePath = getBasePath();
|
||||||
|
@ -14,13 +15,13 @@ export const CompleteU2FRegistrationStep2Path = basePath + "/api/secondfactor/u2
|
||||||
export const InitiateU2FSignInPath = basePath + "/api/secondfactor/u2f/sign_request";
|
export const InitiateU2FSignInPath = basePath + "/api/secondfactor/u2f/sign_request";
|
||||||
export const CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
|
export const CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
|
||||||
|
|
||||||
export const CompletePushNotificationSignInPath = basePath + "/api/secondfactor/duo"
|
export const CompletePushNotificationSignInPath = basePath + "/api/secondfactor/duo";
|
||||||
export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp"
|
export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp";
|
||||||
|
|
||||||
export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start";
|
export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start";
|
||||||
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
|
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
|
||||||
// Do the password reset during completion.
|
// Do the password reset during completion.
|
||||||
export const ResetPasswordPath = basePath + "/api/reset-password"
|
export const ResetPasswordPath = basePath + "/api/reset-password";
|
||||||
|
|
||||||
export const LogoutPath = basePath + "/api/logout";
|
export const LogoutPath = basePath + "/api/logout";
|
||||||
export const StatePath = basePath + "/api/state";
|
export const StatePath = basePath + "/api/state";
|
||||||
|
@ -52,7 +53,7 @@ export function toData<T>(resp: AxiosResponse<ServiceResponse<T>>): T | undefine
|
||||||
if (resp.data && "status" in resp.data && resp.data["status"] === "OK") {
|
if (resp.data && "status" in resp.data && resp.data["status"] === "OK") {
|
||||||
return resp.data.data as T;
|
return resp.data.data as T;
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
||||||
|
@ -61,4 +62,4 @@ export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
|
||||||
return { errored: true, message: errResp.message };
|
return { errored: true, message: errResp.message };
|
||||||
}
|
}
|
||||||
return { errored: false, message: null };
|
return { errored: false, message: null };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { ServiceResponse, hasServiceError, toData } from "./Api";
|
import { ServiceResponse, hasServiceError, toData } from "./Api";
|
||||||
|
|
||||||
export async function PostWithOptionalResponse<T = undefined>(path: string, body?: any) {
|
export async function PostWithOptionalResponse<T = undefined>(path: string, body?: any) {
|
||||||
|
@ -30,4 +31,4 @@ export async function Get<T = undefined>(path: string): Promise<T> {
|
||||||
throw new Error("unexpected type of response");
|
throw new Error("unexpected type of response");
|
||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Get } from "./Client";
|
|
||||||
import { ConfigurationPath } from "./Api";
|
|
||||||
import { toEnum, Method2FA } from "./UserPreferences";
|
|
||||||
import { Configuration } from "../models/Configuration";
|
import { Configuration } from "../models/Configuration";
|
||||||
|
import { ConfigurationPath } from "./Api";
|
||||||
|
import { Get } from "./Client";
|
||||||
|
import { toEnum, Method2FA } from "./UserPreferences";
|
||||||
|
|
||||||
interface ConfigurationPayload {
|
interface ConfigurationPayload {
|
||||||
available_methods: Method2FA[];
|
available_methods: Method2FA[];
|
||||||
|
@ -12,4 +12,4 @@ interface ConfigurationPayload {
|
||||||
export async function getConfiguration(): Promise<Configuration> {
|
export async function getConfiguration(): Promise<Configuration> {
|
||||||
const config = await Get<ConfigurationPayload>(ConfigurationPath);
|
const config = await Get<ConfigurationPayload>(ConfigurationPath);
|
||||||
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
|
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,17 +9,16 @@ interface PostFirstFactorBody {
|
||||||
targetURL?: string;
|
targetURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postFirstFactor(
|
export async function postFirstFactor(username: string, password: string, rememberMe: boolean, targetURL?: string) {
|
||||||
username: string, password: string,
|
|
||||||
rememberMe: boolean, targetURL?: string) {
|
|
||||||
const data: PostFirstFactorBody = {
|
const data: PostFirstFactorBody = {
|
||||||
username, password,
|
username,
|
||||||
keepMeLoggedIn: rememberMe
|
password,
|
||||||
|
keepMeLoggedIn: rememberMe,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (targetURL) {
|
if (targetURL) {
|
||||||
data.targetURL = targetURL;
|
data.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data);
|
const res = await PostWithOptionalResponse<SignInResponse>(FirstFactorPath, data);
|
||||||
return res ? res : {} as SignInResponse;
|
return res ? res : ({} as SignInResponse);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PostWithOptionalResponse } from "./Client";
|
|
||||||
import { CompleteTOTPSignInPath } from "./Api";
|
import { CompleteTOTPSignInPath } from "./Api";
|
||||||
|
import { PostWithOptionalResponse } from "./Client";
|
||||||
import { SignInResponse } from "./SignIn";
|
import { SignInResponse } from "./SignIn";
|
||||||
|
|
||||||
interface CompleteU2FSigninBody {
|
interface CompleteU2FSigninBody {
|
||||||
|
@ -13,4 +13,4 @@ export function completeTOTPSignIn(passcode: string, targetURL: string | undefin
|
||||||
body.targetURL = targetURL;
|
body.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
|
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { PostWithOptionalResponse } from "./Client";
|
|
||||||
import { CompletePushNotificationSignInPath } from "./Api";
|
import { CompletePushNotificationSignInPath } from "./Api";
|
||||||
|
import { PostWithOptionalResponse } from "./Client";
|
||||||
import { SignInResponse } from "./SignIn";
|
import { SignInResponse } from "./SignIn";
|
||||||
|
|
||||||
interface CompleteU2FSigninBody {
|
interface CompleteU2FSigninBody {
|
||||||
|
@ -12,4 +12,4 @@ export function completePushNotificationSignIn(targetURL: string | undefined) {
|
||||||
body.targetURL = targetURL;
|
body.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
return PostWithOptionalResponse<SignInResponse>(CompletePushNotificationSignInPath, body);
|
return PostWithOptionalResponse<SignInResponse>(CompletePushNotificationSignInPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import {
|
|
||||||
InitiateTOTPRegistrationPath, CompleteTOTPRegistrationPath,
|
|
||||||
InitiateU2FRegistrationPath, CompleteU2FRegistrationStep1Path,
|
|
||||||
CompleteU2FRegistrationStep2Path
|
|
||||||
} from "./Api";
|
|
||||||
import U2fApi from "u2f-api";
|
import U2fApi from "u2f-api";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InitiateTOTPRegistrationPath,
|
||||||
|
CompleteTOTPRegistrationPath,
|
||||||
|
InitiateU2FRegistrationPath,
|
||||||
|
CompleteU2FRegistrationStep1Path,
|
||||||
|
CompleteU2FRegistrationStep2Path,
|
||||||
|
} from "./Api";
|
||||||
import { Post, PostWithOptionalResponse } from "./Client";
|
import { Post, PostWithOptionalResponse } from "./Client";
|
||||||
|
|
||||||
export async function initiateTOTPRegistrationProcess() {
|
export async function initiateTOTPRegistrationProcess() {
|
||||||
|
@ -16,28 +19,27 @@ interface CompleteTOTPRegistrationResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeTOTPRegistrationProcess(processToken: string) {
|
export async function completeTOTPRegistrationProcess(processToken: string) {
|
||||||
return Post<CompleteTOTPRegistrationResponse>(
|
return Post<CompleteTOTPRegistrationResponse>(CompleteTOTPRegistrationPath, { token: processToken });
|
||||||
CompleteTOTPRegistrationPath, { token: processToken });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function initiateU2FRegistrationProcess() {
|
export async function initiateU2FRegistrationProcess() {
|
||||||
return PostWithOptionalResponse(InitiateU2FRegistrationPath);
|
return PostWithOptionalResponse(InitiateU2FRegistrationPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface U2RRegistrationStep1Response {
|
interface U2RRegistrationStep1Response {
|
||||||
appId: string,
|
appId: string;
|
||||||
registerRequests: [{
|
registerRequests: [
|
||||||
version: string,
|
{
|
||||||
challenge: string,
|
version: string;
|
||||||
}]
|
challenge: string;
|
||||||
|
},
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeU2FRegistrationProcessStep1(processToken: string) {
|
export async function completeU2FRegistrationProcessStep1(processToken: string) {
|
||||||
return Post<U2RRegistrationStep1Response>(
|
return Post<U2RRegistrationStep1Response>(CompleteU2FRegistrationStep1Path, { token: processToken });
|
||||||
CompleteU2FRegistrationStep1Path, { token: processToken });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function completeU2FRegistrationProcessStep2(response: U2fApi.RegisterResponse) {
|
export async function completeU2FRegistrationProcessStep2(response: U2fApi.RegisterResponse) {
|
||||||
return PostWithOptionalResponse(CompleteU2FRegistrationStep2Path, response);
|
return PostWithOptionalResponse(CompleteU2FRegistrationStep2Path, response);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { InitiateResetPasswordPath, CompleteResetPasswordPath, ResetPasswordPath } from "./Api";
|
import { InitiateResetPasswordPath, CompleteResetPasswordPath, ResetPasswordPath } from "./Api";
|
||||||
import { PostWithOptionalResponse } from "./Client";
|
import { PostWithOptionalResponse } from "./Client";
|
||||||
|
|
||||||
|
|
||||||
export async function initiateResetPasswordProcess(username: string) {
|
export async function initiateResetPasswordProcess(username: string) {
|
||||||
return PostWithOptionalResponse(InitiateResetPasswordPath, { username });
|
return PostWithOptionalResponse(InitiateResetPasswordPath, { username });
|
||||||
}
|
}
|
||||||
|
@ -12,4 +11,4 @@ export async function completeResetPasswordProcess(token: string) {
|
||||||
|
|
||||||
export async function resetPassword(newPassword: string) {
|
export async function resetPassword(newPassword: string) {
|
||||||
return PostWithOptionalResponse(ResetPasswordPath, { password: newPassword });
|
return PostWithOptionalResponse(ResetPasswordPath, { password: newPassword });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { Post, PostWithOptionalResponse } from "./Client";
|
|
||||||
import { InitiateU2FSignInPath, CompleteU2FSignInPath } from "./Api";
|
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
|
|
||||||
|
import { InitiateU2FSignInPath, CompleteU2FSignInPath } from "./Api";
|
||||||
|
import { Post, PostWithOptionalResponse } from "./Client";
|
||||||
import { SignInResponse } from "./SignIn";
|
import { SignInResponse } from "./SignIn";
|
||||||
|
|
||||||
interface InitiateU2FSigninResponse {
|
interface InitiateU2FSigninResponse {
|
||||||
appId: string,
|
appId: string;
|
||||||
challenge: string,
|
challenge: string;
|
||||||
registeredKeys: {
|
registeredKeys: {
|
||||||
appId: string,
|
appId: string;
|
||||||
keyHandle: string,
|
keyHandle: string;
|
||||||
version: string,
|
version: string;
|
||||||
}[]
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initiateU2FSignin() {
|
export async function initiateU2FSignin() {
|
||||||
|
@ -28,4 +29,4 @@ export function completeU2FSignin(signResponse: u2fApi.SignResponse, targetURL:
|
||||||
body.targetURL = targetURL;
|
body.targetURL = targetURL;
|
||||||
}
|
}
|
||||||
return PostWithOptionalResponse<SignInResponse>(CompleteU2FSignInPath, body);
|
return PostWithOptionalResponse<SignInResponse>(CompleteU2FSignInPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
export type SignInResponse = { redirect: string } | undefined;
|
||||||
export type SignInResponse = { redirect: string } | undefined;
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { PostWithOptionalResponse } from "./Client";
|
|
||||||
import { LogoutPath } from "./Api";
|
import { LogoutPath } from "./Api";
|
||||||
|
import { PostWithOptionalResponse } from "./Client";
|
||||||
|
|
||||||
export async function signOut() {
|
export async function signOut() {
|
||||||
return PostWithOptionalResponse(LogoutPath);
|
return PostWithOptionalResponse(LogoutPath);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Get } from "./Client";
|
|
||||||
import { StatePath } from "./Api";
|
import { StatePath } from "./Api";
|
||||||
|
import { Get } from "./Client";
|
||||||
|
|
||||||
export enum AuthenticationLevel {
|
export enum AuthenticationLevel {
|
||||||
Unauthenticated = 0,
|
Unauthenticated = 0,
|
||||||
|
@ -9,9 +9,9 @@ export enum AuthenticationLevel {
|
||||||
|
|
||||||
export interface AutheliaState {
|
export interface AutheliaState {
|
||||||
username: string;
|
username: string;
|
||||||
authentication_level: AuthenticationLevel
|
authentication_level: AuthenticationLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getState(): Promise<AutheliaState> {
|
export async function getState(): Promise<AutheliaState> {
|
||||||
return Get<AutheliaState>(StatePath);
|
return Get<AutheliaState>(StatePath);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Get, PostWithOptionalResponse } from "./Client";
|
|
||||||
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
|
|
||||||
import { SecondFactorMethod } from "../models/Methods";
|
import { SecondFactorMethod } from "../models/Methods";
|
||||||
import { UserInfo } from "../models/UserInfo";
|
import { UserInfo } from "../models/UserInfo";
|
||||||
|
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
|
||||||
|
import { Get, PostWithOptionalResponse } from "./Client";
|
||||||
|
|
||||||
export type Method2FA = "u2f" | "totp" | "mobile_push";
|
export type Method2FA = "u2f" | "totp" | "mobile_push";
|
||||||
|
|
||||||
|
@ -44,6 +44,5 @@ export async function getUserPreferences(): Promise<UserInfo> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setPreferred2FAMethod(method: SecondFactorMethod) {
|
export function setPreferred2FAMethod(method: SecondFactorMethod) {
|
||||||
return PostWithOptionalResponse(UserInfo2FAMethodPath,
|
return PostWithOptionalResponse(UserInfo2FAMethodPath, { method: toString(method) } as MethodPreferencePayload);
|
||||||
{ method: toString(method) } as MethodPreferencePayload);
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { configure } from 'enzyme';
|
import { configure } from "enzyme";
|
||||||
import Adapter from 'enzyme-adapter-react-16';
|
import Adapter from "enzyme-adapter-react-16";
|
||||||
document.body.setAttribute("data-basepath", "");
|
document.body.setAttribute("data-basepath", "");
|
||||||
document.body.setAttribute("data-rememberme", "true");
|
document.body.setAttribute("data-rememberme", "true");
|
||||||
document.body.setAttribute("data-resetpassword", "true");
|
document.body.setAttribute("data-resetpassword", "true");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { getBasePath } from "./BasePath";
|
import { getBasePath } from "./BasePath";
|
||||||
|
|
||||||
__webpack_public_path__ = "/"
|
__webpack_public_path__ = "/";
|
||||||
|
|
||||||
if (getBasePath() !== "") {
|
if (getBasePath() !== "") {
|
||||||
__webpack_public_path__ = getBasePath() + "/"
|
__webpack_public_path__ = getBasePath() + "/";
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ import { getEmbeddedVariable } from "./Configuration";
|
||||||
|
|
||||||
export function getBasePath() {
|
export function getBasePath() {
|
||||||
return getEmbeddedVariable("basepath");
|
return getEmbeddedVariable("basepath");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
export function getEmbeddedVariable(variableName: string) {
|
export function getEmbeddedVariable(variableName: string) {
|
||||||
const value = document.body.getAttribute(`data-${variableName}`);
|
const value = document.body.getAttribute(`data-${variableName}`);
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
throw new Error(`No ${variableName} embedded variable detected`);
|
throw new Error(`No ${variableName} embedded variable detected`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRememberMe() {
|
export function getRememberMe() {
|
||||||
return getEmbeddedVariable("rememberme") === "true";
|
return getEmbeddedVariable("rememberme") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getResetPassword() {
|
export function getResetPassword() {
|
||||||
return getEmbeddedVariable("resetpassword") === "true";
|
return getEmbeddedVariable("resetpassword") === "true";
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,5 @@ import queryString from "query-string";
|
||||||
|
|
||||||
export function extractIdentityToken(locationSearch: string) {
|
export function extractIdentityToken(locationSearch: string) {
|
||||||
const queryParams = queryString.parse(locationSearch);
|
const queryParams = queryString.parse(locationSearch);
|
||||||
return (queryParams && "token" in queryParams)
|
return queryParams && "token" in queryParams ? (queryParams["token"] as string) : null;
|
||||||
? queryParams["token"] as string
|
}
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useCallback, useState } from "react";
|
||||||
import LoginLayout from "../../layouts/LoginLayout";
|
|
||||||
import classnames from "classnames";
|
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { makeStyles, Typography, Button, IconButton, Link, CircularProgress, TextField } from "@material-ui/core";
|
import { makeStyles, Typography, Button, IconButton, Link, CircularProgress, TextField } from "@material-ui/core";
|
||||||
import QRCode from 'qrcode.react';
|
import { red } from "@material-ui/core/colors";
|
||||||
|
import classnames from "classnames";
|
||||||
|
import QRCode from "qrcode.react";
|
||||||
|
import { useHistory, useLocation } from "react-router";
|
||||||
|
|
||||||
import AppStoreBadges from "../../components/AppStoreBadges";
|
import AppStoreBadges from "../../components/AppStoreBadges";
|
||||||
import { GoogleAuthenticator } from "../../constants";
|
import { GoogleAuthenticator } from "../../constants";
|
||||||
import { useHistory, useLocation } from "react-router";
|
|
||||||
import { completeTOTPRegistrationProcess } from "../../services/RegisterDevice";
|
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import LoginLayout from "../../layouts/LoginLayout";
|
||||||
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { red } from "@material-ui/core/colors";
|
|
||||||
import { extractIdentityToken } from "../../utils/IdentityToken";
|
|
||||||
import { FirstFactorRoute } from "../../Routes";
|
import { FirstFactorRoute } from "../../Routes";
|
||||||
|
import { completeTOTPRegistrationProcess } from "../../services/RegisterDevice";
|
||||||
|
import { extractIdentityToken } from "../../utils/IdentityToken";
|
||||||
|
|
||||||
const RegisterOneTimePassword = function () {
|
const RegisterOneTimePassword = function () {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
@ -32,7 +34,7 @@ const RegisterOneTimePassword = function () {
|
||||||
|
|
||||||
const handleDoneClick = () => {
|
const handleDoneClick = () => {
|
||||||
history.push(FirstFactorRoute);
|
history.push(FirstFactorRoute);
|
||||||
}
|
};
|
||||||
|
|
||||||
const completeRegistrationProcess = useCallback(async () => {
|
const completeRegistrationProcess = useCallback(async () => {
|
||||||
if (!processToken) {
|
if (!processToken) {
|
||||||
|
@ -52,74 +54,77 @@ const RegisterOneTimePassword = function () {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, [processToken, createErrorNotification]);
|
}, [processToken, createErrorNotification]);
|
||||||
|
|
||||||
useEffect(() => { completeRegistrationProcess() }, [completeRegistrationProcess]);
|
useEffect(() => {
|
||||||
|
completeRegistrationProcess();
|
||||||
|
}, [completeRegistrationProcess]);
|
||||||
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
className={style.secretButtons}
|
className={style.secretButtons}
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(`${text}`);
|
navigator.clipboard.writeText(`${text}`);
|
||||||
createSuccessNotification(`${action}`);
|
createSuccessNotification(`${action}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} />
|
<FontAwesomeIcon icon={icon} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
const qrcodeFuzzyStyle = (isLoading || hasErrored) ? style.fuzzy : undefined
|
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Scan QRCode">
|
<LoginLayout title="Scan QRCode">
|
||||||
<div className={style.root}>
|
<div className={style.root}>
|
||||||
<div className={style.googleAuthenticator}>
|
<div className={style.googleAuthenticator}>
|
||||||
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
|
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
|
||||||
<AppStoreBadges
|
<AppStoreBadges
|
||||||
iconSize={128}
|
iconSize={128}
|
||||||
targetBlank
|
targetBlank
|
||||||
className={style.googleAuthenticatorBadges}
|
className={style.googleAuthenticatorBadges}
|
||||||
googlePlayLink={GoogleAuthenticator.googlePlay}
|
googlePlayLink={GoogleAuthenticator.googlePlay}
|
||||||
appleStoreLink={GoogleAuthenticator.appleStore} />
|
appleStoreLink={GoogleAuthenticator.appleStore}
|
||||||
</div>
|
/>
|
||||||
<div className={style.qrcodeContainer}>
|
</div>
|
||||||
<Link href={secretURL}>
|
<div className={style.qrcodeContainer}>
|
||||||
<QRCode
|
<Link href={secretURL}>
|
||||||
value={secretURL}
|
<QRCode value={secretURL} className={classnames(qrcodeFuzzyStyle, style.qrcode)} size={256} />
|
||||||
className={classnames(qrcodeFuzzyStyle, style.qrcode)}
|
{!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null}
|
||||||
size={256} />
|
{hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null}
|
||||||
{!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null}
|
</Link>
|
||||||
{hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null}
|
</div>
|
||||||
</Link>
|
<div>
|
||||||
</div>
|
{secretURL !== "empty" ? (
|
||||||
<div>
|
<TextField
|
||||||
{secretURL !== "empty"
|
id="secret-url"
|
||||||
? <TextField
|
label="Secret"
|
||||||
id="secret-url"
|
className={style.secret}
|
||||||
label="Secret"
|
value={secretURL}
|
||||||
className={style.secret}
|
InputProps={{
|
||||||
value={secretURL}
|
readOnly: true,
|
||||||
InputProps={{
|
}}
|
||||||
readOnly: true
|
/>
|
||||||
}} /> : null}
|
) : null}
|
||||||
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
|
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
|
||||||
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
|
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
className={style.doneButton}
|
className={style.doneButton}
|
||||||
onClick={handleDoneClick}
|
onClick={handleDoneClick}
|
||||||
disabled={isLoading}>
|
disabled={isLoading}
|
||||||
Done
|
>
|
||||||
</Button>
|
Done
|
||||||
</div>
|
</Button>
|
||||||
</LoginLayout>
|
</div>
|
||||||
)
|
</LoginLayout>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default RegisterOneTimePassword
|
export default RegisterOneTimePassword;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
paddingTop: theme.spacing(4),
|
paddingTop: theme.spacing(4),
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
|
@ -129,7 +134,7 @@ const useStyles = makeStyles(theme => ({
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
fuzzy: {
|
fuzzy: {
|
||||||
filter: "blur(10px)"
|
filter: "blur(10px)",
|
||||||
},
|
},
|
||||||
secret: {
|
secret: {
|
||||||
marginTop: theme.spacing(1),
|
marginTop: theme.spacing(1),
|
||||||
|
@ -163,5 +168,5 @@ const useStyles = makeStyles(theme => ({
|
||||||
left: "calc(128px - 64px)",
|
left: "calc(128px - 64px)",
|
||||||
color: red[400],
|
color: red[400],
|
||||||
fontSize: "128px",
|
fontSize: "128px",
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import LoginLayout from "../../layouts/LoginLayout";
|
|
||||||
import FingerTouchIcon from "../../components/FingerTouchIcon";
|
|
||||||
import { makeStyles, Typography, Button } from "@material-ui/core";
|
import { makeStyles, Typography, Button } from "@material-ui/core";
|
||||||
import { useHistory, useLocation } from "react-router";
|
import { useHistory, useLocation } from "react-router";
|
||||||
import { FirstFactorPath } from "../../services/Api";
|
|
||||||
import { extractIdentityToken } from "../../utils/IdentityToken";
|
|
||||||
import { completeU2FRegistrationProcessStep1, completeU2FRegistrationProcessStep2 } from "../../services/RegisterDevice";
|
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
|
||||||
import u2fApi from "u2f-api";
|
import u2fApi from "u2f-api";
|
||||||
|
|
||||||
|
import FingerTouchIcon from "../../components/FingerTouchIcon";
|
||||||
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
|
import LoginLayout from "../../layouts/LoginLayout";
|
||||||
|
import { FirstFactorPath } from "../../services/Api";
|
||||||
|
import {
|
||||||
|
completeU2FRegistrationProcessStep1,
|
||||||
|
completeU2FRegistrationProcessStep2,
|
||||||
|
} from "../../services/RegisterDevice";
|
||||||
|
import { extractIdentityToken } from "../../utils/IdentityToken";
|
||||||
|
|
||||||
const RegisterSecurityKey = function () {
|
const RegisterSecurityKey = function () {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -18,10 +23,9 @@ const RegisterSecurityKey = function () {
|
||||||
|
|
||||||
const processToken = extractIdentityToken(location.search);
|
const processToken = extractIdentityToken(location.search);
|
||||||
|
|
||||||
|
|
||||||
const handleBackClick = () => {
|
const handleBackClick = () => {
|
||||||
history.push(FirstFactorPath);
|
history.push(FirstFactorPath);
|
||||||
}
|
};
|
||||||
|
|
||||||
const registerStep1 = useCallback(async () => {
|
const registerStep1 = useCallback(async () => {
|
||||||
if (!processToken) {
|
if (!processToken) {
|
||||||
|
@ -37,7 +41,7 @@ const RegisterSecurityKey = function () {
|
||||||
appId: res.appId,
|
appId: res.appId,
|
||||||
challenge: r.challenge,
|
challenge: r.challenge,
|
||||||
version: r.version,
|
version: r.version,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
const registerResponse = await u2fApi.register(registerRequests, [], 60);
|
const registerResponse = await u2fApi.register(registerRequests, [], 60);
|
||||||
await completeU2FRegistrationProcessStep2(registerResponse);
|
await completeU2FRegistrationProcessStep2(registerResponse);
|
||||||
|
@ -45,8 +49,9 @@ const RegisterSecurityKey = function () {
|
||||||
history.push(FirstFactorPath);
|
history.push(FirstFactorPath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("Failed to register your security key. " +
|
createErrorNotification(
|
||||||
"The identity verification process might have timed out.");
|
"Failed to register your security key. The identity verification process might have timed out.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [processToken, createErrorNotification, history]);
|
}, [processToken, createErrorNotification, history]);
|
||||||
|
|
||||||
|
@ -60,20 +65,24 @@ const RegisterSecurityKey = function () {
|
||||||
<FingerTouchIcon size={64} animated />
|
<FingerTouchIcon size={64} animated />
|
||||||
</div>
|
</div>
|
||||||
<Typography className={style.instruction}>Touch the token on your security key</Typography>
|
<Typography className={style.instruction}>Touch the token on your security key</Typography>
|
||||||
<Button color="primary" onClick={handleBackClick}>Retry</Button>
|
<Button color="primary" onClick={handleBackClick}>
|
||||||
<Button color="primary" onClick={handleBackClick}>Cancel</Button>
|
Retry
|
||||||
|
</Button>
|
||||||
|
<Button color="primary" onClick={handleBackClick}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default RegisterSecurityKey
|
export default RegisterSecurityKey;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
paddingTop: theme.spacing(4),
|
paddingTop: theme.spacing(4),
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
},
|
},
|
||||||
instruction: {
|
instruction: {
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactLoading from "react-loading";
|
|
||||||
import { Typography, Grid } from "@material-ui/core";
|
import { Typography, Grid } from "@material-ui/core";
|
||||||
|
import ReactLoading from "react-loading";
|
||||||
|
|
||||||
const LoadingPage = function () {
|
const LoadingPage = function () {
|
||||||
return (
|
return (
|
||||||
|
@ -11,6 +12,6 @@ const LoadingPage = function () {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoadingPage
|
export default LoadingPage;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import SuccessIcon from "../../components/SuccessIcon";
|
|
||||||
import { Typography, makeStyles } from "@material-ui/core";
|
import { Typography, makeStyles } from "@material-ui/core";
|
||||||
|
|
||||||
|
import SuccessIcon from "../../components/SuccessIcon";
|
||||||
|
|
||||||
const Authenticated = function () {
|
const Authenticated = function () {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
return (
|
return (
|
||||||
|
@ -11,14 +13,14 @@ const Authenticated = function () {
|
||||||
</div>
|
</div>
|
||||||
<Typography>Authenticated</Typography>
|
<Typography>Authenticated</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Authenticated
|
export default Authenticated;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
flex: "0 0 100%"
|
flex: "0 0 100%",
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Grid, makeStyles, Button } from "@material-ui/core";
|
import { Grid, makeStyles, Button } from "@material-ui/core";
|
||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
|
|
||||||
import LoginLayout from "../../../layouts/LoginLayout";
|
import LoginLayout from "../../../layouts/LoginLayout";
|
||||||
import { LogoutRoute as SignOutRoute } from "../../../Routes";
|
import { LogoutRoute as SignOutRoute } from "../../../Routes";
|
||||||
import Authenticated from "../Authenticated";
|
import Authenticated from "../Authenticated";
|
||||||
|
@ -15,13 +17,10 @@ const AuthenticatedView = function (props: Props) {
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
history.push(SignOutRoute);
|
history.push(SignOutRoute);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout
|
<LoginLayout id="authenticated-stage" title={`Hi ${props.name}`} showBrand>
|
||||||
id="authenticated-stage"
|
|
||||||
title={`Hi ${props.name}`}
|
|
||||||
showBrand>
|
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
||||||
|
@ -33,17 +32,17 @@ const AuthenticatedView = function (props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default AuthenticatedView
|
export default AuthenticatedView;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
mainContainer: {
|
mainContainer: {
|
||||||
border: "1px solid #d6d6d6",
|
border: "1px solid #d6d6d6",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
padding: theme.spacing(4),
|
padding: theme.spacing(4),
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
import classnames from "classnames";
|
|
||||||
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
|
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
|
||||||
|
import classnames from "classnames";
|
||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
import LoginLayout from "../../../layouts/LoginLayout";
|
|
||||||
import { useNotifications } from "../../../hooks/NotificationsContext";
|
|
||||||
import { postFirstFactor } from "../../../services/FirstFactor";
|
|
||||||
import { ResetPasswordStep1Route } from "../../../Routes";
|
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
||||||
import FixedTextField from "../../../components/FixedTextField";
|
import FixedTextField from "../../../components/FixedTextField";
|
||||||
|
import { useNotifications } from "../../../hooks/NotificationsContext";
|
||||||
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
import LoginLayout from "../../../layouts/LoginLayout";
|
||||||
|
import { ResetPasswordStep1Route } from "../../../Routes";
|
||||||
|
import { postFirstFactor } from "../../../services/FirstFactor";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
@ -47,7 +49,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
const handleSignIn = async () => {
|
const handleSignIn = async () => {
|
||||||
if (username === "" || password === "") {
|
if (username === "" || password === "") {
|
||||||
if (username === "") {
|
if (username === "") {
|
||||||
setUsernameError(true)
|
setUsernameError(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password === "") {
|
if (password === "") {
|
||||||
|
@ -62,8 +64,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
props.onAuthenticationSuccess(res ? res.redirect : undefined);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification(
|
createErrorNotification("Incorrect username or password.");
|
||||||
"Incorrect username or password.");
|
|
||||||
props.onAuthenticationFailure();
|
props.onAuthenticationFailure();
|
||||||
setPassword("");
|
setPassword("");
|
||||||
passwordRef.current.focus();
|
passwordRef.current.focus();
|
||||||
|
@ -75,10 +76,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout
|
<LoginLayout id="first-factor-stage" title="Sign in" showBrand>
|
||||||
id="first-factor-stage"
|
|
||||||
title="Sign in"
|
|
||||||
showBrand>
|
|
||||||
<Grid container spacing={2} className={style.root}>
|
<Grid container spacing={2} className={style.root}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
|
@ -92,21 +90,22 @@ const FirstFactorForm = function (props: Props) {
|
||||||
error={usernameError}
|
error={usernameError}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
fullWidth
|
fullWidth
|
||||||
onChange={v => setUsername(v.target.value)}
|
onChange={(v) => setUsername(v.target.value)}
|
||||||
onFocus={() => setUsernameError(false)}
|
onFocus={() => setUsernameError(false)}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
setUsernameError(true)
|
setUsernameError(true);
|
||||||
} else if (username.length && password.length) {
|
} else if (username.length && password.length) {
|
||||||
handleSignIn();
|
handleSignIn();
|
||||||
} else {
|
} else {
|
||||||
setUsernameError(false)
|
setUsernameError(false);
|
||||||
passwordRef.current.focus();
|
passwordRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
|
@ -120,11 +119,11 @@ const FirstFactorForm = function (props: Props) {
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={password}
|
value={password}
|
||||||
error={passwordError}
|
error={passwordError}
|
||||||
onChange={v => setPassword(v.target.value)}
|
onChange={(v) => setPassword(v.target.value)}
|
||||||
onFocus={() => setPasswordError(false)}
|
onFocus={() => setPasswordError(false)}
|
||||||
type="password"
|
type="password"
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
usernameRef.current.focus();
|
usernameRef.current.focus();
|
||||||
} else if (!password.length) {
|
} else if (!password.length) {
|
||||||
|
@ -133,13 +132,20 @@ const FirstFactorForm = function (props: Props) {
|
||||||
handleSignIn();
|
handleSignIn();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{props.rememberMe || props.resetPassword ?
|
{props.rememberMe || props.resetPassword ? (
|
||||||
<Grid item xs={12} className={props.rememberMe
|
<Grid
|
||||||
? classnames(style.leftAlign, style.actionRow)
|
item
|
||||||
: classnames(style.leftAlign, style.flexEnd, style.actionRow)}>
|
xs={12}
|
||||||
{props.rememberMe ?
|
className={
|
||||||
|
props.rememberMe
|
||||||
|
? classnames(style.leftAlign, style.actionRow)
|
||||||
|
: classnames(style.leftAlign, style.flexEnd, style.actionRow)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.rememberMe ? (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -148,7 +154,7 @@ const FirstFactorForm = function (props: Props) {
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onChange={handleRememberMeChange}
|
onChange={handleRememberMeChange}
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === "Enter") {
|
||||||
if (!username.length) {
|
if (!username.length) {
|
||||||
usernameRef.current.focus();
|
usernameRef.current.focus();
|
||||||
} else if (!password.length) {
|
} else if (!password.length) {
|
||||||
|
@ -158,20 +164,25 @@ const FirstFactorForm = function (props: Props) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
value="rememberMe"
|
value="rememberMe"
|
||||||
color="primary"/>
|
color="primary"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
className={style.rememberMe}
|
className={style.rememberMe}
|
||||||
label="Remember me"
|
label="Remember me"
|
||||||
/> : null}
|
/>
|
||||||
{props.resetPassword ?
|
) : null}
|
||||||
|
{props.resetPassword ? (
|
||||||
<Link
|
<Link
|
||||||
id="reset-password-button"
|
id="reset-password-button"
|
||||||
component="button"
|
component="button"
|
||||||
onClick={handleResetPasswordClick}
|
onClick={handleResetPasswordClick}
|
||||||
className={style.resetLink}>
|
className={style.resetLink}
|
||||||
|
>
|
||||||
Reset password?
|
Reset password?
|
||||||
</Link> : null}
|
</Link>
|
||||||
</Grid> : null}
|
) : null}
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button
|
<Button
|
||||||
id="sign-in-button"
|
id="sign-in-button"
|
||||||
|
@ -179,18 +190,19 @@ const FirstFactorForm = function (props: Props) {
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={handleSignIn}>
|
onClick={handleSignIn}
|
||||||
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default FirstFactorForm
|
export default FirstFactorForm;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
marginTop: theme.spacing(),
|
marginTop: theme.spacing(),
|
||||||
marginBottom: theme.spacing(),
|
marginBottom: theme.spacing(),
|
||||||
|
@ -219,4 +231,4 @@ const useStyles = makeStyles(theme => ({
|
||||||
textAlign: "right",
|
textAlign: "right",
|
||||||
verticalAlign: "bottom",
|
verticalAlign: "bottom",
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,20 +1,26 @@
|
||||||
import React, { useEffect, Fragment, ReactNode, useState, useCallback } from "react";
|
import React, { useEffect, Fragment, ReactNode, useState, useCallback } from "react";
|
||||||
|
|
||||||
import { Switch, Route, Redirect, useHistory, useLocation } from "react-router";
|
import { Switch, Route, Redirect, useHistory, useLocation } from "react-router";
|
||||||
import FirstFactorForm from "./FirstFactor/FirstFactorForm";
|
|
||||||
import SecondFactorForm from "./SecondFactor/SecondFactorForm";
|
import { useConfiguration } from "../../hooks/Configuration";
|
||||||
import {
|
|
||||||
FirstFactorRoute, SecondFactorRoute, SecondFactorTOTPRoute,
|
|
||||||
SecondFactorPushRoute, SecondFactorU2FRoute, AuthenticatedRoute
|
|
||||||
} from "../../Routes";
|
|
||||||
import { useAutheliaState } from "../../hooks/State";
|
|
||||||
import LoadingPage from "../LoadingPage/LoadingPage";
|
|
||||||
import { AuthenticationLevel } from "../../services/State";
|
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
import { useRedirectionURL } from "../../hooks/RedirectionURL";
|
||||||
|
import { useAutheliaState } from "../../hooks/State";
|
||||||
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
|
||||||
import { SecondFactorMethod } from "../../models/Methods";
|
import { SecondFactorMethod } from "../../models/Methods";
|
||||||
import { useConfiguration } from "../../hooks/Configuration";
|
import {
|
||||||
|
FirstFactorRoute,
|
||||||
|
SecondFactorRoute,
|
||||||
|
SecondFactorTOTPRoute,
|
||||||
|
SecondFactorPushRoute,
|
||||||
|
SecondFactorU2FRoute,
|
||||||
|
AuthenticatedRoute,
|
||||||
|
} from "../../Routes";
|
||||||
|
import { AuthenticationLevel } from "../../services/State";
|
||||||
|
import LoadingPage from "../LoadingPage/LoadingPage";
|
||||||
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
import AuthenticatedView from "./AuthenticatedView/AuthenticatedView";
|
||||||
|
import FirstFactorForm from "./FirstFactor/FirstFactorForm";
|
||||||
|
import SecondFactorForm from "./SecondFactor/SecondFactorForm";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
rememberMe: boolean;
|
rememberMe: boolean;
|
||||||
|
@ -35,7 +41,9 @@ const LoginPortal = function (props: Props) {
|
||||||
const redirect = useCallback((url: string) => history.push(url), [history]);
|
const redirect = useCallback((url: string) => history.push(url), [history]);
|
||||||
|
|
||||||
// Fetch the state when portal is mounted.
|
// Fetch the state when portal is mounted.
|
||||||
useEffect(() => { fetchState() }, [fetchState]);
|
useEffect(() => {
|
||||||
|
fetchState();
|
||||||
|
}, [fetchState]);
|
||||||
|
|
||||||
// Fetch preferences and configuration when user is authenticated.
|
// Fetch preferences and configuration when user is authenticated.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -76,9 +84,7 @@ const LoginPortal = function (props: Props) {
|
||||||
// Redirect to the correct stage if not enough authenticated
|
// Redirect to the correct stage if not enough authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state) {
|
if (state) {
|
||||||
const redirectionSuffix = redirectionURL
|
const redirectionSuffix = redirectionURL ? `?rd=${encodeURIComponent(redirectionURL)}` : "";
|
||||||
? `?rd=${encodeURIComponent(redirectionURL)}`
|
|
||||||
: '';
|
|
||||||
|
|
||||||
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
|
||||||
setFirstFactorDisabled(false);
|
setFirstFactorDisabled(false);
|
||||||
|
@ -107,9 +113,10 @@ const LoginPortal = function (props: Props) {
|
||||||
// Refresh state
|
// Refresh state
|
||||||
fetchState();
|
fetchState();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const firstFactorReady = state !== undefined &&
|
const firstFactorReady =
|
||||||
|
state !== undefined &&
|
||||||
state.authentication_level === AuthenticationLevel.Unauthenticated &&
|
state.authentication_level === AuthenticationLevel.Unauthenticated &&
|
||||||
location.pathname === FirstFactorRoute;
|
location.pathname === FirstFactorRoute;
|
||||||
|
|
||||||
|
@ -123,16 +130,20 @@ const LoginPortal = function (props: Props) {
|
||||||
resetPassword={props.resetPassword}
|
resetPassword={props.resetPassword}
|
||||||
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
onAuthenticationStart={() => setFirstFactorDisabled(true)}
|
||||||
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
|
||||||
onAuthenticationSuccess={handleAuthSuccess} />
|
onAuthenticationSuccess={handleAuthSuccess}
|
||||||
|
/>
|
||||||
</ComponentOrLoading>
|
</ComponentOrLoading>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorRoute}>
|
<Route path={SecondFactorRoute}>
|
||||||
{state && userInfo && configuration ? <SecondFactorForm
|
{state && userInfo && configuration ? (
|
||||||
authenticationLevel={state.authentication_level}
|
<SecondFactorForm
|
||||||
userInfo={userInfo}
|
authenticationLevel={state.authentication_level}
|
||||||
configuration={configuration}
|
userInfo={userInfo}
|
||||||
onMethodChanged={() => fetchUserInfo()}
|
configuration={configuration}
|
||||||
onAuthenticationSuccess={handleAuthSuccess} /> : null}
|
onMethodChanged={() => fetchUserInfo()}
|
||||||
|
onAuthenticationSuccess={handleAuthSuccess}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={AuthenticatedRoute} exact>
|
<Route path={AuthenticatedRoute} exact>
|
||||||
{userInfo ? <AuthenticatedView name={userInfo.display_name} /> : null}
|
{userInfo ? <AuthenticatedView name={userInfo.display_name} /> : null}
|
||||||
|
@ -141,10 +152,10 @@ const LoginPortal = function (props: Props) {
|
||||||
<Redirect to={FirstFactorRoute} />
|
<Redirect to={FirstFactorRoute} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default LoginPortal
|
export default LoginPortal;
|
||||||
|
|
||||||
interface ComponentOrLoadingProps {
|
interface ComponentOrLoadingProps {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
@ -160,5 +171,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
|
||||||
</div>
|
</div>
|
||||||
{props.ready ? props.children : null}
|
{props.ready ? props.children : null}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
import { makeStyles } from "@material-ui/core";
|
import { makeStyles } from "@material-ui/core";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ interface IconWithContextProps {
|
||||||
|
|
||||||
const IconWithContext = function (props: IconWithContextProps) {
|
const IconWithContext = function (props: IconWithContextProps) {
|
||||||
const iconSize = 64;
|
const iconSize = 64;
|
||||||
const style = makeStyles(theme => ({
|
const style = makeStyles((theme) => ({
|
||||||
root: {},
|
root: {},
|
||||||
iconContainer: {
|
iconContainer: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -24,21 +25,17 @@ const IconWithContext = function (props: IconWithContextProps) {
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
display: "block",
|
display: "block",
|
||||||
}
|
},
|
||||||
}))();
|
}))();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classnames(props.className, style.root)}>
|
<div className={classnames(props.className, style.root)}>
|
||||||
<div className={style.iconContainer}>
|
<div className={style.iconContainer}>
|
||||||
<div className={style.icon}>
|
<div className={style.icon}>{props.icon}</div>
|
||||||
{props.icon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={style.context}>
|
|
||||||
{props.context}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className={style.context}>{props.context}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default IconWithContext
|
export default IconWithContext;
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { ReactNode, Fragment } from "react";
|
import React, { ReactNode, Fragment } from "react";
|
||||||
|
|
||||||
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
import { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
|
||||||
import InformationIcon from "../../../components/InformationIcon";
|
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
|
||||||
|
import InformationIcon from "../../../components/InformationIcon";
|
||||||
import Authenticated from "../Authenticated";
|
import Authenticated from "../Authenticated";
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
ALREADY_AUTHENTICATED = 1,
|
ALREADY_AUTHENTICATED = 1,
|
||||||
NOT_REGISTERED = 2,
|
NOT_REGISTERED = 2,
|
||||||
METHOD = 3
|
METHOD = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
|
@ -24,47 +26,40 @@ const DefaultMethodContainer = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
|
||||||
let container: ReactNode;
|
let container: ReactNode;
|
||||||
let stateClass: string = '';
|
let stateClass: string = "";
|
||||||
switch (props.state) {
|
switch (props.state) {
|
||||||
case State.ALREADY_AUTHENTICATED:
|
case State.ALREADY_AUTHENTICATED:
|
||||||
container = <Authenticated />
|
container = <Authenticated />;
|
||||||
stateClass = "state-already-authenticated";
|
stateClass = "state-already-authenticated";
|
||||||
break;
|
break;
|
||||||
case State.NOT_REGISTERED:
|
case State.NOT_REGISTERED:
|
||||||
container = <NotRegisteredContainer />
|
container = <NotRegisteredContainer />;
|
||||||
stateClass = "state-not-registered";
|
stateClass = "state-not-registered";
|
||||||
break;
|
break;
|
||||||
case State.METHOD:
|
case State.METHOD:
|
||||||
container = <MethodContainer explanation={props.explanation}>
|
container = <MethodContainer explanation={props.explanation}>{props.children}</MethodContainer>;
|
||||||
{props.children}
|
|
||||||
</MethodContainer>
|
|
||||||
stateClass = "state-method";
|
stateClass = "state-method";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={props.id}>
|
<div id={props.id}>
|
||||||
<Typography variant="h6">{props.title}</Typography>
|
<Typography variant="h6">{props.title}</Typography>
|
||||||
<div className={classnames(style.container, stateClass)} id="2fa-container">
|
<div className={classnames(style.container, stateClass)} id="2fa-container">
|
||||||
<div className={style.containerFlex}>
|
<div className={style.containerFlex}>{container}</div>
|
||||||
{container}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{props.onRegisterClick
|
{props.onRegisterClick ? (
|
||||||
? <Link component="button"
|
<Link component="button" id="register-link" onClick={props.onRegisterClick}>
|
||||||
id="register-link"
|
|
||||||
onClick={props.onRegisterClick}>
|
|
||||||
Not registered yet?
|
Not registered yet?
|
||||||
</Link>
|
</Link>
|
||||||
: null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default DefaultMethodContainer
|
export default DefaultMethodContainer;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
container: {
|
container: {
|
||||||
height: "200px",
|
height: "200px",
|
||||||
},
|
},
|
||||||
|
@ -76,17 +71,21 @@ const useStyles = makeStyles(theme => ({
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function NotRegisteredContainer() {
|
function NotRegisteredContainer() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}><InformationIcon /></div>
|
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}>
|
||||||
<Typography style={{ color: "#5858ff" }}>Register your first device by clicking on the link below</Typography>
|
<InformationIcon />
|
||||||
|
</div>
|
||||||
|
<Typography style={{ color: "#5858ff" }}>
|
||||||
|
Register your first device by clicking on the link below
|
||||||
|
</Typography>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MethodContainerProps {
|
interface MethodContainerProps {
|
||||||
|
@ -101,5 +100,5 @@ function MethodContainer(props: MethodContainerProps) {
|
||||||
<div style={{ marginBottom: theme.spacing(2) }}>{props.children}</div>
|
<div style={{ marginBottom: theme.spacing(2) }}>{props.children}</div>
|
||||||
<Typography>{props.explanation}</Typography>
|
<Typography>{props.explanation}</Typography>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
import { Dialog, Grid, makeStyles, DialogContent, Button, DialogActions, Typography, useTheme } from "@material-ui/core";
|
|
||||||
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
import {
|
||||||
import PieChartIcon from "../../../components/PieChartIcon";
|
Dialog,
|
||||||
import { SecondFactorMethod } from "../../../models/Methods";
|
Grid,
|
||||||
|
makeStyles,
|
||||||
|
DialogContent,
|
||||||
|
Button,
|
||||||
|
DialogActions,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
|
||||||
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
||||||
|
import PieChartIcon from "../../../components/PieChartIcon";
|
||||||
|
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
||||||
|
import { SecondFactorMethod } from "../../../models/Methods";
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -18,37 +29,45 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const pieChartIcon = <PieChartIcon width={24} height={24} maxProgress={1000} progress={150}
|
const pieChartIcon = (
|
||||||
color={theme.palette.primary.main} backgroundColor={"white"} />
|
<PieChartIcon
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
maxProgress={1000}
|
||||||
|
progress={150}
|
||||||
|
color={theme.palette.primary.main}
|
||||||
|
backgroundColor={"white"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog open={props.open} className={style.root} onClose={props.onClose}>
|
||||||
open={props.open}
|
|
||||||
className={style.root}
|
|
||||||
onClose={props.onClose}>
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Grid container justify="center" spacing={1} id="methods-dialog">
|
<Grid container justify="center" spacing={1} id="methods-dialog">
|
||||||
{props.methods.has(SecondFactorMethod.TOTP)
|
{props.methods.has(SecondFactorMethod.TOTP) ? (
|
||||||
? <MethodItem
|
<MethodItem
|
||||||
id="one-time-password-option"
|
id="one-time-password-option"
|
||||||
method="One-Time Password"
|
method="One-Time Password"
|
||||||
icon={pieChartIcon}
|
icon={pieChartIcon}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.TOTP)} />
|
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
|
||||||
: null}
|
/>
|
||||||
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported
|
) : null}
|
||||||
? <MethodItem
|
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
|
||||||
|
<MethodItem
|
||||||
id="security-key-option"
|
id="security-key-option"
|
||||||
method="Security Key"
|
method="Security Key"
|
||||||
icon={<FingerTouchIcon size={32} />}
|
icon={<FingerTouchIcon size={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.U2F)} />
|
onClick={() => props.onClick(SecondFactorMethod.U2F)}
|
||||||
: null}
|
/>
|
||||||
{props.methods.has(SecondFactorMethod.MobilePush)
|
) : null}
|
||||||
? <MethodItem
|
{props.methods.has(SecondFactorMethod.MobilePush) ? (
|
||||||
|
<MethodItem
|
||||||
id="push-notification-option"
|
id="push-notification-option"
|
||||||
method="Push Notification"
|
method="Push Notification"
|
||||||
icon={<PushNotificationIcon width={32} height={32} />}
|
icon={<PushNotificationIcon width={32} height={32} />}
|
||||||
onClick={() => props.onClick(SecondFactorMethod.MobilePush)} />
|
onClick={() => props.onClick(SecondFactorMethod.MobilePush)}
|
||||||
: null}
|
/>
|
||||||
|
) : null}
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
@ -57,16 +76,16 @@ const MethodSelectionDialog = function (props: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MethodSelectionDialog
|
export default MethodSelectionDialog;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
interface MethodItemProps {
|
interface MethodItemProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -77,7 +96,7 @@ interface MethodItemProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function MethodItem(props: MethodItemProps) {
|
function MethodItem(props: MethodItemProps) {
|
||||||
const style = makeStyles(theme => ({
|
const style = makeStyles((theme) => ({
|
||||||
item: {
|
item: {
|
||||||
paddingTop: theme.spacing(4),
|
paddingTop: theme.spacing(4),
|
||||||
paddingBottom: theme.spacing(4),
|
paddingBottom: theme.spacing(4),
|
||||||
|
@ -89,18 +108,23 @@ function MethodItem(props: MethodItemProps) {
|
||||||
},
|
},
|
||||||
buttonRoot: {
|
buttonRoot: {
|
||||||
display: "block",
|
display: "block",
|
||||||
}
|
},
|
||||||
}))();
|
}))();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} className="method-option" id={props.id}>
|
<Grid item xs={12} className="method-option" id={props.id}>
|
||||||
<Button className={style.item} color="primary"
|
<Button
|
||||||
|
className={style.item}
|
||||||
|
color="primary"
|
||||||
classes={{ root: style.buttonRoot }}
|
classes={{ root: style.buttonRoot }}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
onClick={props.onClick}>
|
onClick={props.onClick}
|
||||||
|
>
|
||||||
<div className={style.icon}>{props.icon}</div>
|
<div className={style.icon}>{props.icon}</div>
|
||||||
<div><Typography>{props.method}</Typography></div>
|
<div>
|
||||||
|
<Typography>{props.method}</Typography>
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
import OtpInput from "react-otp-input";
|
|
||||||
import TimerIcon from "../../../components/TimerIcon";
|
|
||||||
import { makeStyles } from "@material-ui/core";
|
import { makeStyles } from "@material-ui/core";
|
||||||
import classnames from "classnames";
|
import classnames from "classnames";
|
||||||
|
import OtpInput from "react-otp-input";
|
||||||
|
|
||||||
|
import SuccessIcon from "../../../components/SuccessIcon";
|
||||||
|
import TimerIcon from "../../../components/TimerIcon";
|
||||||
import IconWithContext from "./IconWithContext";
|
import IconWithContext from "./IconWithContext";
|
||||||
import { State } from "./OneTimePasswordMethod";
|
import { State } from "./OneTimePasswordMethod";
|
||||||
import SuccessIcon from "../../../components/SuccessIcon";
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
passcode: string;
|
passcode: string;
|
||||||
state: State;
|
state: State;
|
||||||
period: number
|
period: number;
|
||||||
|
|
||||||
onChange: (passcode: string) => void;
|
onChange: (passcode: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -26,22 +28,18 @@ const OTPDial = function (props: Props) {
|
||||||
numInputs={6}
|
numInputs={6}
|
||||||
isDisabled={props.state === State.InProgress || props.state === State.Success}
|
isDisabled={props.state === State.InProgress || props.state === State.Success}
|
||||||
hasErrored={props.state === State.Failure}
|
hasErrored={props.state === State.Failure}
|
||||||
inputStyle={classnames(style.otpDigitInput, props.state === State.Failure ? style.inputError : "")} />
|
inputStyle={classnames(style.otpDigitInput, props.state === State.Failure ? style.inputError : "")}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return <IconWithContext icon={<Icon state={props.state} period={props.period} />} context={dial} />;
|
||||||
<IconWithContext
|
};
|
||||||
icon={<Icon state={props.state} period={props.period} />}
|
|
||||||
context={dial} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OTPDial
|
export default OTPDial;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
timeProgress: {
|
timeProgress: {},
|
||||||
},
|
|
||||||
register: {
|
register: {
|
||||||
marginTop: theme.spacing(),
|
marginTop: theme.spacing(),
|
||||||
},
|
},
|
||||||
|
@ -59,7 +57,7 @@ const useStyles = makeStyles(theme => ({
|
||||||
},
|
},
|
||||||
inputError: {
|
inputError: {
|
||||||
border: "1px solid rgba(255, 2, 2, 0.95)",
|
border: "1px solid rgba(255, 2, 2, 0.95)",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IconProps {
|
interface IconProps {
|
||||||
|
@ -70,8 +68,10 @@ interface IconProps {
|
||||||
function Icon(props: IconProps) {
|
function Icon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{props.state !== State.Success ? <TimerIcon backgroundColor="#000" color="#FFFFFF" width={64} height={64} period={props.period} /> : null}
|
{props.state !== State.Success ? (
|
||||||
|
<TimerIcon backgroundColor="#000" color="#FFFFFF" width={64} height={64} period={props.period} />
|
||||||
|
) : null}
|
||||||
{props.state === State.Success ? <SuccessIcon /> : null}
|
{props.state === State.Success ? <SuccessIcon /> : null}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
import { completeTOTPSignIn } from "../../../services/OneTimePassword";
|
||||||
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
import OTPDial from "./OTPDial";
|
import OTPDial from "./OTPDial";
|
||||||
import { completeTOTPSignIn } from "../../../services/OneTimePassword";
|
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
Idle = 1,
|
Idle = 1,
|
||||||
|
@ -16,7 +17,7 @@ export interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
authenticationLevel: AuthenticationLevel;
|
authenticationLevel: AuthenticationLevel;
|
||||||
registered: boolean;
|
registered: boolean;
|
||||||
totp_period: number
|
totp_period: number;
|
||||||
|
|
||||||
onRegisterClick: () => void;
|
onRegisterClick: () => void;
|
||||||
onSignInError: (err: Error) => void;
|
onSignInError: (err: Error) => void;
|
||||||
|
@ -25,9 +26,9 @@ export interface Props {
|
||||||
|
|
||||||
const OneTimePasswordMethod = function (props: Props) {
|
const OneTimePasswordMethod = function (props: Props) {
|
||||||
const [passcode, setPasscode] = useState("");
|
const [passcode, setPasscode] = useState("");
|
||||||
const [state, setState] = useState(props.authenticationLevel === AuthenticationLevel.TwoFactor
|
const [state, setState] = useState(
|
||||||
? State.Success
|
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
|
||||||
: State.Idle);
|
);
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
|
|
||||||
const { onSignInSuccess, onSignInError } = props;
|
const { onSignInSuccess, onSignInError } = props;
|
||||||
|
@ -67,7 +68,9 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
}
|
}
|
||||||
}, [props.authenticationLevel, setState]);
|
}, [props.authenticationLevel, setState]);
|
||||||
|
|
||||||
useEffect(() => { signInFunc() }, [signInFunc]);
|
useEffect(() => {
|
||||||
|
signInFunc();
|
||||||
|
}, [signInFunc]);
|
||||||
|
|
||||||
let methodState = MethodContainerState.METHOD;
|
let methodState = MethodContainerState.METHOD;
|
||||||
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
|
@ -82,14 +85,11 @@ const OneTimePasswordMethod = function (props: Props) {
|
||||||
title="One-Time Password"
|
title="One-Time Password"
|
||||||
explanation="Enter one-time password"
|
explanation="Enter one-time password"
|
||||||
state={methodState}
|
state={methodState}
|
||||||
onRegisterClick={props.onRegisterClick}>
|
onRegisterClick={props.onRegisterClick}
|
||||||
<OTPDial
|
>
|
||||||
passcode={passcode}
|
<OTPDial passcode={passcode} onChange={setPasscode} state={state} period={props.totp_period} />
|
||||||
onChange={setPasscode}
|
|
||||||
state={state}
|
|
||||||
period={props.totp_period} />
|
|
||||||
</MethodContainer>
|
</MethodContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default OneTimePasswordMethod
|
export default OneTimePasswordMethod;
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { useEffect, useCallback, useState, ReactNode } from "react";
|
import React, { useEffect, useCallback, useState, ReactNode } from "react";
|
||||||
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
|
||||||
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
|
||||||
import { completePushNotificationSignIn } from "../../../services/PushNotification";
|
|
||||||
import { Button, makeStyles } from "@material-ui/core";
|
import { Button, makeStyles } from "@material-ui/core";
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
||||||
import { useIsMountedRef } from "../../../hooks/Mounted";
|
|
||||||
import SuccessIcon from "../../../components/SuccessIcon";
|
|
||||||
import FailureIcon from "../../../components/FailureIcon";
|
import FailureIcon from "../../../components/FailureIcon";
|
||||||
|
import PushNotificationIcon from "../../../components/PushNotificationIcon";
|
||||||
|
import SuccessIcon from "../../../components/SuccessIcon";
|
||||||
|
import { useIsMountedRef } from "../../../hooks/Mounted";
|
||||||
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
import { completePushNotificationSignIn } from "../../../services/PushNotification";
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
SignInInProgress = 1,
|
SignInInProgress = 1,
|
||||||
|
@ -50,7 +52,7 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
setState(State.Success);
|
setState(State.Success);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!mounted.current) return;
|
if (!mounted.current) return;
|
||||||
onSignInSuccessCallback(res ? res.redirect : undefined)
|
onSignInSuccessCallback(res ? res.redirect : undefined);
|
||||||
}, 1500);
|
}, 1500);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// If the request was initiated and the user changed 2FA method in the meantime,
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
||||||
|
@ -63,7 +65,9 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
}
|
}
|
||||||
}, [onSignInErrorCallback, onSignInSuccessCallback, setState, redirectionURL, mounted, props.authenticationLevel]);
|
}, [onSignInErrorCallback, onSignInSuccessCallback, setState, redirectionURL, mounted, props.authenticationLevel]);
|
||||||
|
|
||||||
useEffect(() => { signInFunc() }, [signInFunc]);
|
useEffect(() => {
|
||||||
|
signInFunc();
|
||||||
|
}, [signInFunc]);
|
||||||
|
|
||||||
// Set successful state if user is already authenticated.
|
// Set successful state if user is already authenticated.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -94,23 +98,24 @@ const PushNotificationMethod = function (props: Props) {
|
||||||
id={props.id}
|
id={props.id}
|
||||||
title="Push Notification"
|
title="Push Notification"
|
||||||
explanation="A notification has been sent to your smartphone"
|
explanation="A notification has been sent to your smartphone"
|
||||||
state={methodState}>
|
state={methodState}
|
||||||
<div className={style.icon}>
|
>
|
||||||
{icon}
|
<div className={style.icon}>{icon}</div>
|
||||||
</div>
|
<div className={state !== State.Failure ? "hidden" : ""}>
|
||||||
<div className={(state !== State.Failure) ? "hidden" : ""}>
|
<Button color="secondary" onClick={signInFunc}>
|
||||||
<Button color="secondary" onClick={signInFunc}>Retry</Button>
|
Retry
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</MethodContainer>
|
</MethodContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default PushNotificationMethod
|
export default PushNotificationMethod;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
width: "64px",
|
width: "64px",
|
||||||
height: "64px",
|
height: "64px",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,26 +1,28 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import { Grid, makeStyles, Button } from "@material-ui/core";
|
import { Grid, makeStyles, Button } from "@material-ui/core";
|
||||||
import MethodSelectionDialog from "./MethodSelectionDialog";
|
|
||||||
import { SecondFactorMethod } from "../../../models/Methods";
|
|
||||||
import { useHistory, Switch, Route, Redirect } from "react-router";
|
import { useHistory, Switch, Route, Redirect } from "react-router";
|
||||||
import LoginLayout from "../../../layouts/LoginLayout";
|
import u2fApi from "u2f-api";
|
||||||
|
|
||||||
import { useNotifications } from "../../../hooks/NotificationsContext";
|
import { useNotifications } from "../../../hooks/NotificationsContext";
|
||||||
|
import LoginLayout from "../../../layouts/LoginLayout";
|
||||||
|
import { Configuration } from "../../../models/Configuration";
|
||||||
|
import { SecondFactorMethod } from "../../../models/Methods";
|
||||||
|
import { UserInfo } from "../../../models/UserInfo";
|
||||||
import {
|
import {
|
||||||
initiateTOTPRegistrationProcess,
|
LogoutRoute as SignOutRoute,
|
||||||
initiateU2FRegistrationProcess
|
SecondFactorTOTPRoute,
|
||||||
} from "../../../services/RegisterDevice";
|
SecondFactorPushRoute,
|
||||||
import SecurityKeyMethod from "./SecurityKeyMethod";
|
SecondFactorU2FRoute,
|
||||||
|
SecondFactorRoute,
|
||||||
|
} from "../../../Routes";
|
||||||
|
import { initiateTOTPRegistrationProcess, initiateU2FRegistrationProcess } from "../../../services/RegisterDevice";
|
||||||
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
|
import { setPreferred2FAMethod } from "../../../services/UserPreferences";
|
||||||
|
import MethodSelectionDialog from "./MethodSelectionDialog";
|
||||||
import OneTimePasswordMethod from "./OneTimePasswordMethod";
|
import OneTimePasswordMethod from "./OneTimePasswordMethod";
|
||||||
import PushNotificationMethod from "./PushNotificationMethod";
|
import PushNotificationMethod from "./PushNotificationMethod";
|
||||||
import {
|
import SecurityKeyMethod from "./SecurityKeyMethod";
|
||||||
LogoutRoute as SignOutRoute, SecondFactorTOTPRoute,
|
|
||||||
SecondFactorPushRoute, SecondFactorU2FRoute, SecondFactorRoute
|
|
||||||
} from "../../../Routes";
|
|
||||||
import { setPreferred2FAMethod } from "../../../services/UserPreferences";
|
|
||||||
import { UserInfo } from "../../../models/UserInfo";
|
|
||||||
import { Configuration } from "../../../models/Configuration";
|
|
||||||
import u2fApi from "u2f-api";
|
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
|
||||||
|
|
||||||
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process.";
|
const EMAIL_SENT_NOTIFICATION = "An email has been sent to your address to complete the process.";
|
||||||
|
|
||||||
|
@ -46,7 +48,8 @@ const SecondFactorForm = function (props: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
u2fApi.ensureSupport().then(
|
u2fApi.ensureSupport().then(
|
||||||
() => setU2fSupported(true),
|
() => setU2fSupported(true),
|
||||||
() => console.error("U2F not supported"));
|
() => console.error("U2F not supported"),
|
||||||
|
);
|
||||||
}, [setU2fSupported]);
|
}, [setU2fSupported]);
|
||||||
|
|
||||||
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
|
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
|
||||||
|
@ -63,12 +66,12 @@ const SecondFactorForm = function (props: Props) {
|
||||||
createErrorNotification("There was a problem initiating the registration process");
|
createErrorNotification("There was a problem initiating the registration process");
|
||||||
}
|
}
|
||||||
setRegistrationInProgress(false);
|
setRegistrationInProgress(false);
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleMethodSelectionClick = () => {
|
const handleMethodSelectionClick = () => {
|
||||||
setMethodSelectionOpen(true);
|
setMethodSelectionOpen(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleMethodSelected = async (method: SecondFactorMethod) => {
|
const handleMethodSelected = async (method: SecondFactorMethod) => {
|
||||||
try {
|
try {
|
||||||
|
@ -79,23 +82,21 @@ const SecondFactorForm = function (props: Props) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("There was an issue updating preferred second factor method");
|
createErrorNotification("There was an issue updating preferred second factor method");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleLogoutClick = () => {
|
const handleLogoutClick = () => {
|
||||||
history.push(SignOutRoute);
|
history.push(SignOutRoute);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout
|
<LoginLayout id="second-factor-stage" title={`Hi ${props.userInfo.display_name}`} showBrand>
|
||||||
id="second-factor-stage"
|
|
||||||
title={`Hi ${props.userInfo.display_name}`}
|
|
||||||
showBrand>
|
|
||||||
<MethodSelectionDialog
|
<MethodSelectionDialog
|
||||||
open={methodSelectionOpen}
|
open={methodSelectionOpen}
|
||||||
methods={props.configuration.available_methods}
|
methods={props.configuration.available_methods}
|
||||||
u2fSupported={u2fSupported}
|
u2fSupported={u2fSupported}
|
||||||
onClose={() => setMethodSelectionOpen(false)}
|
onClose={() => setMethodSelectionOpen(false)}
|
||||||
onClick={handleMethodSelected} />
|
onClick={handleMethodSelected}
|
||||||
|
/>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
|
||||||
|
@ -116,8 +117,9 @@ const SecondFactorForm = function (props: Props) {
|
||||||
registered={props.userInfo.has_totp}
|
registered={props.userInfo.has_totp}
|
||||||
totp_period={props.configuration.totp_period}
|
totp_period={props.configuration.totp_period}
|
||||||
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
|
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
|
||||||
onSignInError={err => createErrorNotification(err.message)}
|
onSignInError={(err) => createErrorNotification(err.message)}
|
||||||
onSignInSuccess={props.onAuthenticationSuccess} />
|
onSignInSuccess={props.onAuthenticationSuccess}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorU2FRoute} exact>
|
<Route path={SecondFactorU2FRoute} exact>
|
||||||
<SecurityKeyMethod
|
<SecurityKeyMethod
|
||||||
|
@ -126,15 +128,17 @@ const SecondFactorForm = function (props: Props) {
|
||||||
// Whether the user has a U2F device registered already
|
// Whether the user has a U2F device registered already
|
||||||
registered={props.userInfo.has_u2f}
|
registered={props.userInfo.has_u2f}
|
||||||
onRegisterClick={initiateRegistration(initiateU2FRegistrationProcess)}
|
onRegisterClick={initiateRegistration(initiateU2FRegistrationProcess)}
|
||||||
onSignInError={err => createErrorNotification(err.message)}
|
onSignInError={(err) => createErrorNotification(err.message)}
|
||||||
onSignInSuccess={props.onAuthenticationSuccess} />
|
onSignInSuccess={props.onAuthenticationSuccess}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorPushRoute} exact>
|
<Route path={SecondFactorPushRoute} exact>
|
||||||
<PushNotificationMethod
|
<PushNotificationMethod
|
||||||
id="push-notification-method"
|
id="push-notification-method"
|
||||||
authenticationLevel={props.authenticationLevel}
|
authenticationLevel={props.authenticationLevel}
|
||||||
onSignInError={err => createErrorNotification(err.message)}
|
onSignInError={(err) => createErrorNotification(err.message)}
|
||||||
onSignInSuccess={props.onAuthenticationSuccess} />
|
onSignInSuccess={props.onAuthenticationSuccess}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={SecondFactorRoute}>
|
<Route path={SecondFactorRoute}>
|
||||||
<Redirect to={SecondFactorTOTPRoute} />
|
<Redirect to={SecondFactorTOTPRoute} />
|
||||||
|
@ -143,12 +147,12 @@ const SecondFactorForm = function (props: Props) {
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SecondFactorForm
|
export default SecondFactorForm;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
methodContainer: {
|
methodContainer: {
|
||||||
border: "1px solid #d6d6d6",
|
border: "1px solid #d6d6d6",
|
||||||
borderRadius: "10px",
|
borderRadius: "10px",
|
||||||
|
@ -156,4 +160,4 @@ const useStyles = makeStyles(theme => ({
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import React, { useCallback, useEffect, useState, Fragment } from "react";
|
import React, { useCallback, useEffect, useState, Fragment } from "react";
|
||||||
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
|
||||||
import { makeStyles, Button, useTheme } from "@material-ui/core";
|
import { makeStyles, Button, useTheme } from "@material-ui/core";
|
||||||
import { initiateU2FSignin, completeU2FSignin } from "../../../services/SecurityKey";
|
|
||||||
import u2fApi from "u2f-api";
|
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
||||||
import { useIsMountedRef } from "../../../hooks/Mounted";
|
|
||||||
import { useTimer } from "../../../hooks/Timer";
|
|
||||||
import LinearProgressBar from "../../../components/LinearProgressBar";
|
|
||||||
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
|
||||||
import FailureIcon from "../../../components/FailureIcon";
|
|
||||||
import IconWithContext from "./IconWithContext";
|
|
||||||
import { CSSProperties } from "@material-ui/styles";
|
import { CSSProperties } from "@material-ui/styles";
|
||||||
|
import u2fApi from "u2f-api";
|
||||||
|
|
||||||
|
import FailureIcon from "../../../components/FailureIcon";
|
||||||
|
import FingerTouchIcon from "../../../components/FingerTouchIcon";
|
||||||
|
import LinearProgressBar from "../../../components/LinearProgressBar";
|
||||||
|
import { useIsMountedRef } from "../../../hooks/Mounted";
|
||||||
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
import { useTimer } from "../../../hooks/Timer";
|
||||||
|
import { initiateU2FSignin, completeU2FSignin } from "../../../services/SecurityKey";
|
||||||
import { AuthenticationLevel } from "../../../services/State";
|
import { AuthenticationLevel } from "../../../services/State";
|
||||||
|
import IconWithContext from "./IconWithContext";
|
||||||
|
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
WaitTouch = 1,
|
WaitTouch = 1,
|
||||||
|
@ -35,7 +37,7 @@ const SecurityKeyMethod = function (props: Props) {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
const redirectionURL = useRedirectionURL();
|
const redirectionURL = useRedirectionURL();
|
||||||
const mounted = useIsMountedRef();
|
const mounted = useIsMountedRef();
|
||||||
const [timerPercent, triggerTimer,] = useTimer(signInTimeout * 1000 - 500);
|
const [timerPercent, triggerTimer] = useTimer(signInTimeout * 1000 - 500);
|
||||||
|
|
||||||
const { onSignInSuccess, onSignInError } = props;
|
const { onSignInSuccess, onSignInError } = props;
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
@ -61,7 +63,7 @@ const SecurityKeyMethod = function (props: Props) {
|
||||||
challenge: signRequest.challenge,
|
challenge: signRequest.challenge,
|
||||||
keyHandle: r.keyHandle,
|
keyHandle: r.keyHandle,
|
||||||
version: r.version,
|
version: r.version,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
const signResponse = await u2fApi.sign(signRequests, signInTimeout);
|
const signResponse = await u2fApi.sign(signRequests, signInTimeout);
|
||||||
// If the request was initiated and the user changed 2FA method in the meantime,
|
// If the request was initiated and the user changed 2FA method in the meantime,
|
||||||
|
@ -79,9 +81,19 @@ const SecurityKeyMethod = function (props: Props) {
|
||||||
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
onSignInErrorCallback(new Error("Failed to initiate security key sign in process"));
|
||||||
setState(State.Failure);
|
setState(State.Failure);
|
||||||
}
|
}
|
||||||
}, [onSignInSuccessCallback, onSignInErrorCallback, redirectionURL, mounted, triggerTimer, props.authenticationLevel, props.registered]);
|
}, [
|
||||||
|
onSignInSuccessCallback,
|
||||||
|
onSignInErrorCallback,
|
||||||
|
redirectionURL,
|
||||||
|
mounted,
|
||||||
|
triggerTimer,
|
||||||
|
props.authenticationLevel,
|
||||||
|
props.registered,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => { doInitiateSignIn() }, [doInitiateSignIn]);
|
useEffect(() => {
|
||||||
|
doInitiateSignIn();
|
||||||
|
}, [doInitiateSignIn]);
|
||||||
|
|
||||||
let methodState = MethodContainerState.METHOD;
|
let methodState = MethodContainerState.METHOD;
|
||||||
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
|
||||||
|
@ -96,20 +108,21 @@ const SecurityKeyMethod = function (props: Props) {
|
||||||
title="Security Key"
|
title="Security Key"
|
||||||
explanation="Touch the token of your security key"
|
explanation="Touch the token of your security key"
|
||||||
state={methodState}
|
state={methodState}
|
||||||
onRegisterClick={props.onRegisterClick}>
|
onRegisterClick={props.onRegisterClick}
|
||||||
|
>
|
||||||
<div className={style.icon}>
|
<div className={style.icon}>
|
||||||
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
|
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
|
||||||
</div>
|
</div>
|
||||||
</MethodContainer>
|
</MethodContainer>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SecurityKeyMethod
|
export default SecurityKeyMethod;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
icon: {
|
icon: {
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface IconProps {
|
interface IconProps {
|
||||||
|
@ -125,22 +138,32 @@ function Icon(props: IconProps) {
|
||||||
|
|
||||||
const progressBarStyle: CSSProperties = {
|
const progressBarStyle: CSSProperties = {
|
||||||
marginTop: theme.spacing(),
|
marginTop: theme.spacing(),
|
||||||
}
|
};
|
||||||
|
|
||||||
const touch = <IconWithContext
|
const touch = (
|
||||||
icon={<FingerTouchIcon size={64} animated strong />}
|
<IconWithContext
|
||||||
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
|
icon={<FingerTouchIcon size={64} animated strong />}
|
||||||
className={state === State.WaitTouch ? undefined : "hidden"} />
|
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
|
||||||
|
className={state === State.WaitTouch ? undefined : "hidden"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const failure = <IconWithContext
|
const failure = (
|
||||||
icon={<FailureIcon />}
|
<IconWithContext
|
||||||
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>}
|
icon={<FailureIcon />}
|
||||||
className={state === State.Failure ? undefined : "hidden"} />
|
context={
|
||||||
|
<Button color="secondary" onClick={props.onRetryClick}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
className={state === State.Failure ? undefined : "hidden"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{touch}
|
{touch}
|
||||||
{failure}
|
{failure}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import React, { useEffect, useCallback, useState } from "react";
|
import React, { useEffect, useCallback, useState } from "react";
|
||||||
import LoginLayout from "../../../layouts/LoginLayout";
|
|
||||||
import { useNotifications } from "../../../hooks/NotificationsContext";
|
|
||||||
import { signOut } from "../../../services/SignOut";
|
|
||||||
import { Typography, makeStyles } from "@material-ui/core";
|
import { Typography, makeStyles } from "@material-ui/core";
|
||||||
import { Redirect } from "react-router";
|
import { Redirect } from "react-router";
|
||||||
import { FirstFactorRoute } from "../../../Routes";
|
|
||||||
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
|
||||||
import { useIsMountedRef } from "../../../hooks/Mounted";
|
|
||||||
|
|
||||||
export interface Props { }
|
import { useIsMountedRef } from "../../../hooks/Mounted";
|
||||||
|
import { useNotifications } from "../../../hooks/NotificationsContext";
|
||||||
|
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
|
||||||
|
import LoginLayout from "../../../layouts/LoginLayout";
|
||||||
|
import { FirstFactorRoute } from "../../../Routes";
|
||||||
|
import { signOut } from "../../../services/SignOut";
|
||||||
|
|
||||||
|
export interface Props {}
|
||||||
|
|
||||||
const SignOut = function (props: Props) {
|
const SignOut = function (props: Props) {
|
||||||
const mounted = useIsMountedRef();
|
const mounted = useIsMountedRef();
|
||||||
|
@ -33,29 +35,29 @@ const SignOut = function (props: Props) {
|
||||||
}
|
}
|
||||||
}, [createErrorNotification, setTimedOut, mounted]);
|
}, [createErrorNotification, setTimedOut, mounted]);
|
||||||
|
|
||||||
useEffect(() => { doSignOut() }, [doSignOut]);
|
useEffect(() => {
|
||||||
|
doSignOut();
|
||||||
|
}, [doSignOut]);
|
||||||
|
|
||||||
if (timedOut) {
|
if (timedOut) {
|
||||||
if (redirectionURL) {
|
if (redirectionURL) {
|
||||||
window.location.href = redirectionURL;
|
window.location.href = redirectionURL;
|
||||||
} else {
|
} else {
|
||||||
return <Redirect to={FirstFactorRoute} />
|
return <Redirect to={FirstFactorRoute} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Sign out">
|
<LoginLayout title="Sign out">
|
||||||
<Typography className={style.typo} >
|
<Typography className={style.typo}>You're being signed out and redirected...</Typography>
|
||||||
You're being signed out and redirected...
|
|
||||||
</Typography>
|
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SignOut
|
export default SignOut;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
typo: {
|
typo: {
|
||||||
padding: theme.spacing(),
|
padding: theme.spacing(),
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import LoginLayout from "../../layouts/LoginLayout";
|
|
||||||
import { Grid, Button, makeStyles } from "@material-ui/core";
|
import { Grid, Button, makeStyles } from "@material-ui/core";
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
|
||||||
import { useHistory } from "react-router";
|
import { useHistory } from "react-router";
|
||||||
import { initiateResetPasswordProcess } from "../../services/ResetPassword";
|
|
||||||
import { FirstFactorRoute } from "../../Routes";
|
|
||||||
import FixedTextField from "../../components/FixedTextField";
|
import FixedTextField from "../../components/FixedTextField";
|
||||||
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
|
import LoginLayout from "../../layouts/LoginLayout";
|
||||||
|
import { FirstFactorRoute } from "../../Routes";
|
||||||
|
import { initiateResetPasswordProcess } from "../../services/ResetPassword";
|
||||||
|
|
||||||
const ResetPasswordStep1 = function () {
|
const ResetPasswordStep1 = function () {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
@ -26,15 +28,15 @@ const ResetPasswordStep1 = function () {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
createErrorNotification("There was an issue initiating the password reset process.");
|
createErrorNotification("There was an issue initiating the password reset process.");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleResetClick = () => {
|
const handleResetClick = () => {
|
||||||
doInitiateResetPasswordProcess();
|
doInitiateResetPasswordProcess();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleCancelClick = () => {
|
const handleCancelClick = () => {
|
||||||
history.push(FirstFactorRoute);
|
history.push(FirstFactorRoute);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Reset password" id="reset-password-step1-stage">
|
<LoginLayout title="Reset password" id="reset-password-step1-stage">
|
||||||
|
@ -49,19 +51,17 @@ const ResetPasswordStep1 = function () {
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === "Enter") {
|
||||||
doInitiateResetPasswordProcess();
|
doInitiateResetPasswordProcess();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Button
|
<Button id="reset-button" variant="contained" color="primary" fullWidth onClick={handleResetClick}>
|
||||||
id="reset-button"
|
Reset
|
||||||
variant="contained"
|
</Button>
|
||||||
color="primary"
|
|
||||||
fullWidth
|
|
||||||
onClick={handleResetClick}>Reset</Button>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -69,18 +69,21 @@ const ResetPasswordStep1 = function () {
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={handleCancelClick}>Cancel</Button>
|
onClick={handleCancelClick}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ResetPasswordStep1
|
export default ResetPasswordStep1;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import React, { useState, useCallback, useEffect } from "react";
|
import React, { useState, useCallback, useEffect } from "react";
|
||||||
import LoginLayout from "../../layouts/LoginLayout";
|
|
||||||
import classnames from "classnames";
|
|
||||||
import { Grid, Button, makeStyles } from "@material-ui/core";
|
import { Grid, Button, makeStyles } from "@material-ui/core";
|
||||||
import { useNotifications } from "../../hooks/NotificationsContext";
|
import classnames from "classnames";
|
||||||
import { useHistory, useLocation } from "react-router";
|
import { useHistory, useLocation } from "react-router";
|
||||||
import { completeResetPasswordProcess, resetPassword } from "../../services/ResetPassword";
|
|
||||||
import { FirstFactorRoute } from "../../Routes";
|
|
||||||
import { extractIdentityToken } from "../../utils/IdentityToken";
|
|
||||||
import FixedTextField from "../../components/FixedTextField";
|
import FixedTextField from "../../components/FixedTextField";
|
||||||
|
import { useNotifications } from "../../hooks/NotificationsContext";
|
||||||
|
import LoginLayout from "../../layouts/LoginLayout";
|
||||||
|
import { FirstFactorRoute } from "../../Routes";
|
||||||
|
import { completeResetPasswordProcess, resetPassword } from "../../services/ResetPassword";
|
||||||
|
import { extractIdentityToken } from "../../utils/IdentityToken";
|
||||||
|
|
||||||
const ResetPasswordStep2 = function () {
|
const ResetPasswordStep2 = function () {
|
||||||
const style = useStyles();
|
const style = useStyles();
|
||||||
|
@ -36,8 +38,9 @@ const ResetPasswordStep2 = function () {
|
||||||
setFormDisabled(false);
|
setFormDisabled(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createErrorNotification("There was an issue completing the process. " +
|
createErrorNotification(
|
||||||
"The verification token might have expired.");
|
"There was an issue completing the process. The verification token might have expired.",
|
||||||
|
);
|
||||||
setFormDisabled(true);
|
setFormDisabled(true);
|
||||||
}
|
}
|
||||||
}, [processToken, createErrorNotification]);
|
}, [processToken, createErrorNotification]);
|
||||||
|
@ -54,11 +57,11 @@ const ResetPasswordStep2 = function () {
|
||||||
if (password2 === "") {
|
if (password2 === "") {
|
||||||
setErrorPassword2(true);
|
setErrorPassword2(true);
|
||||||
}
|
}
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (password1 !== password2) {
|
if (password1 !== password2) {
|
||||||
setErrorPassword1(true);
|
setErrorPassword1(true);
|
||||||
setErrorPassword2(true)
|
setErrorPassword2(true);
|
||||||
createErrorNotification("Passwords do not match.");
|
createErrorNotification("Passwords do not match.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -76,13 +79,11 @@ const ResetPasswordStep2 = function () {
|
||||||
createErrorNotification("There was an issue resetting the password.");
|
createErrorNotification("There was an issue resetting the password.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleResetClick = () =>
|
const handleResetClick = () => doResetPassword();
|
||||||
doResetPassword();
|
|
||||||
|
|
||||||
const handleCancelClick = () =>
|
const handleCancelClick = () => history.push(FirstFactorRoute);
|
||||||
history.push(FirstFactorRoute);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoginLayout title="Enter new password" id="reset-password-step2-stage">
|
<LoginLayout title="Enter new password" id="reset-password-step2-stage">
|
||||||
|
@ -95,9 +96,10 @@ const ResetPasswordStep2 = function () {
|
||||||
type="password"
|
type="password"
|
||||||
value={password1}
|
value={password1}
|
||||||
disabled={formDisabled}
|
disabled={formDisabled}
|
||||||
onChange={e => setPassword1(e.target.value)}
|
onChange={(e) => setPassword1(e.target.value)}
|
||||||
error={errorPassword1}
|
error={errorPassword1}
|
||||||
className={classnames(style.fullWidth)} />
|
className={classnames(style.fullWidth)}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<FixedTextField
|
<FixedTextField
|
||||||
|
@ -107,15 +109,16 @@ const ResetPasswordStep2 = function () {
|
||||||
type="password"
|
type="password"
|
||||||
disabled={formDisabled}
|
disabled={formDisabled}
|
||||||
value={password2}
|
value={password2}
|
||||||
onChange={e => setPassword2(e.target.value)}
|
onChange={(e) => setPassword2(e.target.value)}
|
||||||
error={errorPassword2}
|
error={errorPassword2}
|
||||||
onKeyPress={(ev) => {
|
onKeyPress={(ev) => {
|
||||||
if (ev.key === 'Enter') {
|
if (ev.key === "Enter") {
|
||||||
doResetPassword();
|
doResetPassword();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={classnames(style.fullWidth)} />
|
className={classnames(style.fullWidth)}
|
||||||
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -125,7 +128,10 @@ const ResetPasswordStep2 = function () {
|
||||||
name="password1"
|
name="password1"
|
||||||
disabled={formDisabled}
|
disabled={formDisabled}
|
||||||
onClick={handleResetClick}
|
onClick={handleResetClick}
|
||||||
className={style.fullWidth}>Reset</Button>
|
className={style.fullWidth}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -134,21 +140,24 @@ const ResetPasswordStep2 = function () {
|
||||||
color="primary"
|
color="primary"
|
||||||
name="password2"
|
name="password2"
|
||||||
onClick={handleCancelClick}
|
onClick={handleCancelClick}
|
||||||
className={style.fullWidth}>Cancel</Button>
|
className={style.fullWidth}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</LoginLayout>
|
</LoginLayout>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ResetPasswordStep2
|
export default ResetPasswordStep2;
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
marginBottom: theme.spacing(2),
|
marginBottom: theme.spacing(2),
|
||||||
},
|
},
|
||||||
fullWidth: {
|
fullWidth: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}
|
},
|
||||||
}))
|
}));
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
/// <reference path="react-otp-input/index.d.ts" />
|
/// <reference path="react-otp-input/index.d.ts" />
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
|
declare module "react-otp-input";
|
||||||
declare module 'react-otp-input';
|
|
||||||
|
|
|
@ -4780,6 +4780,11 @@ escodegen@^1.14.1:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
source-map "~0.6.1"
|
source-map "~0.6.1"
|
||||||
|
|
||||||
|
eslint-config-prettier@^7.1.0:
|
||||||
|
version "7.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.1.0.tgz#5402eb559aa94b894effd6bddfa0b1ca051c858f"
|
||||||
|
integrity sha512-9sm5/PxaFG7qNJvJzTROMM1Bk1ozXVTKI0buKOyb0Bsr1hrwi0H/TzxF/COtf1uxikIK8SwhX7K6zg78jAzbeA==
|
||||||
|
|
||||||
eslint-config-react-app@^6.0.0:
|
eslint-config-react-app@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e"
|
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e"
|
||||||
|
@ -4787,6 +4792,11 @@ eslint-config-react-app@^6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
confusing-browser-globals "^1.0.10"
|
confusing-browser-globals "^1.0.10"
|
||||||
|
|
||||||
|
eslint-formatter-rdjson@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-formatter-rdjson/-/eslint-formatter-rdjson-1.0.3.tgz#ab1bb2174aefdc802befaaf7f385d44b97f6d9a4"
|
||||||
|
integrity sha512-YqqNcP+xiLEWXz1GdAGKhnIRgtjOeiTPoG/hSIx/SzOt4n+fwcxRuiJE3DKpfSIJjodXS617qfRa6cZx/kIkbw==
|
||||||
|
|
||||||
eslint-import-resolver-node@^0.3.4:
|
eslint-import-resolver-node@^0.3.4:
|
||||||
version "0.3.4"
|
version "0.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
|
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
|
||||||
|
@ -4795,6 +4805,17 @@ eslint-import-resolver-node@^0.3.4:
|
||||||
debug "^2.6.9"
|
debug "^2.6.9"
|
||||||
resolve "^1.13.1"
|
resolve "^1.13.1"
|
||||||
|
|
||||||
|
eslint-import-resolver-typescript@^2.3.0:
|
||||||
|
version "2.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.3.0.tgz#0870988098bc6c6419c87705e6b42bee89425445"
|
||||||
|
integrity sha512-MHSXvmj5e0SGOOBhBbt7C+fWj1bJbtSYFAD85Xeg8nvUtuooTod2HQb8bfhE9f5QyyNxEfgzqOYFCvmdDIcCuw==
|
||||||
|
dependencies:
|
||||||
|
debug "^4.1.1"
|
||||||
|
glob "^7.1.6"
|
||||||
|
is-glob "^4.0.1"
|
||||||
|
resolve "^1.17.0"
|
||||||
|
tsconfig-paths "^3.9.0"
|
||||||
|
|
||||||
eslint-module-utils@^2.6.0:
|
eslint-module-utils@^2.6.0:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
|
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6"
|
||||||
|
@ -4854,6 +4875,13 @@ eslint-plugin-jsx-a11y@^6.3.1:
|
||||||
jsx-ast-utils "^3.1.0"
|
jsx-ast-utils "^3.1.0"
|
||||||
language-tags "^1.0.5"
|
language-tags "^1.0.5"
|
||||||
|
|
||||||
|
eslint-plugin-prettier@^3.3.0:
|
||||||
|
version "3.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.0.tgz#61e295349a65688ffac0b7808ef0a8244bdd8d40"
|
||||||
|
integrity sha512-tMTwO8iUWlSRZIwS9k7/E4vrTsfvsrcM5p1eftyuqWH25nKsz/o6/54I7jwQ/3zobISyC7wMy9ZsFwgTxOcOpQ==
|
||||||
|
dependencies:
|
||||||
|
prettier-linter-helpers "^1.0.0"
|
||||||
|
|
||||||
eslint-plugin-react-hooks@^4.2.0:
|
eslint-plugin-react-hooks@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
|
||||||
|
@ -5240,6 +5268,11 @@ fast-deep-equal@^3.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4"
|
||||||
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
|
integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==
|
||||||
|
|
||||||
|
fast-diff@^1.1.2:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||||
|
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||||
|
|
||||||
fast-glob@^3.1.1:
|
fast-glob@^3.1.1:
|
||||||
version "3.2.4"
|
version "3.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3"
|
||||||
|
@ -9353,6 +9386,18 @@ prepend-http@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||||
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
|
||||||
|
|
||||||
|
prettier-linter-helpers@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||||
|
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||||
|
dependencies:
|
||||||
|
fast-diff "^1.1.2"
|
||||||
|
|
||||||
|
prettier@^2.2.1:
|
||||||
|
version "2.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
|
||||||
|
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
|
||||||
|
|
||||||
pretty-bytes@^5.3.0:
|
pretty-bytes@^5.3.0:
|
||||||
version "5.4.1"
|
version "5.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b"
|
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b"
|
||||||
|
|
Loading…
Reference in New Issue