First complete version of the Authelia frontend in React.

This commit is contained in:
Clement Michaud 2019-01-10 21:43:21 +01:00
parent 721ad59545
commit be357f8e69
37 changed files with 4154 additions and 1851 deletions

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,20 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@material-ui/core": "^3.4.0", "@material-ui/core": "3.7.1",
"@material-ui/icons": "^3.0.1",
"@types/classnames": "^2.2.7",
"@types/jss": "^9.5.7",
"@types/node": "^10.12.2", "@types/node": "^10.12.2",
"@types/react": "^16.4.18", "@types/react": "^16.4.18",
"@types/react-dom": "^16.0.9", "@types/react-dom": "^16.0.9",
"@types/react-router-dom": "^4.3.1", "@types/react-router-dom": "^4.3.1",
"classnames": "^2.2.6",
"jss": "^9.8.7",
"react": "^16.6.0", "react": "^16.6.0",
"react-dom": "^16.6.0", "react-dom": "^16.6.0",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-scripts": "2.1.1", "react-scripts": "^2.1.3",
"typescript": "^3.1.6" "typescript": "^3.1.6"
}, },
"scripts": { "scripts": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -19,7 +19,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Authelia - Portal</title>
</head> </head>
<body> <body>
<noscript> <noscript>

View File

@ -1,19 +1,22 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import './App.css'; import './App.css';
import { BrowserRouter as Router, Route, Link } from "react-router-dom"; import { Router, Route, Switch } from "react-router-dom";
import { FirstFactor } from './pages/first-factor/first-factor'; import { routes } from './routes/index';
import { SecondFactor } from './pages/second-factor/second-factor'; import { createBrowserHistory } from 'history';
import ConfirmationSent from './pages/confirmation-sent/confirmation-sent';
const history = createBrowserHistory();
class App extends Component { class App extends Component {
render() { render() {
return ( return (
<Router> <Router history={history}>
<div className="App"> <div className="App">
<Route exact path="/" component={FirstFactor} /> <Switch>
<Route exact path="/2fa" component={SecondFactor} /> {routes.map((r, key) => {
<Route exact path="/confirmation" component={ConfirmationSent} /> return <Route path={r.path} component={r.component} key={key}/>
})}
</Switch>
</div> </div>
</Router> </Router>
); );

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 55 55" style="enable-background:new 0 0 55 55;" xml:space="preserve">
<path d="M55,27.5C55,12.337,42.663,0,27.5,0S0,12.337,0,27.5c0,8.009,3.444,15.228,8.926,20.258l-0.026,0.023l0.892,0.752
c0.058,0.049,0.121,0.089,0.179,0.137c0.474,0.393,0.965,0.766,1.465,1.127c0.162,0.117,0.324,0.234,0.489,0.348
c0.534,0.368,1.082,0.717,1.642,1.048c0.122,0.072,0.245,0.142,0.368,0.212c0.613,0.349,1.239,0.678,1.88,0.98
c0.047,0.022,0.095,0.042,0.142,0.064c2.089,0.971,4.319,1.684,6.651,2.105c0.061,0.011,0.122,0.022,0.184,0.033
c0.724,0.125,1.456,0.225,2.197,0.292c0.09,0.008,0.18,0.013,0.271,0.021C25.998,54.961,26.744,55,27.5,55
c0.749,0,1.488-0.039,2.222-0.098c0.093-0.008,0.186-0.013,0.279-0.021c0.735-0.067,1.461-0.164,2.178-0.287
c0.062-0.011,0.125-0.022,0.187-0.034c2.297-0.412,4.495-1.109,6.557-2.055c0.076-0.035,0.153-0.068,0.229-0.104
c0.617-0.29,1.22-0.603,1.811-0.936c0.147-0.083,0.293-0.167,0.439-0.253c0.538-0.317,1.067-0.648,1.581-1
c0.185-0.126,0.366-0.259,0.549-0.391c0.439-0.316,0.87-0.642,1.289-0.983c0.093-0.075,0.193-0.14,0.284-0.217l0.915-0.764
l-0.027-0.023C51.523,42.802,55,35.55,55,27.5z M2,27.5C2,13.439,13.439,2,27.5,2S53,13.439,53,27.5
c0,7.577-3.325,14.389-8.589,19.063c-0.294-0.203-0.59-0.385-0.893-0.537l-8.467-4.233c-0.76-0.38-1.232-1.144-1.232-1.993v-2.957
c0.196-0.242,0.403-0.516,0.617-0.817c1.096-1.548,1.975-3.27,2.616-5.123c1.267-0.602,2.085-1.864,2.085-3.289v-3.545
c0-0.867-0.318-1.708-0.887-2.369v-4.667c0.052-0.52,0.236-3.448-1.883-5.864C34.524,9.065,31.541,8,27.5,8
s-7.024,1.065-8.867,3.168c-2.119,2.416-1.935,5.346-1.883,5.864v4.667c-0.568,0.661-0.887,1.502-0.887,2.369v3.545
c0,1.101,0.494,2.128,1.34,2.821c0.81,3.173,2.477,5.575,3.093,6.389v2.894c0,0.816-0.445,1.566-1.162,1.958l-7.907,4.313
c-0.252,0.137-0.502,0.297-0.752,0.476C5.276,41.792,2,35.022,2,27.5z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,35 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
fields: {
marginBottom: theme.spacing.unit,
},
field: {
paddingBottom: theme.spacing.unit * 2,
},
input: {
width: '100%',
},
buttons: {
'& button': {
width: '100%',
},
},
controls: {
display: 'inline-block',
width: '100%',
fontSize: '0.875rem',
},
rememberMe: {
float: 'left',
},
resetPassword: {
padding: '12px 0px',
float: 'right',
'& a': {
color: 'black',
},
},
}));
export default styles;

View File

@ -0,0 +1,16 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
form: {
padding: '20px 0px',
},
field: {
width: '100%',
},
button: {
marginTop: '20px',
width: '100%',
}
}));
export default styles;

View File

@ -0,0 +1,18 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
form: {
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
},
field: {
width: '100%',
marginBottom: theme.spacing.unit * 2,
},
button: {
marginTop: theme.spacing.unit * 2,
width: '100%',
}
}));
export default styles;

View File

@ -0,0 +1,38 @@
import { createStyles, Theme } from "@material-ui/core";
const styles = createStyles((theme: Theme) => ({
container: {
position: 'relative',
paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit,
},
body: {
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
},
image: {
width: '120px',
},
imageContainer: {
textAlign: 'center',
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2,
},
footer: {
fontSize: theme.typography.fontSize * 0.9,
},
registerDevice: {
float: 'right',
},
totpField: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2,
width: '100%',
},
totpButton: {
width: '100%',
}
}));
export default styles;

View File

@ -0,0 +1,3 @@
export const AUTHELIA_URL = "https://www.authelia.com/"
export const AUTHELIA_GITHUB_URL = "https://github.com/clems4ever/authelia";

View File

@ -2,14 +2,7 @@
.mainContent { .mainContent {
width: 440px; width: 440px;
margin: 0 auto; margin: 0 auto;
padding: 1.5em 3em 3em 3em; padding: 50px 0px;
}
/* HEADER */
.header {
margin-bottom: 2em;
text-align: center;
} }
/* FRAME */ /* FRAME */
@ -18,26 +11,32 @@
box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px; box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px;
background-color: white; background-color: white;
border-radius: 5px; border-radius: 5px;
padding: 30px 40px;
} }
.innerFrame { .innerFrame {
width: 100%; width: 100%;
} }
.title {
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px solid #c7c7c7;
margin-bottom: 20px;
display: inline-block;
padding-right: 10px;
padding-bottom: 5px;
}
/* FOOTER */ /* FOOTER */
.footer { .footer {
margin-top: 1em; margin-top: 10px;
text-align: center; text-align: center;
font-size: 0.75em; font-size: 0.65em;
color: grey; color: grey;
} }
.footer img {
width: 64px;
margin: 20px;
}
.footer a { .footer a {
color: grey; color: grey;
} }

View File

@ -0,0 +1,50 @@
import React, { Component } from "react";
import styles from "./PortalLayout.module.css"
import { Route, Switch, Redirect, RouterProps, RouteProps } from "react-router";
import { routes } from '../../routes/routes';
import { AUTHELIA_GITHUB_URL } from "../../constants";
interface Props extends RouterProps, RouteProps {}
export default class PortalLayout extends Component<Props> {
private renderTitle() {
if (!this.props.location) return;
for (let i in routes) {
const route = routes[i];
if (route.path && route.path.indexOf(this.props.location.pathname) > -1) {
return route.title.toUpperCase();
}
}
return;
}
render() {
return (
<div className={styles.mainContent}>
<div className={styles.frame}>
<div className={styles.innerFrame}>
<div className={styles.title}>
{this.renderTitle()}
</div>
<div className={styles.content}>
<Switch>
{routes.map((r, key) => {
return <Route path={r.path} component={r.component} exact={true} key={key} />
})}
<Redirect to='/' />
</Switch>
</div>
</div>
</div>
<div className={styles.footer}>
<div>Powered by <a href={AUTHELIA_GITHUB_URL}>Authelia</a></div>
</div>
</div>
)
}
}

View File

@ -1 +0,0 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 230 160"><style>.st0{fill:#fff}.st1,.st2,.st3{opacity:.8;fill:#f52757}.st2,.st3{opacity:.7}.st3{opacity:.6}.st4{opacity:.68;fill:#1e88e5}.st5,.st6,.st7{opacity:.76;fill:#1e88e5}.st6,.st7{opacity:.84}.st7{opacity:.94}.st8{opacity:.65}.st8,.st9{fill:#f52757}.st10{fill:#1e88e5}.st11{opacity:.92}.st11,.st12{fill:#0d47a1}</style><path class="st0" d="M93.1 4.3H49.4L5.7 80l43.7 75.7h43.7l65.6-113.6L180.6 80l-43.7 75.7h43.7L224.3 80 180.6 4.3h-43.7L71.3 117.9 49.4 80z"/><path class="st1" d="M115 42.1l21.8-37.8 21.9 37.8z"/><path class="st2" d="M115 42.1L136.8 80l21.9-37.9z"/><path class="st3" d="M93.1 80L115 42.1 136.8 80z"/><path class="st4" d="M49.4 4.3l21.9 37.8L93.1 4.3z"/><path class="st5" d="M27.6 42.1L49.4 4.3l21.9 37.8z"/><path class="st6" d="M27.6 42.1L49.4 80l21.9-37.9z"/><path class="st7" d="M5.7 80l21.9-37.9L49.4 80z"/><path class="st8" d="M93.1 80l21.9 37.9L136.8 80z"/><path class="st2" d="M71.3 117.9L93.1 80l21.9 37.9z"/><path class="st1" d="M71.3 117.9l21.8 37.8 21.9-37.8z"/><path class="st9" d="M49.4 155.7l21.9-37.8 21.8 37.8z"/><path class="st10" d="M5.7 80l21.9 37.9L49.4 80z"/><path class="st4" d="M180.6 155.7l-21.9-37.8-21.8 37.8z"/><path class="st5" d="M202.4 117.9l-21.8 37.8-21.9-37.8z"/><path class="st6" d="M202.4 117.9L180.6 80l-21.9 37.9z"/><path class="st7" d="M224.3 80l-21.9 37.9L180.6 80z"/><path class="st10" d="M224.3 80l-21.9-37.9L180.6 80z"/><path class="st11" d="M71.3 117.9L49.4 80l-21.8 37.9z"/><path class="st12" d="M71.3 117.9l-21.9 37.8-21.8-37.8z"/><path class="st9" d="M136.8 4.3l21.9 37.8 21.9-37.8z"/><path class="st12" d="M202.4 42.1L180.6 4.3l-21.9 37.8z"/><path class="st11" d="M202.4 42.1L180.6 80l-21.9-37.9z"/></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,15 +0,0 @@
.main {
padding: 2em;
}
.image {
width: 100%;
}
.image img {
width: 64px;
display: block;
margin: 2em auto;
text-align: center;
}

View File

@ -1,23 +0,0 @@
import React, { Component } from "react";
import FormTemplate from '../../templates/form-template';
import mail from '../../mail.png';
import styles from './confirmation-sent.module.css';
export default class ConfirmationSent extends Component {
render() {
return (
<FormTemplate title="Confirmation e-mail">
<div className={styles.main}>
<p>An e-mail has been sent to your address.</p>
<div className={styles.image}>
<img src={mail} alt="mail" />
</div>
<p>Please click on the link provided in the e-mail to confirm the operation.</p>
</div>
</FormTemplate>
)
}
}

View File

@ -1,39 +0,0 @@
.main {
padding: 2em 3em 2em 3em;
}
.field {
width: 100%;
margin-bottom: 20px;
}
.field .input {
width: 100%;
}
.controls {
display: inline-block;
width: 100%;
font-size: 0.875rem;
}
.controls .rememberMe {
float: left;
}
.controls .resetPassword {
padding: 12px 0px;
float: right;
}
.controls .resetPassword a {
color: black;
}
.buttons {
margin-top: 20px;
}
.buttons button {
width: 100%;
}

View File

@ -1,83 +0,0 @@
import React, { Component } from "react";
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import FormTemplate from '../../templates/form-template';
import styles from "./first-factor.module.css"
interface State {
rememberMe: boolean;
}
export class FirstFactor extends Component<any, State> {
constructor(props: any) {
super(props)
this.state = {
rememberMe: false
}
}
toggleRememberMe = () => {
this.setState({
rememberMe: !(this.state.rememberMe)
})
}
render() {
return (
<FormTemplate title="Sign in">
<div className={styles.main}>
<div className={styles.fields}>
<div className={styles.field}>
<TextField
className={styles.input}
id="username"
label="Username">
</TextField>
</div>
<div className={styles.field}>
<TextField
className={styles.input}
id="password"
label="Password"
type="password">
</TextField>
</div>
</div>
<div className={styles.controlArea}>
<div className={styles.controls}>
<div className={styles.rememberMe}>
<FormControlLabel
control={
<Checkbox
checked={this.state.rememberMe}
onChange={this.toggleRememberMe}
color="primary"
/>
}
label="Remember me"
/>
</div>
<div className={styles.resetPassword}>
<a href="/">Forgot password?</a>
</div>
</div>
<div className={styles.buttons}>
<Button
variant="contained"
color="primary"
className={styles.button}>
Login
</Button>
</div>
</div>
</div>
</FormTemplate>
)
}
}

View File

@ -1,37 +0,0 @@
.main {
text-align: center;
padding: 2em 3em 2em 3em;
min-height: 350px;
}
.authenticate {
margin: 1em;
}
.authenticate hr {
width: 100%;
}
.authenticate .u2f img {
width: 128px;
margin-top: 1em;
}
.authenticate .totpField {
margin: 1em 0em;
}
.register {
margin: 1em;
}
.register .buttons {
margin: 2em 0em;
}
.register .buttons button {
margin: 0.8em 0em;
width: 100%;
}

View File

@ -1,103 +0,0 @@
import React, { Component } from "react";
import TextField from '@material-ui/core/TextField';
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import RestoreIcon from '@material-ui/core/Icon';
import FavoriteIcon from '@material-ui/core/Icon';
import Button from '@material-ui/core/Button';
import FormTemplate from '../../templates/form-template';
import styles from './second-factor.module.css';
import pendrive from '../../pendrive.png'
interface State {
mode: number;
}
export class SecondFactor extends Component<any, State> {
constructor(props: any) {
super(props);
this.state = {
mode: 0
}
}
onMenuChanged(event: any, value: number) {
this.setState({mode: value});
}
renderInner() {
const registerDevice = (
<div className={styles.register}>
<div>Register a new device</div>
<div className={styles.buttons}>
<Button variant="contained" color="primary">
Security key
</Button>
<Button variant="contained" color="primary">
One-time password
</Button>
</div>
</div>
)
const authenticate = (
<div className={styles.authenticate}>
<div className={styles.u2f}>
Touch your security key
<div>
<img src={pendrive} alt="usb key"/>
</div>
</div>
<table style={{width: '60%', margin: '2em auto'}}>
<tbody>
<tr>
<td style={{width: '40%'}}><hr/></td>
<td style={{width: '20%'}}>or</td>
<td style={{width: '40%'}}><hr/></td>
</tr>
</tbody>
</table>
<div className={styles.totp}>
Provide a one-time password
<div className={styles.totpField}>
<TextField
id="otp"
variant="outlined"
label="Password">
</TextField>
</div>
</div>
</div>
)
if (this.state.mode == 0) {
return authenticate;
}
else if (this.state.mode == 1) {
return registerDevice;
}
}
render() {
return (
<FormTemplate title="2-Factor">
<div className={styles.main}>
{this.renderInner()}
</div>
<BottomNavigation
value={this.state.mode}
onChange={this.onMenuChanged.bind(this)}
showLabels
className={styles.menu}
>
<BottomNavigationAction label="Authenticate" icon={<RestoreIcon />} />
<BottomNavigationAction label="Register" icon={<FavoriteIcon />} />
</BottomNavigation>
</FormTemplate>
)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,6 @@
import PortalLayout from "../layouts/PortalLayout/PortalLayout";
export const routes = [{
path: '/',
component: PortalLayout
}];

View File

@ -0,0 +1,37 @@
import FirstFactorView from "../views/FirstFactorView/FirstFactorView";
import SecondFactorView from "../views/SecondFactorView/SecondFactorView";
import ConfirmationSent from "../views/ConfirmationSentView/ConfirmationSentView";
import OneTimePasswordRegistrationView from "../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView";
import SecurityKeyRegistrationView from "../views/SecurityKeyRegistrationView/SecurityKeyRegistrationView";
import ForgotPasswordView from "../views/ForgotPasswordView/ForgotPasswordView";
import ResetPasswordView from "../views/ResetPasswordView/ResetPasswordView";
export const routes = [{
path: '/',
title: 'Login',
component: FirstFactorView,
}, {
path: '/2fa',
title: '2-factor',
component: SecondFactorView,
}, {
path: '/confirm',
title: 'e-mail sent',
component: ConfirmationSent
}, {
path: '/one-time-password-registration',
title: 'One-time password registration',
component: OneTimePasswordRegistrationView,
}, {
path: '/security-key-registration',
title: 'Security key registration',
component: SecurityKeyRegistrationView,
}, {
path: '/forgot-password',
title: 'Forgot password',
component: ForgotPasswordView,
}, {
path: '/reset-password',
title: 'Reset password',
component: ResetPasswordView,
}]

View File

@ -1,30 +0,0 @@
import React, { Component } from "react";
import logo from '../logo.svg';
import styles from "./form-template.module.css"
interface Props {
title: string;
}
export default class FormTemplate extends Component<Props> {
render() {
const children = this.props.children;
return (
<div className={styles.mainContent}>
<div className={styles.header}>
<h1>{this.props.title}</h1>
</div>
<div className={styles.frame}>
<div className={styles.innerFrame}>
{children}
</div>
</div>
<div className={styles.footer}>
<img src={logo} alt="logo"></img>
<div>Powered by <a href="#">Authelia</a></div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,26 @@
.image {
width: 100%;
display: inline-block
}
.image img {
width: 64px;
display: block;
}
.left {
width: 24%;
display: inline-block;
}
.right {
width: 76%;
display: inline-block;
vertical-align: top;
}
.buttonContainer {
padding-top: 20px;
}

View File

@ -0,0 +1,34 @@
import React, { Component } from "react";
import classnames from 'classnames';
import mail from '../../assets/images/mail.png';
import styles from './ConfirmationSentView.module.css';
import { Button } from "@material-ui/core";
import { RouterProps } from "react-router";
interface Props extends RouterProps {}
export default class ConfirmationSent extends Component<Props> {
render() {
return (
<div className={styles.main}>
<div className={classnames(styles.image, styles.left)}>
<img src={mail} alt="mail" />
</div>
<div className={styles.right}>
Please check your e-mails and follow the instructions to confirm the operation.
<div className={styles.buttonContainer}>
<Button
onClick={() => this.props.history.push('/')}
className={styles.button}
variant="contained"
color="primary">
Back to login
</Button>
</div>
</div>
</div>
)
}
}

View File

@ -0,0 +1,105 @@
import React, { Component, KeyboardEvent } from "react";
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import { Link } from "react-router-dom";
import { RouterProps } from "react-router";
import { WithStyles, withStyles } from "@material-ui/core";
import firstFactorViewStyles from '../../assets/jss/views/FirstFactorView/FirstFactorView';
interface Props extends RouterProps, WithStyles {}
interface State {
rememberMe: boolean;
}
class FirstFactorView extends Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
rememberMe: false
}
}
toggleRememberMe = () => {
this.setState({
rememberMe: !(this.state.rememberMe)
})
}
onLoginClicked = () => {
this.authenticate();
}
onPasswordKeyPressed = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
this.authenticate();
}
}
render() {
const { classes } = this.props;
return (
<div>
<div className={classes.fields}>
<div className={classes.field}>
<TextField
className={classes.input}
variant="outlined"
id="username"
label="Username">
</TextField>
</div>
<div className={classes.field}>
<TextField
className={classes.input}
id="password"
variant="outlined"
label="Password"
type="password"
onKeyPress={this.onPasswordKeyPressed}>
</TextField>
</div>
</div>
<div>
<div className={classes.buttons}>
<Button
onClick={this.onLoginClicked}
variant="contained"
color="primary">
Login
</Button>
</div>
<div className={classes.controls}>
<div className={classes.rememberMe}>
<FormControlLabel
control={
<Checkbox
checked={this.state.rememberMe}
onChange={this.toggleRememberMe}
color="primary"
/>
}
label="Remember me"
/>
</div>
<div className={classes.resetPassword}>
<Link to="/forgot-password">Forgot password?</Link>
</div>
</div>
</div>
</div>
)
}
private authenticate() {
this.props.history.push('/2fa');
}
}
export default withStyles(firstFactorViewStyles)(FirstFactorView);

View File

@ -0,0 +1,35 @@
import React, { Component } from "react";
import { TextField, WithStyles, withStyles, Button } from "@material-ui/core";
import styles from '../../assets/jss/views/ForgotPasswordView/ForgotPasswordView';
import { RouterProps } from "react-router";
interface Props extends WithStyles, RouterProps {}
class ForgotPasswordView extends Component<Props> {
render() {
const { classes } = this.props;
return (
<div>
<div>What's you e-mail address?</div>
<div className={classes.form}>
<TextField
className={classes.field}
variant="outlined"
id="email"
label="E-mail">
</TextField>
<Button
onClick={() => this.props.history.push('/reset-password')}
variant="contained"
color="primary"
className={classes.button}>
Next
</Button>
</div>
</div>
);
}
}
export default withStyles(styles)(ForgotPasswordView);

View File

@ -0,0 +1,8 @@
import React, { Component } from "react";
export default class OneTimePasswordRegistrationView extends Component {
render() {
return (<div></div>)
}
}

View File

@ -0,0 +1,41 @@
import React, { Component } from "react";
import { TextField, Button, WithStyles, withStyles } from "@material-ui/core";
import { RouterProps } from "react-router";
import styles from '../../assets/jss/views/ResetPasswordView/ResetPasswordView';
interface Props extends RouterProps, WithStyles {};
class ResetPasswordView extends Component<Props> {
render() {
const { classes } = this.props;
return (
<div>
<div>Enter your new password</div>
<div className={classes.form}>
<TextField
className={classes.field}
variant="outlined"
id="password1"
label="New password">
</TextField>
<TextField
className={classes.field}
variant="outlined"
id="password2"
label="Confirm password">
</TextField>
<Button
onClick={() => this.props.history.push('/')}
variant="contained"
color="primary"
className={classes.button}>
Next
</Button>
</div>
</div>
)
}
}
export default withStyles(styles)(ResetPasswordView);

View File

@ -0,0 +1,96 @@
import React, { Component } from 'react';
import { WithStyles, withStyles, Button, TextField } from '@material-ui/core';
import styles from '../../assets/jss/views/SecondFactorView/SecondFactorView';
import securityKeyImage from '../../assets/images/security-key-hand.png';
type Mode = 'u2f' | 'totp';
interface Props extends WithStyles {};
interface State {
mode: Mode;
}
class SecondFactorView extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
mode: 'u2f',
}
}
private toggleMode = () => {
if (this.state.mode === 'u2f') {
this.setState({mode: 'totp'});
} else if (this.state.mode === 'totp') {
this.setState({mode: 'u2f'});
}
}
private renderU2f() {
const { classes } = this.props;
return (
<div>
<div className={classes.imageContainer}>
<img src={securityKeyImage} alt='security key' className={classes.image}/>
</div>
<div>Insert your security key into a USB port and touch the gold disk.</div>
</div>
)
}
private renderTotp() {
const { classes } = this.props;
return (
<div>
<div>Provide a one-time password.</div>
<TextField
className={classes.totpField}
name="password"
id="password"
variant="outlined"
label="Password">
</TextField>
<Button
className={classes.totpButton}
variant="contained"
color="primary">
OK
</Button>
</div>
)
}
private renderMode() {
if (this.state.mode === 'u2f') {
return this.renderU2f();
} else if (this.state.mode === 'totp') {
return this.renderTotp();
}
}
render() {
const { classes } = this.props;
return (
<div className={classes.container}>
<div className={classes.body}>
{this.renderMode()}
</div>
<hr />
<div className={classes.footer}>
<a
className={classes.otherMethod}
href="#"
onClick={this.toggleMode}>
{this.state.mode === 'u2f' ? 'Use one-time password' : 'Use security key'}
</a>
<a className={classes.registerDevice} href="/security-key-registration">Register device</a>
</div>
</div>
)
}
}
export default withStyles(styles)(SecondFactorView);

View File

@ -0,0 +1,8 @@
import React, { Component } from "react";
export default class SecurityKeyRegistrationView extends Component {
render() {
return (<div></div>)
}
}

View File

@ -12,7 +12,12 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "preserve" "jsx": "preserve",
"lib": [
"dom",
"dom.iterable",
"esnext"
]
}, },
"include": [ "include": [
"src" "src"