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

View File

@@ -0,0 +1,23 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const deleteComment = async (c: Context<{ Bindings: Bindings }>) => {
const id = c.req.query('id');
if (!id) {
return c.json({ message: "Missing id" }, 400);
}
// 从数据库中直接删除评论
const { success } = await c.env.CWD_DB.prepare(
"DELETE FROM Comment WHERE id = ?"
).bind(id).run();
if (!success) {
return c.json({ message: "Delete operation failed" }, 500);
}
return c.json({
message: `Comment deleted, id: ${id}.`
});
};

View File

@@ -0,0 +1,14 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const exportComments = async (c: Context<{ Bindings: Bindings }>) => {
try {
const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY created DESC'
).all();
return c.json(results);
} catch (e: any) {
return c.json({ message: e.message || '导出失败' }, 500);
}
};

View File

@@ -0,0 +1,18 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const getAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
try {
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('admin_notify_email')
.first<{ value: string }>();
const email = row?.value || null;
return c.json({ email });
} catch (e: any) {
return c.json({ message: e.message }, 500);
}
};

View File

@@ -0,0 +1,170 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
try {
const body = await c.req.json();
const rawComments = Array.isArray(body) ? body : [body];
if (rawComments.length === 0) {
return c.json({ message: '导入数据为空' }, 400);
}
// 映射 Twikoo / Artalk 数据结构到 CWD 结构
const comments = rawComments.map((item: any) => {
// Twikoo 特征检测
const isTwikoo =
item.href !== undefined || item.nick !== undefined || item.comment !== undefined;
// Artalk 特征检测 (page_key 是 Artalk 特有的)
const isArtalk = item.page_key !== undefined && item.content !== undefined;
if (isArtalk) {
// Artalk 映射逻辑
// 处理 ID: Artalk ID 通常是数字
let id = undefined;
if (typeof item.id === 'number') {
id = item.id;
} else if (typeof item.id === 'string' && /^\d+$/.test(item.id)) {
id = parseInt(item.id, 10);
}
// 处理时间
let created = Date.now();
if (item.created_at) {
created = new Date(item.created_at).getTime();
}
return {
id, // >>> id
created, // >>> created_at 转为时间戳
post_slug: item.page_key || '', // >>> page_key
name: item.nick || 'Anonymous', // >>> nick
email: item.email || '', // >>> email
url: item.link || null, // >>> link
ip_address: item.ip || null, // >>> ip
device: null, // >>> 保持空
os: null, // >>> 保持空
browser: null, // >>> 保持空
ua: item.ua || null, // >>> ua
content_text: item.content || '', // >>> content
content_html: item.content || '', // >>> content
parent_id: null, // >>> 保持 null
status: 'approved' // >>> 保持 "approved"
};
}
if (isTwikoo) {
// 处理 ID: 如果 _id 是数字则保留,否则丢弃(让数据库自增)
// Twikoo 的 _id 通常是 ObjectId 字符串,无法直接存入 INTEGER PRIMARY KEY
// 除非 _id 恰好是数字
let id = undefined;
if (typeof item._id === 'number') {
id = item._id;
} else if (typeof item._id === 'string' && /^\d+$/.test(item._id)) {
id = parseInt(item._id, 10);
}
// 处理时间
let created = Date.now();
if (item.created) {
// 支持时间戳或 ISO 字符串
created = new Date(item.created).getTime();
}
return {
id, // 可能为 undefined
created, // >>> created
post_slug: item.href || "", // >>> href
name: item.nick || "Anonymous", // >>> nick
email: item.mail || "", // >>> mail
url: item.link || null, // >>> link
ip_address: item.ip || null, // >>> ip
device: null, // >>> 保持空
os: null, // >>> 保持空
browser: null, // >>> 保持空
ua: item.ua || null, // >>> ua
content_text: item.comment || "", // >>> comment
content_html: item.comment || "", // >>> comment
parent_id: null, // >>> 保持 null
status: "approved" // >>> approved
};
}
// 否则假设已经是 CWD 格式
return item;
});
// 按 ID 升序排序,防止因外键约束导致插入失败(子评论先于父评论插入)
// 对于 Twikoo 导入id 可能不存在,或者被重置。
// 如果 parent_id 全部为 null则排序其实不重要没有依赖
comments.sort((a: any, b: any) => {
const idA = a.id || 0;
const idB = b.id || 0;
return idA - idB;
});
const stmts = comments.map((comment: any) => {
const {
id,
created,
post_slug,
name,
email,
url,
ip_address,
device,
os,
browser,
ua,
content_text,
content_html,
parent_id,
status
} = comment;
const fields = [
'created', 'post_slug', 'name', 'email', 'url',
'ip_address', 'device', 'os', 'browser', 'ua',
'content_text', 'content_html', 'parent_id', 'status'
];
const values = [
created || Date.now(),
post_slug || "",
name || "Anonymous",
email || "",
url || null,
ip_address || null,
device || null,
os || null,
browser || null,
ua || null,
content_text || "",
content_html || "",
parent_id || null,
status || "approved"
];
if (id !== undefined && id !== null) {
fields.unshift('id');
values.unshift(id);
}
const placeholders = fields.map(() => '?').join(', ');
const sql = `INSERT OR REPLACE INTO Comment (${fields.join(', ')}) VALUES (${placeholders})`;
return c.env.CWD_DB.prepare(sql).bind(...values);
});
// 批量执行,每批 50 条
const BATCH_SIZE = 50;
for (let i = 0; i < stmts.length; i += BATCH_SIZE) {
const batch = stmts.slice(i, i + BATCH_SIZE);
await c.env.CWD_DB.batch(batch);
}
return c.json({ message: `成功导入 ${comments.length} 条评论` });
} catch (e: any) {
console.error(e);
return c.json({ message: e.message || '导入失败' }, 500);
}
};

View File

@@ -0,0 +1,55 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { getCravatar } from '../../utils/getAvatar';
const COMMENT_AVATAR_PREFIX_KEY = 'comment_avatar_prefix';
export const listComments = async (c: Context<{ Bindings: Bindings }>) => {
const page = parseInt(c.req.query('page') || '1');
const limit = 10;
const offset = (page - 1) * limit;
const totalCount = await c.env.CWD_DB.prepare(
'SELECT COUNT(*) as count FROM Comment'
).first<{ count: number }>();
const { results } = await c.env.CWD_DB.prepare(
'SELECT * FROM Comment ORDER BY created DESC LIMIT ? OFFSET ?'
)
.bind(limit, offset)
.all();
await c.env.CWD_DB.prepare(
'CREATE TABLE IF NOT EXISTS Settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)'
).run();
const avatarRow = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind(COMMENT_AVATAR_PREFIX_KEY)
.first<{ value: string }>();
const avatarPrefix = avatarRow?.value || null;
const data = await Promise.all(
results.map(async (row: any) => ({
id: row.id,
created: row.created,
name: row.name,
email: row.email,
postSlug: row.post_slug,
url: row.url,
ipAddress: row.ip_address,
contentText: row.content_text,
contentHtml: row.content_html,
status: row.status,
ua: row.ua,
avatar: await getCravatar(row.email, avatarPrefix || undefined)
}))
);
return c.json({
data,
pagination: {
page,
limit,
total: Math.ceil(((totalCount?.count as number) || 0) / limit)
}
});
};

View File

@@ -0,0 +1,62 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
// 简单配置:允许尝试 5 次,锁定 30 分钟
const MAX_ATTEMPTS = 5;
const LOCK_TIME = 30 * 60; // 秒
export const adminLogin = async (c: Context<{ Bindings: Bindings }>) => {
const data = await c.req.json();
const ip = c.req.header('cf-connecting-ip') || '127.0.0.1';
const blockKey = `block:${ip}`;
const attemptKey = `attempts:${ip}`;
// 1. 检查 IP 是否被封禁
const isBlocked = await c.env.CWD_AUTH_KV.get(blockKey);
if (isBlocked) {
return c.json({ message: 'IP is blocked due to multiple failed login attempts' }, 403);
}
// 2. 验证用户名密码
const ADMIN_NAME = c.env.ADMIN_NAME || 'Admin';
const ADMIN_PASSWORD = c.env.ADMIN_PASSWORD || 'password';
const isValid = data.name === ADMIN_NAME && data.password === ADMIN_PASSWORD;
if (!isValid) {
// --- 登录失败逻辑 ---
// 获取当前失败次数
const attempts = parseInt((await c.env.CWD_AUTH_KV.get(attemptKey)) || '0') + 1;
if (attempts >= MAX_ATTEMPTS) {
// 达到上限,封禁 30 分钟
await c.env.CWD_AUTH_KV.put(blockKey, '1', { expirationTtl: LOCK_TIME });
await c.env.CWD_AUTH_KV.delete(attemptKey); // 清除尝试计数
return c.json({ message: 'IP is blocked due to multiple failed login attempts' }, 403);
} else {
// 记录失败次数,设置 10 分钟内连续失败才计数
await c.env.CWD_AUTH_KV.put(attemptKey, attempts.toString(), { expirationTtl: 600 });
return c.json({ message: 'Invalid username or password', failedAttempts: attempts }, 401);
}
}
// --- 3. 登录成功逻辑 ---
await c.env.CWD_AUTH_KV.delete(attemptKey);
// 生成 Token (你的 tempKey)
const tempKey = crypto.randomUUID();
// 将 Token 存入 KV有效期 24 小时86400秒
await c.env.CWD_AUTH_KV.put(
`token:${tempKey}`,
JSON.stringify({
user: data.name,
ip: ip,
}),
{ expirationTtl: 86400 }
);
return c.json({
data: { key: tempKey },
});
};

View File

@@ -0,0 +1,22 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { isValidEmail } from '../../utils/email';
export const setAdminEmail = async (c: Context<{ Bindings: Bindings }>) => {
try {
const { email } = await c.req.json();
if (!email || !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();
await c.env.CWD_DB.prepare('REPLACE INTO Settings (key, value) VALUES (?, ?)')
.bind('admin_notify_email', email)
.run();
return c.json({ message: '保存成功' });
} catch (e: any) {
return c.json({ message: e.message }, 500);
}
};

View File

@@ -0,0 +1,31 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
import { sendTestEmail, EmailNotificationSettings, isValidEmail } from '../../utils/email';
export const testEmail = async (c: Context<{ Bindings: Bindings }>) => {
try {
const body = await c.req.json();
const toEmail = body.toEmail;
if (!toEmail || !isValidEmail(toEmail)) {
return c.json({ message: '请输入有效的接收邮箱' }, 400);
}
const smtp: EmailNotificationSettings['smtp'] = body.smtp;
if (!smtp || !smtp.user || !smtp.pass) {
return c.json({ message: 'SMTP 配置不完整' }, 400);
}
const result = await sendTestEmail(c.env, toEmail, smtp);
if (result.success) {
return c.json({ message: '邮件发送成功' });
} else {
return c.json({ message: '邮件发送失败: ' + result.message }, 500);
}
} catch (e: any) {
return c.json({ message: e.message || '测试失败' }, 500);
}
};

View File

@@ -0,0 +1,23 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const updateStatus = async (c: Context<{ Bindings: Bindings }>) => {
const id = c.req.query('id');
const status = c.req.query('status'); // 按照你规范中 URL 参数的形式
if (!id || !status) {
return c.json({ message: "Missing id or status" }, 400);
}
const { success } = await c.env.CWD_DB.prepare(
"UPDATE Comment SET status = ? WHERE id = ?"
).bind(status, id).run();
if (!success) {
return c.json({ message: "Update failed" }, 500);
}
return c.json({
message: `Comment status updated, id: ${id}, status: ${status}.`
});
};

View File

@@ -0,0 +1,108 @@
import { Context } from 'hono'
import { Bindings } from '../../bindings'
import { getCravatar } from '../../utils/getAvatar'
export const getComments = async (c: Context<{ Bindings: Bindings }>) => {
const post_slug = c.req.query('post_slug')
const page = parseInt(c.req.query('page') || '1')
const limit = Math.min(parseInt(c.req.query('limit') || '20'), 50)
const nested = c.req.query('nested') !== 'false'
const avatar_prefix = c.req.query('avatar_prefix')
const offset = (page - 1) * limit
if (!post_slug) return c.json({ message: "post_slug is required" }, 400)
try {
// 1. 查询审核通过的评论
const query = `
SELECT id, name, email, url, content_text as contentText,
content_html as contentHtml, created, parent_id as parentId,
post_slug as postSlug
FROM Comment
WHERE post_slug = ? AND status = "approved"
ORDER BY created DESC
`
const { results } = await c.env.CWD_DB.prepare(query).bind(post_slug).all()
// 2. 批量处理头像并格式化
const allComments = await Promise.all(results.map(async (row: any) => ({
...row,
avatar: await getCravatar(row.email, avatar_prefix || undefined),
replies: []
})))
// 3. 处理嵌套逻辑扁平化2级往后的回复都放在根评论的 replies 中)
if (nested) {
const commentMap = new Map()
const rootComments: any[] = []
// 建立评论映射
allComments.forEach(comment => commentMap.set(comment.id, comment))
// 找出所有根评论
allComments.forEach(comment => {
if (!comment.parentId) {
rootComments.push(comment)
}
})
// 为每个非根评论找到其根评论,并添加 replyToAuthor 字段
allComments.forEach(comment => {
if (comment.parentId) {
// 获取直接父评论的作者名
const parentComment = commentMap.get(comment.parentId)
if (parentComment) {
comment.replyToAuthor = parentComment.name
}
// 向上查找根评论
let rootId = comment.parentId
let current = commentMap.get(rootId)
while (current && current.parentId) {
rootId = current.parentId
current = commentMap.get(rootId)
}
// 将回复添加到根评论的 replies 中
const rootComment = commentMap.get(rootId)
if (rootComment && !rootComment.parentId) {
rootComment.replies.push(comment)
}
}
})
// 对每个根评论的 replies 按时间正序排列
rootComments.forEach(root => {
root.replies.sort((a: any, b: any) =>
a.created - b.created
)
})
// 对根评论进行分页
const paginatedData = rootComments.slice(offset, offset + limit)
return c.json({
data: paginatedData,
pagination: {
page,
limit,
total: Math.ceil(rootComments.length / limit),
totalCount: allComments.length,
}
})
} else {
// 非嵌套逻辑直接分页
const paginatedData = allComments.slice(offset, offset + limit)
return c.json({
data: paginatedData,
pagination: {
page,
limit,
total: Math.ceil(allComments.length / limit),
totalCount: allComments.length,
}
})
}
} catch (e: any) {
return c.json({ message: e.message }, 500)
}
}

View File

@@ -0,0 +1,272 @@
import { Context } from 'hono';
import { UAParser } from 'ua-parser-js';
import { marked } from 'marked';
import xss from 'xss';
import { Bindings } from '../../bindings';
import {
sendCommentNotification,
sendCommentReplyNotification,
isValidEmail,
getAdminNotifyEmail,
loadEmailNotificationSettings,
EmailNotificationSettings
} from '../../utils/email';
// 检查内容,将<script>标签之间的内容删除
export function checkContent(content: string): string {
return content.replace(/<script[\s\S]*?<\/script>/g, "");
}
export const postComment = async (c: Context<{ Bindings: Bindings }>) => {
const data = await c.req.json();
if (!data || typeof data !== 'object') {
return c.json({ message: '无效的请求体' }, 400);
}
const { post_slug, content: rawContent, name: rawName, email, url, post_title, post_url, adminToken } = data;
const parentId = (data as any).parent_id ?? (data as any).parentId ?? null;
if (!post_slug || typeof post_slug !== 'string') {
return c.json({ message: 'post_slug 必填' }, 400);
}
if (!rawContent || typeof rawContent !== 'string') {
return c.json({ message: '评论内容不能为空' }, 400);
}
if (!rawName || typeof rawName !== 'string') {
return c.json({ message: '昵称不能为空' }, 400);
}
if (!email || typeof email !== 'string') {
return c.json({ message: '邮箱不能为空' }, 400);
}
if (!isValidEmail(email)) {
return c.json({ message: '邮箱格式不正确' }, 400);
}
const ua = c.req.header('user-agent') || "";
const ip = c.req.header('cf-connecting-ip') || "127.0.0.1";
const adminEmail = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('comment_admin_email')
.first<string>('value');
const requireReviewRaw = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('comment_require_review')
.first<string>('value');
const requireReview = requireReviewRaw === '1';
const blockedIpsRow = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('comment_blocked_ips')
.first<{ value: string }>();
const blockedIpsValue = blockedIpsRow?.value || '';
const blockedIps = blockedIpsValue
? blockedIpsValue.split(',').map((d) => d.trim()).filter(Boolean)
: [];
if (blockedIps.length && blockedIps.includes(ip)) {
return c.json({ message: '当前 IP 已被限制评论,请联系站长进行处理' }, 403);
}
const blockedEmailsRow = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('comment_blocked_emails')
.first<{ value: string }>();
const blockedEmailsValue = blockedEmailsRow?.value || '';
const blockedEmails = blockedEmailsValue
? blockedEmailsValue.split(',').map((d) => d.trim()).filter(Boolean)
: [];
if (blockedEmails.length && blockedEmails.includes(email)) {
return c.json({ message: '当前邮箱已被限制评论,请联系站长进行处理' }, 403);
}
let isAdminComment = false;
if (adminEmail && email === adminEmail) {
const adminKey = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?')
.bind('comment_admin_key_hash')
.first<string>('value');
if (adminKey) {
const lockKey = `admin_lock:${ip}`;
const isLocked = await c.env.CWD_AUTH_KV.get(lockKey);
if (isLocked) {
return c.json({ message: "验证失败次数过多,请 30 分钟后再试" }, 403);
}
if (!adminToken) {
return c.json({ message: "请输入管理员密钥", requireAuth: true }, 401);
}
if (adminToken !== adminKey) {
const failKey = `admin_fail:${ip}`;
const failsStr = await c.env.CWD_AUTH_KV.get(failKey);
let fails = failsStr ? parseInt(failsStr) : 0;
fails++;
if (fails >= 3) {
await c.env.CWD_AUTH_KV.put(lockKey, '1', { expirationTtl: 1800 });
await c.env.CWD_AUTH_KV.delete(failKey);
return c.json({ message: "验证失败次数过多,请 30 分钟后再试" }, 403);
} else {
await c.env.CWD_AUTH_KV.put(failKey, fails.toString(), { expirationTtl: 3600 });
return c.json({ message: "密钥错误" }, 401);
}
}
await c.env.CWD_AUTH_KV.delete(`admin_fail:${ip}`);
isAdminComment = true;
}
}
// 2. 检查评论频率控制 (对应 canPostComment)
// 这里建议使用 D1 查最近一条评论的时间,或者直接放行(如果使用了 Cloudflare WAF
const lastComment = await c.env.CWD_DB.prepare(
'SELECT created FROM Comment WHERE ip_address = ? ORDER BY created DESC LIMIT 1'
).bind(ip).first<{ created: number }>();
if (lastComment) {
const lastTime = lastComment.created;
if (Date.now() - lastTime < 10 * 1000) {
return c.json({ message: "评论频繁等10s后再试" }, 429);
}
}
// 3. 准备数据
const cleanedContent = checkContent(rawContent);
const contentText = cleanedContent;
const name = checkContent(rawName);
// Markdown 渲染与 XSS 过滤
const html = await marked.parse(cleanedContent, { async: true });
const contentHtml = xss(html, {
whiteList: {
...xss.whiteList,
code: ['class'],
span: ['class', 'style'],
pre: ['class'],
div: ['class', 'style'],
img: ['src', 'alt', 'title', 'width', 'height', 'style']
}
});
console.log('PostComment:request', {
postSlug: post_slug,
hasParent: parentId !== null && parentId !== undefined,
name,
email,
ip
});
const uaParser = new UAParser(ua);
const uaResult = uaParser.getResult();
const defaultStatus = requireReview && !isAdminComment ? "pending" : "approved";
try {
const { success } = await c.env.CWD_DB.prepare(`
INSERT INTO Comment (
created, post_slug, name, email, url, ip_address,
os, browser, device, ua, content_text, content_html,
parent_id, status
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
Date.now(),
post_slug,
name,
email,
url || null,
ip,
`${uaResult.os.name || ""} ${uaResult.os.version || ""}`.trim(),
`${uaResult.browser.name || ""} ${uaResult.browser.version || ""}`.trim(),
uaResult.device.model || uaResult.device.type || "Desktop",
ua,
contentText,
contentHtml,
parentId || null,
defaultStatus
).run();
if (!success) throw new Error("Database insert failed");
console.log('PostComment:inserted', {
postSlug: post_slug,
hasParent: parentId !== null && parentId !== undefined,
ip
});
let notifySettings: EmailNotificationSettings = {
globalEnabled: true
};
try {
notifySettings = await loadEmailNotificationSettings(c.env);
} catch (e) {
console.error('PostComment:mailDispatch:loadEmailSettingsFailed', e);
}
if (!notifySettings.globalEnabled) {
console.log('PostComment:mailDispatch:disabledByGlobalConfig');
} else {
console.log('PostComment:mailDispatch:start', {
hasParent: parentId !== null && parentId !== undefined
});
c.executionCtx.waitUntil((async () => {
try {
if (parentId !== null && parentId !== undefined) {
let adminEmail: string | null = null;
try {
adminEmail = await getAdminNotifyEmail(c.env);
} catch (e) {
console.error('PostComment:mailDispatch:userReply:getAdminEmailFailed', e);
}
const isAdminReply = !!adminEmail && email === adminEmail;
const parentComment = await c.env.CWD_DB.prepare(
"SELECT name, email, content_html FROM Comment WHERE id = ?"
).bind(parentId).first<{ name: string, email: string, content_html: string }>();
if (parentComment && parentComment.email && parentComment.email !== email) {
if (isValidEmail(parentComment.email)) {
console.log('PostComment:mailDispatch:userReply:send', {
toEmail: parentComment.email,
toName: parentComment.name
});
await sendCommentReplyNotification(c.env, {
toEmail: parentComment.email,
toName: parentComment.name,
postTitle: data.post_title,
parentComment: parentComment.content_html,
replyAuthor: name,
replyContent: contentHtml,
postUrl: data.post_url,
}, notifySettings.smtp, notifySettings.templates?.reply);
console.log('PostComment:mailDispatch:userReply:sent', {
toEmail: parentComment.email
});
}
}
} else {
console.log('PostComment:mailDispatch:admin:send');
await sendCommentNotification(c.env, {
postTitle: data.post_title,
postUrl: data.post_url,
commentAuthor: name,
commentContent: contentHtml
}, notifySettings.smtp, notifySettings.templates?.admin);
console.log('PostComment:mailDispatch:admin:sent');
}
} catch (mailError) {
console.error("Mail Notification Failed:", mailError);
}
})());
}
if (defaultStatus === "pending") {
return c.json({
message: '已提交评论,待管理员审核后显示',
status: defaultStatus
});
}
return c.json({
message: '评论已提交',
status: defaultStatus
});
} catch (e: any) {
console.error("Create Comment Error:", e);
return c.json({ message: "Internal Server Error" }, 500);
}
};

View File

@@ -0,0 +1,49 @@
import { Context } from 'hono';
import { Bindings } from '../../bindings';
export const verifyAdminKey = async (c: Context<{ Bindings: Bindings }>) => {
const data = await c.req.json();
const { adminToken } = data;
const ip = c.req.header('cf-connecting-ip') || "127.0.0.1";
if (!adminToken) {
return c.json({ message: "请输入管理员密钥" }, 401);
}
// Check lock
const lockKey = `admin_lock:${ip}`;
const isLocked = await c.env.CWD_AUTH_KV.get(lockKey);
if (isLocked) {
return c.json({ message: "验证失败次数过多请30分钟后再试" }, 403);
}
const adminKey = await c.env.CWD_DB.prepare('SELECT value FROM Settings WHERE key = ?').bind('comment_admin_key_hash').first<string>('value');
if (!adminKey) {
// If no key set, verification is technically successful or not needed?
// If key is not set, we can't verify. Return success?
// Requirement says "Provide admin key setting...".
return c.json({ message: "未设置管理员密钥" }, 200);
}
if (adminToken !== adminKey) {
// Handle failure
const failKey = `admin_fail:${ip}`;
const failsStr = await c.env.CWD_AUTH_KV.get(failKey);
let fails = failsStr ? parseInt(failsStr) : 0;
fails++;
if (fails >= 3) {
await c.env.CWD_AUTH_KV.put(lockKey, '1', { expirationTtl: 1800 }); // 30 mins
await c.env.CWD_AUTH_KV.delete(failKey);
return c.json({ message: "验证失败次数过多请30分钟后再试" }, 403);
} else {
await c.env.CWD_AUTH_KV.put(failKey, fails.toString(), { expirationTtl: 3600 }); // 1 hour reset
return c.json({ message: "密钥错误" }, 401);
}
}
// Success
await c.env.CWD_AUTH_KV.delete(`admin_fail:${ip}`);
return c.json({ message: "验证通过" });
};