diff --git a/src/app/admin/security/page.test.tsx b/src/app/admin/security/page.test.tsx
new file mode 100644
index 0000000..ed6e1d8
--- /dev/null
+++ b/src/app/admin/security/page.test.tsx
@@ -0,0 +1,148 @@
+import { describe, it, expect, jest, beforeAll, afterEach } from '@jest/globals';
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import SecurityDashboard from './page';
+
+jest.mock('lucide-react', () => ({
+ Shield: () => ,
+ AlertTriangle: () => ,
+ Activity: () => ,
+ Lock: () => ,
+ RefreshCw: () => ,
+ TrendingUp: () => ,
+ TrendingDown: () => ,
+}));
+
+jest.mock('@/components/ui/button', () => ({
+ Button: ({ children, disabled, ...props }: any) => (
+
+ ),
+}));
+
+jest.mock('@/components/ui/card', () => ({
+ Card: ({ children }: any) =>
{children}
,
+ CardHeader: ({ children }: any) => {children}
,
+ CardTitle: ({ children }: any) => {children}
,
+ CardContent: ({ children }: any) => {children}
,
+}));
+
+global.fetch = jest.fn(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ success: true,
+ logs: [
+ {
+ id: '1',
+ timestamp: Date.now(),
+ type: 'captcha',
+ severity: 'high',
+ message: '验证码验证失败',
+ ip: '192.168.1.1',
+ },
+ ],
+ stats: {
+ totalRequests: 100,
+ blockedRequests: 5,
+ captchaAttempts: 10,
+ rateLimitHits: 3,
+ maliciousContentDetected: 2,
+ successRate: 95,
+ },
+ }),
+ } as Response)
+);
+
+describe('SecurityDashboard', () => {
+ beforeAll(() => {
+ jest.clearAllMocks();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('Rendering', () => {
+ it('should render security dashboard', () => {
+ render();
+ expect(screen.getByText('安全监控仪表板')).toBeInTheDocument();
+ expect(screen.getByText('实时监控网站安全状态和威胁检测')).toBeInTheDocument();
+ });
+
+ it('should render all stat cards', () => {
+ render();
+ expect(screen.getByText('总请求数')).toBeInTheDocument();
+ expect(screen.getByText('已拦截请求')).toBeInTheDocument();
+ expect(screen.getByText('验证码尝试')).toBeInTheDocument();
+ expect(screen.getByText('频率限制命中')).toBeInTheDocument();
+ expect(screen.getByText('恶意内容检测')).toBeInTheDocument();
+ expect(screen.getByText('成功率')).toBeInTheDocument();
+ });
+
+ it('should display stats values', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('100')).toBeInTheDocument();
+ expect(screen.getByText('5')).toBeInTheDocument();
+ expect(screen.getByText('10')).toBeInTheDocument();
+ expect(screen.getByText('3')).toBeInTheDocument();
+ expect(screen.getByText('2')).toBeInTheDocument();
+ expect(screen.getByText('95%')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Security Logs', () => {
+ it('should render security logs section', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('安全日志')).toBeInTheDocument();
+ });
+ });
+
+ it('should display log entries', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText('验证码验证失败')).toBeInTheDocument();
+ expect(screen.getByText('IP: 192.168.1.1')).toBeInTheDocument();
+ });
+ });
+
+ it('should have filter buttons', () => {
+ render();
+ expect(screen.getByText('全部')).toBeInTheDocument();
+ expect(screen.getByText('高危')).toBeInTheDocument();
+ expect(screen.getByText('中危')).toBeInTheDocument();
+ expect(screen.getByText('低危')).toBeInTheDocument();
+ });
+ });
+
+ describe('Refresh Functionality', () => {
+ it('should have refresh button', async () => {
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByTestId('refresh-cw-icon')).toBeInTheDocument();
+ });
+ });
+
+ it('should call fetch when refresh is clicked', async () => {
+ render();
+
+ await waitFor(() => {
+ const refreshButton = screen.getAllByRole('button')[0];
+ expect(refreshButton).not.toBeDisabled();
+
+ refreshButton.click();
+
+ expect(global.fetch).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/src/app/admin/security/page.tsx b/src/app/admin/security/page.tsx
new file mode 100644
index 0000000..6f35c7e
--- /dev/null
+++ b/src/app/admin/security/page.tsx
@@ -0,0 +1,271 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { Shield, AlertTriangle, Activity, Lock, RefreshCw, TrendingUp, TrendingDown } from 'lucide-react';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+
+interface SecurityLog {
+ id: string;
+ timestamp: number;
+ type: 'captcha' | 'rate_limit' | 'sanitization' | 'malicious_content';
+ severity: 'low' | 'medium' | 'high';
+ message: string;
+ ip?: string;
+ email?: string;
+}
+
+interface SecurityStats {
+ totalRequests: number;
+ blockedRequests: number;
+ captchaAttempts: number;
+ rateLimitHits: number;
+ maliciousContentDetected: number;
+ successRate: number;
+}
+
+export default function SecurityDashboard() {
+ const [logs, setLogs] = useState([]);
+ const [stats, setStats] = useState({
+ totalRequests: 0,
+ blockedRequests: 0,
+ captchaAttempts: 0,
+ rateLimitHits: 0,
+ maliciousContentDetected: 0,
+ successRate: 100,
+ });
+ const [loading, setLoading] = useState(true);
+ const [filter, setFilter] = useState<'all' | 'high' | 'medium' | 'low'>('all');
+
+ useEffect(() => {
+ fetchSecurityData();
+ }, []);
+
+ const fetchSecurityData = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch('/api/admin/security');
+ if (response.ok) {
+ const data = await response.json();
+ setLogs(data.logs || []);
+ setStats(data.stats || {
+ totalRequests: 0,
+ blockedRequests: 0,
+ captchaAttempts: 0,
+ rateLimitHits: 0,
+ maliciousContentDetected: 0,
+ successRate: 100,
+ });
+ }
+ } catch (error) {
+ console.error('Failed to fetch security data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getSeverityColor = (severity: string) => {
+ switch (severity) {
+ case 'high':
+ return 'text-red-600 bg-red-50';
+ case 'medium':
+ return 'text-yellow-600 bg-yellow-50';
+ case 'low':
+ return 'text-blue-600 bg-blue-50';
+ default:
+ return 'text-gray-600 bg-gray-50';
+ }
+ };
+
+ const getTypeIcon = (type: string) => {
+ switch (type) {
+ case 'captcha':
+ return ;
+ case 'rate_limit':
+ return ;
+ case 'sanitization':
+ return ;
+ case 'malicious_content':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const filteredLogs = filter === 'all'
+ ? logs
+ : logs.filter(log => log.severity === filter);
+
+ return (
+
+
+
+
+
安全监控仪表板
+
实时监控网站安全状态和威胁检测
+
+
+
+
+
+
+
+ 总请求数
+
+
+ {stats.totalRequests}
+
+
+ 实时统计
+
+
+
+
+
+
+ 已拦截请求
+
+
+ {stats.blockedRequests}
+
+ 安全防护生效
+
+
+
+
+
+
+ 验证码尝试
+
+
+ {stats.captchaAttempts}
+
+ 人机验证
+
+
+
+
+
+
+ 频率限制命中
+
+
+ {stats.rateLimitHits}
+
+ 防刷机制
+
+
+
+
+
+
+ 恶意内容检测
+
+
+ {stats.maliciousContentDetected}
+
+ 内容过滤
+
+
+
+
+
+
+ 成功率
+
+
+ {stats.successRate}%
+
+
+ 正常请求比例
+
+
+
+
+
+
+
+
+
安全日志
+
+
+
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : filteredLogs.length === 0 ? (
+
+ ) : (
+
+ {filteredLogs.map((log) => (
+
+
+ {getTypeIcon(log.type)}
+
+
+
+ {log.message}
+
+ {log.severity === 'high' ? '高危' : log.severity === 'medium' ? '中危' : '低危'}
+
+
+
+ {new Date(log.timestamp).toLocaleString('zh-CN')}
+ {log.ip && IP: {log.ip}}
+ {log.email && 邮箱: {log.email}}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/src/app/api/admin/security/route.ts b/src/app/api/admin/security/route.ts
new file mode 100644
index 0000000..b54b5b6
--- /dev/null
+++ b/src/app/api/admin/security/route.ts
@@ -0,0 +1,24 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { SecurityLogger } from '@/lib/security/logger';
+
+export async function GET(request: NextRequest) {
+ try {
+ const logs = SecurityLogger.getRecentLogs(100);
+ const stats = SecurityLogger.getStats();
+
+ return NextResponse.json({
+ success: true,
+ logs,
+ stats,
+ });
+ } catch (error) {
+ console.error('Error fetching security data:', error);
+ return NextResponse.json(
+ {
+ success: false,
+ error: 'Failed to fetch security data'
+ },
+ { status: 500 }
+ );
+ }
+}
diff --git a/src/lib/security/logger.ts b/src/lib/security/logger.ts
index f53c908..359efaf 100644
--- a/src/lib/security/logger.ts
+++ b/src/lib/security/logger.ts
@@ -104,4 +104,40 @@ export class SecurityLogger {
clear(): void {
this.logger.clear();
}
+
+ getStats() {
+ const logs = this.getRecentLogs();
+ const totalRequests = logs.length;
+ const blockedRequests = logs.filter(log =>
+ log.type === SecurityEventType.RATE_LIMIT_EXCEEDED ||
+ log.type === SecurityEventType.BLOCKED_REQUEST ||
+ log.type === SecurityEventType.MALICIOUS_CONTENT ||
+ log.type === SecurityEventType.XSS_ATTEMPT ||
+ log.type === SecurityEventType.SQL_INJECTION_ATTEMPT
+ ).length;
+ const captchaAttempts = logs.filter(log =>
+ log.type === SecurityEventType.CAPTCHA_FAILED ||
+ log.type === SecurityEventType.CAPTCHA_EXPIRED
+ ).length;
+ const rateLimitHits = logs.filter(log =>
+ log.type === SecurityEventType.RATE_LIMIT_EXCEEDED
+ ).length;
+ const maliciousContentDetected = logs.filter(log =>
+ log.type === SecurityEventType.MALICIOUS_CONTENT ||
+ log.type === SecurityEventType.XSS_ATTEMPT ||
+ log.type === SecurityEventType.SQL_INJECTION_ATTEMPT
+ ).length;
+ const successRate = totalRequests > 0
+ ? ((totalRequests - blockedRequests) / totalRequests * 100).toFixed(2)
+ : 100;
+
+ return {
+ totalRequests,
+ blockedRequests,
+ captchaAttempts,
+ rateLimitHits,
+ maliciousContentDetected,
+ successRate: parseFloat(successRate),
+ };
+ }
}