Skip to content

Commit 2a626a3

Browse files
committed
Kinda working checkpoint magic
1 parent 4eeec58 commit 2a626a3

File tree

4 files changed

+119
-3
lines changed

4 files changed

+119
-3
lines changed

resources/assets/styles/components/forms.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ input[type=number] {
5050
*/
5151
.input:not(.open-label) {
5252
@apply .appearance-none .p-3 .rounded .border .border-neutral-200 .text-neutral-800 .w-full;
53+
min-width: 0;
5354
transition: border 150ms linear;
5455

5556
&:focus {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import * as React from 'react';
2+
import { RouteComponentProps } from 'react-router';
3+
import { connect } from 'react-redux';
4+
import { pushFlashMessage, clearAllFlashMessages } from '@/redux/actions/flash';
5+
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
6+
7+
type State = Readonly<{
8+
isLoading: boolean;
9+
errorMessage?: string;
10+
code: string;
11+
}>;
12+
13+
class LoginCheckpointContainer extends React.PureComponent<RouteComponentProps, State> {
14+
state: State = {
15+
code: '',
16+
isLoading: false,
17+
};
18+
19+
moveToNextInput (e: React.KeyboardEvent<HTMLInputElement>, isBackspace: boolean = false) {
20+
const form = e.currentTarget.form;
21+
22+
if (form) {
23+
const index = Array.prototype.indexOf.call(form, e.currentTarget);
24+
const element = form.elements[index + (isBackspace ? -1 : 1)];
25+
26+
// @ts-ignore
27+
element && element.focus();
28+
}
29+
}
30+
31+
handleNumberInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
32+
const number = Number(e.key);
33+
if (isNaN(number)) {
34+
return;
35+
}
36+
37+
this.setState(s => ({ code: s.code + number.toString() }));
38+
this.moveToNextInput(e);
39+
};
40+
41+
handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => {
42+
const isBackspace = e.key === 'Delete' || e.key === 'Backspace';
43+
44+
if (!isBackspace || e.currentTarget.value.length > 0) {
45+
e.currentTarget.value = '';
46+
return;
47+
}
48+
49+
this.setState(s => ({ code: s.code.substring(0, s.code.length - 2) }));
50+
e.currentTarget.value = '';
51+
this.moveToNextInput(e, true);
52+
};
53+
54+
render () {
55+
return (
56+
<React.Fragment>
57+
<h2 className={'text-center text-neutral-100 font-medium py-4'}>
58+
Device Checkpoint
59+
</h2>
60+
<NetworkErrorMessage message={this.state.errorMessage}/>
61+
<form className={'login-box'} onSubmit={() => null}>
62+
<p className={'text-sm text-neutral-700'}>
63+
This account is protected with two-factor authentication. Please provide an authentication
64+
code from your device in order to continue.
65+
</p>
66+
<div className={'flex mt-6'}>
67+
{
68+
[1, 2, 3, 4, 5, 6].map((_, index) => (
69+
<input
70+
autoFocus={index === 0}
71+
key={`input_${index}`}
72+
type={'number'}
73+
onKeyPress={this.handleNumberInput}
74+
onKeyDown={this.handleBackspace}
75+
maxLength={1}
76+
className={`input block flex-1 text-center text-lg ${index === 5 ? undefined : 'mr-6'}`}
77+
/>
78+
))
79+
}
80+
</div>
81+
<div className={'mt-6'}>
82+
<button
83+
type={'submit'}
84+
className={'btn btn-primary btn-jumbo'}
85+
disabled={this.state.isLoading || this.state.code.length !== 6}
86+
>
87+
{this.state.isLoading ?
88+
<span className={'spinner white'}>&nbsp;</span>
89+
:
90+
'Continue'
91+
}
92+
</button>
93+
</div>
94+
</form>
95+
</React.Fragment>
96+
);
97+
}
98+
}
99+
100+
const mapDispatchToProps = {
101+
pushFlashMessage,
102+
clearAllFlashMessages,
103+
};
104+
105+
export default connect(null, mapDispatchToProps)(LoginCheckpointContainer);

resources/scripts/components/auth/LoginContainer.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import OpenInputField from '@/components/forms/OpenInputField';
3-
import { Link } from 'react-router-dom';
3+
import { Link, RouteComponentProps } from 'react-router-dom';
44
import login from '@/api/auth/login';
55
import { httpErrorToHuman } from '@/api/http';
66
import NetworkErrorMessage from '@/components/NetworkErrorMessage';
@@ -12,7 +12,7 @@ type State = Readonly<{
1212
password?: string;
1313
}>;
1414

15-
export default class LoginContainer extends React.PureComponent<{}, State> {
15+
export default class LoginContainer extends React.PureComponent<RouteComponentProps, State> {
1616
username = React.createRef<HTMLInputElement>();
1717

1818
state: State = {
@@ -27,7 +27,15 @@ export default class LoginContainer extends React.PureComponent<{}, State> {
2727
this.setState({ isLoading: true }, () => {
2828
login(username!, password!)
2929
.then(response => {
30+
if (response.complete) {
31+
// @ts-ignore
32+
window.location = response.intended || '/';
33+
return;
34+
}
3035

36+
this.props.history.replace('/login/checkpoint', {
37+
token: response.token,
38+
});
3139
})
3240
.catch(error => this.setState({
3341
isLoading: false,

resources/scripts/routers/AuthenticationRouter.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
55
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
66
import FlashMessageRender from '@/components/FlashMessageRender';
77
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
8+
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
89

910
export default class AuthenticationRouter extends React.PureComponent {
1011
render () {
@@ -17,7 +18,8 @@ export default class AuthenticationRouter extends React.PureComponent {
1718
<section>
1819
<FlashMessageRender/>
1920
<Switch location={location}>
20-
<Route path={'/login'} component={LoginContainer}/>
21+
<Route path={'/login'} component={LoginContainer} exact/>
22+
<Route path={'/login/checkpoint'} component={LoginCheckpointContainer}/>
2123
<Route path={'/password'} component={ForgotPasswordContainer} exact/>
2224
<Route path={'/password/reset/:token'} component={ResetPasswordContainer}/>
2325
<Route path={'/checkpoint'}/>

0 commit comments

Comments
 (0)