- 服务端: fetchAllConfigs 和 fetchChangedConfigs 添加 Token 校验 - 前端: 应用列表添加 Token 列和生成/删除 Token 按钮 - 客户端: Python/Node.js/Java 客户端传递 token 参数 - Java: 简化 buildTokenParameter 直接传递 token 值
162 lines
4.5 KiB
JavaScript
162 lines
4.5 KiB
JavaScript
import { Config } from './config.js';
|
|
import { Cache } from './cache.js';
|
|
import { Listener } from './listener.js';
|
|
import { decryptConfig } from './crypto.js';
|
|
|
|
export class ConfigClient {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.cache = new Cache(config.cacheDir);
|
|
this.listener = new Listener();
|
|
this.configs = {};
|
|
this.secret = '';
|
|
this.globalSecret = '';
|
|
this.lastTime = 0;
|
|
this.started = false;
|
|
this.timer = null;
|
|
}
|
|
|
|
onChange(handler) {
|
|
this.listener.onChange(handler);
|
|
}
|
|
|
|
get(key) {
|
|
return this.configs[key] ?? null;
|
|
}
|
|
|
|
getAll() {
|
|
return { ...this.configs };
|
|
}
|
|
|
|
async start() {
|
|
if (this.started) {
|
|
return;
|
|
}
|
|
this.started = true;
|
|
|
|
try {
|
|
await this.fetchAll();
|
|
} catch (err) {
|
|
console.warn('[ConfigClient] fetch all configs error:', err.message, ', trying cache');
|
|
const cached = this.cache.read();
|
|
if (cached) {
|
|
this.applyConfigs(cached);
|
|
console.info('[ConfigClient] loaded', Object.keys(this.configs).length, 'configs from cache');
|
|
} else {
|
|
throw new Error(`fetch configs failed and no cache: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
this.timer = setInterval(() => {
|
|
this.fetchChanged().catch((err) => {
|
|
console.error('[ConfigClient] sync error:', err);
|
|
});
|
|
}, this.config.syncInterval * 1000);
|
|
}
|
|
|
|
stop() {
|
|
if (this.timer) {
|
|
clearInterval(this.timer);
|
|
this.timer = null;
|
|
}
|
|
}
|
|
|
|
async fetchAll() {
|
|
const url = `${this.config.baseUrl}/api/fetchAllConfigs?appName=${encodeURIComponent(this.config.appName)}&env=${encodeURIComponent(this.config.env)}&version=${encodeURIComponent(this.config.version)}&ignoreGlobal=${this.config.ignoreGlobal}&token=${encodeURIComponent(this.config.token)}`;
|
|
const resp = await fetch(url, {
|
|
headers: this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {},
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
if (!resp.ok) {
|
|
throw new Error(`HTTP ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
if (data.code != null && data.code !== 0) {
|
|
throw new Error(`server error: ${data.msg || 'unknown'}`);
|
|
}
|
|
|
|
const secret = data['kenaito.config.encrypt-secret'];
|
|
if (secret) {
|
|
this.secret = secret;
|
|
}
|
|
const globalSecret = data['kenaito.config.global-encrypt-secret'];
|
|
if (globalSecret) {
|
|
this.globalSecret = globalSecret;
|
|
}
|
|
const ts = data['latest.fetch.timestamp'];
|
|
if (ts) {
|
|
this.lastTime = parseInt(ts, 10);
|
|
}
|
|
|
|
this.cache.write(data);
|
|
this.applyConfigs(data);
|
|
console.info('[ConfigClient] fetched', Object.keys(this.configs).length, 'configs');
|
|
}
|
|
|
|
async fetchChanged() {
|
|
const url = `${this.config.baseUrl}/api/fetchChangedConfigs`;
|
|
const payload = {
|
|
appName: this.config.appName,
|
|
env: this.config.env,
|
|
version: this.config.version,
|
|
lastTime: this.lastTime,
|
|
ignoreGlobal: this.config.ignoreGlobal,
|
|
token: this.config.token,
|
|
};
|
|
const resp = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(this.config.token ? { Authorization: `Bearer ${this.config.token}` } : {}),
|
|
},
|
|
body: JSON.stringify(payload),
|
|
signal: AbortSignal.timeout(10000),
|
|
});
|
|
if (!resp.ok) {
|
|
throw new Error(`HTTP ${resp.status}`);
|
|
}
|
|
const result = await resp.json();
|
|
if (result.code !== 0) {
|
|
throw new Error(`server error code: ${result.code}`);
|
|
}
|
|
|
|
const changedData = result.data || {};
|
|
const ts = changedData['latest.fetch.timestamp'];
|
|
if (ts) {
|
|
this.lastTime = parseInt(ts, 10);
|
|
}
|
|
delete changedData['latest.fetch.timestamp'];
|
|
|
|
const changed = {};
|
|
for (const [k, v] of Object.entries(changedData)) {
|
|
changed[k] = this.tryDecrypt(v);
|
|
}
|
|
|
|
Object.assign(this.configs, changed);
|
|
this.listener.notify(changed);
|
|
console.info('[ConfigClient]', Object.keys(changed).length, 'configs changed');
|
|
}
|
|
|
|
applyConfigs(data) {
|
|
for (const [k, v] of Object.entries(data)) {
|
|
if (k.startsWith('kenaito.') || k === 'latest.fetch.timestamp') {
|
|
continue;
|
|
}
|
|
if (typeof v !== 'string') {
|
|
continue;
|
|
}
|
|
this.configs[k] = this.tryDecrypt(v);
|
|
}
|
|
}
|
|
|
|
tryDecrypt(value) {
|
|
let decrypted = decryptConfig(this.secret, value);
|
|
if (decrypted === value && this.globalSecret) {
|
|
decrypted = decryptConfig(this.globalSecret, value);
|
|
}
|
|
return decrypted;
|
|
}
|
|
}
|
|
|
|
export { Config } from './config.js';
|