diff --git a/cwd-comments-admin/src/api/admin.ts b/cwd-comments-admin/src/api/admin.ts index f8c74de..26dc39d 100644 --- a/cwd-comments-admin/src/api/admin.ts +++ b/cwd-comments-admin/src/api/admin.ts @@ -43,6 +43,7 @@ export type CommentSettingsResponse = { adminKey?: string | null; adminKeySet?: boolean; requireReview?: boolean; + blockedIps?: string[]; }; export type EmailNotifySettingsResponse = { @@ -137,10 +138,15 @@ export function saveCommentSettings(data: { allowedDomains?: string[]; adminKey?: string; requireReview?: boolean; + blockedIps?: string[]; }): Promise<{ message: string }> { return put<{ message: string }>('/admin/settings/comments', data); } +export function blockIp(ip: string): Promise<{ message: string }> { + return post<{ message: string }>('/admin/comments/block-ip', { ip }); +} + export function exportComments(): Promise { return get('/admin/comments/export'); } diff --git a/cwd-comments-admin/src/views/CommentsView.vue b/cwd-comments-admin/src/views/CommentsView.vue index d4bded2..b5792a1 100644 --- a/cwd-comments-admin/src/views/CommentsView.vue +++ b/cwd-comments-admin/src/views/CommentsView.vue @@ -38,6 +38,9 @@
{{ item.name }}
{{ item.email }}
{{ formatDate(item.created) }} +
+ {{ item.ipAddress }} +
@@ -176,6 +179,7 @@ import { fetchComments, deleteComment, updateCommentStatus, + blockIp, } from "../api/admin"; const comments = ref([]); @@ -292,6 +296,21 @@ async function removeComment(item: CommentItem) { } } +async function handleBlockIp(item: CommentItem) { + if (!item.ipAddress) { + return; + } + if (!window.confirm(`确认将 IP ${item.ipAddress} 加入黑名单吗?`)) { + return; + } + try { + const res = await blockIp(item.ipAddress); + window.alert(res.message || "已加入 IP 黑名单"); + } catch (e: any) { + error.value = e.message || "屏蔽 IP 失败"; + } +} + onMounted(() => { loadComments(); }); @@ -621,4 +640,12 @@ onMounted(() => { border: 1px solid #d0d7de; font-size: 12px; } + +.cell-ip-text { + cursor: pointer; +} + +.cell-ip-text:hover { + text-decoration: underline; +} diff --git a/cwd-comments-admin/src/views/SettingsView.vue b/cwd-comments-admin/src/views/SettingsView.vue index f0668c0..1a01f1b 100644 --- a/cwd-comments-admin/src/views/SettingsView.vue +++ b/cwd-comments-admin/src/views/SettingsView.vue @@ -48,6 +48,15 @@ placeholder="例如: example.com, test.com" > +
+ + +
@@ -305,6 +314,7 @@ const commentAdminBadge = ref(""); const avatarPrefix = ref(""); const commentAdminEnabled = ref(false); const allowedDomains = ref(""); +const blockedIps = ref(""); const commentAdminKey = ref(""); const adminKeySet = ref(false); const requireReview = ref(false); @@ -365,6 +375,9 @@ async function load() { allowedDomains.value = commentRes.allowedDomains ? commentRes.allowedDomains.join(", ") : ""; + blockedIps.value = commentRes.blockedIps + ? commentRes.blockedIps.join(", ") + : ""; commentAdminKey.value = commentRes.adminKey || ""; adminKeySet.value = !!commentRes.adminKeySet; requireReview.value = !!commentRes.requireReview; @@ -498,6 +511,10 @@ async function saveComment() { .filter(Boolean), adminKey: commentAdminKey.value || undefined, requireReview: requireReview.value, + blockedIps: blockedIps.value + .split(/[,,\n]/) + .map((d) => d.trim()) + .filter(Boolean), }); showToast(res.message || "保存成功", "success"); diff --git a/cwd-comments-api/src/api/public/postComment.ts b/cwd-comments-api/src/api/public/postComment.ts index 916f5e3..82b5a8b 100644 --- a/cwd-comments-api/src/api/public/postComment.ts +++ b/cwd-comments-api/src/api/public/postComment.ts @@ -52,6 +52,17 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { .first('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); + } + let isAdminComment = false; if (adminEmail && email === adminEmail) { @@ -63,7 +74,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { const lockKey = `admin_lock:${ip}`; const isLocked = await c.env.CWD_AUTH_KV.get(lockKey); if (isLocked) { - return c.json({ message: "验证失败次数过多,请30分钟后再试" }, 403); + return c.json({ message: "验证失败次数过多,请 30 分钟后再试" }, 403); } if (!adminToken) { @@ -79,7 +90,7 @@ export const postComment = async (c: Context<{ Bindings: Bindings }>) => { 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); + return c.json({ message: "验证失败次数过多,请 30 分钟后再试" }, 403); } else { await c.env.CWD_AUTH_KV.put(failKey, fails.toString(), { expirationTtl: 3600 }); return c.json({ message: "密钥错误" }, 401); diff --git a/cwd-comments-api/src/index.ts b/cwd-comments-api/src/index.ts index 5fe24af..7216a0e 100644 --- a/cwd-comments-api/src/index.ts +++ b/cwd-comments-api/src/index.ts @@ -32,6 +32,7 @@ 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'; async function loadCommentSettings(env: Bindings) { @@ -45,10 +46,11 @@ async function loadCommentSettings(env: Bindings) { COMMENT_ADMIN_ENABLED_KEY, COMMENT_ALLOWED_DOMAINS_KEY, COMMENT_ADMIN_KEY_HASH_KEY, - COMMENT_REQUIRE_REVIEW_KEY + COMMENT_REQUIRE_REVIEW_KEY, + COMMENT_BLOCKED_IPS_KEY ]; const { results } = await env.CWD_DB.prepare( - 'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?)' + 'SELECT key, value FROM Settings WHERE key IN (?, ?, ?, ?, ?, ?, ?, ?)' ) .bind(...keys) .all<{ key: string; value: string }>(); @@ -66,6 +68,11 @@ async function loadCommentSettings(env: Bindings) { 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 allowedDomainsRaw = map.get(COMMENT_ALLOWED_DOMAINS_KEY) ?? ''; const allowedDomains = allowedDomainsRaw @@ -79,6 +86,7 @@ async function loadCommentSettings(env: Bindings) { adminEnabled, allowedDomains, requireReview, + blockedIps, adminKey: map.get(COMMENT_ADMIN_KEY_HASH_KEY) ?? null, adminKeySet: !!map.get(COMMENT_ADMIN_KEY_HASH_KEY) }; @@ -94,6 +102,7 @@ async function saveCommentSettings( allowedDomains?: string[]; adminKey?: string; requireReview?: boolean; + blockedIps?: string[]; } ) { await env.CWD_DB.prepare( @@ -134,6 +143,10 @@ async function saveCommentSettings( ? '1' : '0' : undefined + }, + { + key: COMMENT_BLOCKED_IPS_KEY, + value: settings.blockedIps ? settings.blockedIps.join(',') : undefined } ]; @@ -192,6 +205,7 @@ app.get('/api/config/comments', async (c) => { const { adminKey, adminKeySet, + blockedIps, ...publicSettings } = settings as any; @@ -257,6 +271,7 @@ app.put('/admin/settings/comments', async (c) => { 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 adminEmail = rawAdminEmail.trim(); const adminBadge = rawAdminBadge.trim(); @@ -273,6 +288,9 @@ app.put('/admin/settings/comments', async (c) => { typeof rawRequireReview === 'boolean' ? rawRequireReview : rawRequireReview === '1' || rawRequireReview === 1; + const blockedIps = rawBlockedIps + .map((d: any) => (typeof d === 'string' ? d.trim() : '')) + .filter(Boolean); if (adminEmail && !isValidEmail(adminEmail)) { return c.json({ message: '邮箱格式不正确' }, 400); @@ -285,7 +303,8 @@ app.put('/admin/settings/comments', async (c) => { adminEnabled, allowedDomains, adminKey, - requireReview + requireReview, + blockedIps }); return c.json({ message: '保存成功' }); @@ -294,4 +313,43 @@ app.put('/admin/settings/comments', async (c) => { } }); +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); + } +}); + export default app;