Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit 3981e54

Browse files
author
Tom Linton
authored
Implement 2FA reset for T3 (#4528)
* 2fa reset implementation * Layout tweaks * Add tests * Remove payload from submitOtpUpdateSuccess action * Check otpKey not empty and fix typo * Add discord webhook on 2fa reset * Fix import
1 parent 723484d commit 3981e54

File tree

12 files changed

+7513
-7225
lines changed

12 files changed

+7513
-7225
lines changed

infra/token-transfer-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"numeral": "2.0.6",
4949
"optimize-css-assets-webpack-plugin": "5.0.3",
5050
"prettier": "1.19.1",
51-
"react": "16.13.1",
51+
"react": "16.14.0",
5252
"react-app-polyfill": "1.0.6",
5353
"react-bootstrap": "1.0.0",
5454
"react-chartjs-2": "2.9.0",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import agent from '@/utils/agent'
2+
import { apiUrl } from '@/constants'
3+
4+
export const VERIFY_OTP_PENDING = 'VERIFY_OTP_PENDING'
5+
export const VERIFY_OTP_SUCCESS = 'VERIFY_OTP_SUCCESS'
6+
export const VERIFY_OTP_ERROR = 'VERIFY_OTP_ERROR'
7+
export const SUBMIT_OTP_UPDATE_PENDING = 'SUBMIT_OTP_UPDATE_PENDING'
8+
export const SUBMIT_OTP_UPDATE_SUCCESS = 'SUBMIT_OTP_UPDATE_SUCCESS'
9+
export const SUBMIT_OTP_UPDATE_ERROR = 'SUBMIT_OTP_UPDATE_ERROR'
10+
11+
function verifyOtpPending() {
12+
return {
13+
type: VERIFY_OTP_PENDING
14+
}
15+
}
16+
17+
function verifyOtpSuccess(payload) {
18+
return {
19+
type: VERIFY_OTP_SUCCESS,
20+
payload
21+
}
22+
}
23+
24+
function verifyOtpError(error) {
25+
return {
26+
type: VERIFY_OTP_ERROR,
27+
error
28+
}
29+
}
30+
31+
function submitOtpUpdatePending() {
32+
return {
33+
type: SUBMIT_OTP_UPDATE_PENDING
34+
}
35+
}
36+
37+
function submitOtpUpdateSuccess() {
38+
return {
39+
type: SUBMIT_OTP_UPDATE_SUCCESS
40+
}
41+
}
42+
43+
function submitOtpUpdateError(error) {
44+
return {
45+
type: SUBMIT_OTP_UPDATE_ERROR,
46+
error
47+
}
48+
}
49+
50+
export function verifyOtp(data) {
51+
return dispatch => {
52+
dispatch(verifyOtpPending())
53+
54+
return agent
55+
.post(`${apiUrl}/api/user/otp`)
56+
.send(data)
57+
.then(response => dispatch(verifyOtpSuccess(response.body)))
58+
.catch(error => {
59+
dispatch(verifyOtpError(error))
60+
throw error
61+
})
62+
}
63+
}
64+
65+
export function submitOtpUpdate(data) {
66+
return dispatch => {
67+
dispatch(submitOtpUpdatePending())
68+
69+
return agent
70+
.post(`${apiUrl}/api/user/otp`)
71+
.send(data)
72+
.then(response => dispatch(submitOtpUpdateSuccess()))
73+
.catch(error => {
74+
dispatch(submitOtpUpdateError(error))
75+
throw error
76+
})
77+
}
78+
}
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import React, { Component } from 'react'
2+
import { connect } from 'react-redux'
3+
import { bindActionCreators } from 'redux'
4+
import ReactGA from 'react-ga'
5+
import get from 'lodash.get'
6+
7+
import { formInput, formFeedback } from '@/utils/formHelpers'
8+
import { verifyOtp, submitOtpUpdate } from '@/actions/otp'
9+
import {
10+
getVerifyError as getOtpVerifyError,
11+
getIsVerifying as getOtpIsVerifying,
12+
getUpdateError as getOtpUpdateError,
13+
getIsUpdating as getOtpIsUpdating,
14+
getOtp
15+
} from '@/reducers/otp'
16+
import Modal from '@/components/Modal'
17+
import SuccessIcon from '@/assets/success-icon.svg'
18+
import GoogleAuthenticatorIcon from '@/assets/google-authenticator.svg'
19+
20+
class OtpUpdateModal extends Component {
21+
constructor(props) {
22+
super(props)
23+
this.state = this.getInitialState()
24+
}
25+
26+
getInitialState = () => {
27+
const initialState = {
28+
oldCode: '',
29+
oldCodeError: null,
30+
newCode: '',
31+
newCodeError: null,
32+
modalState: 'OTP'
33+
}
34+
return initialState
35+
}
36+
37+
componentDidMount() {
38+
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`)
39+
}
40+
41+
componentDidUpdate(prevProps, prevState) {
42+
if (get(prevProps, 'otpVerifyError') !== this.props.otpVerifyError) {
43+
this.handleServerError(this.props.otpVerifyError)
44+
}
45+
46+
if (get(prevProps, 'otpUpdateError') !== this.props.otpUpdateError) {
47+
this.handleServerError(this.props.otpUpdateError)
48+
}
49+
50+
if (prevState.modalState !== this.state.modalState) {
51+
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`)
52+
}
53+
}
54+
55+
handleServerError(error) {
56+
if (error && error.status === 422) {
57+
// Parse validation errors from API
58+
if (error.response.body && error.response.body.errors) {
59+
error.response.body.errors.forEach(e => {
60+
this.setState({ [`${e.param}Error`]: e.msg })
61+
})
62+
} else {
63+
console.error(error.response.body)
64+
}
65+
}
66+
}
67+
68+
handleVerifySubmit = async event => {
69+
event.preventDefault()
70+
71+
try {
72+
await this.props.verifyOtp({ oldCode: this.state.oldCode })
73+
} catch (error) {
74+
// Error will be displayed in form, don't continue to two factor input
75+
return
76+
}
77+
78+
this.setState({
79+
modalState: 'Form'
80+
})
81+
}
82+
83+
handleFormSubmit = async event => {
84+
event.preventDefault()
85+
86+
try {
87+
await this.props.submitOtpUpdate({
88+
otpKey: this.props.otp.otpKey,
89+
oldCode: this.state.oldCode,
90+
newCode: this.state.newCode
91+
})
92+
} catch (error) {
93+
// Error will be displayed in form, don't continue to two factor input
94+
return
95+
}
96+
97+
this.setState({
98+
modalState: 'Thanks'
99+
})
100+
}
101+
102+
handleModalClose = () => {
103+
// Reset the state of the modal back to defaults
104+
this.setState(this.getInitialState())
105+
if (this.props.onModalClose) {
106+
this.props.onModalClose()
107+
}
108+
}
109+
110+
render() {
111+
return (
112+
<Modal appendToId="main" onClose={this.handleModalClose} closeBtn={true}>
113+
{this.state.modalState === 'OTP' && this.renderOtp()}
114+
{this.state.modalState === 'Form' && this.renderForm()}
115+
{this.state.modalState === 'Thanks' && this.renderThanks()}
116+
</Modal>
117+
)
118+
}
119+
120+
// First step, verify existing code
121+
renderOtp() {
122+
const input = formInput(this.state, state => this.setState(state))
123+
const Feedback = formFeedback(this.state)
124+
125+
return (
126+
<>
127+
<div className="row align-items-center mb-3 text-center text-sm-left">
128+
<div className="d-none d-sm-block col-sm-2">
129+
<GoogleAuthenticatorIcon width="80%" height="80%" />
130+
</div>
131+
<div className="col">
132+
<h1 className="my-2">Update 2FA</h1>
133+
</div>
134+
</div>
135+
136+
<hr />
137+
138+
<form onSubmit={this.handleVerifySubmit}>
139+
<div className="col-12 col-sm-8 offset-sm-2">
140+
<p>Enter the code generated by Google Authenticator.</p>
141+
<div className="form-group">
142+
<label htmlFor="oldCode">Code</label>
143+
<div
144+
className={`input-group ${
145+
this.state.oldCodeError ? 'is-invalid' : ''
146+
}`}
147+
>
148+
<input {...input('oldCode')} type="number" />
149+
</div>
150+
<div className={this.state.oldCodeError ? 'input-group-fix' : ''}>
151+
{Feedback('oldCode')}
152+
</div>
153+
</div>
154+
</div>
155+
<div className="actions mt-5">
156+
<div className="row">
157+
<div className="col text-left d-none d-sm-block">
158+
<button
159+
className="btn btn-outline-primary btn-lg"
160+
onClick={this.handleModalClose}
161+
>
162+
Cancel
163+
</button>
164+
</div>
165+
<div className="col text-sm-right mb-3 mb-sm-0">
166+
<button
167+
type="submit"
168+
className="btn btn-primary btn-lg"
169+
disabled={this.props.otpIsUpdating}
170+
>
171+
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'}
172+
</button>
173+
</div>
174+
</div>
175+
</div>
176+
</form>
177+
</>
178+
)
179+
}
180+
181+
// Second step, update 2FA app and verify new code
182+
renderForm() {
183+
const input = formInput(this.state, state => this.setState(state))
184+
const Feedback = formFeedback(this.state)
185+
186+
return (
187+
<>
188+
<div className="row align-items-center mb-3 text-center text-sm-left">
189+
<div className="d-none d-sm-block col-sm-2">
190+
<GoogleAuthenticatorIcon width="80%" height="80%" />
191+
</div>
192+
<div className="col">
193+
<h1 className="my-2">Verify 2FA</h1>
194+
</div>
195+
</div>
196+
197+
<hr />
198+
199+
<form onSubmit={this.handleFormSubmit}>
200+
<div>
201+
<p>
202+
Scan the QR code or use the secret key and enter the code
203+
generated by Google Authenticator to verify your new 2FA.
204+
</p>
205+
<img
206+
src={get(this.props.otp, 'otpQrUrl')}
207+
style={{ margin: '20px 0' }}
208+
/>
209+
</div>
210+
<p>
211+
<strong>Secret Key</strong>
212+
<br />
213+
{get(this.props.otp, 'encodedKey')}{' '}
214+
</p>
215+
<div className="col-12 col-sm-8 offset-sm-2">
216+
<div className="form-group">
217+
<label htmlFor="newCode">Code</label>
218+
<div
219+
className={`input-group ${
220+
this.state.newCodeError ? 'is-invalid' : ''
221+
}`}
222+
>
223+
<input {...input('newCode')} type="number" />
224+
</div>
225+
<div className={this.state.newCodeError ? 'input-group-fix' : ''}>
226+
{Feedback('newCode')}
227+
</div>
228+
</div>
229+
</div>
230+
<div className="actions mt-5">
231+
<div className="row">
232+
<div className="col text-left d-none d-sm-block">
233+
<button
234+
className="btn btn-outline-primary btn-lg"
235+
onClick={this.handleModalClose}
236+
>
237+
Cancel
238+
</button>
239+
</div>
240+
<div className="col text-sm-right mb-3 mb-sm-0">
241+
<button
242+
type="submit"
243+
className="btn btn-primary btn-lg"
244+
disabled={this.props.otpIsUpdating}
245+
>
246+
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'}
247+
</button>
248+
</div>
249+
</div>
250+
</div>
251+
</form>
252+
</>
253+
)
254+
}
255+
256+
renderThanks() {
257+
return (
258+
<>
259+
<div className="my-3">
260+
<SuccessIcon style={{ marginRight: '-46px' }} />
261+
</div>
262+
<h1 className="mb-2">Success</h1>
263+
<p>Your settings have been updated.</p>
264+
<div className="actions mt-5">
265+
<div className="row">
266+
<div className="col text-right">
267+
<button
268+
className="btn btn-primary btn-lg"
269+
onClick={this.props.onModalClose}
270+
>
271+
Done
272+
</button>
273+
</div>
274+
</div>
275+
</div>
276+
</>
277+
)
278+
}
279+
}
280+
281+
const mapStateToProps = ({ otp }) => {
282+
return {
283+
otpVerifyError: getOtpVerifyError(otp),
284+
otpIsVerifying: getOtpIsVerifying(otp),
285+
otpUpdateError: getOtpUpdateError(otp),
286+
otpIsUpdating: getOtpIsUpdating(otp),
287+
otp: getOtp(otp)
288+
}
289+
}
290+
291+
const mapDispatchToProps = dispatch =>
292+
bindActionCreators(
293+
{
294+
verifyOtp,
295+
submitOtpUpdate
296+
},
297+
dispatch
298+
)
299+
300+
export default connect(mapStateToProps, mapDispatchToProps)(OtpUpdateModal)

0 commit comments

Comments
 (0)