feat(github): add GitHub token configuration with localization and tests

This commit is contained in:
Supra4E8C
2026-10-08 00:56:25 +08:00
parent 2ce627defa
commit ebb4289bc6
11 changed files with 133 additions and 0 deletions
@@ -61,6 +61,18 @@ export function SectionConnectivity({
<ApiKeysField values={values} disabled={disabled} onChange={onChange} />
<FieldAnchor fieldId="githubToken">
<Input
label={t('config_management.visual.sections.server.github_token')}
type="password"
autoComplete="new-password"
value={values.githubToken}
onChange={(e) => onChange({ githubToken: e.target.value })}
disabled={disabled}
hint={t('config_management.visual.sections.server.github_token_hint')}
/>
</FieldAnchor>
<FieldAnchor fieldId="trustedProxies">
<FieldShell
label={t('config_management.visual.serverExtras.trustedProxies.label')}
+1
View File
@@ -135,6 +135,7 @@ export const FIELD_VALUE_KEYS: Record<string, readonly string[]> = {
codexLiveMediaRelayICEServers: ['codexLiveMediaRelayICEServers'],
// ── connectivity ──────────────────────────────────────────────────────────
githubToken: ['githubToken'],
trustedProxies: ['trustedProxies'],
discoveryEnabled: ['discoveryEnabled'],
discoveryServiceName: ['discoveryServiceName'],
+8
View File
@@ -222,6 +222,14 @@ export const CONFIG_FIELD_SEARCH_INDEX: ConfigFieldSearchEntry[] = [
},
// ── connectivity ──────────────────────────────────────────────────────────
{
fieldId: 'githubToken',
sectionId: 'connectivity',
labelKey: L('sections.server.github_token'),
hintKey: L('sections.server.github_token_hint'),
yamlKeys: ['server', 'github-token'],
keywords: ['GITHUB_TOKEN', 'GitHub', 'token'],
},
{
fieldId: 'trustedProxies',
sectionId: 'connectivity',
@@ -6,6 +6,7 @@ import { readConfigBoolean } from './visualConfigBoolean';
// Backend: config_v8.go, trusted_proxies.go and discovery/service.go.
export const SERVER_FIELDS = [
{ key: 'githubToken', path: ['server', 'github-token'], kind: 'string' },
{ key: 'trustedProxies', path: ['server', 'trusted-proxies'], kind: 'list' },
{ key: 'discoveryEnabled', path: ['server', 'discovery', 'enabled'], kind: 'boolean' },
{ key: 'discoveryServiceName', path: ['server', 'discovery', 'service-name'], kind: 'string' },
+2
View File
@@ -1263,6 +1263,8 @@
"signature_title": "Signature Validation"
},
"server": {
"github_token": "Global GitHub Token",
"github_token_hint": "Used for server-side GitHub requests, such as panel updates and plugin downloads. Takes precedence over GITHUB_TOKEN; leave empty to use environment credentials, or unauthenticated requests if none are set. Matching plugin store authentication rules take precedence.",
"host": "Host Address",
"port": "Port"
},
+2
View File
@@ -1266,6 +1266,8 @@
"signature_title": "Проверка подписей"
},
"server": {
"github_token": "Глобальный токен GitHub",
"github_token_hint": "Используется для серверных запросов к GitHub, например обновлений панели и загрузки плагинов. Имеет приоритет над GITHUB_TOKEN; пустое значение использует учётные данные из окружения, а при их отсутствии — анонимные запросы. Совпадающие правила авторизации магазина плагинов имеют приоритет.",
"host": "Адрес хоста",
"port": "Порт"
},
+2
View File
@@ -1055,6 +1055,8 @@
"devin_title": "Devin"
},
"server": {
"github_token": "Token GitHub toàn cục",
"github_token_hint": "Dùng cho các yêu cầu GitHub từ máy chủ, như cập nhật bảng điều khiển và tải plugin. Ưu tiên hơn GITHUB_TOKEN; để trống để dùng thông tin xác thực từ biến môi trường, hoặc gửi yêu cầu ẩn danh nếu chưa thiết lập. Các quy tắc xác thực phù hợp của kho plugin được ưu tiên.",
"host": "Địa chỉ Host",
"port": "Cổng"
},
+2
View File
@@ -1262,6 +1262,8 @@
"signature_title": "签名校验"
},
"server": {
"github_token": "全局 GitHub 令牌",
"github_token_hint": "用于面板更新、插件下载等服务端 GitHub 请求。优先于 GITHUB_TOKEN;留空时使用环境变量凭据,未设置凭据则匿名请求。插件商店匹配的认证规则优先。",
"host": "主机地址",
"port": "端口"
},
+2
View File
@@ -1288,6 +1288,8 @@
"signature_title": "簽章驗證"
},
"server": {
"github_token": "全域 GitHub 權杖",
"github_token_hint": "用於面板更新、外掛下載等伺服器端 GitHub 請求。優先於 GITHUB_TOKEN;留空時使用環境變數憑證,未設定憑證則匿名請求。外掛商店符合的驗證規則優先。",
"host": "主機位址",
"port": "連接埠"
},
+2
View File
@@ -117,6 +117,7 @@ export type PluginStoreAuthRule = {
/** UI draft keys; YAML persistence uses the v8 tree, not these flattened names. */
export type VisualConfigValues = {
githubToken: string;
trustedProxies: string[];
discoveryEnabled: boolean;
discoveryServiceName: string;
@@ -221,6 +222,7 @@ export const makeClientId = () => {
};
export const DEFAULT_VISUAL_VALUES: VisualConfigValues = {
githubToken: '',
trustedProxies: [],
discoveryEnabled: false,
discoveryServiceName: '',
+99
View File
@@ -0,0 +1,99 @@
import { describe, expect, test } from 'bun:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nextProvider } from 'react-i18next';
import { parseDocument } from 'yaml';
import i18n from '@/i18n';
import { SectionConnectivity } from '@/features/config/components/sections/SectionConnectivity';
import { findConfigFieldById, searchConfigFields } from '@/features/config/searchIndex';
import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig';
import { runVisualConfig } from './helpers/visualConfig';
const source = `# config
server:
github-token: fixture-token # credential
future: preserved
plugins:
store-auth:
- match: https://api.github.com/
type: none
`;
describe('global GitHub token configuration', () => {
test('loads, replaces and clears the canonical field without changing store auth', () => {
expect(runVisualConfig(source).visualValues.githubToken).toBe('fixture-token');
for (const token of [' replacement-fixture ', '']) {
const config = runVisualConfig(source, [{ githubToken: token }]);
expect([...config.visualDirtyFields]).toEqual(['githubToken']);
const output = config.applyVisualChangesToYaml(source);
const expected = parseDocument(source).toJS();
expected.server['github-token'] = token.trim();
expect(parseDocument(output).toJS()).toEqual(expected);
expect(output).toContain('# credential');
expect(runVisualConfig(output).visualValues.githubToken).toBe(token.trim());
}
});
test('preserves untouched tokens and does not materialize an absent token', () => {
const config = runVisualConfig(source, [{ port: '9000' }]);
expect(
parseDocument(config.applyVisualChangesToYaml(source)).getIn(['server', 'github-token'])
).toBe('fixture-token');
for (const yaml of ['{}', 'server: null', 'server: {github-token: null}']) {
const loaded = runVisualConfig(yaml);
expect(loaded.visualValues.githubToken).toBe('');
expect(parseDocument(loaded.applyVisualChangesToYaml(yaml)).toJS()).toEqual(
parseDocument(yaml).toJS()
);
const edited = runVisualConfig(yaml, [{ githubToken: 'fixture-token' }]);
expect(
parseDocument(edited.applyVisualChangesToYaml(yaml)).getIn(['server', 'github-token'])
).toBe('fixture-token');
}
});
test('registers the canonical search path', () => {
expect(findConfigFieldById('githubToken')).toMatchObject({
sectionId: 'connectivity',
yamlKeys: ['server', 'github-token'],
});
expect(
searchConfigFields('server.github-token', () => '').map((entry) => entry.fieldId)
).toContain('githubToken');
});
test('renders a masked, labelled input with localized hints and disabled support', () => {
for (const lng of ['en', 'zh-CN', 'zh-TW', 'ru', 'vi']) {
const translations = i18n.cloneInstance({ lng });
const key = 'config_management.visual.sections.server.github_token';
for (const path of [key, `${key}_hint`]) {
expect(typeof translations.getResource(lng, 'translation', path)).toBe('string');
}
for (const disabled of [false, true]) {
const markup = renderToStaticMarkup(
createElement(
I18nextProvider,
{ i18n: translations },
createElement(SectionConnectivity, {
values: DEFAULT_VISUAL_VALUES,
disabled,
onChange: () => {},
})
)
);
const field = markup
.split('id="cfg-field-githubToken"')[1]
.split('id="cfg-field-trustedProxies"')[0];
const input = field.match(/<input\b[^>]*>/)?.[0] ?? '';
expect(input).toContain('type="password"');
expect(input).toContain('autoComplete="new-password"');
expect(input.includes('disabled=""')).toBe(disabled);
const id = input.match(/\bid="([^"]+)"/)?.[1];
expect(id).toBeTruthy();
expect(field).toContain(`for="${id}"`);
expect(field).toContain(`id="${id}-hint"`);
expect(input).toContain(`aria-describedby="${id}-hint"`);
}
}
});
});