diff --git a/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java b/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java index dc05b27f3..e219a594c 100644 --- a/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java +++ b/backend/src/main/java/com/park/utmstack/domain/application_modules/validators/UtmModuleConfigValidator.java @@ -11,7 +11,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -32,7 +36,7 @@ public boolean validate(UtmModule module, List keys public boolean validate(UtmModule module, List keys, List dbConfigs) { if (keys.isEmpty()) return false; - List configDTOs = dbConfigs.stream() + List configDTOs = new ArrayList<>(dbConfigs.stream() .map(dbConf -> { UtmModuleGroupConfiguration override = findInKeys(keys, dbConf.getConfKey()); String value; @@ -45,7 +49,17 @@ public boolean validate(UtmModule module, List keys } return new UtmModuleGroupConfDTO(dbConf.getConfDataType(),dbConf.getConfKey(), value); }) - .toList(); + .collect(Collectors.toList())); + + Set dbKeys = dbConfigs.stream() + .map(UtmModuleGroupConfiguration::getConfKey) + .collect(Collectors.toCollection(HashSet::new)); + + keys.stream() + .filter(k -> !dbKeys.contains(k.getConfKey())) + .filter(k -> !Constants.MASKED_VALUE.equals(k.getConfValue())) + .map(k -> new UtmModuleGroupConfDTO(k.getConfDataType(), k.getConfKey(), k.getConfValue())) + .forEach(configDTOs::add); UtmModuleGroupConfWrapperDTO body = new UtmModuleGroupConfWrapperDTO(configDTOs); diff --git a/frontend/src/app/app-module/guides/guide-soc-ai/guide-soc-ai.component.ts b/frontend/src/app/app-module/guides/guide-soc-ai/guide-soc-ai.component.ts index aba4d8fda..389fdcbeb 100644 --- a/frontend/src/app/app-module/guides/guide-soc-ai/guide-soc-ai.component.ts +++ b/frontend/src/app/app-module/guides/guide-soc-ai/guide-soc-ai.component.ts @@ -5,6 +5,7 @@ import {UtmModuleGroupConfService} from '../../shared/services/utm-module-group- import {UtmModuleGroupConfType} from '../../shared/type/utm-module-group-conf.type'; import {UtmToastService} from '../../../shared/alert/utm-toast.service'; import {ModuleChangeStatusBehavior} from '../../shared/behavior/module-change-status.behavior'; +import { finalize } from 'rxjs/operators'; interface ProviderConfig { id: string; @@ -52,6 +53,10 @@ export class GuideSocAiComponent implements OnInit { private rawConfigs: UtmModuleGroupConfType[] = []; private groupId: number; + // Original masked customHeaders value (if backend returned an opaque mask like "*****") + private originalMaskedCustomHeaders: string | null = null; + private readonly maskedDisplay = '***'; + providers: ProviderConfig[] = [ { id: 'openai', @@ -297,7 +302,12 @@ export class GuideSocAiComponent implements OnInit { private loadConfig() { this.loading = true; - this.moduleGroupService.query({moduleId: this.integrationId}).subscribe(response => { + this.moduleGroupService.query({moduleId: this.integrationId}).pipe( + finalize(()=>{ + this.loading = false; + this.cdr.detectChanges(); + }) + ).subscribe(response => { const groups = response.body || []; if (groups.length > 0) { this.groupId = groups[0].id; @@ -344,6 +354,7 @@ export class GuideSocAiComponent implements OnInit { 'changeAlertStatus': changeStatus ? changeStatus.confValue : 'false', }; this.customModelValue = ''; + this.originalMaskedCustomHeaders = null; // Only load provider-specific values if viewing the saved provider if (isCurrentSavedProvider) { @@ -373,16 +384,25 @@ export class GuideSocAiComponent implements OnInit { // Check if API key exists in custom headers — show masked if so const customHeaders = this.getConf('utmstack.socai.customHeaders'); if (customHeaders && customHeaders.confValue && customHeaders.confValue !== '{}') { - try { - const headers = JSON.parse(customHeaders.confValue); - const authConfig = this.providerAuthHeaders[this.activeProvider]; - if (authConfig && headers[authConfig.headerName]) { - // API key exists — show masked, don't expose the real value - this.formValues['apiKey'] = '*****'; - } - } catch (e) {} - this.formValues['customHeaders'] = customHeaders.confValue; - this.parseHeadersFromJson(customHeaders.confValue); + const raw = customHeaders.confValue; + if (this.isMaskedValue(raw)) { + // Backend returned the whole confValue masked — preserve original for save + this.originalMaskedCustomHeaders = raw; + this.formValues['customHeaders'] = raw; + this.formValues['apiKey'] = this.maskedDisplay; + this.headerRows = [{key: this.maskedDisplay, value: this.maskedDisplay}]; + } else { + try { + const headers = JSON.parse(raw); + const authConfig = this.providerAuthHeaders[this.activeProvider]; + if (authConfig && headers[authConfig.headerName]) { + // API key exists — show masked, don't expose the real value + this.formValues['apiKey'] = this.maskedDisplay; + } + } catch (e) {} + this.formValues['customHeaders'] = raw; + this.parseHeadersFromJson(raw); + } } const maxTokensConf = this.getConf('utmstack.socai.maxTokens'); @@ -422,46 +442,56 @@ export class GuideSocAiComponent implements OnInit { const changes: UtmModuleGroupConfType[] = []; // Set provider - this.pushChange(changes, 'utmstack.socai.provider', this.activeProvider); + this.pushChange(changes, 'utmstack.socai.provider', this.activeProvider, 'text'); // Set model - this.pushChange(changes, 'utmstack.socai.model', this.getModelValue()); + this.pushChange(changes, 'utmstack.socai.model', this.getModelValue(), 'text'); // Set URL for providers that need it (azure, ollama, custom) if (this.formValues['url']) { - this.pushChange(changes, 'utmstack.socai.url', this.formValues['url']); + this.pushChange(changes, 'utmstack.socai.url', this.formValues['url'], 'text'); } // Set maxTokens if (this.formValues['maxTokens']) { - this.pushChange(changes, 'utmstack.socai.maxTokens', this.formValues['maxTokens']); + this.pushChange(changes, 'utmstack.socai.maxTokens', this.formValues['maxTokens'], 'text'); } // Set behavior toggles - this.pushChange(changes, 'utmstack.socai.autoAnalyze', this.formValues['autoAnalyze'] || 'false'); - this.pushChange(changes, 'utmstack.socai.incidentCreation', this.formValues['incidentCreation'] || 'false'); - this.pushChange(changes, 'utmstack.socai.changeAlertStatus', this.formValues['changeAlertStatus'] || 'false'); + this.pushChange(changes, 'utmstack.socai.autoAnalyze', this.formValues['autoAnalyze'] || 'false', 'text'); + this.pushChange(changes, 'utmstack.socai.incidentCreation', this.formValues['incidentCreation'] || 'false', 'text'); + this.pushChange(changes, 'utmstack.socai.changeAlertStatus', this.formValues['changeAlertStatus'] || 'false', 'text'); // Build auth headers if (this.activeProvider === 'custom') { // Custom provider: user manages auth type and headers directly - this.pushChange(changes, 'utmstack.socai.authType', this.formValues['authType'] || 'custom-headers'); - this.pushChange(changes, 'utmstack.socai.customHeaders', this.formValues['customHeaders'] || '{}'); + this.pushChange(changes, 'utmstack.socai.authType', this.formValues['authType'] || 'custom-headers', 'text'); + if (this.originalMaskedCustomHeaders && this.hasMaskedHeaderRows()) { + // User didn't replace the masked rows — preserve original masked value + this.pushChange(changes, 'utmstack.socai.customHeaders', this.originalMaskedCustomHeaders, 'password'); + } else { + this.pushChange(changes, 'utmstack.socai.customHeaders', this.formValues['customHeaders'] || '{}', 'password'); + } } else if (this.activeProvider === 'ollama') { // Ollama: no auth needed - this.pushChange(changes, 'utmstack.socai.authType', 'none'); - this.pushChange(changes, 'utmstack.socai.customHeaders', '{}'); + this.pushChange(changes, 'utmstack.socai.authType', 'none', 'text'); + this.pushChange(changes, 'utmstack.socai.customHeaders', '{}', 'password'); } else { // Known providers: build auth header from API key const authConfig = this.providerAuthHeaders[this.activeProvider]; - if (authConfig && this.formValues['apiKey'] && this.formValues['apiKey'] !== '*****') { + const apiKey = this.formValues['apiKey']; + if (authConfig && apiKey && !this.isMaskedValue(apiKey)) { // User entered a new API key — build auth headers const headers: {[k: string]: string} = {}; - headers[authConfig.headerName] = authConfig.headerValuePrefix + this.formValues['apiKey']; - this.pushChange(changes, 'utmstack.socai.authType', 'custom-headers'); - this.pushChange(changes, 'utmstack.socai.customHeaders', JSON.stringify(headers)); + headers[authConfig.headerName] = authConfig.headerValuePrefix + apiKey; + this.pushChange(changes, 'utmstack.socai.authType', 'custom-headers', 'text'); + this.pushChange(changes, 'utmstack.socai.customHeaders', JSON.stringify(headers), 'password'); + } else if (this.originalMaskedCustomHeaders && apiKey && this.isMaskedValue(apiKey)) { + // User didn't change the masked API key — preserve original masked value + this.pushChange(changes, 'utmstack.socai.authType', 'custom-headers', 'text'); + this.pushChange(changes, 'utmstack.socai.customHeaders', this.originalMaskedCustomHeaders, 'password'); } - // If apiKey is '*****', don't touch customHeaders — keep existing value in DB + // Otherwise: don't touch customHeaders — keep existing value in DB } this.moduleGroupConfService.update({ @@ -486,7 +516,12 @@ export class GuideSocAiComponent implements OnInit { ); } - private pushChange(changes: UtmModuleGroupConfType[], confKey: string, value: string) { + private pushChange( + changes: UtmModuleGroupConfType[], + confKey: string, + value: string, + confDataType: 'list' | 'password' | 'file' | 'bool' | 'select' | 'text' = 'text' + ) { const existing = this.getConf(confKey); if (existing) { changes.push({ @@ -495,6 +530,19 @@ export class GuideSocAiComponent implements OnInit { confOptions: existing.confOptions ? JSON.stringify(existing.confOptions) : existing.confOptions, confVisibility: existing.confVisibility ? JSON.stringify(existing.confVisibility) : existing.confVisibility, }); + } else { + changes.push({ + id: undefined, + groupId: this.groupId, + confKey, + confValue: value, + confName: confKey.split('.')[2] || confKey, + confDataType, + confDescription: confKey, + confRequired: true, + confOptions: undefined, + confVisibility: undefined, + }); } } @@ -543,6 +591,14 @@ export class GuideSocAiComponent implements OnInit { this.formValues['customHeaders'] = JSON.stringify(obj); } + private isMaskedValue(value: string): boolean { + return !!value && /^\*+$/.test(value); + } + + private hasMaskedHeaderRows(): boolean { + return this.headerRows.some(r => this.isMaskedValue(r.key) || this.isMaskedValue(r.value)); + } + private parseHeadersFromJson(json: string) { this.headerRows = []; try {