feat(mindspace): add opt-in local analytics
This commit is contained in:
+59
-1
@@ -1,4 +1,33 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const PUBLIC_PAGE_LIMIT_KEY = 'public_page_limit';
|
||||
const ANALYTICS_ENABLED_KEY = 'analytics_enabled';
|
||||
const ANALYTICS_WEBSITE_ID_KEY = 'analytics_website_id';
|
||||
const ANALYTICS_URL_KEY = 'analytics_url';
|
||||
const ANALYTICS_DOMAINS_KEY = 'analytics_domains';
|
||||
const ANALYTICS_ID_SECRET_KEY = 'analytics_id_secret';
|
||||
|
||||
function secretKey(env = process.env) {
|
||||
return crypto.createHash('sha256').update(String(env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret')).digest();
|
||||
}
|
||||
|
||||
function encryptSecret(value, env = process.env) {
|
||||
const iv = crypto.randomBytes(12);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', secretKey(env), iv);
|
||||
const ciphertext = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]);
|
||||
return JSON.stringify({ v: 1, iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), data: ciphertext.toString('base64') });
|
||||
}
|
||||
|
||||
function decryptSecret(value, env = process.env) {
|
||||
try {
|
||||
const payload = JSON.parse(String(value));
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey(env), Buffer.from(payload.iv, 'base64'));
|
||||
decipher.setAuthTag(Buffer.from(payload.tag, 'base64'));
|
||||
return Buffer.concat([decipher.update(Buffer.from(payload.data, 'base64')), decipher.final()]).toString('utf8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function asPositiveInteger(value, fallback) {
|
||||
const parsed = Number(value);
|
||||
@@ -20,6 +49,13 @@ async function ensureConfigTable(pool) {
|
||||
export function defaultMindSpaceConfig(env = process.env) {
|
||||
return {
|
||||
publicPageLimit: asPositiveInteger(env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5, 5),
|
||||
analytics: {
|
||||
enabled: String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true',
|
||||
websiteId: String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim(),
|
||||
analyticsUrl: String(env.MEMIND_ANALYTICS_URL ?? 'http://127.0.0.1:3100').trim(),
|
||||
domains: String(env.MEMIND_ANALYTICS_DOMAINS ?? '127.0.0.1,localhost').trim(),
|
||||
idSecretConfigured: Boolean(String(env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,7 +81,7 @@ export async function ensureMindSpaceConfig(pool, { env = process.env, seedDefau
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadMindSpaceConfig(pool, { env = process.env } = {}) {
|
||||
export async function loadMindSpaceConfig(pool, { env = process.env, includeAnalyticsSecret = false } = {}) {
|
||||
const config = defaultMindSpaceConfig(env);
|
||||
try {
|
||||
const rows = await readConfigRows(pool);
|
||||
@@ -53,6 +89,15 @@ export async function loadMindSpaceConfig(pool, { env = process.env } = {}) {
|
||||
if (row.key === PUBLIC_PAGE_LIMIT_KEY) {
|
||||
config.publicPageLimit = asPositiveInteger(row.value, config.publicPageLimit);
|
||||
}
|
||||
if (row.key === ANALYTICS_ENABLED_KEY) config.analytics.enabled = row.value === 'true';
|
||||
if (row.key === ANALYTICS_WEBSITE_ID_KEY) config.analytics.websiteId = String(row.value ?? '');
|
||||
if (row.key === ANALYTICS_URL_KEY) config.analytics.analyticsUrl = String(row.value ?? '');
|
||||
if (row.key === ANALYTICS_DOMAINS_KEY) config.analytics.domains = String(row.value ?? '');
|
||||
if (row.key === ANALYTICS_ID_SECRET_KEY) {
|
||||
const secret = decryptSecret(row.value, env);
|
||||
config.analytics.idSecretConfigured = Boolean(secret);
|
||||
if (includeAnalyticsSecret) config.analytics.idSecret = secret;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.code === 'ER_NO_SUCH_TABLE') return config;
|
||||
@@ -73,6 +118,14 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
|
||||
}
|
||||
updates.push([PUBLIC_PAGE_LIMIT_KEY, String(publicPageLimit), '公开页面数量上限']);
|
||||
}
|
||||
if (patch?.analytics) {
|
||||
const analytics = patch.analytics;
|
||||
if (analytics.enabled !== undefined) updates.push([ANALYTICS_ENABLED_KEY, String(Boolean(analytics.enabled)), '本地 Umami 分析开关']);
|
||||
if (analytics.websiteId !== undefined) updates.push([ANALYTICS_WEBSITE_ID_KEY, String(analytics.websiteId ?? '').trim(), '本地 Umami Website ID']);
|
||||
if (analytics.analyticsUrl !== undefined) updates.push([ANALYTICS_URL_KEY, String(analytics.analyticsUrl || 'http://127.0.0.1:3100').trim(), '本地 Umami 地址']);
|
||||
if (analytics.domains !== undefined) updates.push([ANALYTICS_DOMAINS_KEY, String(analytics.domains ?? '').trim(), '本地统计域名']);
|
||||
if (analytics.idSecret !== undefined && String(analytics.idSecret).trim()) updates.push([ANALYTICS_ID_SECRET_KEY, encryptSecret(String(analytics.idSecret).trim(), env), '本地分析匿名化密钥']);
|
||||
}
|
||||
|
||||
if (updates.length === 0) return loadMindSpaceConfig(pool, { env });
|
||||
|
||||
@@ -93,4 +146,9 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
|
||||
|
||||
export const mindspaceConfigInternals = {
|
||||
PUBLIC_PAGE_LIMIT_KEY,
|
||||
ANALYTICS_ENABLED_KEY,
|
||||
ANALYTICS_WEBSITE_ID_KEY,
|
||||
ANALYTICS_URL_KEY,
|
||||
ANALYTICS_DOMAINS_KEY,
|
||||
ANALYTICS_ID_SECRET_KEY,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user