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
App.test.tsxApp.tsxRoutes.ts
components
constants.ts
hooks
index.tsx
layouts
models
react-app-env.d.tsserviceWorker.ts
services
setupTests.js
utils
views
types
index.d.ts
react-otp-input
yarn.lock

View File

@ -2,6 +2,10 @@
set +u
if [[ $BUILDKITE_LABEL == ":service_dog: Linting" ]]; then
cd web && yarn install && cd ../
fi
if [[ $BUILDKITE_LABEL =~ ":selenium:" ]]; then
DEFAULT_ARCH=coverage
echo "--- :docker: Extract, load and tag build container"

View File

@ -5,4 +5,8 @@ runner:
- '%E%f:%l:%c: %m'
- '%E%f:%l: %m'
- '%C%.%#'
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",
"enzyme": "^3.11.0",
"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",
"query-string": "^6.13.8",
"react": "^16.14.0",
@ -43,6 +47,7 @@
"scripts": {
"start": "craco start",
"build": "react-scripts build",
"lint": "eslint '*/**/*.{js,ts,tsx}' --fix",
"coverage": "craco build",
"test": "react-scripts test --coverage --no-cache",
"report": "nyc report -r clover -r json -r lcov -r text",
@ -62,5 +67,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint-formatter-rdjson": "^1.0.3"
}
}

View File

@ -1,7 +1,9 @@
import React from 'react';
import { shallow } from "enzyme";
import App from './App';
import React from "react";
it('renders without crashing', () => {
shallow(<App />);
import { shallow } from "enzyme";
import App from "./App";
it("renders without crashing", () => {
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 {
BrowserRouter as Router, Route, Switch, Redirect
} from "react-router-dom";
import ResetPasswordStep1 from './views/ResetPassword/ResetPasswordStep1';
import ResetPasswordStep2 from './views/ResetPassword/ResetPasswordStep2';
import RegisterSecurityKey from './views/DeviceRegistration/RegisterSecurityKey';
import RegisterOneTimePassword from './views/DeviceRegistration/RegisterOneTimePassword';
import {
FirstFactorRoute, ResetPasswordStep2Route,
ResetPasswordStep1Route, RegisterSecurityKeyRoute,
FirstFactorRoute,
ResetPasswordStep2Route,
ResetPasswordStep1Route,
RegisterSecurityKeyRoute,
RegisterOneTimePasswordRoute,
LogoutRoute,
} from "./Routes";
import LoginPortal from './views/LoginPortal/LoginPortal';
import NotificationsContext from './hooks/NotificationsContext';
import { Notification } from './models/Notifications';
import NotificationBar from './components/NotificationBar';
import SignOut from './views/LoginPortal/SignOut/SignOut';
import { getRememberMe, getResetPassword } from './utils/Configuration';
import '@fortawesome/fontawesome-svg-core/styles.css'
import { config as faConfig } from '@fortawesome/fontawesome-svg-core';
import { getBasePath } from './utils/BasePath';
import { getBasePath } from "./utils/BasePath";
import { getRememberMe, getResetPassword } from "./utils/Configuration";
import RegisterOneTimePassword from "./views/DeviceRegistration/RegisterOneTimePassword";
import RegisterSecurityKey from "./views/DeviceRegistration/RegisterSecurityKey";
import LoginPortal from "./views/LoginPortal/LoginPortal";
import SignOut from "./views/LoginPortal/SignOut/SignOut";
import ResetPasswordStep1 from "./views/ResetPassword/ResetPasswordStep1";
import ResetPasswordStep2 from "./views/ResetPassword/ResetPasswordStep2";
import "@fortawesome/fontawesome-svg-core/styles.css";
faConfig.autoAddCss = false;
@ -28,7 +31,7 @@ const App: React.FC = () => {
const [notification, setNotification] = useState(null as Notification | null);
return (
<NotificationsContext.Provider value={{ notification, setNotification }} >
<NotificationsContext.Provider value={{ notification, setNotification }}>
<Router basename={getBasePath()}>
<NotificationBar onClose={() => setNotification(null)} />
<Switch>
@ -48,9 +51,7 @@ const App: React.FC = () => {
<SignOut />
</Route>
<Route path={FirstFactorRoute}>
<LoginPortal
rememberMe={getRememberMe()}
resetPassword={getResetPassword()} />
<LoginPortal rememberMe={getRememberMe()} resetPassword={getResetPassword()} />
</Route>
<Route path="/">
<Redirect to={FirstFactorRoute} />
@ -59,6 +60,6 @@ const App: React.FC = () => {
</Router>
</NotificationsContext.Provider>
);
}
};
export default App;

View File

@ -1,4 +1,3 @@
export const FirstFactorRoute = "/";
export const AuthenticatedRoute = "/authenticated";
@ -11,4 +10,4 @@ export const ResetPasswordStep1Route = "/reset-password/step1";
export const ResetPasswordStep2Route = "/reset-password/step2";
export const RegisterSecurityKeyRoute = "/security-key/register";
export const RegisterOneTimePasswordRoute = "/one-time-password/register";
export const LogoutRoute = "/logout";
export const LogoutRoute = "/logout";

View File

@ -1,12 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import React from "react";
import ReactDOM from "react-dom";
import AppStoreBadges from "./AppStoreBadges";
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<AppStoreBadges
iconSize={32}
appleStoreLink="http://apple"
googlePlayLink="http://google" />, div);
it("renders without crashing", () => {
const div = document.createElement("div");
ReactDOM.render(<AppStoreBadges iconSize={32} appleStoreLink="http://apple" googlePlayLink="http://google" />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -1,8 +1,10 @@
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 AppleStore from "../assets/images/applestore-badge.svg";
import GooglePlay from "../assets/images/googleplay-badge.svg";
export interface Props {
iconSize: number;
googlePlayLink: string;
@ -25,8 +27,8 @@ const AppStoreBadges = function (props: Props) {
<Link href={props.appleStoreLink} target={target}>
<img src={AppleStore} alt="apple store" style={{ width }} />
</Link>
</div >
)
}
</div>
);
};
export default AppStoreBadges
export default AppStoreBadges;

View File

@ -1,23 +1,25 @@
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';
import React from "react";
it('renders without crashing', () => {
const div = document.createElement('div');
import { SnackbarContent } from "@material-ui/core";
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.unmountComponentAtNode(div);
});
it('should contain the message', () => {
it("should contain the message", () => {
const el = mount(<ColoredSnackbarContent level="success" message="this is a success" />);
expect(el.text()).to.contain("this is a success");
});
/* 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" />);
expect(el.find(SnackbarContent).props().className!.indexOf("success") > -1).to.be.true;
@ -30,4 +32,4 @@ it('should have correct color', () => {
el = shallow(<ColoredSnackbarContent level="warning" message="this is an warning" />);
expect(el.find(SnackbarContent).props().className!.indexOf("warning") > -1).to.be.true;
});
/* eslint-enable @typescript-eslint/no-unused-expressions */
/* eslint-enable @typescript-eslint/no-unused-expressions */

View File

@ -1,13 +1,13 @@
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 { amber, green } from '@material-ui/core/colors';
import classnames from "classnames";
import { amber, green } from "@material-ui/core/colors";
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 = {
success: CheckCircleIcon,
@ -39,13 +39,14 @@ const ColoredSnackbarContent = function (props: Props) {
{message}
</span>
}
{...others} />
)
}
{...others}
/>
);
};
export default ColoredSnackbarContent
export default ColoredSnackbarContent;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
success: {
backgroundColor: green[600],
},
@ -66,7 +67,7 @@ const useStyles = makeStyles(theme => ({
marginRight: theme.spacing(1),
},
message: {
display: 'flex',
alignItems: 'center',
display: "flex",
alignItems: "center",
},
}))
}));

View File

@ -1,7 +1,9 @@
import React from 'react';
import React from "react";
import { mount } from "enzyme";
import FailureIcon from "./FailureIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<FailureIcon />);
});
});

View File

@ -1,13 +1,12 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
export interface Props { }
import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Props {}
const FailureIcon = function (props: Props) {
return (
<FontAwesomeIcon icon={faTimesCircle} size="4x" color="red" className="failure-icon" />
)
}
return <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 FingerTouchIcon from "./FingerTouchIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<FingerTouchIcon size={32} />);
});
});

View File

@ -1,21 +1,32 @@
import React from "react";
import style from "./FingerTouchIcon.module.css";
import classnames from "classnames";
export interface Props {
size: number;
import style from "./FingerTouchIcon.module.css";
animated?: boolean;
strong?: boolean;
export interface Props {
size: number;
animated?: boolean;
strong?: boolean;
}
const FingerTouchIcon = function (props: Props) {
const shakingClass = (props.animated) ? style.shaking : undefined;
const strong = (props.strong) ? style.strong : undefined;
const shakingClass = props.animated ? style.shaking : undefined;
const strong = props.strong ? style.strong : undefined;
return (
<svg 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
<svg
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-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
@ -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
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
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
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
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
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>
)
}
);
};
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 FixedTextField from "./FixedTextField";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<FixedTextField />);
});
});

View File

@ -1,34 +1,37 @@
import React from "react";
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
import { makeStyles } from "@material-ui/core";
import TextField, { TextFieldProps } from "@material-ui/core/TextField";
/**
* This component fixes outlined TextField
* https://github.com/mui-org/material-ui/issues/14530#issuecomment-463576879
*
*
* @param props the TextField props
*/
const FixedTextField = function (props: TextFieldProps) {
const style = useStyles();
return (
<TextField {...props}
<TextField
{...props}
InputLabelProps={{
classes: {
root: style.label
}
root: style.label,
},
}}
inputProps={{autoCapitalize: props.autoCapitalize}}>
inputProps={{ autoCapitalize: props.autoCapitalize }}
>
{props.children}
</TextField>
);
}
};
export default FixedTextField
export default FixedTextField;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
label: {
backgroundColor: theme.palette.background.default,
paddingLeft: 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 InformationIcon from "./InformationIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<InformationIcon />);
});
});

View File

@ -1,13 +1,12 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
export interface Props { }
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export interface Props {}
const InformationIcon = function (props: Props) {
return (
<FontAwesomeIcon icon={faInfoCircle} size="4x" color="#5858ff" className="information-icon" />
)
}
return <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 LinearProgressBar from "./LinearProgressBar";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<LinearProgressBar value={40} />);
});
});

View File

@ -1,4 +1,5 @@
import React from "react";
import { makeStyles, LinearProgress } from "@material-ui/core";
import { CSSProperties } from "@material-ui/styles";
@ -10,13 +11,13 @@ export interface Props {
}
const LinearProgressBar = function (props: Props) {
const style = makeStyles(theme => ({
const style = makeStyles((theme) => ({
progressRoot: {
height: props.height ? props.height : theme.spacing(),
},
transition: {
transition: "transform .2s linear",
}
},
}))();
return (
<LinearProgress
@ -24,11 +25,12 @@ const LinearProgressBar = function (props: Props) {
variant="determinate"
classes={{
root: style.progressRoot,
bar1Determinate: style.transition
bar1Determinate: style.transition,
}}
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 NotificationBar from "./NotificationBar";
it('renders without crashing', () => {
mount(<NotificationBar onClose={() => { }} />);
});
it("renders without crashing", () => {
mount(<NotificationBar onClose={() => {}} />);
});

View File

@ -1,8 +1,10 @@
import React, { useState, useEffect } from "react";
import { Snackbar } from "@material-ui/core";
import ColoredSnackbarContent from "./ColoredSnackbarContent";
import { useNotifications } from "../hooks/NotificationsContext";
import { Notification } from "../models/Notifications";
import ColoredSnackbarContent from "./ColoredSnackbarContent";
export interface Props {
onClose: () => void;
@ -26,13 +28,15 @@ const NotificationBar = function (props: Props) {
anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={tmpNotification ? tmpNotification.timeout * 1000 : 10000}
onClose={props.onClose}
onExited={() => setTmpNotification(null)}>
onExited={() => setTmpNotification(null)}
>
<ColoredSnackbarContent
className="notification"
level={tmpNotification ? tmpNotification.level : "info"}
message={tmpNotification ? tmpNotification.message : ""} />
message={tmpNotification ? tmpNotification.message : ""}
/>
</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 PieChartIcon from "./PieChartIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
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">
<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="5" cx="13" cy="13" fill="none"
<circle
r="5"
cx="13"
cy="13"
fill="none"
stroke={color}
strokeWidth="10"
strokeDasharray={`${props.progress} ${maxProgress}`}
transform="rotate(-90) translate(-26)" />
transform="rotate(-90) translate(-26)"
/>
</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 PushNotificationIcon from "./PushNotificationIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<PushNotificationIcon width={32} height={32} />);
});
});

View File

@ -1,6 +1,7 @@
import React from "react";
import { useIntermittentClass } from "../hooks/IntermittentClass";
import style from "./PushNotificationIcon.module.css";
import {useIntermittentClass} from "../hooks/IntermittentClass";
export interface Props {
width: number;
@ -13,34 +14,59 @@ const PushNotificationIcon = function (props: Props) {
const idleMilliseconds = 2500;
const wiggleMilliseconds = 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 (
<svg x="0px" y="0px" viewBox="0 0 60 60" width={props.width} height={props.height} className={wiggleClass}>
<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
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 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"/>
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
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>
<path d="M56.612,4.569c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c3.736,3.736,3.736,9.815,0,13.552
c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
C61.128,16.434,61.128,9.085,56.612,4.569z"/>
<path d="M52.401,6.845c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c1.237,1.237,1.918,2.885,1.918,4.639
s-0.681,3.401-1.918,4.638c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
c1.615-1.614,2.504-3.764,2.504-6.052S54.017,8.459,52.401,6.845z"/>
<path d="M4.802,5.983c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0c-4.516,4.516-4.516,11.864,0,16.38
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
C1.065,15.799,1.065,9.72,4.802,5.983z"/>
<path d="M9.013,6.569c-0.391-0.391-1.023-0.391-1.414,0c-1.615,1.614-2.504,3.764-2.504,6.052s0.889,4.438,2.504,6.053
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
c-1.237-1.237-1.918-2.885-1.918-4.639S7.775,9.22,9.013,7.983C9.403,7.593,9.403,6.96,9.013,6.569z"/>
</svg>
)
}
export default PushNotificationIcon
<path
d="M56.612,4.569c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c3.736,3.736,3.736,9.815,0,13.552
c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
C61.128,16.434,61.128,9.085,56.612,4.569z"
/>
<path
d="M52.401,6.845c-0.391-0.391-1.023-0.391-1.414,0s-0.391,1.023,0,1.414c1.237,1.237,1.918,2.885,1.918,4.639
s-0.681,3.401-1.918,4.638c-0.391,0.391-0.391,1.023,0,1.414c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293
c1.615-1.614,2.504-3.764,2.504-6.052S54.017,8.459,52.401,6.845z"
/>
<path
d="M4.802,5.983c0.391-0.391,0.391-1.023,0-1.414s-1.023-0.391-1.414,0c-4.516,4.516-4.516,11.864,0,16.38
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
C1.065,15.799,1.065,9.72,4.802,5.983z"
/>
<path
d="M9.013,6.569c-0.391-0.391-1.023-0.391-1.414,0c-1.615,1.614-2.504,3.764-2.504,6.052s0.889,4.438,2.504,6.053
c0.195,0.195,0.451,0.293,0.707,0.293s0.512-0.098,0.707-0.293c0.391-0.391,0.391-1.023,0-1.414
c-1.237-1.237-1.918-2.885-1.918-4.639S7.775,9.22,9.013,7.983C9.403,7.593,9.403,6.96,9.013,6.569z"
/>
</svg>
);
};
export default PushNotificationIcon;

View File

@ -1,7 +1,9 @@
import React from 'react';
import React from "react";
import { mount } from "enzyme";
import SuccessIcon from "./SuccessIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<SuccessIcon />);
});
});

View File

@ -1,11 +1,10 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const SuccessIcon = function () {
return (
<FontAwesomeIcon icon={faCheckCircle} size="4x" color="green" className="success-icon" />
)
}
return <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 TimerIcon from "./TimerIcon";
it('renders without crashing', () => {
it("renders without crashing", () => {
mount(<TimerIcon width={32} height={32} />);
});
});

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect } from "react";
import PieChartIcon from "./PieChartIcon";
export interface Props {
@ -16,21 +17,26 @@ const TimerIcon = function (props: Props) {
useEffect(() => {
// 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);
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);
}, 100);
return () => clearInterval(interval);
}, [props]);
return (
<PieChartIcon width={props.width} height={props.height}
progress={timeProgress} maxProgress={radius}
backgroundColor={props.backgroundColor} color={props.color} />
)
}
<PieChartIcon
width={props.width}
height={props.height}
progress={timeProgress}
maxProgress={radius}
backgroundColor={props.backgroundColor}
color={props.color}
/>
);
};
export default TimerIcon
export default TimerIcon;

View File

@ -1,5 +1,4 @@
export const GoogleAuthenticator = {
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",
};
};

View File

@ -1,6 +1,6 @@
import { useRemoteCall } from "./RemoteCall";
import { getConfiguration } from "../services/Configuration";
import { useRemoteCall } from "./RemoteCall";
export function useConfiguration() {
return useRemoteCall(getConfiguration, []);
}
}

View File

@ -4,7 +4,8 @@ export function useIntermittentClass(
classname: string,
activeMilliseconds: number,
inactiveMillisecond: number,
startMillisecond?: number) {
startMillisecond?: number,
) {
const [currentClass, setCurrentClass] = useState("");
const [firstTime, setFirstTime] = useState(true);
@ -34,4 +35,4 @@ export function useIntermittentClass(
}, [currentClass, classname, activeMilliseconds, inactiveMillisecond, startMillisecond, firstTime]);
return currentClass;
}
}

View File

@ -4,7 +4,9 @@ export function useIsMountedRef() {
const isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => { isMountedRef.current = false };
return () => {
isMountedRef.current = false;
};
});
return isMountedRef;
}
}

View File

@ -1,33 +1,33 @@
import { Level } from "../components/ColoredSnackbarContent";
import { useCallback, createContext, useContext } from "react";
import { Level } from "../components/ColoredSnackbarContent";
import { Notification } from "../models/Notifications";
const defaultOptions = {
timeout: 5,
}
};
interface NotificationContextProps {
notification: Notification | null;
setNotification: (n: Notification | null) => void;
}
const NotificationsContext = createContext<NotificationContextProps>(
{ notification: null, setNotification: () => { } });
const NotificationsContext = createContext<NotificationContextProps>({ notification: null, setNotification: () => {} });
export default NotificationsContext;
export function useNotifications() {
let useNotificationsProps = useContext(NotificationsContext);
const notificationBuilder = (level: Level) => {
return (message: string, timeout?: number) => {
useNotificationsProps.setNotification({
level, message,
timeout: timeout ? timeout : defaultOptions.timeout
level,
message,
timeout: timeout ? timeout : defaultOptions.timeout,
});
}
}
};
};
const resetNotification = () => useNotificationsProps.setNotification(null);
/* eslint-disable react-hooks/exhaustive-deps */
@ -38,7 +38,6 @@ export function useNotifications() {
/* eslint-enable react-hooks/exhaustive-deps */
const isActive = useNotificationsProps.notification !== null;
return {
notification: useNotificationsProps.notification,
resetNotification,
@ -46,6 +45,6 @@ export function useNotifications() {
createSuccessNotification,
createWarnNotification,
createErrorNotification,
isActive
}
}
isActive,
};
}

View File

@ -4,7 +4,5 @@ import { useLocation } from "react-router";
export function useRedirectionURL() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
return (queryParams && "rd" in queryParams)
? queryParams["rd"] as string
: undefined;
}
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
}

View File

@ -1,9 +1,11 @@
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)
: [Ret | undefined, PromisifiedFunction<void>, boolean, Error | undefined] {
export function useRemoteCall<Ret>(
fn: PromisifiedFunction<Ret>,
deps: DependencyList,
): [Ret | undefined, PromisifiedFunction<void>, boolean, Error | undefined] {
const [data, setData] = useState(undefined as Ret | undefined);
const [inProgress, setInProgress] = useState(false);
const [error, setError] = useState(undefined as Error | undefined);
@ -22,10 +24,5 @@ export function useRemoteCall<Ret>(fn: PromisifiedFunction<Ret>, deps: Dependenc
}
}, [setInProgress, setError, fnCallback]);
return [
data,
triggerCallback,
inProgress,
error,
]
}
return [data, triggerCallback, inProgress, error];
}

View File

@ -3,4 +3,4 @@ import { useRemoteCall } from "./RemoteCall";
export function useAutheliaState() {
return useRemoteCall(getState, []);
}
}

View File

@ -21,8 +21,8 @@ export function useTimer(timeoutMs: number): [number, () => void, () => void] {
}
const intervalNode = setInterval(() => {
const elapsedMs = (startDate) ? new Date().getTime() - startDate.getTime() : 0;
let p = elapsedMs / timeoutMs * 100.0;
const elapsedMs = startDate ? new Date().getTime() - startDate.getTime() : 0;
let p = (elapsedMs / timeoutMs) * 100.0;
if (p >= 100) {
p = 100;
setStartDate(undefined);
@ -33,9 +33,5 @@ export function useTimer(timeoutMs: number): [number, () => void, () => void] {
return () => clearInterval(intervalNode);
}, [startDate, setPercent, setStartDate, timeoutMs]);
return [
percent,
trigger,
clear,
]
}
return [percent, trigger, clear];
}

View File

@ -3,4 +3,4 @@ import { useRemoteCall } from "./RemoteCall";
export function useUserPreferences() {
return useRemoteCall(getUserPreferences, []);
}
}

View File

@ -1,11 +1,13 @@
import './utils/AssetPath';
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import "./utils/AssetPath";
import React from "react";
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
// unregister() to register() below. Note this comes with some pitfalls.

View File

@ -1,8 +1,9 @@
import React, { ReactNode } from "react";
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 { ReactComponent as UserSvg } from "../assets/images/user.svg";
export interface Props {
id?: string;
@ -14,13 +15,7 @@ export interface Props {
const LoginLayout = function (props: Props) {
const style = useStyles();
return (
<Grid
id={props.id}
className={style.root}
container
spacing={0}
alignItems="center"
justify="center">
<Grid id={props.id} className={style.root} container spacing={0} alignItems="center" justify="center">
<Container maxWidth="xs" className={style.rootContainer}>
<Grid container>
<Grid item xs={12}>
@ -34,27 +29,28 @@ const LoginLayout = function (props: Props) {
<Grid item xs={12} className={style.body}>
{props.children}
</Grid>
{props.showBrand ? <Grid item xs={12}>
<Link
href="https://github.com/authelia/authelia"
target="_blank"
className={style.poweredBy}>
Powered by Authelia
</Link>
</Grid>
: null
}
{props.showBrand ? (
<Grid item xs={12}>
<Link
href="https://github.com/authelia/authelia"
target="_blank"
className={style.poweredBy}
>
Powered by Authelia
</Link>
</Grid>
) : null}
</Grid>
</Container>
</Grid>
);
}
};
export default LoginLayout
export default LoginLayout;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
minHeight: '90vh',
minHeight: "90vh",
textAlign: "center",
// marginTop: theme.spacing(10),
},
@ -71,5 +67,5 @@ const useStyles = makeStyles(theme => ({
poweredBy: {
fontSize: "0.7em",
color: grey[500],
}
}))
},
}));

View File

@ -4,4 +4,4 @@ export interface Configuration {
available_methods: Set<SecondFactorMethod>;
second_factor_enabled: boolean;
totp_period: number;
}
}

View File

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

View File

@ -4,4 +4,4 @@ export interface Notification {
message: string;
level: Level;
timeout: number;
}
}

View File

@ -1,2 +1,2 @@
/// <reference types="react-scripts" />
declare var __webpack_public_path__: string;
declare var __webpack_public_path__: string;

View File

@ -11,133 +11,123 @@
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/),
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
(process as { env: { [key: string]: string } }).env.PUBLIC_URL,
window.location.href
);
if (publicUrl.origin !== window.location.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
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL((process as { env: { [key: string]: string } }).env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.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
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
"This web app is being served cache-first by a service " +
"worker. To learn more, visit https://bit.ly/CRA-PWA",
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
"New content is available and will be used when all " +
"tabs for this page are closed. See https://bit.ly/CRA-PWA.",
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log("Content is cached for offline use.");
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch((error) => {
console.error("Error during service worker registration:", error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log("No internet connection found. App is running in offline mode.");
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}

View File

@ -1,4 +1,5 @@
import { AxiosResponse } from "axios";
import { getBasePath } from "../utils/BasePath";
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 CompleteU2FSignInPath = basePath + "/api/secondfactor/u2f/sign";
export const CompletePushNotificationSignInPath = basePath + "/api/secondfactor/duo"
export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp"
export const CompletePushNotificationSignInPath = basePath + "/api/secondfactor/duo";
export const CompleteTOTPSignInPath = basePath + "/api/secondfactor/totp";
export const InitiateResetPasswordPath = basePath + "/api/reset-password/identity/start";
export const CompleteResetPasswordPath = basePath + "/api/reset-password/identity/finish";
// 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 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") {
return resp.data.data as T;
}
return undefined
return undefined;
}
export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
@ -61,4 +62,4 @@ export function hasServiceError<T>(resp: AxiosResponse<ServiceResponse<T>>) {
return { errored: true, message: errResp.message };
}
return { errored: false, message: null };
}
}

View File

@ -1,4 +1,5 @@
import axios from "axios";
import { ServiceResponse, hasServiceError, toData } from "./Api";
export async function PostWithOptionalResponse<T = undefined>(path: string, body?: any) {
@ -30,4 +31,4 @@ export async function Get<T = undefined>(path: string): Promise<T> {
throw new Error("unexpected type of response");
}
return d;
}
}

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 { ConfigurationPath } from "./Api";
import { Get } from "./Client";
import { toEnum, Method2FA } from "./UserPreferences";
interface ConfigurationPayload {
available_methods: Method2FA[];
@ -12,4 +12,4 @@ interface ConfigurationPayload {
export async function getConfiguration(): Promise<Configuration> {
const config = await Get<ConfigurationPayload>(ConfigurationPath);
return { ...config, available_methods: new Set(config.available_methods.map(toEnum)) };
}
}

View File

@ -9,17 +9,16 @@ interface PostFirstFactorBody {
targetURL?: string;
}
export async function postFirstFactor(
username: string, password: string,
rememberMe: boolean, targetURL?: string) {
export async function postFirstFactor(username: string, password: string, rememberMe: boolean, targetURL?: string) {
const data: PostFirstFactorBody = {
username, password,
keepMeLoggedIn: rememberMe
username,
password,
keepMeLoggedIn: rememberMe,
};
if (targetURL) {
data.targetURL = targetURL;
}
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 { PostWithOptionalResponse } from "./Client";
import { SignInResponse } from "./SignIn";
interface CompleteU2FSigninBody {
@ -13,4 +13,4 @@ export function completeTOTPSignIn(passcode: string, targetURL: string | undefin
body.targetURL = targetURL;
}
return PostWithOptionalResponse<SignInResponse>(CompleteTOTPSignInPath, body);
}
}

View File

@ -1,5 +1,5 @@
import { PostWithOptionalResponse } from "./Client";
import { CompletePushNotificationSignInPath } from "./Api";
import { PostWithOptionalResponse } from "./Client";
import { SignInResponse } from "./SignIn";
interface CompleteU2FSigninBody {
@ -12,4 +12,4 @@ export function completePushNotificationSignIn(targetURL: string | undefined) {
body.targetURL = targetURL;
}
return PostWithOptionalResponse<SignInResponse>(CompletePushNotificationSignInPath, body);
}
}

View File

@ -1,9 +1,12 @@
import {
InitiateTOTPRegistrationPath, CompleteTOTPRegistrationPath,
InitiateU2FRegistrationPath, CompleteU2FRegistrationStep1Path,
CompleteU2FRegistrationStep2Path
} from "./Api";
import U2fApi from "u2f-api";
import {
InitiateTOTPRegistrationPath,
CompleteTOTPRegistrationPath,
InitiateU2FRegistrationPath,
CompleteU2FRegistrationStep1Path,
CompleteU2FRegistrationStep2Path,
} from "./Api";
import { Post, PostWithOptionalResponse } from "./Client";
export async function initiateTOTPRegistrationProcess() {
@ -16,28 +19,27 @@ interface CompleteTOTPRegistrationResponse {
}
export async function completeTOTPRegistrationProcess(processToken: string) {
return Post<CompleteTOTPRegistrationResponse>(
CompleteTOTPRegistrationPath, { token: processToken });
return Post<CompleteTOTPRegistrationResponse>(CompleteTOTPRegistrationPath, { token: processToken });
}
export async function initiateU2FRegistrationProcess() {
return PostWithOptionalResponse(InitiateU2FRegistrationPath);
}
interface U2RRegistrationStep1Response {
appId: string,
registerRequests: [{
version: string,
challenge: string,
}]
appId: string;
registerRequests: [
{
version: string;
challenge: string;
},
];
}
export async function completeU2FRegistrationProcessStep1(processToken: string) {
return Post<U2RRegistrationStep1Response>(
CompleteU2FRegistrationStep1Path, { token: processToken });
return Post<U2RRegistrationStep1Response>(CompleteU2FRegistrationStep1Path, { token: processToken });
}
export async function completeU2FRegistrationProcessStep2(response: U2fApi.RegisterResponse) {
return PostWithOptionalResponse(CompleteU2FRegistrationStep2Path, response);
}
}

View File

@ -1,7 +1,6 @@
import { InitiateResetPasswordPath, CompleteResetPasswordPath, ResetPasswordPath } from "./Api";
import { PostWithOptionalResponse } from "./Client";
export async function initiateResetPasswordProcess(username: string) {
return PostWithOptionalResponse(InitiateResetPasswordPath, { username });
}
@ -12,4 +11,4 @@ export async function completeResetPasswordProcess(token: string) {
export async function resetPassword(newPassword: string) {
return PostWithOptionalResponse(ResetPasswordPath, { password: newPassword });
}
}

View File

@ -1,16 +1,17 @@
import { Post, PostWithOptionalResponse } from "./Client";
import { InitiateU2FSignInPath, CompleteU2FSignInPath } from "./Api";
import u2fApi from "u2f-api";
import { InitiateU2FSignInPath, CompleteU2FSignInPath } from "./Api";
import { Post, PostWithOptionalResponse } from "./Client";
import { SignInResponse } from "./SignIn";
interface InitiateU2FSigninResponse {
appId: string,
challenge: string,
appId: string;
challenge: string;
registeredKeys: {
appId: string,
keyHandle: string,
version: string,
}[]
appId: string;
keyHandle: string;
version: string;
}[];
}
export async function initiateU2FSignin() {
@ -28,4 +29,4 @@ export function completeU2FSignin(signResponse: u2fApi.SignResponse, targetURL:
body.targetURL = targetURL;
}
return PostWithOptionalResponse<SignInResponse>(CompleteU2FSignInPath, body);
}
}

View File

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

View File

@ -1,6 +1,6 @@
import { PostWithOptionalResponse } from "./Client";
import { LogoutPath } from "./Api";
import { PostWithOptionalResponse } from "./Client";
export async function signOut() {
return PostWithOptionalResponse(LogoutPath);
}
}

View File

@ -1,5 +1,5 @@
import { Get } from "./Client";
import { StatePath } from "./Api";
import { Get } from "./Client";
export enum AuthenticationLevel {
Unauthenticated = 0,
@ -9,9 +9,9 @@ export enum AuthenticationLevel {
export interface AutheliaState {
username: string;
authentication_level: AuthenticationLevel
authentication_level: AuthenticationLevel;
}
export async function getState(): Promise<AutheliaState> {
return Get<AutheliaState>(StatePath);
}
}

View File

@ -1,7 +1,7 @@
import { Get, PostWithOptionalResponse } from "./Client";
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
import { SecondFactorMethod } from "../models/Methods";
import { UserInfo } from "../models/UserInfo";
import { UserInfoPath, UserInfo2FAMethodPath } from "./Api";
import { Get, PostWithOptionalResponse } from "./Client";
export type Method2FA = "u2f" | "totp" | "mobile_push";
@ -44,6 +44,5 @@ export async function getUserPreferences(): Promise<UserInfo> {
}
export function setPreferred2FAMethod(method: SecondFactorMethod) {
return PostWithOptionalResponse(UserInfo2FAMethodPath,
{ method: toString(method) } as MethodPreferencePayload);
}
return PostWithOptionalResponse(UserInfo2FAMethodPath, { method: toString(method) } as MethodPreferencePayload);
}

View File

@ -1,5 +1,5 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
document.body.setAttribute("data-basepath", "");
document.body.setAttribute("data-rememberme", "true");
document.body.setAttribute("data-resetpassword", "true");

View File

@ -1,7 +1,7 @@
import { getBasePath } from "./BasePath";
__webpack_public_path__ = "/"
__webpack_public_path__ = "/";
if (getBasePath() !== "") {
__webpack_public_path__ = getBasePath() + "/"
}
__webpack_public_path__ = getBasePath() + "/";
}

View File

@ -2,4 +2,4 @@ import { getEmbeddedVariable } from "./Configuration";
export function getBasePath() {
return getEmbeddedVariable("basepath");
}
}

View File

@ -1,16 +1,16 @@
export function getEmbeddedVariable(variableName: string) {
const value = document.body.getAttribute(`data-${variableName}`);
if (value === null) {
throw new Error(`No ${variableName} embedded variable detected`);
}
const value = document.body.getAttribute(`data-${variableName}`);
if (value === null) {
throw new Error(`No ${variableName} embedded variable detected`);
}
return value;
return value;
}
export function getRememberMe() {
return getEmbeddedVariable("rememberme") === "true";
return getEmbeddedVariable("rememberme") === "true";
}
export function getResetPassword() {
return getEmbeddedVariable("resetpassword") === "true";
}
return getEmbeddedVariable("resetpassword") === "true";
}

View File

@ -2,7 +2,5 @@ import queryString from "query-string";
export function extractIdentityToken(locationSearch: string) {
const queryParams = queryString.parse(locationSearch);
return (queryParams && "token" in queryParams)
? queryParams["token"] as string
: null;
}
return queryParams && "token" in queryParams ? (queryParams["token"] as string) : null;
}

View File

@ -1,18 +1,20 @@
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 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 { GoogleAuthenticator } from "../../constants";
import { useHistory, useLocation } from "react-router";
import { completeTOTPRegistrationProcess } from "../../services/RegisterDevice";
import { useNotifications } from "../../hooks/NotificationsContext";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition, faCopy, faKey, faTimesCircle } from "@fortawesome/free-solid-svg-icons";
import { red } from "@material-ui/core/colors";
import { extractIdentityToken } from "../../utils/IdentityToken";
import LoginLayout from "../../layouts/LoginLayout";
import { FirstFactorRoute } from "../../Routes";
import { completeTOTPRegistrationProcess } from "../../services/RegisterDevice";
import { extractIdentityToken } from "../../utils/IdentityToken";
const RegisterOneTimePassword = function () {
const style = useStyles();
@ -32,7 +34,7 @@ const RegisterOneTimePassword = function () {
const handleDoneClick = () => {
history.push(FirstFactorRoute);
}
};
const completeRegistrationProcess = useCallback(async () => {
if (!processToken) {
@ -52,74 +54,77 @@ const RegisterOneTimePassword = function () {
setIsLoading(false);
}, [processToken, createErrorNotification]);
useEffect(() => { completeRegistrationProcess() }, [completeRegistrationProcess]);
useEffect(() => {
completeRegistrationProcess();
}, [completeRegistrationProcess]);
function SecretButton(text: string | undefined, action: string, icon: IconDefinition) {
return (
<IconButton
className={style.secretButtons}
color="primary"
onClick={() => {
navigator.clipboard.writeText(`${text}`);
createSuccessNotification(`${action}`);
}}
>
<FontAwesomeIcon icon={icon} />
</IconButton>
)
<IconButton
className={style.secretButtons}
color="primary"
onClick={() => {
navigator.clipboard.writeText(`${text}`);
createSuccessNotification(`${action}`);
}}
>
<FontAwesomeIcon icon={icon} />
</IconButton>
);
}
const qrcodeFuzzyStyle = (isLoading || hasErrored) ? style.fuzzy : undefined
const qrcodeFuzzyStyle = isLoading || hasErrored ? style.fuzzy : undefined;
return (
<LoginLayout title="Scan QRCode">
<div className={style.root}>
<div className={style.googleAuthenticator}>
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
<AppStoreBadges
iconSize={128}
targetBlank
className={style.googleAuthenticatorBadges}
googlePlayLink={GoogleAuthenticator.googlePlay}
appleStoreLink={GoogleAuthenticator.appleStore} />
</div>
<div className={style.qrcodeContainer}>
<Link href={secretURL}>
<QRCode
value={secretURL}
className={classnames(qrcodeFuzzyStyle, style.qrcode)}
size={256} />
{!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null}
{hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null}
</Link>
</div>
<div>
{secretURL !== "empty"
? <TextField
id="secret-url"
label="Secret"
className={style.secret}
value={secretURL}
InputProps={{
readOnly: true
}} /> : null}
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
</div>
<Button
variant="contained"
color="primary"
className={style.doneButton}
onClick={handleDoneClick}
disabled={isLoading}>
Done
</Button>
</div>
</LoginLayout>
)
}
<LoginLayout title="Scan QRCode">
<div className={style.root}>
<div className={style.googleAuthenticator}>
<Typography className={style.googleAuthenticatorText}>Need Google Authenticator?</Typography>
<AppStoreBadges
iconSize={128}
targetBlank
className={style.googleAuthenticatorBadges}
googlePlayLink={GoogleAuthenticator.googlePlay}
appleStoreLink={GoogleAuthenticator.appleStore}
/>
</div>
<div className={style.qrcodeContainer}>
<Link href={secretURL}>
<QRCode value={secretURL} className={classnames(qrcodeFuzzyStyle, style.qrcode)} size={256} />
{!hasErrored && isLoading ? <CircularProgress className={style.loader} size={128} /> : null}
{hasErrored ? <FontAwesomeIcon className={style.failureIcon} icon={faTimesCircle} /> : null}
</Link>
</div>
<div>
{secretURL !== "empty" ? (
<TextField
id="secret-url"
label="Secret"
className={style.secret}
value={secretURL}
InputProps={{
readOnly: true,
}}
/>
) : null}
{secretBase32 ? SecretButton(secretBase32, "OTP Secret copied to clipboard.", faKey) : null}
{secretURL !== "empty" ? SecretButton(secretURL, "OTP URL copied to clipboard.", faCopy) : null}
</div>
<Button
variant="contained"
color="primary"
className={style.doneButton}
onClick={handleDoneClick}
disabled={isLoading}
>
Done
</Button>
</div>
</LoginLayout>
);
};
export default RegisterOneTimePassword
export default RegisterOneTimePassword;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
@ -129,7 +134,7 @@ const useStyles = makeStyles(theme => ({
marginBottom: theme.spacing(2),
},
fuzzy: {
filter: "blur(10px)"
filter: "blur(10px)",
},
secret: {
marginTop: theme.spacing(1),
@ -163,5 +168,5 @@ const useStyles = makeStyles(theme => ({
left: "calc(128px - 64px)",
color: red[400],
fontSize: "128px",
}
}))
},
}));

View File

@ -1,14 +1,19 @@
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 { 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 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 style = useStyles();
const history = useHistory();
@ -18,10 +23,9 @@ const RegisterSecurityKey = function () {
const processToken = extractIdentityToken(location.search);
const handleBackClick = () => {
history.push(FirstFactorPath);
}
};
const registerStep1 = useCallback(async () => {
if (!processToken) {
@ -37,7 +41,7 @@ const RegisterSecurityKey = function () {
appId: res.appId,
challenge: r.challenge,
version: r.version,
})
});
}
const registerResponse = await u2fApi.register(registerRequests, [], 60);
await completeU2FRegistrationProcessStep2(registerResponse);
@ -45,8 +49,9 @@ const RegisterSecurityKey = function () {
history.push(FirstFactorPath);
} catch (err) {
console.error(err);
createErrorNotification("Failed to register your security key. " +
"The identity verification process might have timed out.");
createErrorNotification(
"Failed to register your security key. The identity verification process might have timed out.",
);
}
}, [processToken, createErrorNotification, history]);
@ -60,20 +65,24 @@ const RegisterSecurityKey = function () {
<FingerTouchIcon size={64} animated />
</div>
<Typography className={style.instruction}>Touch the token on your security key</Typography>
<Button color="primary" onClick={handleBackClick}>Retry</Button>
<Button color="primary" onClick={handleBackClick}>Cancel</Button>
<Button color="primary" onClick={handleBackClick}>
Retry
</Button>
<Button color="primary" onClick={handleBackClick}>
Cancel
</Button>
</LoginLayout>
)
}
);
};
export default RegisterSecurityKey
export default RegisterSecurityKey;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
icon: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
},
instruction: {
paddingBottom: theme.spacing(4),
}
}))
},
}));

View File

@ -1,6 +1,7 @@
import React from "react";
import ReactLoading from "react-loading";
import { Typography, Grid } from "@material-ui/core";
import ReactLoading from "react-loading";
const LoadingPage = function () {
return (
@ -11,6 +12,6 @@ const LoadingPage = function () {
</Grid>
</Grid>
);
}
};
export default LoadingPage
export default LoadingPage;

View File

@ -1,7 +1,9 @@
import React from "react";
import SuccessIcon from "../../components/SuccessIcon";
import { Typography, makeStyles } from "@material-ui/core";
import SuccessIcon from "../../components/SuccessIcon";
const Authenticated = function () {
const classes = useStyles();
return (
@ -11,14 +13,14 @@ const Authenticated = function () {
</div>
<Typography>Authenticated</Typography>
</div>
)
}
);
};
export default Authenticated
export default Authenticated;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
iconContainer: {
marginBottom: theme.spacing(2),
flex: "0 0 100%"
}
}))
flex: "0 0 100%",
},
}));

View File

@ -1,6 +1,8 @@
import React from "react";
import { Grid, makeStyles, Button } from "@material-ui/core";
import { useHistory } from "react-router";
import LoginLayout from "../../../layouts/LoginLayout";
import { LogoutRoute as SignOutRoute } from "../../../Routes";
import Authenticated from "../Authenticated";
@ -15,13 +17,10 @@ const AuthenticatedView = function (props: Props) {
const handleLogoutClick = () => {
history.push(SignOutRoute);
}
};
return (
<LoginLayout
id="authenticated-stage"
title={`Hi ${props.name}`}
showBrand>
<LoginLayout id="authenticated-stage" title={`Hi ${props.name}`} showBrand>
<Grid container>
<Grid item xs={12}>
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
@ -33,17 +32,17 @@ const AuthenticatedView = function (props: Props) {
</Grid>
</Grid>
</LoginLayout>
)
}
);
};
export default AuthenticatedView
export default AuthenticatedView;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
mainContainer: {
border: "1px solid #d6d6d6",
borderRadius: "10px",
padding: theme.spacing(4),
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
}
}))
},
}));

View File

@ -1,13 +1,15 @@
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
import classnames from "classnames";
import { makeStyles, Grid, Button, FormControlLabel, Checkbox, Link } from "@material-ui/core";
import classnames from "classnames";
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 { 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 {
disabled: boolean;
@ -47,7 +49,7 @@ const FirstFactorForm = function (props: Props) {
const handleSignIn = async () => {
if (username === "" || password === "") {
if (username === "") {
setUsernameError(true)
setUsernameError(true);
}
if (password === "") {
@ -62,8 +64,7 @@ const FirstFactorForm = function (props: Props) {
props.onAuthenticationSuccess(res ? res.redirect : undefined);
} catch (err) {
console.error(err);
createErrorNotification(
"Incorrect username or password.");
createErrorNotification("Incorrect username or password.");
props.onAuthenticationFailure();
setPassword("");
passwordRef.current.focus();
@ -75,10 +76,7 @@ const FirstFactorForm = function (props: Props) {
};
return (
<LoginLayout
id="first-factor-stage"
title="Sign in"
showBrand>
<LoginLayout id="first-factor-stage" title="Sign in" showBrand>
<Grid container spacing={2} className={style.root}>
<Grid item xs={12}>
<FixedTextField
@ -92,21 +90,22 @@ const FirstFactorForm = function (props: Props) {
error={usernameError}
disabled={disabled}
fullWidth
onChange={v => setUsername(v.target.value)}
onChange={(v) => setUsername(v.target.value)}
onFocus={() => setUsernameError(false)}
autoCapitalize="none"
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
if (ev.key === "Enter") {
if (!username.length) {
setUsernameError(true)
setUsernameError(true);
} else if (username.length && password.length) {
handleSignIn();
} else {
setUsernameError(false)
setUsernameError(false);
passwordRef.current.focus();
}
}
}} />
}}
/>
</Grid>
<Grid item xs={12}>
<FixedTextField
@ -120,11 +119,11 @@ const FirstFactorForm = function (props: Props) {
disabled={disabled}
value={password}
error={passwordError}
onChange={v => setPassword(v.target.value)}
onChange={(v) => setPassword(v.target.value)}
onFocus={() => setPasswordError(false)}
type="password"
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
if (ev.key === "Enter") {
if (!username.length) {
usernameRef.current.focus();
} else if (!password.length) {
@ -133,13 +132,20 @@ const FirstFactorForm = function (props: Props) {
handleSignIn();
ev.preventDefault();
}
}} />
}}
/>
</Grid>
{props.rememberMe || props.resetPassword ?
<Grid item xs={12} className={props.rememberMe
? classnames(style.leftAlign, style.actionRow)
: classnames(style.leftAlign, style.flexEnd, style.actionRow)}>
{props.rememberMe ?
{props.rememberMe || props.resetPassword ? (
<Grid
item
xs={12}
className={
props.rememberMe
? classnames(style.leftAlign, style.actionRow)
: classnames(style.leftAlign, style.flexEnd, style.actionRow)
}
>
{props.rememberMe ? (
<FormControlLabel
control={
<Checkbox
@ -148,7 +154,7 @@ const FirstFactorForm = function (props: Props) {
checked={rememberMe}
onChange={handleRememberMeChange}
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
if (ev.key === "Enter") {
if (!username.length) {
usernameRef.current.focus();
} else if (!password.length) {
@ -158,20 +164,25 @@ const FirstFactorForm = function (props: Props) {
}
}}
value="rememberMe"
color="primary"/>
color="primary"
/>
}
className={style.rememberMe}
label="Remember me"
/> : null}
{props.resetPassword ?
/>
) : null}
{props.resetPassword ? (
<Link
id="reset-password-button"
component="button"
onClick={handleResetPasswordClick}
className={style.resetLink}>
className={style.resetLink}
>
Reset password?
</Link> : null}
</Grid> : null}
</Link>
) : null}
</Grid>
) : null}
<Grid item xs={12}>
<Button
id="sign-in-button"
@ -179,18 +190,19 @@ const FirstFactorForm = function (props: Props) {
color="primary"
fullWidth
disabled={disabled}
onClick={handleSignIn}>
onClick={handleSignIn}
>
Sign in
</Button>
</Grid>
</Grid>
</LoginLayout>
)
}
);
};
export default FirstFactorForm
export default FirstFactorForm;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(),
marginBottom: theme.spacing(),
@ -219,4 +231,4 @@ const useStyles = makeStyles(theme => ({
textAlign: "right",
verticalAlign: "bottom",
},
}));
}));

View File

@ -1,20 +1,26 @@
import React, { useEffect, Fragment, ReactNode, useState, useCallback } from "react";
import { Switch, Route, Redirect, useHistory, useLocation } from "react-router";
import FirstFactorForm from "./FirstFactor/FirstFactorForm";
import SecondFactorForm from "./SecondFactor/SecondFactorForm";
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 { useConfiguration } from "../../hooks/Configuration";
import { useNotifications } from "../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../hooks/RedirectionURL";
import { useAutheliaState } from "../../hooks/State";
import { useUserPreferences as userUserInfo } from "../../hooks/UserInfo";
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 FirstFactorForm from "./FirstFactor/FirstFactorForm";
import SecondFactorForm from "./SecondFactor/SecondFactorForm";
export interface Props {
rememberMe: boolean;
@ -35,7 +41,9 @@ const LoginPortal = function (props: Props) {
const redirect = useCallback((url: string) => history.push(url), [history]);
// Fetch the state when portal is mounted.
useEffect(() => { fetchState() }, [fetchState]);
useEffect(() => {
fetchState();
}, [fetchState]);
// Fetch preferences and configuration when user is authenticated.
useEffect(() => {
@ -76,9 +84,7 @@ const LoginPortal = function (props: Props) {
// Redirect to the correct stage if not enough authenticated
useEffect(() => {
if (state) {
const redirectionSuffix = redirectionURL
? `?rd=${encodeURIComponent(redirectionURL)}`
: '';
const redirectionSuffix = redirectionURL ? `?rd=${encodeURIComponent(redirectionURL)}` : "";
if (state.authentication_level === AuthenticationLevel.Unauthenticated) {
setFirstFactorDisabled(false);
@ -107,9 +113,10 @@ const LoginPortal = function (props: Props) {
// Refresh state
fetchState();
}
}
};
const firstFactorReady = state !== undefined &&
const firstFactorReady =
state !== undefined &&
state.authentication_level === AuthenticationLevel.Unauthenticated &&
location.pathname === FirstFactorRoute;
@ -123,16 +130,20 @@ const LoginPortal = function (props: Props) {
resetPassword={props.resetPassword}
onAuthenticationStart={() => setFirstFactorDisabled(true)}
onAuthenticationFailure={() => setFirstFactorDisabled(false)}
onAuthenticationSuccess={handleAuthSuccess} />
onAuthenticationSuccess={handleAuthSuccess}
/>
</ComponentOrLoading>
</Route>
<Route path={SecondFactorRoute}>
{state && userInfo && configuration ? <SecondFactorForm
authenticationLevel={state.authentication_level}
userInfo={userInfo}
configuration={configuration}
onMethodChanged={() => fetchUserInfo()}
onAuthenticationSuccess={handleAuthSuccess} /> : null}
{state && userInfo && configuration ? (
<SecondFactorForm
authenticationLevel={state.authentication_level}
userInfo={userInfo}
configuration={configuration}
onMethodChanged={() => fetchUserInfo()}
onAuthenticationSuccess={handleAuthSuccess}
/>
) : null}
</Route>
<Route path={AuthenticatedRoute} exact>
{userInfo ? <AuthenticatedView name={userInfo.display_name} /> : null}
@ -141,10 +152,10 @@ const LoginPortal = function (props: Props) {
<Redirect to={FirstFactorRoute} />
</Route>
</Switch>
)
}
);
};
export default LoginPortal
export default LoginPortal;
interface ComponentOrLoadingProps {
ready: boolean;
@ -160,5 +171,5 @@ function ComponentOrLoading(props: ComponentOrLoadingProps) {
</div>
{props.ready ? props.children : null}
</Fragment>
)
}
);
}

View File

@ -1,4 +1,5 @@
import React, { ReactNode } from "react";
import { makeStyles } from "@material-ui/core";
import classnames from "classnames";
@ -11,7 +12,7 @@ interface IconWithContextProps {
const IconWithContext = function (props: IconWithContextProps) {
const iconSize = 64;
const style = makeStyles(theme => ({
const style = makeStyles((theme) => ({
root: {},
iconContainer: {
display: "flex",
@ -24,21 +25,17 @@ const IconWithContext = function (props: IconWithContextProps) {
},
context: {
display: "block",
}
},
}))();
return (
<div className={classnames(props.className, style.root)}>
<div className={style.iconContainer}>
<div className={style.icon}>
{props.icon}
</div>
</div>
<div className={style.context}>
{props.context}
<div className={style.icon}>{props.icon}</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 { makeStyles, Typography, Link, useTheme } from "@material-ui/core";
import InformationIcon from "../../../components/InformationIcon";
import classnames from "classnames";
import InformationIcon from "../../../components/InformationIcon";
import Authenticated from "../Authenticated";
export enum State {
ALREADY_AUTHENTICATED = 1,
NOT_REGISTERED = 2,
METHOD = 3
METHOD = 3,
}
export interface Props {
@ -24,47 +26,40 @@ const DefaultMethodContainer = function (props: Props) {
const style = useStyles();
let container: ReactNode;
let stateClass: string = '';
let stateClass: string = "";
switch (props.state) {
case State.ALREADY_AUTHENTICATED:
container = <Authenticated />
container = <Authenticated />;
stateClass = "state-already-authenticated";
break;
case State.NOT_REGISTERED:
container = <NotRegisteredContainer />
container = <NotRegisteredContainer />;
stateClass = "state-not-registered";
break;
case State.METHOD:
container = <MethodContainer explanation={props.explanation}>
{props.children}
</MethodContainer>
container = <MethodContainer explanation={props.explanation}>{props.children}</MethodContainer>;
stateClass = "state-method";
break;
}
return (
<div id={props.id}>
<Typography variant="h6">{props.title}</Typography>
<div className={classnames(style.container, stateClass)} id="2fa-container">
<div className={style.containerFlex}>
{container}
</div>
<div className={style.containerFlex}>{container}</div>
</div>
{props.onRegisterClick
? <Link component="button"
id="register-link"
onClick={props.onRegisterClick}>
{props.onRegisterClick ? (
<Link component="button" id="register-link" onClick={props.onRegisterClick}>
Not registered yet?
</Link>
: null}
) : null}
</div>
)
}
);
};
export default DefaultMethodContainer
export default DefaultMethodContainer;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
container: {
height: "200px",
},
@ -76,17 +71,21 @@ const useStyles = makeStyles(theme => ({
alignItems: "center",
alignContent: "center",
justifyContent: "center",
}
},
}));
function NotRegisteredContainer() {
const theme = useTheme();
return (
<Fragment>
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}><InformationIcon /></div>
<Typography style={{ color: "#5858ff" }}>Register your first device by clicking on the link below</Typography>
<div style={{ marginBottom: theme.spacing(2), flex: "0 0 100%" }}>
<InformationIcon />
</div>
<Typography style={{ color: "#5858ff" }}>
Register your first device by clicking on the link below
</Typography>
</Fragment>
)
);
}
interface MethodContainerProps {
@ -101,5 +100,5 @@ function MethodContainer(props: MethodContainerProps) {
<div style={{ marginBottom: theme.spacing(2) }}>{props.children}</div>
<Typography>{props.explanation}</Typography>
</Fragment>
)
}
);
}

View File

@ -1,9 +1,20 @@
import React, { ReactNode } from "react";
import { Dialog, Grid, makeStyles, DialogContent, Button, DialogActions, Typography, useTheme } from "@material-ui/core";
import PushNotificationIcon from "../../../components/PushNotificationIcon";
import PieChartIcon from "../../../components/PieChartIcon";
import { SecondFactorMethod } from "../../../models/Methods";
import {
Dialog,
Grid,
makeStyles,
DialogContent,
Button,
DialogActions,
Typography,
useTheme,
} from "@material-ui/core";
import FingerTouchIcon from "../../../components/FingerTouchIcon";
import PieChartIcon from "../../../components/PieChartIcon";
import PushNotificationIcon from "../../../components/PushNotificationIcon";
import { SecondFactorMethod } from "../../../models/Methods";
export interface Props {
open: boolean;
@ -18,37 +29,45 @@ const MethodSelectionDialog = function (props: Props) {
const style = useStyles();
const theme = useTheme();
const pieChartIcon = <PieChartIcon width={24} height={24} maxProgress={1000} progress={150}
color={theme.palette.primary.main} backgroundColor={"white"} />
const pieChartIcon = (
<PieChartIcon
width={24}
height={24}
maxProgress={1000}
progress={150}
color={theme.palette.primary.main}
backgroundColor={"white"}
/>
);
return (
<Dialog
open={props.open}
className={style.root}
onClose={props.onClose}>
<Dialog open={props.open} className={style.root} onClose={props.onClose}>
<DialogContent>
<Grid container justify="center" spacing={1} id="methods-dialog">
{props.methods.has(SecondFactorMethod.TOTP)
? <MethodItem
{props.methods.has(SecondFactorMethod.TOTP) ? (
<MethodItem
id="one-time-password-option"
method="One-Time Password"
icon={pieChartIcon}
onClick={() => props.onClick(SecondFactorMethod.TOTP)} />
: null}
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported
? <MethodItem
onClick={() => props.onClick(SecondFactorMethod.TOTP)}
/>
) : null}
{props.methods.has(SecondFactorMethod.U2F) && props.u2fSupported ? (
<MethodItem
id="security-key-option"
method="Security Key"
icon={<FingerTouchIcon size={32} />}
onClick={() => props.onClick(SecondFactorMethod.U2F)} />
: null}
{props.methods.has(SecondFactorMethod.MobilePush)
? <MethodItem
onClick={() => props.onClick(SecondFactorMethod.U2F)}
/>
) : null}
{props.methods.has(SecondFactorMethod.MobilePush) ? (
<MethodItem
id="push-notification-option"
method="Push Notification"
icon={<PushNotificationIcon width={32} height={32} />}
onClick={() => props.onClick(SecondFactorMethod.MobilePush)} />
: null}
onClick={() => props.onClick(SecondFactorMethod.MobilePush)}
/>
) : null}
</Grid>
</DialogContent>
<DialogActions>
@ -57,16 +76,16 @@ const MethodSelectionDialog = function (props: Props) {
</Button>
</DialogActions>
</Dialog>
)
}
);
};
export default MethodSelectionDialog
export default MethodSelectionDialog;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
textAlign: "center",
}
}))
},
}));
interface MethodItemProps {
id: string;
@ -77,7 +96,7 @@ interface MethodItemProps {
}
function MethodItem(props: MethodItemProps) {
const style = makeStyles(theme => ({
const style = makeStyles((theme) => ({
item: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4),
@ -89,18 +108,23 @@ function MethodItem(props: MethodItemProps) {
},
buttonRoot: {
display: "block",
}
},
}))();
return (
<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 }}
variant="contained"
onClick={props.onClick}>
onClick={props.onClick}
>
<div className={style.icon}>{props.icon}</div>
<div><Typography>{props.method}</Typography></div>
<div>
<Typography>{props.method}</Typography>
</div>
</Button>
</Grid>
)
}
);
}

View File

@ -1,16 +1,18 @@
import React, { Fragment } from "react";
import OtpInput from "react-otp-input";
import TimerIcon from "../../../components/TimerIcon";
import { makeStyles } from "@material-ui/core";
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 { State } from "./OneTimePasswordMethod";
import SuccessIcon from "../../../components/SuccessIcon";
export interface Props {
passcode: string;
state: State;
period: number
period: number;
onChange: (passcode: string) => void;
}
@ -26,22 +28,18 @@ const OTPDial = function (props: Props) {
numInputs={6}
isDisabled={props.state === State.InProgress || props.state === State.Success}
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>
)
);
return (
<IconWithContext
icon={<Icon state={props.state} period={props.period} />}
context={dial} />
)
}
return <IconWithContext icon={<Icon state={props.state} period={props.period} />} context={dial} />;
};
export default OTPDial
export default OTPDial;
const useStyles = makeStyles(theme => ({
timeProgress: {
},
const useStyles = makeStyles((theme) => ({
timeProgress: {},
register: {
marginTop: theme.spacing(),
},
@ -59,7 +57,7 @@ const useStyles = makeStyles(theme => ({
},
inputError: {
border: "1px solid rgba(255, 2, 2, 0.95)",
}
},
}));
interface IconProps {
@ -70,8 +68,10 @@ interface IconProps {
function Icon(props: IconProps) {
return (
<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}
</Fragment>
)
}
);
}

View File

@ -1,9 +1,10 @@
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 OTPDial from "./OTPDial";
import { completeTOTPSignIn } from "../../../services/OneTimePassword";
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
import { AuthenticationLevel } from "../../../services/State";
export enum State {
Idle = 1,
@ -16,7 +17,7 @@ export interface Props {
id: string;
authenticationLevel: AuthenticationLevel;
registered: boolean;
totp_period: number
totp_period: number;
onRegisterClick: () => void;
onSignInError: (err: Error) => void;
@ -25,9 +26,9 @@ export interface Props {
const OneTimePasswordMethod = function (props: Props) {
const [passcode, setPasscode] = useState("");
const [state, setState] = useState(props.authenticationLevel === AuthenticationLevel.TwoFactor
? State.Success
: State.Idle);
const [state, setState] = useState(
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
);
const redirectionURL = useRedirectionURL();
const { onSignInSuccess, onSignInError } = props;
@ -67,7 +68,9 @@ const OneTimePasswordMethod = function (props: Props) {
}
}, [props.authenticationLevel, setState]);
useEffect(() => { signInFunc() }, [signInFunc]);
useEffect(() => {
signInFunc();
}, [signInFunc]);
let methodState = MethodContainerState.METHOD;
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
@ -82,14 +85,11 @@ const OneTimePasswordMethod = function (props: Props) {
title="One-Time Password"
explanation="Enter one-time password"
state={methodState}
onRegisterClick={props.onRegisterClick}>
<OTPDial
passcode={passcode}
onChange={setPasscode}
state={state}
period={props.totp_period} />
onRegisterClick={props.onRegisterClick}
>
<OTPDial passcode={passcode} onChange={setPasscode} state={state} period={props.totp_period} />
</MethodContainer>
)
}
);
};
export default OneTimePasswordMethod
export default OneTimePasswordMethod;

View File

@ -1,13 +1,15 @@
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 { useRedirectionURL } from "../../../hooks/RedirectionURL";
import { useIsMountedRef } from "../../../hooks/Mounted";
import SuccessIcon from "../../../components/SuccessIcon";
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 MethodContainer, { State as MethodContainerState } from "./MethodContainer";
export enum State {
SignInInProgress = 1,
@ -50,7 +52,7 @@ const PushNotificationMethod = function (props: Props) {
setState(State.Success);
setTimeout(() => {
if (!mounted.current) return;
onSignInSuccessCallback(res ? res.redirect : undefined)
onSignInSuccessCallback(res ? res.redirect : undefined);
}, 1500);
} catch (err) {
// 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]);
useEffect(() => { signInFunc() }, [signInFunc]);
useEffect(() => {
signInFunc();
}, [signInFunc]);
// Set successful state if user is already authenticated.
useEffect(() => {
@ -94,23 +98,24 @@ const PushNotificationMethod = function (props: Props) {
id={props.id}
title="Push Notification"
explanation="A notification has been sent to your smartphone"
state={methodState}>
<div className={style.icon}>
{icon}
</div>
<div className={(state !== State.Failure) ? "hidden" : ""}>
<Button color="secondary" onClick={signInFunc}>Retry</Button>
state={methodState}
>
<div className={style.icon}>{icon}</div>
<div className={state !== State.Failure ? "hidden" : ""}>
<Button color="secondary" onClick={signInFunc}>
Retry
</Button>
</div>
</MethodContainer>
)
}
);
};
export default PushNotificationMethod
export default PushNotificationMethod;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
icon: {
width: "64px",
height: "64px",
display: "inline-block",
}
}))
},
}));

View File

@ -1,26 +1,28 @@
import React, { useState, useEffect } from "react";
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 LoginLayout from "../../../layouts/LoginLayout";
import u2fApi from "u2f-api";
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 {
initiateTOTPRegistrationProcess,
initiateU2FRegistrationProcess
} from "../../../services/RegisterDevice";
import SecurityKeyMethod from "./SecurityKeyMethod";
LogoutRoute as SignOutRoute,
SecondFactorTOTPRoute,
SecondFactorPushRoute,
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 PushNotificationMethod from "./PushNotificationMethod";
import {
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";
import SecurityKeyMethod from "./SecurityKeyMethod";
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(() => {
u2fApi.ensureSupport().then(
() => setU2fSupported(true),
() => console.error("U2F not supported"));
() => console.error("U2F not supported"),
);
}, [setU2fSupported]);
const initiateRegistration = (initiateRegistrationFunc: () => Promise<void>) => {
@ -63,12 +66,12 @@ const SecondFactorForm = function (props: Props) {
createErrorNotification("There was a problem initiating the registration process");
}
setRegistrationInProgress(false);
}
}
};
};
const handleMethodSelectionClick = () => {
setMethodSelectionOpen(true);
}
};
const handleMethodSelected = async (method: SecondFactorMethod) => {
try {
@ -79,23 +82,21 @@ const SecondFactorForm = function (props: Props) {
console.error(err);
createErrorNotification("There was an issue updating preferred second factor method");
}
}
};
const handleLogoutClick = () => {
history.push(SignOutRoute);
}
};
return (
<LoginLayout
id="second-factor-stage"
title={`Hi ${props.userInfo.display_name}`}
showBrand>
<LoginLayout id="second-factor-stage" title={`Hi ${props.userInfo.display_name}`} showBrand>
<MethodSelectionDialog
open={methodSelectionOpen}
methods={props.configuration.available_methods}
u2fSupported={u2fSupported}
onClose={() => setMethodSelectionOpen(false)}
onClick={handleMethodSelected} />
onClick={handleMethodSelected}
/>
<Grid container>
<Grid item xs={12}>
<Button color="secondary" onClick={handleLogoutClick} id="logout-button">
@ -116,8 +117,9 @@ const SecondFactorForm = function (props: Props) {
registered={props.userInfo.has_totp}
totp_period={props.configuration.totp_period}
onRegisterClick={initiateRegistration(initiateTOTPRegistrationProcess)}
onSignInError={err => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} />
onSignInError={(err) => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess}
/>
</Route>
<Route path={SecondFactorU2FRoute} exact>
<SecurityKeyMethod
@ -126,15 +128,17 @@ const SecondFactorForm = function (props: Props) {
// Whether the user has a U2F device registered already
registered={props.userInfo.has_u2f}
onRegisterClick={initiateRegistration(initiateU2FRegistrationProcess)}
onSignInError={err => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} />
onSignInError={(err) => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess}
/>
</Route>
<Route path={SecondFactorPushRoute} exact>
<PushNotificationMethod
id="push-notification-method"
authenticationLevel={props.authenticationLevel}
onSignInError={err => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess} />
onSignInError={(err) => createErrorNotification(err.message)}
onSignInSuccess={props.onAuthenticationSuccess}
/>
</Route>
<Route path={SecondFactorRoute}>
<Redirect to={SecondFactorTOTPRoute} />
@ -143,12 +147,12 @@ const SecondFactorForm = function (props: Props) {
</Grid>
</Grid>
</LoginLayout>
)
}
);
};
export default SecondFactorForm
export default SecondFactorForm;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
methodContainer: {
border: "1px solid #d6d6d6",
borderRadius: "10px",
@ -156,4 +160,4 @@ const useStyles = makeStyles(theme => ({
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}))
}));

View File

@ -1,17 +1,19 @@
import React, { useCallback, useEffect, useState, Fragment } from "react";
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
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 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 IconWithContext from "./IconWithContext";
import MethodContainer, { State as MethodContainerState } from "./MethodContainer";
export enum State {
WaitTouch = 1,
@ -35,7 +37,7 @@ const SecurityKeyMethod = function (props: Props) {
const style = useStyles();
const redirectionURL = useRedirectionURL();
const mounted = useIsMountedRef();
const [timerPercent, triggerTimer,] = useTimer(signInTimeout * 1000 - 500);
const [timerPercent, triggerTimer] = useTimer(signInTimeout * 1000 - 500);
const { onSignInSuccess, onSignInError } = props;
/* eslint-disable react-hooks/exhaustive-deps */
@ -61,7 +63,7 @@ const SecurityKeyMethod = function (props: Props) {
challenge: signRequest.challenge,
keyHandle: r.keyHandle,
version: r.version,
})
});
}
const signResponse = await u2fApi.sign(signRequests, signInTimeout);
// 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"));
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;
if (props.authenticationLevel === AuthenticationLevel.TwoFactor) {
@ -96,20 +108,21 @@ const SecurityKeyMethod = function (props: Props) {
title="Security Key"
explanation="Touch the token of your security key"
state={methodState}
onRegisterClick={props.onRegisterClick}>
onRegisterClick={props.onRegisterClick}
>
<div className={style.icon}>
<Icon state={state} timer={timerPercent} onRetryClick={doInitiateSignIn} />
</div>
</MethodContainer>
)
}
);
};
export default SecurityKeyMethod
export default SecurityKeyMethod;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
icon: {
display: "inline-block",
}
},
}));
interface IconProps {
@ -125,22 +138,32 @@ function Icon(props: IconProps) {
const progressBarStyle: CSSProperties = {
marginTop: theme.spacing(),
}
};
const touch = <IconWithContext
icon={<FingerTouchIcon size={64} animated strong />}
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
className={state === State.WaitTouch ? undefined : "hidden"} />
const touch = (
<IconWithContext
icon={<FingerTouchIcon size={64} animated strong />}
context={<LinearProgressBar value={props.timer} style={progressBarStyle} height={theme.spacing(2)} />}
className={state === State.WaitTouch ? undefined : "hidden"}
/>
);
const failure = <IconWithContext
icon={<FailureIcon />}
context={<Button color="secondary" onClick={props.onRetryClick}>Retry</Button>}
className={state === State.Failure ? undefined : "hidden"} />
const failure = (
<IconWithContext
icon={<FailureIcon />}
context={
<Button color="secondary" onClick={props.onRetryClick}>
Retry
</Button>
}
className={state === State.Failure ? undefined : "hidden"}
/>
);
return (
<Fragment>
{touch}
{failure}
</Fragment>
)
);
}

View File

@ -1,14 +1,16 @@
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 { Redirect } from "react-router";
import { FirstFactorRoute } from "../../../Routes";
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
import { useIsMountedRef } from "../../../hooks/Mounted";
export interface Props { }
import { useIsMountedRef } from "../../../hooks/Mounted";
import { useNotifications } from "../../../hooks/NotificationsContext";
import { useRedirectionURL } from "../../../hooks/RedirectionURL";
import LoginLayout from "../../../layouts/LoginLayout";
import { FirstFactorRoute } from "../../../Routes";
import { signOut } from "../../../services/SignOut";
export interface Props {}
const SignOut = function (props: Props) {
const mounted = useIsMountedRef();
@ -33,29 +35,29 @@ const SignOut = function (props: Props) {
}
}, [createErrorNotification, setTimedOut, mounted]);
useEffect(() => { doSignOut() }, [doSignOut]);
useEffect(() => {
doSignOut();
}, [doSignOut]);
if (timedOut) {
if (redirectionURL) {
window.location.href = redirectionURL;
} else {
return <Redirect to={FirstFactorRoute} />
return <Redirect to={FirstFactorRoute} />;
}
}
return (
<LoginLayout title="Sign out">
<Typography className={style.typo} >
You're being signed out and redirected...
</Typography>
<Typography className={style.typo}>You're being signed out and redirected...</Typography>
</LoginLayout>
)
}
);
};
export default SignOut
export default SignOut;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
typo: {
padding: theme.spacing(),
}
}))
},
}));

View File

@ -1,11 +1,13 @@
import React, { useState } from "react";
import LoginLayout from "../../layouts/LoginLayout";
import { Grid, Button, makeStyles } from "@material-ui/core";
import { useNotifications } from "../../hooks/NotificationsContext";
import { useHistory } from "react-router";
import { initiateResetPasswordProcess } from "../../services/ResetPassword";
import { FirstFactorRoute } from "../../Routes";
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 style = useStyles();
@ -26,15 +28,15 @@ const ResetPasswordStep1 = function () {
} catch (err) {
createErrorNotification("There was an issue initiating the password reset process.");
}
}
};
const handleResetClick = () => {
doInitiateResetPasswordProcess();
}
};
const handleCancelClick = () => {
history.push(FirstFactorRoute);
}
};
return (
<LoginLayout title="Reset password" id="reset-password-step1-stage">
@ -49,19 +51,17 @@ const ResetPasswordStep1 = function () {
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
if (ev.key === "Enter") {
doInitiateResetPasswordProcess();
ev.preventDefault();
}
}} />
}}
/>
</Grid>
<Grid item xs={6}>
<Button
id="reset-button"
variant="contained"
color="primary"
fullWidth
onClick={handleResetClick}>Reset</Button>
<Button id="reset-button" variant="contained" color="primary" fullWidth onClick={handleResetClick}>
Reset
</Button>
</Grid>
<Grid item xs={6}>
<Button
@ -69,18 +69,21 @@ const ResetPasswordStep1 = function () {
variant="contained"
color="primary"
fullWidth
onClick={handleCancelClick}>Cancel</Button>
onClick={handleCancelClick}
>
Cancel
</Button>
</Grid>
</Grid>
</LoginLayout>
)
}
);
};
export default ResetPasswordStep1
export default ResetPasswordStep1;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}))
}));

View File

@ -1,13 +1,15 @@
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 { useNotifications } from "../../hooks/NotificationsContext";
import classnames from "classnames";
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 { 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 style = useStyles();
@ -36,8 +38,9 @@ const ResetPasswordStep2 = function () {
setFormDisabled(false);
} catch (err) {
console.error(err);
createErrorNotification("There was an issue completing the process. " +
"The verification token might have expired.");
createErrorNotification(
"There was an issue completing the process. The verification token might have expired.",
);
setFormDisabled(true);
}
}, [processToken, createErrorNotification]);
@ -54,11 +57,11 @@ const ResetPasswordStep2 = function () {
if (password2 === "") {
setErrorPassword2(true);
}
return
return;
}
if (password1 !== password2) {
setErrorPassword1(true);
setErrorPassword2(true)
setErrorPassword2(true);
createErrorNotification("Passwords do not match.");
return;
}
@ -76,13 +79,11 @@ const ResetPasswordStep2 = function () {
createErrorNotification("There was an issue resetting the password.");
}
}
}
};
const handleResetClick = () =>
doResetPassword();
const handleResetClick = () => doResetPassword();
const handleCancelClick = () =>
history.push(FirstFactorRoute);
const handleCancelClick = () => history.push(FirstFactorRoute);
return (
<LoginLayout title="Enter new password" id="reset-password-step2-stage">
@ -95,9 +96,10 @@ const ResetPasswordStep2 = function () {
type="password"
value={password1}
disabled={formDisabled}
onChange={e => setPassword1(e.target.value)}
onChange={(e) => setPassword1(e.target.value)}
error={errorPassword1}
className={classnames(style.fullWidth)} />
className={classnames(style.fullWidth)}
/>
</Grid>
<Grid item xs={12}>
<FixedTextField
@ -107,15 +109,16 @@ const ResetPasswordStep2 = function () {
type="password"
disabled={formDisabled}
value={password2}
onChange={e => setPassword2(e.target.value)}
onChange={(e) => setPassword2(e.target.value)}
error={errorPassword2}
onKeyPress={(ev) => {
if (ev.key === 'Enter') {
if (ev.key === "Enter") {
doResetPassword();
ev.preventDefault();
}
}}
className={classnames(style.fullWidth)} />
className={classnames(style.fullWidth)}
/>
</Grid>
<Grid item xs={6}>
<Button
@ -125,7 +128,10 @@ const ResetPasswordStep2 = function () {
name="password1"
disabled={formDisabled}
onClick={handleResetClick}
className={style.fullWidth}>Reset</Button>
className={style.fullWidth}
>
Reset
</Button>
</Grid>
<Grid item xs={6}>
<Button
@ -134,21 +140,24 @@ const ResetPasswordStep2 = function () {
color="primary"
name="password2"
onClick={handleCancelClick}
className={style.fullWidth}>Cancel</Button>
className={style.fullWidth}
>
Cancel
</Button>
</Grid>
</Grid>
</LoginLayout>
)
}
);
};
export default ResetPasswordStep2
export default ResetPasswordStep2;
const useStyles = makeStyles(theme => ({
const useStyles = makeStyles((theme) => ({
root: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
fullWidth: {
width: "100%",
}
}))
},
}));

View File

@ -1 +1 @@
/// <reference path="react-otp-input/index.d.ts" />
/// <reference path="react-otp-input/index.d.ts" />

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:
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:
version "6.0.0"
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:
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:
version "0.3.4"
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"
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:
version "2.6.0"
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"
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:
version "4.2.0"
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"
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:
version "3.2.4"
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"
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:
version "5.4.1"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.4.1.tgz#cd89f79bbcef21e3d21eb0da68ffe93f803e884b"