[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
Amir Zarrinkafsh 2021-01-02 21:58:24 +11:00 committed by GitHub
parent a5ea31e482
commit 689fd7cb95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1197 additions and 916 deletions

View File

@ -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"

View File

@ -6,3 +6,7 @@ runner:
- '%E%f:%l: %m' - '%E%f:%l: %m'
- '%C%.%#' - '%C%.%#'
level: error level: error
eslint:
cmd: cd web && eslint -f rdjson '*/**/*.{js,ts,tsx}'
format: rdjson
level: error

46
web/.eslintrc.js 100644
View File

@ -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
}
}
]
}
};

View File

@ -0,0 +1,9 @@
module.exports = {
printWidth: 120,
tabWidth: 4,
bracketSpacing: true,
jsxBracketSameLine: false,
semi: true,
singleQuote: false,
trailingComma: "all"
};

View File

@ -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"
} }
} }

View File

@ -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";
import App from "./App";
it("renders without crashing", () => {
shallow(<App />); shallow(<App />);
}); });

View File

@ -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;

View File

@ -1,4 +1,3 @@
export const FirstFactorRoute = "/"; export const FirstFactorRoute = "/";
export const AuthenticatedRoute = "/authenticated"; export const AuthenticatedRoute = "/authenticated";

View File

@ -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);
}); });

View File

@ -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;

View File

@ -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;

View File

@ -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",
}, },
})) }));

View File

@ -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 />);
}); });

View File

@ -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;

View File

@ -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} />);
}); });

View File

@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import style from "./FingerTouchIcon.module.css";
import classnames from "classnames"; import classnames from "classnames";
import style from "./FingerTouchIcon.module.css";
export interface Props { export interface Props {
size: number; size: number;
@ -10,12 +12,21 @@ export interface Props {
} }
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;

View File

@ -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 />);
}); });

View File

@ -1,6 +1,7 @@
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
@ -11,24 +12,26 @@ import { makeStyles } from "@material-ui/core";
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),
} },
})); }));

View File

@ -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 />);
}); });

View File

@ -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;

View File

@ -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} />);
}); });

View File

@ -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;

View File

@ -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={() => {}} />);
}); });

View File

@ -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;

View File

@ -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} />);
}); });

View File

@ -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;

View File

@ -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} />);
}); });

View File

@ -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 <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 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"/> 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 />
<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 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"/> 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 />
<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 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"/> 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 />
<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 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"/> 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> </svg>
) );
} };
export default PushNotificationIcon export default PushNotificationIcon;

View File

@ -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 />);
}); });

View File

@ -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;

View File

@ -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} />);
}); });

View File

@ -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;

View File

@ -1,4 +1,3 @@
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",

View File

@ -1,5 +1,5 @@
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, []);

View File

@ -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);

View File

@ -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;
} }

View File

@ -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,
} };
} }

View File

@ -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;
} }

View File

@ -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,
]
} }

View File

@ -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,
]
} }

View File

@ -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.

View File

@ -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 ? (
<Grid item xs={12}>
<Link <Link
href="https://github.com/authelia/authelia" href="https://github.com/authelia/authelia"
target="_blank" target="_blank"
className={style.poweredBy}> className={style.poweredBy}
>
Powered by Authelia Powered by Authelia
</Link> </Link>
</Grid> </Grid>
: null ) : 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],
} },
})) }));

View File

@ -1,6 +1,5 @@
export enum SecondFactorMethod { export enum SecondFactorMethod {
TOTP = 1, TOTP = 1,
U2F = 2, U2F = 2,
MobilePush = 3 MobilePush = 3,
} }

View File

@ -11,13 +11,11 @@
// 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 = {
@ -26,12 +24,9 @@ type Config = {
}; };
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,
window.location.href
);
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // 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 // from what our page is served on. This might happen if a CDN is used to
@ -39,7 +34,7 @@ export function register(config?: Config) {
return; 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) {
@ -50,8 +45,8 @@ export function register(config?: Config) {
// 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 { } else {
@ -65,21 +60,21 @@ export function register(config?: 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
@ -90,7 +85,7 @@ function registerValidSW(swUrl: string, config?: Config) {
// 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) {
@ -101,23 +96,20 @@ function registerValidSW(swUrl: string, config?: Config) {
}; };
}; };
}) })
.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 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload();
}); });
@ -128,15 +120,13 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
} }
}) })
.catch(() => { .catch(() => {
console.log( console.log("No internet connection found. App is running in offline mode.");
'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();
}); });
} }

View File

@ -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>>) {

View File

@ -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) {

View File

@ -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[];

View File

@ -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);
} }

View File

@ -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 {

View File

@ -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 {

View File

@ -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,26 +19,25 @@ 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) {

View File

@ -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 });
} }

View File

@ -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() {

View File

@ -1,2 +1 @@
export type SignInResponse = { redirect: string } | undefined; export type SignInResponse = { redirect: string } | undefined;

View File

@ -1,5 +1,5 @@
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);

View File

@ -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,7 +9,7 @@ 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> {

View File

@ -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);
} }

View File

@ -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");

View File

@ -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() + "/";
} }

View File

@ -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;
} }

View File

@ -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,7 +54,9 @@ 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
@ -65,9 +69,9 @@ const RegisterOneTimePassword = function () {
> >
<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">
@ -79,28 +83,28 @@ const RegisterOneTimePassword = function () {
targetBlank targetBlank
className={style.googleAuthenticatorBadges} className={style.googleAuthenticatorBadges}
googlePlayLink={GoogleAuthenticator.googlePlay} googlePlayLink={GoogleAuthenticator.googlePlay}
appleStoreLink={GoogleAuthenticator.appleStore} /> appleStoreLink={GoogleAuthenticator.appleStore}
/>
</div> </div>
<div className={style.qrcodeContainer}> <div className={style.qrcodeContainer}>
<Link href={secretURL}> <Link href={secretURL}>
<QRCode <QRCode value={secretURL} className={classnames(qrcodeFuzzyStyle, style.qrcode)} size={256} />
value={secretURL}
className={classnames(qrcodeFuzzyStyle, style.qrcode)}
size={256} />
{!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null} {!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null}
{hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null} {hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null}
</Link> </Link>
</div> </div>
<div> <div>
{secretURL !== "empty" {secretURL !== "empty" ? (
? <TextField <TextField
id="secret-url" id="secret-url"
label="Secret" label="Secret"
className={style.secret} className={style.secret}
value={secretURL} value={secretURL}
InputProps={{ InputProps={{
readOnly: true 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>
@ -109,17 +113,18 @@ const RegisterOneTimePassword = function () {
color="primary" color="primary"
className={style.doneButton} className={style.doneButton}
onClick={handleDoneClick} onClick={handleDoneClick}
disabled={isLoading}> disabled={isLoading}
>
Done Done
</Button> </Button>
</div> </div>
</LoginLayout> </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",
} },
})) }));

View File

@ -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),
} },
})) }));

View File

@ -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;

View File

@ -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%",
} },
})) }));

View File

@ -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),
} },
})) }));

View File

@ -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
item
xs={12}
className={
props.rememberMe
? classnames(style.leftAlign, style.actionRow) ? classnames(style.leftAlign, style.actionRow)
: classnames(style.leftAlign, style.flexEnd, style.actionRow)}> : classnames(style.leftAlign, style.flexEnd, style.actionRow)
{props.rememberMe ? }
>
{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(),

View File

@ -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 ? (
<SecondFactorForm
authenticationLevel={state.authentication_level} authenticationLevel={state.authentication_level}
userInfo={userInfo} userInfo={userInfo}
configuration={configuration} configuration={configuration}
onMethodChanged={() => fetchUserInfo()} onMethodChanged={() => fetchUserInfo()}
onAuthenticationSuccess={handleAuthSuccess} /> : null} 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>
) );
} }

View File

@ -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>
<div className={style.context}> );
{props.context} };
</div>
</div>
)
}
export default IconWithContext export default IconWithContext;

View File

@ -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" id="register-link" onClick={props.onRegisterClick}>
? <Link component="button"
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>
) );
} }

View File

@ -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>
) );
} }

View File

@ -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>
) );
} }

View File

@ -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;

View File

@ -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",
} },
})) }));

View File

@ -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),
}, },
})) }));

View File

@ -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 = (
<IconWithContext
icon={<FingerTouchIcon size={64} animated strong />} icon={<FingerTouchIcon size={64} animated strong />}
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />} context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
className={state === State.WaitTouch ? undefined : "hidden"} /> className={state === State.WaitTouch ? undefined : "hidden"}
/>
);
const failure = <IconWithContext const failure = (
<IconWithContext
icon={<FailureIcon />} icon={<FailureIcon />}
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>} context={
className={state === State.Failure ? undefined : "hidden"} /> <Button color="secondary" onClick={props.onRetryClick}>
Retry
</Button>
}
className={state === State.Failure ? undefined : "hidden"}
/>
);
return ( return (
<Fragment> <Fragment>
{touch} {touch}
{failure} {failure}
</Fragment> </Fragment>
) );
} }

View File

@ -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(),
} },
})) }));

View File

@ -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),
}, },
})) }));

View File

@ -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%",
} },
})) }));

View File

@ -1,2 +1 @@
declare module "react-otp-input";
declare module 'react-otp-input';

View File

@ -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"