@@ -53,6 +53,10 @@ export class GuideSocAiComponent implements OnInit {
5353 private rawConfigs : UtmModuleGroupConfType [ ] = [ ] ;
5454 private groupId : number ;
5555
56+ // Original masked customHeaders value (if backend returned an opaque mask like "*****")
57+ private originalMaskedCustomHeaders : string | null = null ;
58+ private readonly maskedDisplay = '***' ;
59+
5660 providers : ProviderConfig [ ] = [
5761 {
5862 id : 'openai' ,
@@ -350,6 +354,7 @@ export class GuideSocAiComponent implements OnInit {
350354 'changeAlertStatus' : changeStatus ? changeStatus . confValue : 'false' ,
351355 } ;
352356 this . customModelValue = '' ;
357+ this . originalMaskedCustomHeaders = null ;
353358
354359 // Only load provider-specific values if viewing the saved provider
355360 if ( isCurrentSavedProvider ) {
@@ -379,16 +384,25 @@ export class GuideSocAiComponent implements OnInit {
379384 // Check if API key exists in custom headers — show masked if so
380385 const customHeaders = this . getConf ( 'utmstack.socai.customHeaders' ) ;
381386 if ( customHeaders && customHeaders . confValue && customHeaders . confValue !== '{}' ) {
382- try {
383- const headers = JSON . parse ( customHeaders . confValue ) ;
384- const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
385- if ( authConfig && headers [ authConfig . headerName ] ) {
386- // API key exists — show masked, don't expose the real value
387- this . formValues [ 'apiKey' ] = '*****' ;
388- }
389- } catch ( e ) { }
390- this . formValues [ 'customHeaders' ] = customHeaders . confValue ;
391- this . parseHeadersFromJson ( customHeaders . confValue ) ;
387+ const raw = customHeaders . confValue ;
388+ if ( this . isMaskedValue ( raw ) ) {
389+ // Backend returned the whole confValue masked — preserve original for save
390+ this . originalMaskedCustomHeaders = raw ;
391+ this . formValues [ 'customHeaders' ] = raw ;
392+ this . formValues [ 'apiKey' ] = this . maskedDisplay ;
393+ this . headerRows = [ { key : this . maskedDisplay , value : this . maskedDisplay } ] ;
394+ } else {
395+ try {
396+ const headers = JSON . parse ( raw ) ;
397+ const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
398+ if ( authConfig && headers [ authConfig . headerName ] ) {
399+ // API key exists — show masked, don't expose the real value
400+ this . formValues [ 'apiKey' ] = this . maskedDisplay ;
401+ }
402+ } catch ( e ) { }
403+ this . formValues [ 'customHeaders' ] = raw ;
404+ this . parseHeadersFromJson ( raw ) ;
405+ }
392406 }
393407
394408 const maxTokensConf = this . getConf ( 'utmstack.socai.maxTokens' ) ;
@@ -452,22 +466,32 @@ export class GuideSocAiComponent implements OnInit {
452466 if ( this . activeProvider === 'custom' ) {
453467 // Custom provider: user manages auth type and headers directly
454468 this . pushChange ( changes , 'utmstack.socai.authType' , this . formValues [ 'authType' ] || 'custom-headers' , 'text' ) ;
455- this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . formValues [ 'customHeaders' ] || '{}' , 'text' ) ;
469+ if ( this . originalMaskedCustomHeaders && this . hasMaskedHeaderRows ( ) ) {
470+ // User didn't replace the masked rows — preserve original masked value
471+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . originalMaskedCustomHeaders , 'password' ) ;
472+ } else {
473+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . formValues [ 'customHeaders' ] || '{}' , 'password' ) ;
474+ }
456475 } else if ( this . activeProvider === 'ollama' ) {
457476 // Ollama: no auth needed
458477 this . pushChange ( changes , 'utmstack.socai.authType' , 'none' , 'text' ) ;
459- this . pushChange ( changes , 'utmstack.socai.customHeaders' , '{}' , 'text ' ) ;
478+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , '{}' , 'password ' ) ;
460479 } else {
461480 // Known providers: build auth header from API key
462481 const authConfig = this . providerAuthHeaders [ this . activeProvider ] ;
463- if ( authConfig && this . formValues [ 'apiKey' ] && this . formValues [ 'apiKey' ] !== '*****' ) {
482+ const apiKey = this . formValues [ 'apiKey' ] ;
483+ if ( authConfig && apiKey && ! this . isMaskedValue ( apiKey ) ) {
464484 // User entered a new API key — build auth headers
465485 const headers : { [ k : string ] : string } = { } ;
466- headers [ authConfig . headerName ] = authConfig . headerValuePrefix + this . formValues [ ' apiKey' ] ;
486+ headers [ authConfig . headerName ] = authConfig . headerValuePrefix + apiKey ;
467487 this . pushChange ( changes , 'utmstack.socai.authType' , 'custom-headers' , 'text' ) ;
468- this . pushChange ( changes , 'utmstack.socai.customHeaders' , JSON . stringify ( headers ) , 'text' ) ;
488+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , JSON . stringify ( headers ) , 'password' ) ;
489+ } else if ( this . originalMaskedCustomHeaders && apiKey && this . isMaskedValue ( apiKey ) ) {
490+ // User didn't change the masked API key — preserve original masked value
491+ this . pushChange ( changes , 'utmstack.socai.authType' , 'custom-headers' , 'text' ) ;
492+ this . pushChange ( changes , 'utmstack.socai.customHeaders' , this . originalMaskedCustomHeaders , 'password' ) ;
469493 }
470- // If apiKey is '*****', don't touch customHeaders — keep existing value in DB
494+ // Otherwise: don't touch customHeaders — keep existing value in DB
471495 }
472496
473497 this . moduleGroupConfService . update ( {
@@ -567,6 +591,14 @@ export class GuideSocAiComponent implements OnInit {
567591 this . formValues [ 'customHeaders' ] = JSON . stringify ( obj ) ;
568592 }
569593
594+ private isMaskedValue ( value : string ) : boolean {
595+ return ! ! value && / ^ \* + $ / . test ( value ) ;
596+ }
597+
598+ private hasMaskedHeaderRows ( ) : boolean {
599+ return this . headerRows . some ( r => this . isMaskedValue ( r . key ) || this . isMaskedValue ( r . value ) ) ;
600+ }
601+
570602 private parseHeadersFromJson ( json : string ) {
571603 this . headerRows = [ ] ;
572604 try {
0 commit comments