1
0
mirror of https://github.com/0rangebananaspy/authelia.git synced 2024-09-14 22:47:21 +07:00

[CI] Add linting option for frontend and enforce styling ()

We now extend the default Eslint configuration and enforce styling with prettier for all of our frontend code.
This commit is contained in:
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
.buildkite/hooks
.reviewdog.yml
web
.eslintrc.js.prettierrc.jspackage.json
src
types/react-otp-input
yarn.lock

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 Normal file
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
}
}
]
}
};

9
web/.prettierrc.js Normal file
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;
@ -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;
@ -26,7 +28,7 @@ const AppStoreBadges = function (props: Props) {
<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"; import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Props {} 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"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Props {} 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 style from "./PushNotificationIcon.module.css";
import { useIntermittentClass } from "../hooks/IntermittentClass"; import { useIntermittentClass } from "../hooks/IntermittentClass";
import style from "./PushNotificationIcon.module.css";
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,12 +1,14 @@
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"; 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 {} export interface Props {}
@ -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"