Kinda working checkpoint magic
This commit is contained in:
parent
4eeec58c59
commit
2a626a3e1f
4 changed files with 119 additions and 3 deletions
|
@ -50,6 +50,7 @@ input[type=number] {
|
||||||
*/
|
*/
|
||||||
.input:not(.open-label) {
|
.input:not(.open-label) {
|
||||||
@apply .appearance-none .p-3 .rounded .border .border-neutral-200 .text-neutral-800 .w-full;
|
@apply .appearance-none .p-3 .rounded .border .border-neutral-200 .text-neutral-800 .w-full;
|
||||||
|
min-width: 0;
|
||||||
transition: border 150ms linear;
|
transition: border 150ms linear;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|
105
resources/scripts/components/auth/LoginCheckpointContainer.tsx
Normal file
105
resources/scripts/components/auth/LoginCheckpointContainer.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
|
||||||
|
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
|
||||||
|
|
||||||
|
type State = Readonly<{
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
code: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
class LoginCheckpointContainer extends React.PureComponent<RouteComponentProps, State> {
|
||||||
|
state: State = {
|
||||||
|
code: '',
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
moveToNextInput (e: React.KeyboardEvent<HTMLInputElement>, isBackspace: boolean = false) {
|
||||||
|
const form = e.currentTarget.form;
|
||||||
|
|
||||||
|
if (form) {
|
||||||
|
const index = Array.prototype.indexOf.call(form, e.currentTarget);
|
||||||
|
const element = form.elements[index + (isBackspace ? -1 : 1)];
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
element && element.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNumberInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const number = Number(e.key);
|
||||||
|
if (isNaN(number)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(s => ({ code: s.code + number.toString() }));
|
||||||
|
this.moveToNextInput(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const isBackspace = e.key === 'Delete' || e.key === 'Backspace';
|
||||||
|
|
||||||
|
if (!isBackspace || e.currentTarget.value.length > 0) {
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(s => ({ code: s.code.substring(0, s.code.length - 2) }));
|
||||||
|
e.currentTarget.value = '';
|
||||||
|
this.moveToNextInput(e, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
|
||||||
|
Device Checkpoint
|
||||||
|
</h2>
|
||||||
|
<NetworkErrorMessage message={this.state.errorMessage}/>
|
||||||
|
<form className={'login-box'} onSubmit={() => null}>
|
||||||
|
<p className={'text-sm text-neutral-700'}>
|
||||||
|
This account is protected with two-factor authentication. Please provide an authentication
|
||||||
|
code from your device in order to continue.
|
||||||
|
</p>
|
||||||
|
<div className={'flex mt-6'}>
|
||||||
|
{
|
||||||
|
[1, 2, 3, 4, 5, 6].map((_, index) => (
|
||||||
|
<input
|
||||||
|
autoFocus={index === 0}
|
||||||
|
key={`input_${index}`}
|
||||||
|
type={'number'}
|
||||||
|
onKeyPress={this.handleNumberInput}
|
||||||
|
onKeyDown={this.handleBackspace}
|
||||||
|
maxLength={1}
|
||||||
|
className={`input block flex-1 text-center text-lg ${index === 5 ? undefined : 'mr-6'}`}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={'mt-6'}>
|
||||||
|
<button
|
||||||
|
type={'submit'}
|
||||||
|
className={'btn btn-primary btn-jumbo'}
|
||||||
|
disabled={this.state.isLoading || this.state.code.length !== 6}
|
||||||
|
>
|
||||||
|
{this.state.isLoading ?
|
||||||
|
<span className={'spinner white'}> </span>
|
||||||
|
:
|
||||||
|
'Continue'
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
pushFlashMessage,
|
||||||
|
clearAllFlashMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToProps)(LoginCheckpointContainer);
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import OpenInputField from '@/components/forms/OpenInputField';
|
import OpenInputField from '@/components/forms/OpenInputField';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link, RouteComponentProps } from 'react-router-dom';
|
||||||
import login from '@/api/auth/login';
|
import login from '@/api/auth/login';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
|
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
|
||||||
|
@ -12,7 +12,7 @@ type State = Readonly<{
|
||||||
password?: string;
|
password?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export default class LoginContainer extends React.PureComponent<{}, State> {
|
export default class LoginContainer extends React.PureComponent<RouteComponentProps, State> {
|
||||||
username = React.createRef<HTMLInputElement>();
|
username = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
state: State = {
|
state: State = {
|
||||||
|
@ -27,7 +27,15 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
|
||||||
this.setState({ isLoading: true }, () => {
|
this.setState({ isLoading: true }, () => {
|
||||||
login(username!, password!)
|
login(username!, password!)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
if (response.complete) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.location = response.intended || '/';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.history.replace('/login/checkpoint', {
|
||||||
|
token: response.token,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(error => this.setState({
|
.catch(error => this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
|
||||||
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
|
||||||
|
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
|
||||||
|
|
||||||
export default class AuthenticationRouter extends React.PureComponent {
|
export default class AuthenticationRouter extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
|
@ -17,7 +18,8 @@ export default class AuthenticationRouter extends React.PureComponent {
|
||||||
<section>
|
<section>
|
||||||
<FlashMessageRender/>
|
<FlashMessageRender/>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={'/login'} component={LoginContainer}/>
|
<Route path={'/login'} component={LoginContainer} exact/>
|
||||||
|
<Route path={'/login/checkpoint'} component={LoginCheckpointContainer}/>
|
||||||
<Route path={'/password'} component={ForgotPasswordContainer} exact/>
|
<Route path={'/password'} component={ForgotPasswordContainer} exact/>
|
||||||
<Route path={'/password/reset/:token'} component={ResetPasswordContainer}/>
|
<Route path={'/password/reset/:token'} component={ResetPasswordContainer}/>
|
||||||
<Route path={'/checkpoint'}/>
|
<Route path={'/checkpoint'}/>
|
||||||
|
|
Loading…
Reference in a new issue