First complete version of the Authelia frontend in React.
5001
client-react/package-lock.json
generated
|
@ -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": {
|
||||||
|
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
BIN
client-react/src/assets/images/security-key-hand.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
client-react/src/assets/images/security-key-large.png
Normal file
After Width: | Height: | Size: 34 KiB |
BIN
client-react/src/assets/images/security-key.png
Normal file
After Width: | Height: | Size: 11 KiB |
51
client-react/src/assets/images/user.svg
Normal 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 |
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
3
client-react/src/constants.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
export const AUTHELIA_URL = "https://www.authelia.com/"
|
||||||
|
export const AUTHELIA_GITHUB_URL = "https://github.com/clems4ever/authelia";
|
|
@ -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;
|
||||||
}
|
}
|
50
client-react/src/layouts/PortalLayout/PortalLayout.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 |
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
.main {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image img {
|
|
||||||
width: 64px;
|
|
||||||
display: block;
|
|
||||||
margin: 2em auto;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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%;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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%;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 6.6 KiB |
6
client-react/src/routes/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import PortalLayout from "../layouts/PortalLayout/PortalLayout";
|
||||||
|
|
||||||
|
export const routes = [{
|
||||||
|
path: '/',
|
||||||
|
component: PortalLayout
|
||||||
|
}];
|
37
client-react/src/routes/routes.ts
Normal 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,
|
||||||
|
}]
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
105
client-react/src/views/FirstFactorView/FirstFactorView.tsx
Normal 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);
|
|
@ -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);
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default class OneTimePasswordRegistrationView extends Component {
|
||||||
|
render() {
|
||||||
|
return (<div></div>)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
96
client-react/src/views/SecondFactorView/SecondFactorView.tsx
Normal 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);
|
|
@ -0,0 +1,8 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default class SecurityKeyRegistrationView extends Component {
|
||||||
|
render() {
|
||||||
|
return (<div></div>)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|