cangku/backend/src/services/yydNotificationCenterSettings.js

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,
};