Files
cwd/cwd-api/src/index.ts
anghunk b559d0da4c chore: 统一版本管理并添加自动发布工作流
- 将子项目版本号重置为 0.0.0,移除 private 标志
- 在根目录添加主 package.json 用于统一版本控制
- 更新 cwd-api 的包引用路径以使用根目录 package.json
- 添加 GitHub Actions 工作流,当根版本号变更时自动创建发布
2026-01-27 16:22:54 +08:00

446 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Hono } from 'hono';
import { Bindings } from './bindings';
import { customCors } from './utils/cors';
import { adminAuth } from './utils/auth';
import {
isValidEmail,
loadEmailNotificationSettings,
saveEmailNotificationSettings
} from './utils/email';
import { loadFeatureSettings } from './utils/featureSettings';
import packageJson from '../../package.json';
import { getComments } from './api/public/getComments';
import { postComment } from './api/public/postComment';
import { verifyAdminKey } from './api/public/verifyAdminKey';
import { adminLogin } from './api/admin/login';
import { deleteComment } from './api/admin/deleteComment';
import { listComments } from './api/admin/listComments';
import { exportComments } from './api/admin/exportComments';
import { importComments } from './api/admin/importComments';
import { updateStatus } from './api/admin/updateStatus';
import { updateComment } from './api/admin/updateComment';
import { getAdminEmail } from './api/admin/getAdminEmail';
import { setAdminEmail } from './api/admin/setAdminEmail';
import { testEmail } from './api/admin/testEmail';
import { getStats } from './api/admin/getStats';
import { getDomains } from './api/admin/getDomains';
import { trackVisit } from './api/public/trackVisit';
import { getVisitOverview, getVisitPages } from './api/admin/visitAnalytics';
import { getLikeStatus, likePage } from './api/public/like';
import { likeComment } from './api/public/likeComment';
import { listLikes } from './api/admin/listLikes';
import { getLikeStats } from './api/admin/likeStats';
import {
getFeatureSettings,
updateFeatureSettings
} from './api/admin/featureSettings';
const app = new Hono<{ Bindings: Bindings }>();
const VERSION = `v${packageJson.version}`;
const COMMENT_ADMIN_EMAIL_KEY = 'comment_admin_email';
const COMMENT_ADMIN_BADGE_KEY = 'comment_admin_badge';
const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
const COMMENT_ADMIN_ENABLED_KEY = 'comment_admin_enabled';
const COMMENT_ALLOWED_DOMAINS_KEY = 'comment_allowed_domains';
const COMMENT_ADMIN_KEY_HASH_KEY = 'comment_admin_key_hash';
const COMMENT_REQUIRE_REVIEW_KEY = 'comment_require_review';
const COMMENT_BLOCKED_IPS_KEY = 'comment_blocked_ips';
const COMMENT_BLOCKED_EMAILS_KEY = 'comment_blocked_emails';
async function loadCommentSettings(env: Bindings) {
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const keys = [
COMMENT_ADMIN_EMAIL_KEY,
COMMENT_ADMIN_BADGE_KEY,
COMMENT_AVATAR_PREFIX_KEY,
COMMENT_ADMIN_ENABLED_KEY,
COMMENT_ALLOWED_DOMAINS_KEY,
COMMENT_ADMIN_KEY_HASH_KEY,
COMMENT_REQUIRE_REVIEW_KEY,
COMMENT_BLOCKED_IPS_KEY,
COMMENT_BLOCKED_EMAILS_KEY
];
const { results } = await env.CWD_DB.prepare(
'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?, ?)'
)
.bind(...keys)
.all<{ key: string; value: string }>();
const map = new Map<string, string>();
for (const row of results) {
if (row && row.key) {
map.set(row.key, row.value);
}
}
const enabledRaw = map.get(COMMENT_ADMIN_ENABLED_KEY) ?? null;
const adminEnabled = enabledRaw === '1';
const requireReviewRaw = map.get(COMMENT_REQUIRE_REVIEW_KEY) ?? null;
const requireReview = requireReviewRaw === '1';
const blockedIpsRaw = map.get(COMMENT_BLOCKED_IPS_KEY) ?? '';
const blockedIps = blockedIpsRaw
? blockedIpsRaw.split(',').map((d) => d.trim()).filter(Boolean)
: [];
const blockedEmailsRaw = map.get(COMMENT_BLOCKED_EMAILS_KEY) ?? '';
const blockedEmails = blockedEmailsRaw
? blockedEmailsRaw.split(',').map((d) => d.trim()).filter(Boolean)
: [];
// 解析允许的域名列表
const allowedDomainsRaw = map.get(COMMENT_ALLOWED_DOMAINS_KEY) ?? '';
const allowedDomains = allowedDomainsRaw
? allowedDomainsRaw.split(',').map((d) => d.trim()).filter(Boolean)
: [];
return {
adminEmail: map.get(COMMENT_ADMIN_EMAIL_KEY) ?? null,
adminBadge: map.get(COMMENT_ADMIN_BADGE_KEY) ?? null,
avatarPrefix: map.get(COMMENT_AVATAR_PREFIX_KEY) ?? null,
adminEnabled,
allowedDomains,
requireReview,
blockedIps,
blockedEmails,
adminKey: map.get(COMMENT_ADMIN_KEY_HASH_KEY) ?? null,
adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY)
};
}
async function saveCommentSettings(
env: Bindings,
settings: {
adminEmail?: string;
adminBadge?: string;
avatarPrefix?: string;
adminEnabled?: boolean;
allowedDomains?: string[];
adminKey?: string;
requireReview?: boolean;
blockedIps?: string[];
blockedEmails?: string[];
}
) {
await env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
let adminKeyValue: string | undefined;
if (settings.adminKey !== undefined) {
adminKeyValue = settings.adminKey;
}
const entries: { key: string; value: string | null | undefined }[] = [
{ key: COMMENT_ADMIN_EMAIL_KEY, value: settings.adminEmail },
{ key: COMMENT_ADMIN_BADGE_KEY, value: settings.adminBadge },
{ key: COMMENT_AVATAR_PREFIX_KEY, value: settings.avatarPrefix },
{
key: COMMENT_ADMIN_ENABLED_KEY,
value:
typeof settings.adminEnabled === 'boolean'
? settings.adminEnabled
? '1'
: '0'
: undefined
},
{
key: COMMENT_ALLOWED_DOMAINS_KEY,
value: settings.allowedDomains ? settings.allowedDomains.join(',') : undefined
},
{
key: COMMENT_ADMIN_KEY_HASH_KEY,
value: adminKeyValue
},
{
key: COMMENT_REQUIRE_REVIEW_KEY,
value:
typeof settings.requireReview === 'boolean'
? settings.requireReview
? '1'
: '0'
: undefined
},
{
key: COMMENT_BLOCKED_IPS_KEY,
value: settings.blockedIps ? settings.blockedIps.join(',') : undefined
},
{
key: COMMENT_BLOCKED_EMAILS_KEY,
value: settings.blockedEmails ? settings.blockedEmails.join(',') : undefined
}
];
for (const entry of entries) {
if (entry.value !== undefined) {
const value = entry.value === null ? '' : entry.value;
const trimmed = typeof value === 'string' ? value.trim() : value;
if (trimmed) {
await env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
.bind(entry.key, trimmed)
.run();
} else {
await env.CWD_DB.prepare('DELETE FROM Settings WHERE key = ?').bind(entry.key).run();
}
}
}
}
app.use('*', async (c, next) => {
console.log('Request:start', {
method: c.req.method,
path: c.req.path,
url: c.req.url,
hasDb: !!c.env.CWD_DB,
hasAuthKv: !!c.env.CWD_AUTH_KV
});
const res = await next();
console.log('Request:end', {
method: c.req.method,
path: c.req.path
});
return res;
});
app.use('/api/*', async (c, next) => {
const corsMiddleware = customCors();
return corsMiddleware(c, next);
});
app.use('/admin/*', async (c, next) => {
const corsMiddleware = customCors();
return corsMiddleware(c, next);
});
app.get('/', (c) => {
return c.html(
`CWD 评论部署成功,当前版本 ${VERSION}<a href="https://cdw.js.org" target="_blank" rel="noreferrer">查看文档</a>`
);
});
app.get('/api/comments', getComments);
app.post('/api/comments', postComment);
app.post('/api/verify-admin', verifyAdminKey);
app.post('/api/analytics/visit', trackVisit);
app.get('/api/like', getLikeStatus);
app.post('/api/like', likePage);
app.post('/api/comments/like', likeComment);
app.get('/api/config/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);
const featureSettings = await loadFeatureSettings(c.env);
const {
adminKey,
adminKeySet,
blockedIps,
blockedEmails,
...publicSettings
} = settings as any;
return c.json({ ...publicSettings, ...featureSettings });
} catch (e: any) {
return c.json({ message: e.message || '加载评论配置失败' }, 500);
}
});
app.post('/admin/login', adminLogin);
app.use('/admin/*', adminAuth);
app.delete('/admin/comments/delete', deleteComment);
app.get('/admin/comments/list', listComments);
app.get('/admin/comments/export', exportComments);
app.post('/admin/comments/import', importComments);
app.put('/admin/comments/status', updateStatus);
app.put('/admin/comments/update', updateComment);
app.get('/admin/stats/comments', getStats);
app.get('/admin/stats/domains', getDomains);
app.get('/admin/analytics/overview', getVisitOverview);
app.get('/admin/analytics/pages', getVisitPages);
app.get('/admin/likes/list', listLikes);
app.get('/admin/likes/stats', getLikeStats);
app.get('/admin/settings/features', getFeatureSettings);
app.put('/admin/settings/features', updateFeatureSettings);
app.get('/admin/settings/email', getAdminEmail);
app.put('/admin/settings/email', setAdminEmail);
app.get('/admin/settings/email-notify', async (c) => {
try {
const settings = await loadEmailNotificationSettings(c.env);
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载邮件通知配置失败' }, 500);
}
});
app.put('/admin/settings/email-notify', async (c) => {
try {
const body = await c.req.json();
const globalEnabled =
typeof body.globalEnabled === 'boolean' ? body.globalEnabled : undefined;
const smtp = body.smtp && typeof body.smtp === 'object' ? body.smtp : undefined;
const templates = body.templates && typeof body.templates === 'object' ? body.templates : undefined;
await saveEmailNotificationSettings(c.env, {
globalEnabled,
smtp,
templates
});
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message || '保存失败' }, 500);
}
});
app.post('/admin/settings/email-test', testEmail);
app.get('/admin/settings/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);
return c.json(settings);
} catch (e: any) {
return c.json({ message: e.message || '加载评论配置失败' }, 500);
}
});
app.put('/admin/settings/comments', async (c) => {
try {
const body = await c.req.json();
const rawAdminEmail = typeof body.adminEmail === 'string' ? body.adminEmail : '';
const rawAdminBadge = typeof body.adminBadge === 'string' ? body.adminBadge : '';
const rawAvatarPrefix = typeof body.avatarPrefix === 'string' ? body.avatarPrefix : '';
const rawAdminEnabled = body.adminEnabled;
const rawAllowedDomains = Array.isArray(body.allowedDomains) ? body.allowedDomains : [];
const rawAdminKey = typeof body.adminKey === 'string' ? body.adminKey : undefined;
const rawRequireReview = body.requireReview;
const rawBlockedIps = Array.isArray(body.blockedIps) ? body.blockedIps : [];
const rawBlockedEmails = Array.isArray(body.blockedEmails) ? body.blockedEmails : [];
const adminEmail = rawAdminEmail.trim();
const adminBadge = rawAdminBadge.trim();
const avatarPrefix = rawAvatarPrefix.trim();
const adminEnabled =
typeof rawAdminEnabled === 'boolean'
? rawAdminEnabled
: rawAdminEnabled === '1' || rawAdminEnabled === 1;
const allowedDomains = rawAllowedDomains
.map((d: any) => (typeof d === 'string' ? d.trim() : ''))
.filter(Boolean);
const adminKey = rawAdminKey; // Can be undefined or empty string
const requireReview =
typeof rawRequireReview === 'boolean'
? rawRequireReview
: rawRequireReview === '1' || rawRequireReview === 1;
const blockedIps = rawBlockedIps
.map((d: any) => (typeof d === 'string' ? d.trim() : ''))
.filter(Boolean);
const blockedEmails = rawBlockedEmails
.map((d: any) => (typeof d === 'string' ? d.trim() : ''))
.filter(Boolean);
if (adminEmail && !isValidEmail(adminEmail)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
await saveCommentSettings(c.env, {
adminEmail,
adminBadge,
avatarPrefix,
adminEnabled,
allowedDomains,
adminKey,
requireReview,
blockedIps,
blockedEmails
});
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message || '保存失败' }, 500);
}
});
app.post('/admin/comments/block-ip', async (c) => {
try {
const body = await c.req.json();
const rawIp = typeof body.ip === 'string' ? body.ip : '';
const ip = rawIp.trim();
if (!ip) {
return c.json({ message: 'IP 地址不能为空' }, 400);
}
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind(COMMENT_BLOCKED_IPS_KEY)
.first<{ value: string }>();
const existing = row?.value || '';
const list = existing
? existing.split(',').map((d) => d.trim()).filter(Boolean)
: [];
if (!list.includes(ip)) {
list.push(ip);
const joined = list.join(',');
await c.env.CWD_DB.prepare(
'REPLACE INTO Settings (key, value) VALUES (?, ?)'
)
.bind(COMMENT_BLOCKED_IPS_KEY, joined)
.run();
}
return c.json({ message: '已加入 IP 黑名单' });
} catch (e: any) {
return c.json({ message: e.message || '操作失败' }, 500);
}
});
app.post('/admin/comments/block-email', async (c) => {
try {
const body = await c.req.json();
const rawEmail = typeof body.email === 'string' ? body.email : '';
const email = rawEmail.trim();
if (!email) {
return c.json({ message: '邮箱不能为空' }, 400);
}
if (!isValidEmail(email)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const row = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind(COMMENT_BLOCKED_EMAILS_KEY)
.first<{ value: string }>();
const existing = row?.value || '';
const list = existing
? existing.split(',').map((d) => d.trim()).filter(Boolean)
: [];
if (!list.includes(email)) {
list.push(email);
const joined = list.join(',');
await c.env.CWD_DB.prepare(
'REPLACE INTO Settings (key, value) VALUES (?, ?)'
)
.bind(COMMENT_BLOCKED_EMAILS_KEY, joined)
.run();
}
return c.json({ message: '已加入邮箱黑名单' });
} catch (e: any) {
return c.json({ message: e.message || '操作失败' }, 500);
}
});
export default app;