diff --git a/src/components/admin-text-setting/index.js b/src/components/admin-text-setting/index.js index 2cb2dbacbe..95c35ea7b9 100644 --- a/src/components/admin-text-setting/index.js +++ b/src/components/admin-text-setting/index.js @@ -1,13 +1,20 @@ import AdminBaseSetting from '../admin-base-setting' -import { createRef } from '@wordpress/element' +import { useRef, useState } from '@wordpress/element' +import { maskSensitiveValue } from '~stackable/util' const AdminTextSetting = props => { - const ref = createRef() + const ref = useRef() + const [ isEditing, setIsEditing ] = useState( false ) + const value = props.maskValue && ! isEditing ? maskSensitiveValue( props.value ) : props.value + return ( { ev.preventDefault() ref.current.focus() + if ( props.maskValue ) { + ref.current.select() + } } } { ...props } > @@ -15,9 +22,22 @@ const AdminTextSetting = props => { ref={ ref } className="ugb-admin-text-setting" type={ props.type } - value={ props.value } + value={ value } placeholder={ props.placeholder } + autoComplete={ props.maskValue ? 'off' : undefined } + onFocus={ () => { + if ( props.maskValue ) { + setIsEditing( true ) + setTimeout( () => ref.current?.select() ) + } + } } + onBlur={ () => { + setIsEditing( false ) + } } onChange={ event => { + if ( props.maskValue && ! isEditing ) { + return + } props.onChange( event.target.value ) event.preventDefault() event.stopPropagation() @@ -32,6 +52,7 @@ AdminTextSetting.defaultProps = { label: '', type: 'text', value: '', + maskValue: false, placeholder: '', onChange: () => {}, } diff --git a/src/util/__test__/index.test.js b/src/util/__test__/index.test.js index 4571fee00c..6ab1b2a855 100644 --- a/src/util/__test__/index.test.js +++ b/src/util/__test__/index.test.js @@ -2,9 +2,22 @@ * Internal dependencies */ import { - hexToRgba, prependCSSClass, compileCSS, + hexToRgba, prependCSSClass, compileCSS, maskSensitiveValue, } from '../' +describe( 'maskSensitiveValue', () => { + it( 'fully masks values with 12 or fewer characters', () => { + expect( maskSensitiveValue( '' ) ).toBe( '' ) + expect( maskSensitiveValue( '123456' ) ).toBe( '******' ) + expect( maskSensitiveValue( '123456789012' ) ).toBe( '************' ) + } ) + + it( 'keeps the first 6 and last 6 characters for longer values', () => { + expect( maskSensitiveValue( '1234567890123' ) ).toBe( '123456*890123' ) + expect( maskSensitiveValue( 'abcdefghijklmnopqrstuvwxyz' ) ).toBe( 'abcdef**************uvwxyz' ) + } ) +} ) + describe( 'hexToRgba', () => { it( 'should work', () => { expect( hexToRgba( '#000000' ) ).toBe( 'rgba(0, 0, 0, 1)' ) diff --git a/src/util/index.js b/src/util/index.js index c3928cca33..b1264659c5 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -35,6 +35,28 @@ import { compare } from 'compare-versions' export const getUniqueBlockClass = uniqueId => uniqueId ? `stk-${ uniqueId }` : '' +/** + * Masks a sensitive string for display while keeping it recognizable. + * + * Shows the first 6 and last 6 characters, and replaces the middle characters + * with asterisks. Values with 12 or fewer characters are fully masked. + * + * @param {string} value The sensitive value to mask. + * + * @return {string} Masked value for display. + */ +export const maskSensitiveValue = value => { + if ( ! value ) { + return value + } + + if ( value.length <= 12 ) { + return '*'.repeat( value.length ) + } + + return `${ value.slice( 0, 6 ) }${ '*'.repeat( value.length - 12 ) }${ value.slice( -6 ) }` +} + /** * Returns an array range of numbers. * diff --git a/src/welcome/admin.js b/src/welcome/admin.js index 2091a76a84..fff0c6588b 100644 --- a/src/welcome/admin.js +++ b/src/welcome/admin.js @@ -1357,6 +1357,7 @@ const Integrations = props => { searchedSettings={ propsToPass.integrations.children } value={ settings.stackable_google_maps_api_key } type="text" + maskValue={ true } onChange={ value => { handleSettingsChange( { stackable_google_maps_api_key: value } ) // eslint-disable-line camelcase } }