This repository has been archived by the owner on Sep 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
- Loading branch information
Tom Linton
authored
Feb 11, 2021
1 parent
723484d
commit 3981e54
Showing
12 changed files
with
7,513 additions
and
7,225 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import agent from '@/utils/agent' | ||
import { apiUrl } from '@/constants' | ||
|
||
export const VERIFY_OTP_PENDING = 'VERIFY_OTP_PENDING' | ||
export const VERIFY_OTP_SUCCESS = 'VERIFY_OTP_SUCCESS' | ||
export const VERIFY_OTP_ERROR = 'VERIFY_OTP_ERROR' | ||
export const SUBMIT_OTP_UPDATE_PENDING = 'SUBMIT_OTP_UPDATE_PENDING' | ||
export const SUBMIT_OTP_UPDATE_SUCCESS = 'SUBMIT_OTP_UPDATE_SUCCESS' | ||
export const SUBMIT_OTP_UPDATE_ERROR = 'SUBMIT_OTP_UPDATE_ERROR' | ||
|
||
function verifyOtpPending() { | ||
return { | ||
type: VERIFY_OTP_PENDING | ||
} | ||
} | ||
|
||
function verifyOtpSuccess(payload) { | ||
return { | ||
type: VERIFY_OTP_SUCCESS, | ||
payload | ||
} | ||
} | ||
|
||
function verifyOtpError(error) { | ||
return { | ||
type: VERIFY_OTP_ERROR, | ||
error | ||
} | ||
} | ||
|
||
function submitOtpUpdatePending() { | ||
return { | ||
type: SUBMIT_OTP_UPDATE_PENDING | ||
} | ||
} | ||
|
||
function submitOtpUpdateSuccess() { | ||
return { | ||
type: SUBMIT_OTP_UPDATE_SUCCESS | ||
} | ||
} | ||
|
||
function submitOtpUpdateError(error) { | ||
return { | ||
type: SUBMIT_OTP_UPDATE_ERROR, | ||
error | ||
} | ||
} | ||
|
||
export function verifyOtp(data) { | ||
return dispatch => { | ||
dispatch(verifyOtpPending()) | ||
|
||
return agent | ||
.post(`${apiUrl}/api/user/otp`) | ||
.send(data) | ||
.then(response => dispatch(verifyOtpSuccess(response.body))) | ||
.catch(error => { | ||
dispatch(verifyOtpError(error)) | ||
throw error | ||
}) | ||
} | ||
} | ||
|
||
export function submitOtpUpdate(data) { | ||
return dispatch => { | ||
dispatch(submitOtpUpdatePending()) | ||
|
||
return agent | ||
.post(`${apiUrl}/api/user/otp`) | ||
.send(data) | ||
.then(response => dispatch(submitOtpUpdateSuccess())) | ||
.catch(error => { | ||
dispatch(submitOtpUpdateError(error)) | ||
throw error | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
import React, { Component } from 'react' | ||
import { connect } from 'react-redux' | ||
import { bindActionCreators } from 'redux' | ||
import ReactGA from 'react-ga' | ||
import get from 'lodash.get' | ||
|
||
import { formInput, formFeedback } from '@/utils/formHelpers' | ||
import { verifyOtp, submitOtpUpdate } from '@/actions/otp' | ||
import { | ||
getVerifyError as getOtpVerifyError, | ||
getIsVerifying as getOtpIsVerifying, | ||
getUpdateError as getOtpUpdateError, | ||
getIsUpdating as getOtpIsUpdating, | ||
getOtp | ||
} from '@/reducers/otp' | ||
import Modal from '@/components/Modal' | ||
import SuccessIcon from '@/assets/success-icon.svg' | ||
import GoogleAuthenticatorIcon from '@/assets/google-authenticator.svg' | ||
|
||
class OtpUpdateModal extends Component { | ||
constructor(props) { | ||
super(props) | ||
this.state = this.getInitialState() | ||
} | ||
|
||
getInitialState = () => { | ||
const initialState = { | ||
oldCode: '', | ||
oldCodeError: null, | ||
newCode: '', | ||
newCodeError: null, | ||
modalState: 'OTP' | ||
} | ||
return initialState | ||
} | ||
|
||
componentDidMount() { | ||
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`) | ||
} | ||
|
||
componentDidUpdate(prevProps, prevState) { | ||
if (get(prevProps, 'otpVerifyError') !== this.props.otpVerifyError) { | ||
this.handleServerError(this.props.otpVerifyError) | ||
} | ||
|
||
if (get(prevProps, 'otpUpdateError') !== this.props.otpUpdateError) { | ||
this.handleServerError(this.props.otpUpdateError) | ||
} | ||
|
||
if (prevState.modalState !== this.state.modalState) { | ||
ReactGA.modalview(`/otpUpdate/${this.state.modalState.toLowerCase()}`) | ||
} | ||
} | ||
|
||
handleServerError(error) { | ||
if (error && error.status === 422) { | ||
// Parse validation errors from API | ||
if (error.response.body && error.response.body.errors) { | ||
error.response.body.errors.forEach(e => { | ||
this.setState({ [`${e.param}Error`]: e.msg }) | ||
}) | ||
} else { | ||
console.error(error.response.body) | ||
} | ||
} | ||
} | ||
|
||
handleVerifySubmit = async event => { | ||
event.preventDefault() | ||
|
||
try { | ||
await this.props.verifyOtp({ oldCode: this.state.oldCode }) | ||
} catch (error) { | ||
// Error will be displayed in form, don't continue to two factor input | ||
return | ||
} | ||
|
||
this.setState({ | ||
modalState: 'Form' | ||
}) | ||
} | ||
|
||
handleFormSubmit = async event => { | ||
event.preventDefault() | ||
|
||
try { | ||
await this.props.submitOtpUpdate({ | ||
otpKey: this.props.otp.otpKey, | ||
oldCode: this.state.oldCode, | ||
newCode: this.state.newCode | ||
}) | ||
} catch (error) { | ||
// Error will be displayed in form, don't continue to two factor input | ||
return | ||
} | ||
|
||
this.setState({ | ||
modalState: 'Thanks' | ||
}) | ||
} | ||
|
||
handleModalClose = () => { | ||
// Reset the state of the modal back to defaults | ||
this.setState(this.getInitialState()) | ||
if (this.props.onModalClose) { | ||
this.props.onModalClose() | ||
} | ||
} | ||
|
||
render() { | ||
return ( | ||
<Modal appendToId="main" onClose={this.handleModalClose} closeBtn={true}> | ||
{this.state.modalState === 'OTP' && this.renderOtp()} | ||
{this.state.modalState === 'Form' && this.renderForm()} | ||
{this.state.modalState === 'Thanks' && this.renderThanks()} | ||
</Modal> | ||
) | ||
} | ||
|
||
// First step, verify existing code | ||
renderOtp() { | ||
const input = formInput(this.state, state => this.setState(state)) | ||
const Feedback = formFeedback(this.state) | ||
|
||
return ( | ||
<> | ||
<div className="row align-items-center mb-3 text-center text-sm-left"> | ||
<div className="d-none d-sm-block col-sm-2"> | ||
<GoogleAuthenticatorIcon width="80%" height="80%" /> | ||
</div> | ||
<div className="col"> | ||
<h1 className="my-2">Update 2FA</h1> | ||
</div> | ||
</div> | ||
|
||
<hr /> | ||
|
||
<form onSubmit={this.handleVerifySubmit}> | ||
<div className="col-12 col-sm-8 offset-sm-2"> | ||
<p>Enter the code generated by Google Authenticator.</p> | ||
<div className="form-group"> | ||
<label htmlFor="oldCode">Code</label> | ||
<div | ||
className={`input-group ${ | ||
this.state.oldCodeError ? 'is-invalid' : '' | ||
}`} | ||
> | ||
<input {...input('oldCode')} type="number" /> | ||
</div> | ||
<div className={this.state.oldCodeError ? 'input-group-fix' : ''}> | ||
{Feedback('oldCode')} | ||
</div> | ||
</div> | ||
</div> | ||
<div className="actions mt-5"> | ||
<div className="row"> | ||
<div className="col text-left d-none d-sm-block"> | ||
<button | ||
className="btn btn-outline-primary btn-lg" | ||
onClick={this.handleModalClose} | ||
> | ||
Cancel | ||
</button> | ||
</div> | ||
<div className="col text-sm-right mb-3 mb-sm-0"> | ||
<button | ||
type="submit" | ||
className="btn btn-primary btn-lg" | ||
disabled={this.props.otpIsUpdating} | ||
> | ||
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'} | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
</> | ||
) | ||
} | ||
|
||
// Second step, update 2FA app and verify new code | ||
renderForm() { | ||
const input = formInput(this.state, state => this.setState(state)) | ||
const Feedback = formFeedback(this.state) | ||
|
||
return ( | ||
<> | ||
<div className="row align-items-center mb-3 text-center text-sm-left"> | ||
<div className="d-none d-sm-block col-sm-2"> | ||
<GoogleAuthenticatorIcon width="80%" height="80%" /> | ||
</div> | ||
<div className="col"> | ||
<h1 className="my-2">Verify 2FA</h1> | ||
</div> | ||
</div> | ||
|
||
<hr /> | ||
|
||
<form onSubmit={this.handleFormSubmit}> | ||
<div> | ||
<p> | ||
Scan the QR code or use the secret key and enter the code | ||
generated by Google Authenticator to verify your new 2FA. | ||
</p> | ||
<img | ||
src={get(this.props.otp, 'otpQrUrl')} | ||
style={{ margin: '20px 0' }} | ||
/> | ||
</div> | ||
<p> | ||
<strong>Secret Key</strong> | ||
<br /> | ||
{get(this.props.otp, 'encodedKey')}{' '} | ||
</p> | ||
<div className="col-12 col-sm-8 offset-sm-2"> | ||
<div className="form-group"> | ||
<label htmlFor="newCode">Code</label> | ||
<div | ||
className={`input-group ${ | ||
this.state.newCodeError ? 'is-invalid' : '' | ||
}`} | ||
> | ||
<input {...input('newCode')} type="number" /> | ||
</div> | ||
<div className={this.state.newCodeError ? 'input-group-fix' : ''}> | ||
{Feedback('newCode')} | ||
</div> | ||
</div> | ||
</div> | ||
<div className="actions mt-5"> | ||
<div className="row"> | ||
<div className="col text-left d-none d-sm-block"> | ||
<button | ||
className="btn btn-outline-primary btn-lg" | ||
onClick={this.handleModalClose} | ||
> | ||
Cancel | ||
</button> | ||
</div> | ||
<div className="col text-sm-right mb-3 mb-sm-0"> | ||
<button | ||
type="submit" | ||
className="btn btn-primary btn-lg" | ||
disabled={this.props.otpIsUpdating} | ||
> | ||
{this.props.otpIsUpdating ? 'Loading...' : 'Continue'} | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</form> | ||
</> | ||
) | ||
} | ||
|
||
renderThanks() { | ||
return ( | ||
<> | ||
<div className="my-3"> | ||
<SuccessIcon style={{ marginRight: '-46px' }} /> | ||
</div> | ||
<h1 className="mb-2">Success</h1> | ||
<p>Your settings have been updated.</p> | ||
<div className="actions mt-5"> | ||
<div className="row"> | ||
<div className="col text-right"> | ||
<button | ||
className="btn btn-primary btn-lg" | ||
onClick={this.props.onModalClose} | ||
> | ||
Done | ||
</button> | ||
</div> | ||
</div> | ||
</div> | ||
</> | ||
) | ||
} | ||
} | ||
|
||
const mapStateToProps = ({ otp }) => { | ||
return { | ||
otpVerifyError: getOtpVerifyError(otp), | ||
otpIsVerifying: getOtpIsVerifying(otp), | ||
otpUpdateError: getOtpUpdateError(otp), | ||
otpIsUpdating: getOtpIsUpdating(otp), | ||
otp: getOtp(otp) | ||
} | ||
} | ||
|
||
const mapDispatchToProps = dispatch => | ||
bindActionCreators( | ||
{ | ||
verifyOtp, | ||
submitOtpUpdate | ||
}, | ||
dispatch | ||
) | ||
|
||
export default connect(mapStateToProps, mapDispatchToProps)(OtpUpdateModal) |
Oops, something went wrong.