Skip to content
Open
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
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-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 ? [] : ''),
);

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,44 @@
/**
* AJAX helpers for 2FA email confirmation (admin settings).
* @return {Promise<Object>}
*/
export async function generateTwoFaConfirmationCode() {
const s = window.spbcSettings;
if (!s || !s.ajaxurl || !s.ajax_nonce) {
return {success: false, data: 'spbcSettings is not available'};
}
const body = new URLSearchParams({
action: 'spbc_generate_confirmation_code',
security: s.ajax_nonce,
});
const r = await fetch(s.ajaxurl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body,
credentials: 'same-origin',
});
return r.json();
}

/**
* @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'};
}
const body = new URLSearchParams({
action: 'spbc_check_confirmation_code',
security: s.ajax_nonce,
code: String(code),
});
const r = await fetch(s.ajaxurl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body,
credentials: 'same-origin',
});
return r.json();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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();
} else {
const msg = typeof res.data === 'string' ?
res.data :
__('Request failed.', 'security-malware-firewall');
alert(msg);
}
} finally {
setResendBusy(false);
}
};

const handleOk = async () => {
if (okBusy) {
return;
}
setOkBusy(true);
try {
const res = await checkTwoFaConfirmationCode(code.trim());
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
Loading