forked from lunanting/cangku
204 lines
7.3 KiB
JavaScript
204 lines
7.3 KiB
JavaScript
'use strict';
|
|
|
|
const database = require('../config/database');
|
|
const {
|
|
getResilientSecret,
|
|
saveResilientSecret,
|
|
} = require('./resilientSecretStore');
|
|
|
|
const CONFIG_KEY = 'yyd_notification_center_config';
|
|
const SECRET_KEY = 'yyd_notification_center_project_secret';
|
|
const DEFAULT_BASE_URL = 'https://sc.jiansun.vip/notification-center';
|
|
const DEFAULT_TIMEOUT_MS = 15000;
|
|
let cachedConfig = null;
|
|
let cachedAt = 0;
|
|
|
|
function normalizeText(value, maxLength = 4000) {
|
|
return String(value ?? '').trim().slice(0, maxLength);
|
|
}
|
|
|
|
function normalizeTimeout(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number)
|
|
? Math.min(Math.max(Math.trunc(number), 3000), 120000)
|
|
: DEFAULT_TIMEOUT_MS;
|
|
}
|
|
|
|
function safeJsonParse(value, fallback = {}) {
|
|
if (value && typeof value === 'object') return value;
|
|
try { return JSON.parse(value || '{}'); } catch (_error) { return fallback; }
|
|
}
|
|
|
|
function stripSecretFields(value) {
|
|
const raw = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : {};
|
|
delete raw.projectSecret;
|
|
delete raw.project_secret;
|
|
delete raw.secret;
|
|
return raw;
|
|
}
|
|
|
|
function maskSecret(value) {
|
|
const text = normalizeText(value);
|
|
if (!text) return '';
|
|
if (text.length <= 8) return `${text.slice(0, 2)}******`;
|
|
return `${text.slice(0, 4)}${'*'.repeat(Math.min(10, text.length - 8))}${text.slice(-4)}`;
|
|
}
|
|
|
|
function getEnvironmentConfig() {
|
|
return {
|
|
baseUrl: normalizeText(process.env.YYD_NOTIFICATION_CENTER_URL || DEFAULT_BASE_URL, 1000),
|
|
projectCode: normalizeText(
|
|
process.env.YYD_NOTIFICATION_PROJECT_CODE
|
|
|| process.env.YYD_NOTIFICATION_CENTER_PROJECT_CODE,
|
|
120
|
|
),
|
|
projectSecret: normalizeText(
|
|
process.env.YYD_NOTIFICATION_PROJECT_SECRET
|
|
|| process.env.YYD_NOTIFICATION_CENTER_PROJECT_SECRET,
|
|
1000
|
|
),
|
|
recipientAlias: normalizeText(
|
|
process.env.ALIBABA_1688_IMAGE_SEARCH_EXPIRY_RECIPIENT_ALIAS || '苏适',
|
|
50
|
|
),
|
|
timeoutMs: normalizeTimeout(process.env.YYD_NOTIFICATION_TIMEOUT_MS),
|
|
source: 'environment',
|
|
};
|
|
}
|
|
|
|
async function upsertNotificationCenterConfig(config) {
|
|
const rows = await database.local.query(
|
|
'SELECT id FROM system_config WHERE config_key = ? LIMIT 1',
|
|
[CONFIG_KEY]
|
|
);
|
|
const value = JSON.stringify(config);
|
|
if (rows[0]) {
|
|
await database.local.query(
|
|
'UPDATE system_config SET config_value = ?, config_type = ? WHERE config_key = ?',
|
|
[value, 'json', CONFIG_KEY]
|
|
);
|
|
} else {
|
|
await database.local.insert('system_config', {
|
|
config_key: CONFIG_KEY,
|
|
config_value: value,
|
|
config_type: 'json',
|
|
});
|
|
}
|
|
}
|
|
|
|
async function getYydNotificationCenterRuntimeConfig({ forceRefresh = false } = {}) {
|
|
if (!forceRefresh && cachedConfig && Date.now() - cachedAt < 30000) return { ...cachedConfig };
|
|
const fallback = getEnvironmentConfig();
|
|
let stored = {};
|
|
let storedReadable = false;
|
|
try {
|
|
const rows = await database.local.query(
|
|
'SELECT config_value AS configValue FROM system_config WHERE config_key = ? LIMIT 1',
|
|
[CONFIG_KEY]
|
|
);
|
|
stored = safeJsonParse(rows[0]?.configValue, {});
|
|
storedReadable = true;
|
|
} catch (error) {
|
|
console.warn('读取统一通知中心配置失败,已回退环境变量:', error.message);
|
|
}
|
|
|
|
const legacySecret = normalizeText(stored.projectSecret || stored.project_secret || stored.secret);
|
|
const secret = await getResilientSecret(SECRET_KEY, {
|
|
legacyValues: [legacySecret],
|
|
fallbackValues: [fallback.projectSecret],
|
|
});
|
|
const sanitizedStored = stripSecretFields(stored);
|
|
cachedConfig = {
|
|
baseUrl: normalizeText(sanitizedStored.baseUrl || sanitizedStored.base_url || fallback.baseUrl, 1000).replace(/\/+$/, ''),
|
|
projectCode: normalizeText(sanitizedStored.projectCode || sanitizedStored.project_code || fallback.projectCode, 120),
|
|
projectSecret: secret.value,
|
|
recipientAlias: normalizeText(sanitizedStored.recipientAlias || sanitizedStored.recipient_alias || fallback.recipientAlias, 50),
|
|
timeoutMs: normalizeTimeout(sanitizedStored.timeoutMs || sanitizedStored.timeout_ms || fallback.timeoutMs),
|
|
source: Object.keys(sanitizedStored).length > 0 ? 'database' : fallback.source,
|
|
secretSource: secret.source,
|
|
};
|
|
|
|
if (storedReadable && legacySecret && secret.value) {
|
|
try {
|
|
await upsertNotificationCenterConfig({
|
|
baseUrl: cachedConfig.baseUrl,
|
|
projectCode: cachedConfig.projectCode,
|
|
recipientAlias: cachedConfig.recipientAlias,
|
|
timeoutMs: cachedConfig.timeoutMs,
|
|
});
|
|
} catch (error) {
|
|
console.warn('清理通知中心旧密钥字段失败:', error.message);
|
|
}
|
|
}
|
|
cachedAt = Date.now();
|
|
return { ...cachedConfig };
|
|
}
|
|
|
|
function validateNotificationCenterConfig(config) {
|
|
if (!config.baseUrl || !config.projectCode || !config.projectSecret || !config.recipientAlias) {
|
|
throw Object.assign(new Error('通知中心地址、项目代码、项目密钥和提醒人不能为空'), { status: 400 });
|
|
}
|
|
let parsed;
|
|
try { parsed = new URL(config.baseUrl); } catch (_error) {
|
|
throw Object.assign(new Error('通知中心地址格式无效'), { status: 400 });
|
|
}
|
|
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
throw Object.assign(new Error('通知中心地址必须使用 HTTP 或 HTTPS'), { status: 400 });
|
|
}
|
|
}
|
|
|
|
async function saveYydNotificationCenterConfig(input = {}) {
|
|
const current = await getYydNotificationCenterRuntimeConfig({ forceRefresh: true });
|
|
const nextSecret = normalizeText(input.projectSecret || input.project_secret || input.secret);
|
|
const config = {
|
|
baseUrl: normalizeText(input.baseUrl || input.base_url || current.baseUrl, 1000).replace(/\/+$/, ''),
|
|
projectCode: normalizeText(input.projectCode || input.project_code || current.projectCode, 120),
|
|
projectSecret: nextSecret || current.projectSecret,
|
|
recipientAlias: normalizeText(input.recipientAlias || input.recipient_alias || current.recipientAlias, 50),
|
|
timeoutMs: normalizeTimeout(input.timeoutMs || input.timeout_ms || current.timeoutMs),
|
|
};
|
|
validateNotificationCenterConfig(config);
|
|
if (nextSecret) await saveResilientSecret(SECRET_KEY, nextSecret);
|
|
await upsertNotificationCenterConfig({
|
|
baseUrl: config.baseUrl,
|
|
projectCode: config.projectCode,
|
|
recipientAlias: config.recipientAlias,
|
|
timeoutMs: config.timeoutMs,
|
|
});
|
|
cachedConfig = { ...config, source: 'database', secretSource: 'database' };
|
|
cachedAt = Date.now();
|
|
return toAdminConfig(cachedConfig);
|
|
}
|
|
|
|
function toAdminConfig(config = {}) {
|
|
return {
|
|
configured: Boolean(config.baseUrl && config.projectCode && config.projectSecret && config.recipientAlias),
|
|
baseUrl: config.baseUrl || '',
|
|
projectCode: config.projectCode || '',
|
|
projectSecretConfigured: Boolean(config.projectSecret),
|
|
projectSecretPreview: maskSecret(config.projectSecret),
|
|
recipientAlias: config.recipientAlias || '苏适',
|
|
timeoutMs: normalizeTimeout(config.timeoutMs),
|
|
source: config.source || 'environment',
|
|
reminderDays: [2, 1, 0],
|
|
};
|
|
}
|
|
|
|
async function getYydNotificationCenterAdminConfig() {
|
|
return toAdminConfig(await getYydNotificationCenterRuntimeConfig({ forceRefresh: true }));
|
|
}
|
|
|
|
module.exports = {
|
|
CONFIG_KEY,
|
|
DEFAULT_BASE_URL,
|
|
SECRET_KEY,
|
|
getEnvironmentConfig,
|
|
getYydNotificationCenterAdminConfig,
|
|
getYydNotificationCenterRuntimeConfig,
|
|
maskSecret,
|
|
saveYydNotificationCenterConfig,
|
|
stripSecretFields,
|
|
toAdminConfig,
|
|
validateNotificationCenterConfig,
|
|
};
|