Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
2 changes: 1 addition & 1 deletion css/spbc-settings.min.css

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion css/src/spbc-settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
100% { clip-path: polygon(50% 0%, 50% 0, 50% 0, 50% 0, 50% 0, 50% 0, 50% 0, 50% 50%, 50% 50%); }
}

#confirmation-code--resend .circle{
#confirmation-code--resend .circle,
.spbc-2fa-confirmation-resend .circle{
margin-left: 5px;
display: inline-block;
vertical-align: text-bottom;
Expand Down
239 changes: 8 additions & 231 deletions inc/spbc-settings.php

Large diffs are not rendered by default.

194 changes: 157 additions & 37 deletions js/public/spbct-react-bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/spbc-admin.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/spbc-admin.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/spbc-settings.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/spbc-settings.min.js.map

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions js/src/react/components/App.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {createContext} from 'react';
import Header from '../pageElements/Header/Header';
import ServiceButtons from '../pageElements/ServiceButtons/ServiceButtons';
import HiddenElements from '../pageElements/HiddenElements/HiddenElements';
import Tabs from '../pageElements/Tabs/Tabs';

export const DataRootContext = createContext(null);
Expand All @@ -27,7 +26,6 @@ export default function App({dataRoot, dataTabs}) {
<DataTabsContext.Provider value={dataTabs}>
<Header data={dataRoot} />
<ServiceButtons />
<HiddenElements data={dataRoot} />
<Tabs data={dataTabs} />
</DataTabsContext.Provider>
</DataRootContext.Provider>
Expand Down
6 changes: 5 additions & 1 deletion js/src/react/components/FormElements/InputRadioGroup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, {useState, useEffect} from 'react';

/**
* InputRadioGroup component
Expand All @@ -24,6 +24,10 @@ export default function InputRadioGroup({
}) {
const [selectedValue, setSelectedValue] = useState(String(initialValue ?? ''));

useEffect(() => {
setSelectedValue(String(initialValue ?? ''));
}, [initialValue]);

const handleChange = (event) => {
const newValue = event.target.value;
setSelectedValue(newValue);
Expand Down
14 changes: 11 additions & 3 deletions js/src/react/components/FormElements/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,25 @@ export default function Select({
className = '',
disabled = false,
placeholder = '',
multiple = false,
...props
}) {
const [selectedValue, setSelectedValue] = useState(initialValue || '');
const [selectedValue, setSelectedValue] = useState(
initialValue !== undefined ?
initialValue :
(multiple ? [] : ''),
);
Comment thread
svfcode marked this conversation as resolved.

useEffect(() => {
if (initialValue !== undefined) {
setSelectedValue(initialValue);
}
}, [initialValue]);
}, [initialValue, multiple]);

const handleChange = (event) => {
const newValue = event.target.value;
const newValue = multiple ?
Array.from(event.target.selectedOptions, (option) => option.value) :
event.target.value;
setSelectedValue(newValue);

// Call `onChange` from props, if exists
Expand All @@ -37,6 +44,7 @@ export default function Select({
onChange={handleChange}
className={className}
disabled={disabled}
multiple={multiple}
{...props}
>
{placeholder && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import SettingsGroup from './SettingsGroup';
const {__} = wp.i18n;

export default function Settings(props) {
const {settingsSlug, settings, keyIsOk} = props;
const {
settingsSlug,
settings,
keyIsOk,
wpUserRolesOptions,
verificationEmail,
authLoginPageUrl,
techSupportUrlPrefix,
loginRenameNotifyAdminEmail,
canActivatePlugins,
shuffleSaltsUnlocked,
} = props;

const settingKeys = Object.keys(settings || {});

Expand All @@ -24,6 +35,13 @@ export default function Settings(props) {
settingsSlug={settingsSlug}
settings={settings[sectionGroupId]}
keyIsOk={keyIsOk}
wpUserRolesOptions={wpUserRolesOptions}
verificationEmail={verificationEmail}
authLoginPageUrl={authLoginPageUrl}
techSupportUrlPrefix={techSupportUrlPrefix}
loginRenameNotifyAdminEmail={loginRenameNotifyAdminEmail}
canActivatePlugins={canActivatePlugins}
shuffleSaltsUnlocked={shuffleSaltsUnlocked}
/>;
}
if (
Expand All @@ -37,6 +55,13 @@ export default function Settings(props) {
settingsSlug={settingsSlug}
settings={settings[sectionGroupId]}
keyIsOk={keyIsOk}
wpUserRolesOptions={wpUserRolesOptions}
verificationEmail={verificationEmail}
authLoginPageUrl={authLoginPageUrl}
techSupportUrlPrefix={techSupportUrlPrefix}
loginRenameNotifyAdminEmail={loginRenameNotifyAdminEmail}
canActivatePlugins={canActivatePlugins}
shuffleSaltsUnlocked={shuffleSaltsUnlocked}
/>;
}
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* AJAX helpers for 2FA email confirmation (admin settings).
* @return {Promise<Object>}
*/
function sendTwoFaRequest(action, payload = {}) {
if (typeof spbcSendAJAXRequest !== 'function') {
return Promise.resolve({success: false, data: 'spbcSendAJAXRequest is not available'});
}

return new Promise((resolve) => {
const data = {
action,
...payload,
};

const params = {
callback: (result) => resolve(result),
error: (jqXHR, textStatus, errorThrown) =>
resolve({success: false, data: errorThrown || textStatus || 'Request failed'}),
};

spbcSendAJAXRequest(data, params);
});
}

export async function generateTwoFaConfirmationCode() {
const s = window.spbcSettings;
if (!s || !s.ajaxurl || !s.ajax_nonce) {
return {success: false, data: 'spbcSettings is not available'};
}
Comment thread
svfcode marked this conversation as resolved.

return sendTwoFaRequest('spbc_generate_confirmation_code');
}

/**
* @param {string} code
* @return {Promise<Object>}
*/
export async function checkTwoFaConfirmationCode(code) {
const s = window.spbcSettings;
if (!s || !s.ajaxurl || !s.ajax_nonce) {
return {success: false, data: 'spbcSettings is not available'};
}

return sendTwoFaRequest('spbc_check_confirmation_code', {
code: String(code),
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import React, {useState, useEffect, useRef, useCallback} from 'react';
import ReactFileViewDialog from '../../../ReactFileViewDialog/ReactFileViewDialog';
import {generateTwoFaConfirmationCode, checkTwoFaConfirmationCode} from './2faConfirmationApi';

const {__, sprintf} = wp.i18n;

/**
* @param {object} props
* @param {boolean} props.isOpen
* @param {function(): void} props.onCancel
* @param {function(): void} props.onVerified
* @param {string} props.verificationEmail
* @return {JSX.Element|null}
*/
export default function TwoFaConfirmationDialog(props) {
const {isOpen, onCancel, onVerified, verificationEmail} = props;
const [code, setCode] = useState('');
const [resendBusy, setResendBusy] = useState(false);
const [resendCooldown, setResendCooldown] = useState(false);
const [okBusy, setOkBusy] = useState(false);
const resendTimerRef = useRef(null);

const clearResendTimer = useCallback(() => {
if (resendTimerRef.current) {
clearTimeout(resendTimerRef.current);
resendTimerRef.current = null;
}
}, []);

const startResendCooldown = useCallback(() => {
clearResendTimer();
setResendCooldown(true);
resendTimerRef.current = setTimeout(() => {
setResendCooldown(false);
resendTimerRef.current = null;
}, 30000);
}, [clearResendTimer]);

useEffect(() => {
if (!isOpen) {
setCode('');
setResendBusy(false);
setResendCooldown(false);
setOkBusy(false);
clearResendTimer();
return;
}
startResendCooldown();
return () => {
clearResendTimer();
};
}, [isOpen, clearResendTimer, startResendCooldown]);

const handleResend = async () => {
if (resendBusy || resendCooldown) {
return;
}
setResendBusy(true);
try {
const res = await generateTwoFaConfirmationCode();
if (res.success) {
setCode('');
startResendCooldown();
Comment thread
svfcode marked this conversation as resolved.
} else {
const msg = typeof res.data === 'string' ?
res.data :
__('Request failed.', 'security-malware-firewall');
alert(msg);
}
} catch (error) {
console.error(error);
Comment thread
svfcode marked this conversation as resolved.
} finally {
setResendBusy(false);
}
};

const handleOk = async () => {
if (okBusy) {
return;
}

setOkBusy(true);

let res;
try {
res = await checkTwoFaConfirmationCode(code.trim());
} catch (e) {
alert(__('Code verification failed. Please try again.', 'security-malware-firewall'));
return;
}
Comment thread
svfcode marked this conversation as resolved.
Outdated

try {
if (res.success) {
clearResendTimer();
onVerified();
} else {
const msg = typeof res.data === 'string' ?
res.data :
__('Code verification failed!', 'security-malware-firewall');
alert(msg);
}
} finally {
setOkBusy(false);
}
};

const email = verificationEmail || '';
const checkInboxMsg = __(
'Check %s inbox for the confirmation code.',
'security-malware-firewall',
);
/* eslint-disable max-len */
const codeValidityNote = __(
'The code is valid for 10 minutes. If you want to change the status in this period, the new code won\'t be sent, please, use the code you\'ve already received.',
'security-malware-firewall',
);
/* eslint-enable max-len */

return (
<ReactFileViewDialog
isOpen={isOpen}
onClose={onCancel}
title={__('Confirmation code', 'security-malware-firewall')}
maxWidth="360px"
>
<p>
{sprintf(
/* translators: %s: email address */
checkInboxMsg,
email,
)}
</p>
<p>
<em>
{codeValidityNote}
</em>
</p>
<p>
<input
type="text"
name="spbct-confirmation-code"
value={code}
onChange={(e) => setCode(e.target.value)}
className="regular-text"
autoComplete="one-time-code"
/>
&nbsp;&nbsp;
<button
type="button"
className="button button-primary spbc-2fa-confirmation-resend"
onClick={handleResend}
disabled={resendBusy || resendCooldown}
>
{__('Resend', 'security-malware-firewall')}
{resendCooldown && (
<span className="circle circle--small -animation--circle -animation--30s">
<span className="circle-inner"></span>
</span>
)}
</button>
</p>
<p style={{marginTop: 16, textAlign: 'right'}}>
<button type="button" className="button" onClick={onCancel} disabled={okBusy}>
{__('Cancel', 'security-malware-firewall')}
</button>
&nbsp;
<button type="button" className="button button-primary" onClick={handleOk} disabled={okBusy}>
{__('OK', 'security-malware-firewall')}
</button>
</p>
</ReactFileViewDialog>
);
}
Loading