mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Merge pull request #279 from ZHOUKAILIAN/fix/auth-file-html-challenge-ui
fix(auth-files): handle HTML challenge content in editor
This commit is contained in:
@@ -34,6 +34,7 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
|
||||
}
|
||||
};
|
||||
const previewText = formatJsonText(updatedText);
|
||||
const invalidContentPreview = editor?.invalidContentPreview ?? '';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -100,60 +101,70 @@ export function AuthFilesPrefixProxyEditorModal(props: AuthFilesPrefixProxyEdito
|
||||
</div>
|
||||
<div className={styles.prefixProxyJsonWrapper}>
|
||||
<label className={styles.prefixProxyLabel}>
|
||||
{t('auth_files.prefix_proxy_source_label')}
|
||||
{editor.json
|
||||
? t('auth_files.prefix_proxy_source_label')
|
||||
: t('auth_files.prefix_proxy_invalid_content_label')}
|
||||
</label>
|
||||
<textarea
|
||||
className={styles.prefixProxyTextarea}
|
||||
rows={10}
|
||||
readOnly
|
||||
value={previewText}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.prefixProxyFields}>
|
||||
<Input
|
||||
label={t('auth_files.prefix_label')}
|
||||
value={editor.prefix}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('prefix', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth_files.proxy_url_label')}
|
||||
value={editor.proxyUrl}
|
||||
placeholder={t('auth_files.proxy_url_placeholder')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('proxyUrl', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth_files.priority_label')}
|
||||
value={editor.priority}
|
||||
placeholder={t('auth_files.priority_placeholder')}
|
||||
hint={t('auth_files.priority_hint')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('priority', e.target.value)}
|
||||
/>
|
||||
<div className="form-group">
|
||||
<label>{t('auth_files.headers_label')}</label>
|
||||
{editor.json ? (
|
||||
<textarea
|
||||
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)}
|
||||
className={styles.prefixProxyTextarea}
|
||||
rows={10}
|
||||
readOnly
|
||||
value={previewText}
|
||||
/>
|
||||
{editor.headersError && <div className="error-box">{editor.headersError}</div>}
|
||||
<div className="hint">{t('auth_files.headers_hint')}</div>
|
||||
</div>
|
||||
<Input
|
||||
label={t('auth_files.note_label')}
|
||||
value={editor.note}
|
||||
placeholder={t('auth_files.note_placeholder')}
|
||||
hint={t('auth_files.note_hint')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('note', e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<pre className={styles.prefixProxyInvalidContentPreview}>
|
||||
{invalidContentPreview}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
{editor.json && (
|
||||
<div className={styles.prefixProxyFields}>
|
||||
<Input
|
||||
label={t('auth_files.prefix_label')}
|
||||
value={editor.prefix}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('prefix', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth_files.proxy_url_label')}
|
||||
value={editor.proxyUrl}
|
||||
placeholder={t('auth_files.proxy_url_placeholder')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('proxyUrl', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label={t('auth_files.priority_label')}
|
||||
value={editor.priority}
|
||||
placeholder={t('auth_files.priority_placeholder')}
|
||||
hint={t('auth_files.priority_hint')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('priority', e.target.value)}
|
||||
/>
|
||||
<div className="form-group">
|
||||
<label>{t('auth_files.headers_label')}</label>
|
||||
<textarea
|
||||
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
|
||||
label={t('auth_files.note_label')}
|
||||
value={editor.note}
|
||||
placeholder={t('auth_files.note_placeholder')}
|
||||
hint={t('auth_files.note_hint')}
|
||||
disabled={disableControls || editor.saving || !editor.json}
|
||||
onChange={(e) => onChange('note', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,9 @@ type AuthFileHeadersErrorKey =
|
||||
| 'auth_files.headers_invalid_json'
|
||||
| 'auth_files.headers_invalid_object'
|
||||
| 'auth_files.headers_invalid_value';
|
||||
type AuthFileContentErrorKey =
|
||||
| 'auth_files.prefix_proxy_invalid_json'
|
||||
| 'auth_files.prefix_proxy_html_challenge';
|
||||
|
||||
export type PrefixProxyEditorField = 'prefix' | 'proxyUrl' | 'priority' | 'note' | 'headersText';
|
||||
|
||||
@@ -23,6 +26,7 @@ export type PrefixProxyEditorState = {
|
||||
error: string | null;
|
||||
originalText: string;
|
||||
rawText: string;
|
||||
invalidContentPreview: string;
|
||||
json: Record<string, unknown> | null;
|
||||
prefix: string;
|
||||
proxyUrl: string;
|
||||
@@ -90,6 +94,47 @@ const parseHeadersText = (
|
||||
const normalizeTextField = (value: unknown): string =>
|
||||
typeof value === 'string' ? value.trim() : '';
|
||||
|
||||
const INVALID_CONTENT_PREVIEW_LIMIT = 1000;
|
||||
|
||||
const buildInvalidContentPreview = (text: string): string => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return '';
|
||||
if (trimmed.length <= INVALID_CONTENT_PREVIEW_LIMIT) return trimmed;
|
||||
return `${trimmed.slice(0, INVALID_CONTENT_PREVIEW_LIMIT)}\n...`;
|
||||
};
|
||||
|
||||
const buildInvalidAuthFileContentState = (
|
||||
text: string,
|
||||
resolveError: (key: AuthFileContentErrorKey) => string
|
||||
): Pick<
|
||||
PrefixProxyEditorState,
|
||||
'loading' | 'error' | 'rawText' | 'originalText' | 'invalidContentPreview'
|
||||
> => ({
|
||||
loading: false,
|
||||
error: resolveError(getAuthFileContentErrorKey(text)),
|
||||
rawText: text,
|
||||
originalText: text,
|
||||
invalidContentPreview: buildInvalidContentPreview(text),
|
||||
});
|
||||
|
||||
const getAuthFileContentErrorKey = (text: string): AuthFileContentErrorKey => {
|
||||
const head = text.trimStart().slice(0, 4096).toLowerCase();
|
||||
const looksLikeHtml =
|
||||
head.startsWith('<!doctype html') ||
|
||||
head.startsWith('<html') ||
|
||||
head.includes('<head') ||
|
||||
head.includes('<body');
|
||||
const looksLikeChallenge =
|
||||
head.includes('cf_chl') ||
|
||||
head.includes('__cf_chl_tk') ||
|
||||
head.includes('challenge-platform') ||
|
||||
head.includes('cloudflare');
|
||||
|
||||
return looksLikeHtml || looksLikeChallenge
|
||||
? 'auth_files.prefix_proxy_html_challenge'
|
||||
: 'auth_files.prefix_proxy_invalid_json';
|
||||
};
|
||||
|
||||
const hasKeys = (value: Record<string, unknown> | AuthFileFieldsPatch | null): boolean =>
|
||||
Boolean(value && Object.keys(value).length > 0);
|
||||
|
||||
@@ -270,7 +315,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
prefixProxyEditor?.headersTouched && prefixProxyEditor.headersError
|
||||
);
|
||||
const prefixProxyUpdatedText =
|
||||
prefixProxyEditor?.json && !hasBlockingValidationError
|
||||
prefixProxyEditor && !hasBlockingValidationError
|
||||
? buildPrefixProxyUpdatedText(prefixProxyEditor, (key) => t(key))
|
||||
: '';
|
||||
|
||||
@@ -302,6 +347,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
error: null,
|
||||
originalText: '',
|
||||
rawText: '',
|
||||
invalidContentPreview: '',
|
||||
json: null,
|
||||
prefix: '',
|
||||
proxyUrl: '',
|
||||
@@ -325,10 +371,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return {
|
||||
...prev,
|
||||
loading: false,
|
||||
error: t('auth_files.prefix_proxy_invalid_json'),
|
||||
rawText: trimmed,
|
||||
originalText: trimmed,
|
||||
...buildInvalidAuthFileContentState(rawText, (key) => t(key)),
|
||||
};
|
||||
});
|
||||
return;
|
||||
@@ -339,10 +382,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
if (!prev || prev.fileName !== name) return prev;
|
||||
return {
|
||||
...prev,
|
||||
loading: false,
|
||||
error: t('auth_files.prefix_proxy_invalid_json'),
|
||||
rawText: trimmed,
|
||||
originalText: trimmed,
|
||||
...buildInvalidAuthFileContentState(rawText, (key) => t(key)),
|
||||
};
|
||||
});
|
||||
return;
|
||||
@@ -370,6 +410,7 @@ export function useAuthFilesPrefixProxyEditor(
|
||||
loading: false,
|
||||
originalText,
|
||||
rawText: originalText,
|
||||
invalidContentPreview: '',
|
||||
json,
|
||||
prefix,
|
||||
proxyUrl,
|
||||
|
||||
@@ -591,6 +591,7 @@
|
||||
"prefix_proxy_loading": "Loading auth file...",
|
||||
"prefix_proxy_info_label": "Auth file info (info)",
|
||||
"prefix_proxy_source_label": "Auth file JSON (preview)",
|
||||
"prefix_proxy_invalid_content_label": "Downloaded content (truncated)",
|
||||
"prefix_label": "Prefix (prefix)",
|
||||
"proxy_url_label": "Proxy URL (proxy_url)",
|
||||
"prefix_placeholder": "",
|
||||
@@ -615,6 +616,7 @@
|
||||
"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_html_challenge": "Downloaded content is an HTML challenge page, not an auth JSON object. Re-authenticate or replace this auth file before editing fields.",
|
||||
"prefix_proxy_saved_success": "Updated auth file \"{{name}}\" successfully",
|
||||
"quota_refresh_success": "Quota refreshed for \"{{name}}\"",
|
||||
"quota_refresh_failed": "Failed to refresh quota for \"{{name}}\": {{message}}"
|
||||
|
||||
@@ -591,6 +591,7 @@
|
||||
"prefix_proxy_loading": "Загрузка файла авторизации...",
|
||||
"prefix_proxy_info_label": "Информация о файле авторизации (info)",
|
||||
"prefix_proxy_source_label": "JSON файла авторизации (предпросмотр)",
|
||||
"prefix_proxy_invalid_content_label": "Скачанное содержимое (сокращено)",
|
||||
"prefix_label": "Префикс (prefix)",
|
||||
"proxy_url_label": "URL прокси (proxy_url)",
|
||||
"prefix_placeholder": "",
|
||||
@@ -609,6 +610,7 @@
|
||||
"note_hint": "Необязательно. Используется для описания назначения или владельца учётных данных; оставьте пустым, чтобы не записывать.",
|
||||
"note_display": "Заметка",
|
||||
"prefix_proxy_invalid_json": "Этот файл авторизации не является JSON-объектом, поэтому поля нельзя редактировать.",
|
||||
"prefix_proxy_html_challenge": "Скачанное содержимое является HTML-страницей проверки, а не JSON-объектом авторизации. Повторно авторизуйтесь или замените файл перед редактированием полей.",
|
||||
"prefix_proxy_saved_success": "Файл авторизации \"{{name}}\" успешно обновлён",
|
||||
"card_tools_title": "Инструменты",
|
||||
"quota_refresh_single": "Обновить квоту",
|
||||
|
||||
@@ -591,6 +591,7 @@
|
||||
"prefix_proxy_loading": "正在加载认证文件...",
|
||||
"prefix_proxy_info_label": "认证文件信息(info)",
|
||||
"prefix_proxy_source_label": "认证文件 JSON(预览)",
|
||||
"prefix_proxy_invalid_content_label": "下载内容(已截断)",
|
||||
"prefix_label": "前缀(prefix)",
|
||||
"proxy_url_label": "代理 URL(proxy_url)",
|
||||
"prefix_placeholder": "",
|
||||
@@ -615,6 +616,7 @@
|
||||
"headers_invalid_object": "自定义请求头必须是 JSON 对象。",
|
||||
"headers_invalid_value": "每个自定义请求头的值都必须是字符串。",
|
||||
"prefix_proxy_invalid_json": "该认证文件不是 JSON 对象,无法编辑字段。",
|
||||
"prefix_proxy_html_challenge": "下载到的是 HTML 验证页面,不是认证 JSON 对象。请重新认证或替换该认证文件后再编辑字段。",
|
||||
"prefix_proxy_saved_success": "已更新认证文件 \"{{name}}\"",
|
||||
"quota_refresh_success": "已刷新 \"{{name}}\" 的额度",
|
||||
"quota_refresh_failed": "刷新 \"{{name}}\" 的额度失败:{{message}}"
|
||||
|
||||
@@ -591,6 +591,7 @@
|
||||
"prefix_proxy_loading": "正在載入驗證檔案...",
|
||||
"prefix_proxy_info_label": "驗證檔案資訊(info)",
|
||||
"prefix_proxy_source_label": "驗證檔案 JSON(預覽)",
|
||||
"prefix_proxy_invalid_content_label": "下載內容(已截斷)",
|
||||
"prefix_label": "前綴(prefix)",
|
||||
"proxy_url_label": "代理 URL(proxy_url)",
|
||||
"prefix_placeholder": "",
|
||||
@@ -615,6 +616,7 @@
|
||||
"headers_invalid_object": "自訂請求標頭必須是 JSON 物件。",
|
||||
"headers_invalid_value": "每個自訂請求標頭的值都必須是字串。",
|
||||
"prefix_proxy_invalid_json": "該驗證檔案不是 JSON 物件,無法編輯欄位。",
|
||||
"prefix_proxy_html_challenge": "下載到的是 HTML 驗證頁面,不是驗證 JSON 物件。請重新驗證或替換該驗證檔案後再編輯欄位。",
|
||||
"prefix_proxy_saved_success": "已更新驗證檔案「{{name}}」",
|
||||
"quota_refresh_success": "已重新整理「{{name}}」的配額",
|
||||
"quota_refresh_failed": "重新整理「{{name}}」的配額失敗:{{message}}"
|
||||
|
||||
@@ -1425,6 +1425,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.prefixProxyInvalidContentPreview {
|
||||
width: 100%;
|
||||
max-height: 220px;
|
||||
margin: 0;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-md;
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.prefixProxyFields {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user