chore: 重构项目结构

This commit is contained in:
anghunk
2026-01-21 10:53:11 +08:00
parent fc86934094
commit 643d5c92e8
59 changed files with 74 additions and 74 deletions

417
cwd-api/src/index.ts Normal file
View File

@@ -0,0 +1,417 @@
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 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 { getAdminEmail } from './api/admin/getAdminEmail';
import { setAdminEmail } from './api/admin/setAdminEmail';
import { testEmail } from './api/admin/testEmail';
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://github.com/anghunk/cwd" target="_blank" rel="noreferrer">查看文档</a>`
);
});
app.get('/api/comments', getComments);
app.post('/api/comments', postComment);
app.post('/api/verify-admin', verifyAdminKey);
app.get('/api/config/comments', async (c) => {
try {
const settings = await loadCommentSettings(c.env);
const {
adminKey,
adminKeySet,
blockedIps,
blockedEmails,
...publicSettings
} = settings as any;
return c.json(publicSettings);
} 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.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;