mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
feat(authFiles): enhance header validation and error handling in editor
This commit is contained in:
@@ -65,7 +65,13 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
|
||||
<Button
|
||||
onClick={onSave}
|
||||
loading={editor?.saving === true}
|
||||
disabled={disableControls || editor?.saving === true || !dirty || !editor?.json}
|
||||
disabled={
|
||||
disableControls ||
|
||||
editor?.saving === true ||
|
||||
!dirty ||
|
||||
!editor?.json ||
|
||||
Boolean(editor?.headersTouched && editor.headersError)
|
||||
}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -141,13 +147,15 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
|
||||
<div className="form-group">
|
||||
<label>{t('auth_files.headers_label')}</label>
|
||||
<textarea
|
||||
className="input"
|
||||
className={`input ${editor.headersError ? styles.prefixProxyTextareaInvalid : ''}`}
|
||||
value={editor.headersText}
|
||||
placeholder={t('auth_files.headers_placeholder')}
|
||||
rows={4}
|
||||
aria-invalid={Boolean(editor.headersError)}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('headersText', e.target.value)}
|
||||
/>
|
||||
{editor.headersError && <div className="error-box">{editor.headersError}</div>}
|
||||
<div className="hint">{t('auth_files.headers_hint')}</div>
|
||||
</div>
|
||||
<Input
|
||||
|
||||
@@ -14,6 +14,12 @@ import {
|
||||
readCodexAuthFileWebsockets,
|
||||
} from '@/features/authFiles/constants';
|
||||
|
||||
type AuthFileHeaders = Record<string, string>;
|
||||
type AuthFileHeadersErrorKey =
|
||||
| 'auth_files.headers_invalid_json'
|
||||
| 'auth_files.headers_invalid_object'
|
||||
| 'auth_files.headers_invalid_value';
|
||||
|
||||
export type PrefixProxyEditorField =
|
||||
| 'prefix'
|
||||
| 'proxyUrl'
|
||||
@@ -45,6 +51,8 @@ export type PrefixProxyEditorState = {
|
||||
note: string;
|
||||
noteTouched: boolean;
|
||||
headersText: string;
|
||||
headersTouched: boolean;
|
||||
headersError: string | null;
|
||||
};
|
||||
|
||||
export type UseAuthFilesPrefixProxyEditorOptions = {
|
||||
@@ -66,7 +74,45 @@ export type UseAuthFilesPrefixProxyEditorResult = {
|
||||
handlePrefixProxySave: () => Promise<void>;
|
||||
};
|
||||
|
||||
const buildPrefixProxyUpdatedText = (editor: PrefixProxyEditorState | null): string => {
|
||||
const isRecordObject = (value: unknown): value is Record<string, unknown> =>
|
||||
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
|
||||
const validateHeadersValue = (value: unknown): AuthFileHeadersErrorKey | null => {
|
||||
if (!isRecordObject(value)) {
|
||||
return 'auth_files.headers_invalid_object';
|
||||
}
|
||||
return Object.values(value).every((item) => typeof item === 'string')
|
||||
? null
|
||||
: 'auth_files.headers_invalid_value';
|
||||
};
|
||||
|
||||
const parseHeadersText = (
|
||||
text: string
|
||||
): { value: AuthFileHeaders | null; errorKey: AuthFileHeadersErrorKey | null } => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
return { value: null, errorKey: null };
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
return { value: null, errorKey: 'auth_files.headers_invalid_json' };
|
||||
}
|
||||
|
||||
const errorKey = validateHeadersValue(parsed);
|
||||
if (errorKey) {
|
||||
return { value: null, errorKey };
|
||||
}
|
||||
|
||||
return { value: parsed as AuthFileHeaders, errorKey: null };
|
||||
};
|
||||
|
||||
const buildPrefixProxyUpdatedText = (
|
||||
editor: PrefixProxyEditorState | null,
|
||||
resolveHeadersError: (key: AuthFileHeadersErrorKey) => string
|
||||
): string => {
|
||||
if (!editor?.json) return editor?.rawText ?? '';
|
||||
const next: Record<string, unknown> = { ...editor.json };
|
||||
if ('prefix' in next || editor.prefix.trim()) {
|
||||
@@ -106,19 +152,16 @@ const buildPrefixProxyUpdatedText = (editor: PrefixProxyEditorState | null): str
|
||||
}
|
||||
}
|
||||
|
||||
if (editor.headersText.trim()) {
|
||||
let parsedHeaders;
|
||||
try {
|
||||
parsedHeaders = JSON.parse(editor.headersText);
|
||||
} catch {
|
||||
throw new Error('Invalid JSON format for Custom Headers. Must be an object.');
|
||||
if (editor.headersTouched) {
|
||||
const { value: parsedHeaders, errorKey } = parseHeadersText(editor.headersText);
|
||||
if (errorKey) {
|
||||
throw new Error(resolveHeadersError(errorKey));
|
||||
}
|
||||
if (!parsedHeaders || typeof parsedHeaders !== 'object' || Array.isArray(parsedHeaders)) {
|
||||
throw new Error('Invalid JSON format for Custom Headers. Must be an object.');
|
||||
if (parsedHeaders) {
|
||||
next.headers = parsedHeaders;
|
||||
} else {
|
||||
delete next.headers;
|
||||
}
|
||||
next.headers = parsedHeaders;
|
||||
} else {
|
||||
delete next.headers;
|
||||
}
|
||||
|
||||
return JSON.stringify(
|
||||
@@ -135,12 +178,13 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
|
||||
const [prefixProxyEditor, setPrefixProxyEditor] = useState<PrefixProxyEditorState | null>(null);
|
||||
|
||||
let prefixProxyUpdatedText = '';
|
||||
try {
|
||||
prefixProxyUpdatedText = buildPrefixProxyUpdatedText(prefixProxyEditor);
|
||||
} catch {
|
||||
// Catch JSON parsing errors during render so the UI doesn't crash.
|
||||
}
|
||||
const hasBlockingValidationError = Boolean(
|
||||
prefixProxyEditor?.headersTouched && prefixProxyEditor.headersError
|
||||
);
|
||||
const prefixProxyUpdatedText =
|
||||
prefixProxyEditor?.json && !hasBlockingValidationError
|
||||
? buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key))
|
||||
: '';
|
||||
|
||||
const prefixProxyDirty =
|
||||
Boolean(prefixProxyEditor?.json) &&
|
||||
@@ -186,6 +230,8 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
note: '',
|
||||
noteTouched: false,
|
||||
headersText: '',
|
||||
headersTouched: false,
|
||||
headersError: null,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -239,8 +285,11 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
const note = typeof json.note === 'string' ? json.note : '';
|
||||
const headers = json.headers;
|
||||
let headersText = '';
|
||||
if (headers && typeof headers === 'object') {
|
||||
let headersError: string | null = null;
|
||||
if (headers !== undefined) {
|
||||
headersText = JSON.stringify(headers, null, 2);
|
||||
const { errorKey } = parseHeadersText(headersText);
|
||||
headersError = errorKey ? t(errorKey) : null;
|
||||
}
|
||||
|
||||
setPrefixProxyEditor((prev) => {
|
||||
@@ -261,6 +310,8 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
note,
|
||||
noteTouched: false,
|
||||
headersText,
|
||||
headersTouched: false,
|
||||
headersError,
|
||||
error: null,
|
||||
};
|
||||
});
|
||||
@@ -286,7 +337,16 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
if (field === 'excludedModelsText') return { ...prev, excludedModelsText: String(value) };
|
||||
if (field === 'disableCooling') return { ...prev, disableCooling: String(value) };
|
||||
if (field === 'note') return { ...prev, note: String(value), noteTouched: true };
|
||||
if (field === 'headersText') return { ...prev, headersText: String(value) };
|
||||
if (field === 'headersText') {
|
||||
const headersText = String(value);
|
||||
const { errorKey } = parseHeadersText(headersText);
|
||||
return {
|
||||
...prev,
|
||||
headersText,
|
||||
headersTouched: true,
|
||||
headersError: errorKey ? t(errorKey) : null,
|
||||
};
|
||||
}
|
||||
return { ...prev, websockets: Boolean(value) };
|
||||
});
|
||||
};
|
||||
@@ -298,7 +358,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
const name = prefixProxyEditor.fileName;
|
||||
let payload = '';
|
||||
try {
|
||||
payload = buildPrefixProxyUpdatedText(prefixProxyEditor);
|
||||
payload = buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key));
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Invalid format';
|
||||
showNotification(errorMessage, 'error');
|
||||
|
||||
@@ -600,6 +600,9 @@
|
||||
"headers_label": "Custom Headers (headers)",
|
||||
"headers_placeholder": "{\n \"Header-Name\": \"value\"\n}",
|
||||
"headers_hint": "Enter custom HTTP headers as a JSON object, e.g., {\"X-My-Header\": \"value\"}",
|
||||
"headers_invalid_json": "Custom headers must be valid JSON.",
|
||||
"headers_invalid_object": "Custom headers must be a JSON object.",
|
||||
"headers_invalid_value": "Each custom header value must be a string.",
|
||||
"prefix_proxy_invalid_json": "This auth file is not a JSON object, so fields cannot be edited.",
|
||||
"prefix_proxy_saved_success": "Updated auth file \"{{name}}\" successfully",
|
||||
"quota_refresh_success": "Quota refreshed for \"{{name}}\"",
|
||||
|
||||
@@ -600,6 +600,9 @@
|
||||
"headers_label": "自定义请求头(headers)",
|
||||
"headers_placeholder": "{\n \"Header-Name\": \"value\"\n}",
|
||||
"headers_hint": "以 JSON 对象格式输入自定义 HTTP 请求头,例如:{\"X-My-Header\": \"value\"}",
|
||||
"headers_invalid_json": "自定义请求头必须是有效的 JSON。",
|
||||
"headers_invalid_object": "自定义请求头必须是 JSON 对象。",
|
||||
"headers_invalid_value": "每个自定义请求头的值都必须是字符串。",
|
||||
"prefix_proxy_invalid_json": "该认证文件不是 JSON 对象,无法编辑字段。",
|
||||
"prefix_proxy_saved_success": "已更新认证文件 \"{{name}}\"",
|
||||
"quota_refresh_success": "已刷新 \"{{name}}\" 的额度",
|
||||
|
||||
@@ -1384,6 +1384,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.prefixProxyTextareaInvalid {
|
||||
border-color: var(--danger-color);
|
||||
box-shadow: 0 0 0 3px rgba($error-color, 0.12);
|
||||
}
|
||||
|
||||
.cardActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in New Issue
Block a user