Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[totp] feat: set and verify tools #2127

Merged
merged 18 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions src/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import { clearStateLocalStorage } from "../lib/local_storage";
import * as pki from "../lib/pki";
import * as sel from "../selectors";
import act from "./methods";
import { PAYWALL_STATUS_PAID, DCC_SUPPORT_VOTE } from "../constants";
import {
PAYWALL_STATUS_PAID,
DCC_SUPPORT_VOTE,
TOTP_DEFAULT_TYPE
} from "../constants";

export const onResetNewUser = act.RESET_NEW_USER;

Expand Down Expand Up @@ -218,11 +222,11 @@ export const onSearchUser = (query, isCMS) => (dispatch) => {
// onLogin handles a user's login. If it is his first login on the app
// after registering, his key will be saved under his email. If so, it
// changes the storage key to his uuid.
export const onLogin = ({ email, password }) =>
export const onLogin = ({ email, password, code }) =>
withCsrf((dispatch, _, csrf) => {
dispatch(act.REQUEST_LOGIN({ email }));
return api
.login(csrf, email, password)
.login(csrf, email, password, code)
.then((response) => {
dispatch(act.RECEIVE_LOGIN(response));
const { userid, username } = response;
Expand Down Expand Up @@ -1531,3 +1535,31 @@ export const onSubmitDccComment = (currentUserID, token, comment, parentid) =>
throw error;
});
});

export const onSetTotp = (code = "", type = TOTP_DEFAULT_TYPE) =>
withCsrf((dispatch, _, csrf) => {
dispatch(act.REQUEST_SET_TOTP({}));
return api
.setTotp(csrf, type, code)
.then((response) => {
dispatch(act.RECEIVE_SET_TOTP(response));
})
.catch((e) => {
dispatch(act.RECEIVE_SET_TOTP(null, e));
throw e;
});
});

export const onVerifyTotp = (code) =>
withCsrf((dispatch, _, csrf) => {
dispatch(act.REQUEST_VERIFY_TOTP({}));
return api
.verifyTotp(csrf, code)
.then((response) => {
dispatch(act.RECEIVE_VERIFY_TOTP(response));
})
.catch((e) => {
dispatch(act.RECEIVE_VERIFY_TOTP(null, e));
throw e;
});
});
6 changes: 6 additions & 0 deletions src/actions/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,9 @@ export const LOAD_DRAFT_DCCS = "LOAD_DRAFT_DCCS";

export const REQUEST_EDIT_CMS_USER = "API_REQUEST_EDIT_CMS_USER";
export const RECEIVE_EDIT_CMS_USER = "API_RECEIVE_EDIT_CMS_USER";

export const REQUEST_SET_TOTP = "API_REQUEST_SET_TOTP";
export const RECEIVE_SET_TOTP = "API_RECEIVE_SET_TOTP";

export const REQUEST_VERIFY_TOTP = "API_REQUEST_VERIFY_TOTP";
export const RECEIVE_VERIFY_TOTP = "API_RECEIVE_VERIFY_TOTP";
94 changes: 94 additions & 0 deletions src/components/DigitsInput/DigitsInput.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, { useState, useRef, useEffect } from "react";
import PropTypes from "prop-types";
import { NumberInput, classNames } from "pi-ui";
import styles from "./DigitsInput.module.css";

const getDigitsArrayFromCode = (code = "", length) => {
let newDigits = code.split("").slice(0, length);
if (newDigits.length < length) {
const fillArray = Array(length - newDigits.length).fill("");
newDigits = [...newDigits, ...fillArray];
}
return newDigits;
};

const DigitsInput = ({ length, onChange, className, code, tabIndex }) => {
const [digits, setDigits] = useState(getDigitsArrayFromCode(code, length));
const [focused, setFocused] = useState(false);
const inputRef = useRef(null);

const handleChangeDigit = (e) => {
e && e.preventDefault();
const newCode = e.target.value.toString();
const numbersOnlyCode = newCode.replace(/^\D+/g, "");
onChange(getDigitsArrayFromCode(numbersOnlyCode, length).join(""));
};

useEffect(() => {
setDigits(getDigitsArrayFromCode(code, length));
}, [code, length]);

const onChangeDigit = (index) => {
const newDigits = [...digits];
newDigits[index] = "";
inputRef.current.selectionStart = index;
inputRef.current.selectionEnd = index + 1;
inputRef.current.focus();
};

return (
<>
<input
type="text"
className={styles.mainInput}
autoFocus
onChange={handleChangeDigit}
value={digits.join("")}
ref={inputRef}
onBlur={() => {
setFocused(false);
}}
/>
<div
className={classNames(
className,
styles.digitsWrapper,
focused && styles.focusedInput
)}>
{digits.map((value, index) => {
return (
<NumberInput
id={`id-digit-${index}`}
key={`digit-${index}`}
defaultValue={value}
tabIndex={tabIndex}
onFocus={(e) => {
e && e.target && e.target.value
? onChangeDigit(index)
: inputRef.current.focus();
setFocused(true);
}}
/>
);
})}
</div>
</>
);
};

DigitsInput.propTypes = {
length: PropTypes.number,
onChange: PropTypes.func,
className: PropTypes.string,
code: PropTypes.string,
tabIndex: PropTypes.number
};

DigitsInput.defaultProps = {
tabIndex: 1,
length: 6,
onChange: () => {},
onFill: () => {}
};

export default DigitsInput;
37 changes: 37 additions & 0 deletions src/components/DigitsInput/DigitsInput.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.digitsWrapper {
display: inline-block;
}

.digitsWrapper :first-child {
margin-top: 1rem;
}

.digitsWrapper > div {
margin-right: 2rem;
}

.digitsWrapper > div > input:hover {
border-bottom: 0.1rem solid var(--color-primary-dark);
}

/* Chrome, Safari, Edge, Opera */
.digitsWrapper input::-webkit-outer-spin-button,
.digitsWrapper input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}

/* Firefox */
.digitsWrapper input[type="number"] {
-moz-appearance: textfield;
}

.focusedInput :first-child > input {
border-bottom: 0.1rem solid var(--color-primary);
}

.mainInput {
opacity: 0;
height: 0;
width: 0;
}
1 change: 1 addition & 0 deletions src/components/DigitsInput/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./DigitsInput";
2 changes: 1 addition & 1 deletion src/components/ModalConfirm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ const ModalConfirm = ({

ModalConfirm.propTypes = {
title: PropTypes.string,
message: PropTypes.string,
message: PropTypes.node,
show: PropTypes.bool,
onClose: PropTypes.func,
onSubmit: PropTypes.func,
Expand Down
4 changes: 4 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,7 @@ export const MONTHS_LABELS = [
"Nov",
"Dec"
];

export const TOTP_CODE_LENGTH = 6;
export const TOTP_DEFAULT_TYPE = 1;
export const TOTP_MISSING_LOGIN_ERROR = 79;
9 changes: 6 additions & 3 deletions src/containers/User/Detail/Detail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Identity from "./Identity";
import Preferences from "./Preferences";
import ManageContractor from "./ManageContractor";
import ProposalsOwned from "./ProposalsOwned";
import Totp from "../Totp";
import useModalContext from "src/hooks/utils/useModalContext";

const getTabComponents = ({ user, ...rest }) => {
Expand All @@ -42,7 +43,8 @@ const getTabComponents = ({ user, ...rest }) => {
[tabValues.MANAGE_DCC]: <ManageContractor userID={user.userid} {...rest} />,
[tabValues.PROPOSALS_OWNED]: (
<ProposalsOwned proposalsOwned={user.proposalsowned} />
)
),
[tabValues.TOTP]: <Totp />
};
return mapTabValueToComponent;
};
Expand Down Expand Up @@ -82,7 +84,7 @@ const UserDetail = ({
(!isUserPageOwner || !ownsProposals)
)
return true;

if (tabLabel === tabValues.TOTP && !isUserPageOwner) return true;
return false;
};
const filterByRecordType = (tabLabel) => {
Expand Down Expand Up @@ -112,7 +114,8 @@ const UserDetail = ({
tabValues.INVOICES,
tabValues.DRAFTS,
tabValues.MANAGE_DCC,
tabValues.PROPOSALS_OWNED
tabValues.PROPOSALS_OWNED,
tabValues.TOTP
].filter((tab) => !isTabDisabled(tab) && filterByRecordType(tab));
}, [
isUserPageOwner,
Expand Down
3 changes: 2 additions & 1 deletion src/containers/User/Detail/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export const tabValues = {
INVOICES: "Invoices",
DRAFTS: "Drafts",
MANAGE_DCC: "Manage Contractor",
PROPOSALS_OWNED: "Proposals Owned"
PROPOSALS_OWNED: "Proposals Owned",
TOTP: "Two-Factor Authentication"
};

/**
Expand Down
Loading