mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-01 04:59:37 +08:00
✨ feat(security): 完成配置密文存储前后端闭环
- 补齐连接与代理密文字段的保留替换清空语义 - 接通保存复制删除导入接口并返回 secretless 视图 - 刷新 Wails 绑定并补充实现留痕文档
This commit is contained in:
86
frontend/src/utils/connectionSecretDraft.test.ts
Normal file
86
frontend/src/utils/connectionSecretDraft.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveConnectionSecretDraft } from './connectionSecretDraft';
|
||||
|
||||
describe('resolveConnectionSecretDraft', () => {
|
||||
it('keeps an existing stored secret when edit form leaves the field blank', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: true,
|
||||
valueInput: '',
|
||||
clearSecret: false,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('');
|
||||
expect(result.clearStoredSecret).toBe(false);
|
||||
expect(result.keepsStoredSecret).toBe(true);
|
||||
expect(result.hasSecretAfterSave).toBe(true);
|
||||
});
|
||||
|
||||
it('replaces the stored secret when a new value is entered', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: true,
|
||||
valueInput: ' mongodb://demo ',
|
||||
clearSecret: false,
|
||||
trimInput: true,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('mongodb://demo');
|
||||
expect(result.clearStoredSecret).toBe(false);
|
||||
expect(result.keepsStoredSecret).toBe(false);
|
||||
expect(result.hasSecretAfterSave).toBe(true);
|
||||
});
|
||||
|
||||
it('clears the stored secret when explicitly requested', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: true,
|
||||
valueInput: '',
|
||||
clearSecret: true,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('');
|
||||
expect(result.clearStoredSecret).toBe(true);
|
||||
expect(result.keepsStoredSecret).toBe(false);
|
||||
expect(result.hasSecretAfterSave).toBe(false);
|
||||
});
|
||||
|
||||
it('prefers a newly entered value over a stale clear toggle', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: true,
|
||||
valueInput: 'new-password',
|
||||
clearSecret: true,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('new-password');
|
||||
expect(result.clearStoredSecret).toBe(false);
|
||||
expect(result.keepsStoredSecret).toBe(false);
|
||||
expect(result.hasSecretAfterSave).toBe(true);
|
||||
});
|
||||
|
||||
it('does not emit a clear flag for a brand new blank field', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: false,
|
||||
valueInput: '',
|
||||
clearSecret: false,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('');
|
||||
expect(result.clearStoredSecret).toBe(false);
|
||||
expect(result.keepsStoredSecret).toBe(false);
|
||||
expect(result.hasSecretAfterSave).toBe(false);
|
||||
});
|
||||
|
||||
it('supports force clearing stored secrets', () => {
|
||||
const result = resolveConnectionSecretDraft({
|
||||
hasSecret: true,
|
||||
valueInput: 'temporary',
|
||||
clearSecret: false,
|
||||
forceClear: true,
|
||||
});
|
||||
|
||||
expect(result.value).toBe('');
|
||||
expect(result.clearStoredSecret).toBe(true);
|
||||
expect(result.keepsStoredSecret).toBe(false);
|
||||
expect(result.hasSecretAfterSave).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
63
frontend/src/utils/connectionSecretDraft.ts
Normal file
63
frontend/src/utils/connectionSecretDraft.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export interface ConnectionSecretDraftInput {
|
||||
valueInput?: string;
|
||||
hasSecret?: boolean;
|
||||
clearSecret?: boolean;
|
||||
forceClear?: boolean;
|
||||
trimInput?: boolean;
|
||||
}
|
||||
|
||||
export interface ConnectionSecretDraftResult {
|
||||
value: string;
|
||||
clearStoredSecret: boolean;
|
||||
keepsStoredSecret: boolean;
|
||||
hasSecretAfterSave: boolean;
|
||||
}
|
||||
|
||||
export function resolveConnectionSecretDraft(input: ConnectionSecretDraftInput): ConnectionSecretDraftResult {
|
||||
const rawValue = input.valueInput ?? '';
|
||||
const value = input.trimInput ? String(rawValue).trim() : String(rawValue);
|
||||
|
||||
if (input.forceClear) {
|
||||
return {
|
||||
value: '',
|
||||
clearStoredSecret: true,
|
||||
keepsStoredSecret: false,
|
||||
hasSecretAfterSave: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (value !== '') {
|
||||
return {
|
||||
value,
|
||||
clearStoredSecret: false,
|
||||
keepsStoredSecret: false,
|
||||
hasSecretAfterSave: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.clearSecret) {
|
||||
return {
|
||||
value: '',
|
||||
clearStoredSecret: true,
|
||||
keepsStoredSecret: false,
|
||||
hasSecretAfterSave: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.hasSecret) {
|
||||
return {
|
||||
value: '',
|
||||
clearStoredSecret: false,
|
||||
keepsStoredSecret: true,
|
||||
hasSecretAfterSave: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: '',
|
||||
clearStoredSecret: false,
|
||||
keepsStoredSecret: false,
|
||||
hasSecretAfterSave: false,
|
||||
};
|
||||
}
|
||||
|
||||
41
frontend/src/utils/providerSecretDraft.test.ts
Normal file
41
frontend/src/utils/providerSecretDraft.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveProviderSecretDraft } from './providerSecretDraft';
|
||||
|
||||
describe('resolveProviderSecretDraft', () => {
|
||||
it('keeps existing provider secret when edit form leaves apiKey blank', () => {
|
||||
const result = resolveProviderSecretDraft({
|
||||
hasSecret: true,
|
||||
apiKeyInput: '',
|
||||
clearSecret: false,
|
||||
});
|
||||
|
||||
expect(result.mode).toBe('keep');
|
||||
expect(result.apiKey).toBe('');
|
||||
expect(result.hasSecret).toBe(true);
|
||||
});
|
||||
|
||||
it('replaces the provider secret when a new apiKey is entered', () => {
|
||||
const result = resolveProviderSecretDraft({
|
||||
hasSecret: true,
|
||||
apiKeyInput: ' sk-new ',
|
||||
clearSecret: false,
|
||||
});
|
||||
|
||||
expect(result.mode).toBe('replace');
|
||||
expect(result.apiKey).toBe('sk-new');
|
||||
expect(result.hasSecret).toBe(true);
|
||||
});
|
||||
|
||||
it('clears the stored provider secret when requested', () => {
|
||||
const result = resolveProviderSecretDraft({
|
||||
hasSecret: true,
|
||||
apiKeyInput: '',
|
||||
clearSecret: true,
|
||||
});
|
||||
|
||||
expect(result.mode).toBe('clear');
|
||||
expect(result.apiKey).toBe('');
|
||||
expect(result.hasSecret).toBe(false);
|
||||
});
|
||||
});
|
||||
47
frontend/src/utils/providerSecretDraft.ts
Normal file
47
frontend/src/utils/providerSecretDraft.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
export type ProviderSecretDraftMode = 'keep' | 'replace' | 'clear';
|
||||
|
||||
export interface ProviderSecretDraftInput {
|
||||
hasSecret?: boolean;
|
||||
apiKeyInput?: string;
|
||||
clearSecret?: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderSecretDraftResult {
|
||||
mode: ProviderSecretDraftMode;
|
||||
apiKey: string;
|
||||
hasSecret: boolean;
|
||||
}
|
||||
|
||||
export function resolveProviderSecretDraft(input: ProviderSecretDraftInput): ProviderSecretDraftResult {
|
||||
const apiKey = String(input.apiKeyInput || '').trim();
|
||||
|
||||
if (input.clearSecret) {
|
||||
return {
|
||||
mode: 'clear',
|
||||
apiKey: '',
|
||||
hasSecret: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (apiKey) {
|
||||
return {
|
||||
mode: 'replace',
|
||||
apiKey,
|
||||
hasSecret: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.hasSecret) {
|
||||
return {
|
||||
mode: 'keep',
|
||||
apiKey: '',
|
||||
hasSecret: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'clear',
|
||||
apiKey: '',
|
||||
hasSecret: false,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user