First complete version of the Authelia frontend in React.
4991
client-react/package-lock.json
generated
|
@ -3,15 +3,20 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"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/react": "^16.4.18",
|
||||
"@types/react-dom": "^16.0.9",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"classnames": "^2.2.6",
|
||||
"jss": "^9.8.7",
|
||||
"react": "^16.6.0",
|
||||
"react-dom": "^16.6.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts": "2.1.1",
|
||||
"react-scripts": "^2.1.3",
|
||||
"typescript": "^3.1.6"
|
||||
},
|
||||
"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.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<title>Authelia - Portal</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import React, { Component } from 'react';
|
||||
import './App.css';
|
||||
|
||||
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
|
||||
import { FirstFactor } from './pages/first-factor/first-factor';
|
||||
import { SecondFactor } from './pages/second-factor/second-factor';
|
||||
import ConfirmationSent from './pages/confirmation-sent/confirmation-sent';
|
||||
import { Router, Route, Switch } from "react-router-dom";
|
||||
import { routes } from './routes/index';
|
||||
import { createBrowserHistory } from 'history';
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Router>
|
||||
<Router history={history}>
|
||||
<div className="App">
|
||||
<Route exact path="/" component={FirstFactor} />
|
||||
<Route exact path="/2fa" component={SecondFactor} />
|
||||
<Route exact path="/confirmation" component={ConfirmationSent} />
|
||||
<Switch>
|
||||
{routes.map((r, key) => {
|
||||
return <Route path={r.path} component={r.component} key={key}/>
|
||||
})}
|
||||
</Switch>
|
||||
</div>
|
||||
</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 {
|
||||
width: 440px;
|
||||
margin: 0 auto;
|
||||
padding: 1.5em 3em 3em 3em;
|
||||
}
|
||||
|
||||
/* HEADER */
|
||||
|
||||
.header {
|
||||
margin-bottom: 2em;
|
||||
text-align: center;
|
||||
padding: 50px 0px;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 30px 40px;
|
||||
}
|
||||
|
||||
.innerFrame {
|
||||
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 {
|
||||
margin-top: 1em;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.75em;
|
||||
font-size: 0.65em;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.footer img {
|
||||
width: 64px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
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,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
|