From e2058c53c2b04a3bfd68f55bb958eac41d5c8f82 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Wed, 24 Jun 2026 16:45:10 +0800 Subject: [PATCH 1/4] fix: hide api keys --- src/components/admin-text-setting/index.js | 18 +++++++++++++++++- src/util/index.js | 18 ++++++++++++++++++ src/welcome/admin.js | 1 + 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/components/admin-text-setting/index.js b/src/components/admin-text-setting/index.js index 2cb2dbacbe..5bd04b77d9 100644 --- a/src/components/admin-text-setting/index.js +++ b/src/components/admin-text-setting/index.js @@ -1,13 +1,19 @@ import AdminBaseSetting from '../admin-base-setting' import { createRef } from '@wordpress/element' +import { maskSensitiveValue } from '~stackable/util' const AdminTextSetting = props => { const ref = createRef() + const maskedValue = props.maskValue ? maskSensitiveValue( props.value ) : props.value + return ( { ev.preventDefault() ref.current.focus() + if ( props.maskValue ) { + ref.current.select() + } } } { ...props } > @@ -15,9 +21,18 @@ const AdminTextSetting = props => { ref={ ref } className="ugb-admin-text-setting" type={ props.type } - value={ props.value } + value={ maskedValue } placeholder={ props.placeholder } + autoComplete={ props.maskValue ? 'off' : undefined } + onFocus={ () => { + if ( props.maskValue ) { + ref.current.select() + } + } } onChange={ event => { + if ( props.maskValue && event.target.value === maskedValue ) { + return + } props.onChange( event.target.value ) event.preventDefault() event.stopPropagation() @@ -32,6 +47,7 @@ AdminTextSetting.defaultProps = { label: '', type: 'text', value: '', + maskValue: false, placeholder: '', onChange: () => {}, } diff --git a/src/util/index.js b/src/util/index.js index c3928cca33..d7e9e467a9 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -35,6 +35,24 @@ 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 returned as-is. + * + * @param {string} value The sensitive value to mask. + * + * @return {string} Masked value for display. + */ +export const maskSensitiveValue = value => { + if ( ! value || value.length <= 12 ) { + return value + } + + 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 } } From 6b3f9861c3557b0119d540348f714d2516f1b753 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Wed, 1 Jul 2026 10:53:47 +0800 Subject: [PATCH 2/4] fix: the masked value should only for disply --- src/components/admin-text-setting/index.js | 15 ++++++++++----- src/util/index.js | 8 ++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/admin-text-setting/index.js b/src/components/admin-text-setting/index.js index 5bd04b77d9..24903974af 100644 --- a/src/components/admin-text-setting/index.js +++ b/src/components/admin-text-setting/index.js @@ -1,10 +1,11 @@ import AdminBaseSetting from '../admin-base-setting' -import { createRef } from '@wordpress/element' +import { createRef, useState } from '@wordpress/element' import { maskSensitiveValue } from '~stackable/util' const AdminTextSetting = props => { const ref = createRef() - const maskedValue = props.maskValue ? maskSensitiveValue( props.value ) : props.value + const [ isEditing, setIsEditing ] = useState( false ) + const value = props.maskValue && ! isEditing ? maskSensitiveValue( props.value ) : props.value return ( { ref={ ref } className="ugb-admin-text-setting" type={ props.type } - value={ maskedValue } + value={ value } placeholder={ props.placeholder } autoComplete={ props.maskValue ? 'off' : undefined } onFocus={ () => { if ( props.maskValue ) { - ref.current.select() + setIsEditing( true ) + setTimeout( () => ref.current?.select() ) } } } + onBlur={ () => { + setIsEditing( false ) + } } onChange={ event => { - if ( props.maskValue && event.target.value === maskedValue ) { + if ( props.maskValue && ! isEditing ) { return } props.onChange( event.target.value ) diff --git a/src/util/index.js b/src/util/index.js index d7e9e467a9..b1264659c5 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -39,17 +39,21 @@ 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 returned as-is. + * 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 || value.length <= 12 ) { + if ( ! value ) { return value } + if ( value.length <= 12 ) { + return '*'.repeat( value.length ) + } + return `${ value.slice( 0, 6 ) }${ '*'.repeat( value.length - 12 ) }${ value.slice( -6 ) }` } From 0aa79f69bc407f0adb6214175ceb60bf6f1652c8 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Wed, 1 Jul 2026 11:16:27 +0800 Subject: [PATCH 3/4] fix: update the test --- src/util/__test__/index.test.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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)' ) From 9bb24f8c8c95ce14e8241fda6e4163fa14797a75 Mon Sep 17 00:00:00 2001 From: Alquen Sarmiento Date: Thu, 2 Jul 2026 11:07:53 +0800 Subject: [PATCH 4/4] fix: use useRef instead --- src/components/admin-text-setting/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/admin-text-setting/index.js b/src/components/admin-text-setting/index.js index 24903974af..95c35ea7b9 100644 --- a/src/components/admin-text-setting/index.js +++ b/src/components/admin-text-setting/index.js @@ -1,9 +1,9 @@ import AdminBaseSetting from '../admin-base-setting' -import { createRef, useState } 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