471 lines
15 KiB
TypeScript
471 lines
15 KiB
TypeScript
'use client';
|
||
import React, { useState, useEffect } from 'react';
|
||
|
||
// 验证码测试工具 - 专注于验证功能的测试
|
||
// 注意:尽管控制台显示NextAuth错误,但这不影响本工具的核心功能
|
||
export default function VerifyCodeTest() {
|
||
// 状态管理
|
||
const [email, setEmail] = useState('');
|
||
const [isSending, setIsSending] = useState(false);
|
||
const [message, setMessage] = useState('');
|
||
const [messageType, setMessageType] = useState<'success' | 'error' | 'info'>('info');
|
||
const [logs, setLogs] = useState<string[]>([]);
|
||
const [testMode, setTestMode] = useState<'real' | 'simulation'>('simulation');
|
||
|
||
// 添加CSS样式
|
||
useEffect(() => {
|
||
// 避免水合错误的样式注入方式
|
||
const styleId = 'verify-code-test-styles';
|
||
if (!document.getElementById(styleId)) {
|
||
const style = document.createElement('style');
|
||
style.id = styleId;
|
||
style.textContent = `
|
||
.verify-code-container {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
min-height: 100vh;
|
||
padding: 2rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
background: linear-gradient(to bottom, #f0f4f8, #e2e8f0);
|
||
}
|
||
.verify-code-header {
|
||
text-align: center;
|
||
margin-bottom: 2rem;
|
||
}
|
||
.verify-code-header h1 {
|
||
color: #3730a3;
|
||
font-size: 2rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.verify-code-header p {
|
||
color: #6b7280;
|
||
font-size: 1rem;
|
||
}
|
||
.verify-code-card {
|
||
background: white;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
padding: 1.5rem;
|
||
width: 100%;
|
||
max-width: 450px;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.verify-code-form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
.verify-code-form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
.verify-code-form-group input[type="email"] {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 0.375rem;
|
||
font-size: 1rem;
|
||
}
|
||
.verify-code-form-group input[type="email"]:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
.verify-code-btn {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border: none;
|
||
border-radius: 0.375rem;
|
||
font-weight: 500;
|
||
font-size: 1rem;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
}
|
||
.verify-code-btn-primary {
|
||
background-color: #3b82f6;
|
||
color: white;
|
||
}
|
||
.verify-code-btn-primary:hover:not(:disabled) {
|
||
background-color: #2563eb;
|
||
}
|
||
.verify-code-btn-primary:disabled {
|
||
background-color: #d1d5db;
|
||
color: #9ca3af;
|
||
cursor: not-allowed;
|
||
}
|
||
.verify-code-message {
|
||
margin-top: 1rem;
|
||
padding: 0.75rem;
|
||
border-radius: 0.375rem;
|
||
border: 1px solid;
|
||
}
|
||
.verify-code-message-success {
|
||
background-color: #dcfce7;
|
||
color: #15803d;
|
||
border-color: #bbf7d0;
|
||
}
|
||
.verify-code-message-error {
|
||
background-color: #fee2e2;
|
||
color: #b91c1c;
|
||
border-color: #fecaca;
|
||
}
|
||
.verify-code-info-box {
|
||
margin-top: 1rem;
|
||
padding: 1rem;
|
||
background-color: #eff6ff;
|
||
border: 1px solid #dbeafe;
|
||
border-radius: 0.375rem;
|
||
font-size: 0.9rem;
|
||
}
|
||
.verify-code-info-box h3 {
|
||
margin-top: 0;
|
||
color: #1e40af;
|
||
font-size: 1rem;
|
||
}
|
||
.verify-code-info-box p {
|
||
margin-bottom: 0.5rem;
|
||
color: #374151;
|
||
}
|
||
.verify-code-info-box ul {
|
||
margin-top: 0.5rem;
|
||
margin-bottom: 0;
|
||
padding-left: 1.5rem;
|
||
}
|
||
.verify-code-test-mode {
|
||
margin-top: 1rem;
|
||
}
|
||
.verify-code-test-mode-options {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
.verify-code-test-mode-options label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
cursor: pointer;
|
||
}
|
||
.verify-code-test-mode-description {
|
||
font-size: 0.8rem;
|
||
color: #6b7280;
|
||
}
|
||
.verify-code-logs-container {
|
||
width: 100%;
|
||
max-width: 600px;
|
||
background: white;
|
||
border-radius: 0.5rem;
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||
padding: 1.5rem;
|
||
}
|
||
.verify-code-logs-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 1rem;
|
||
}
|
||
.verify-code-logs-header h3 {
|
||
margin: 0;
|
||
color: #1f2937;
|
||
font-size: 1.1rem;
|
||
}
|
||
.verify-code-clear-logs-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #3b82f6;
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
}
|
||
.verify-code-clear-logs-btn:hover {
|
||
text-decoration: underline;
|
||
}
|
||
.verify-code-logs {
|
||
background-color: #f9fafb;
|
||
padding: 1rem;
|
||
border-radius: 0.375rem;
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.8rem;
|
||
line-height: 1.4;
|
||
}
|
||
.verify-code-logs-empty {
|
||
color: #9ca3af;
|
||
font-style: italic;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
return () => {
|
||
const style = document.getElementById(styleId);
|
||
if (style) {
|
||
style.remove();
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
// 添加日志
|
||
const addLog = (text: string) => {
|
||
setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${text}`]);
|
||
};
|
||
|
||
// 清除日志
|
||
const clearLogs = () => {
|
||
setLogs([]);
|
||
};
|
||
|
||
// 发送验证码
|
||
const sendVerificationCode = async () => {
|
||
// 重置状态
|
||
setMessage('');
|
||
setLogs([]);
|
||
setIsSending(true);
|
||
addLog('开始发送验证码...');
|
||
addLog(`测试模式: ${testMode === 'real' ? '真实API调用' : '本地模拟'}`);
|
||
|
||
// 验证邮箱格式
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(email)) {
|
||
setMessageType('error');
|
||
setMessage('请输入有效的邮箱地址');
|
||
setIsSending(false);
|
||
return;
|
||
}
|
||
|
||
// 模拟模式
|
||
if (testMode === 'simulation') {
|
||
try {
|
||
addLog(`模拟发送验证码到: ${email}`);
|
||
|
||
// 模拟后端处理流程
|
||
addLog('模拟验证邮箱格式...');
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
addLog('模拟生成6位数字验证码...');
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
// 生成测试验证码
|
||
const testCode = Math.floor(100000 + Math.random() * 900000).toString();
|
||
addLog(`模拟验证码已生成: ${testCode}`);
|
||
|
||
addLog('模拟存储验证码到Redis (10分钟有效期)...');
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
addLog('模拟发送HTML格式邮件...');
|
||
await new Promise(resolve => setTimeout(resolve, 200));
|
||
|
||
addLog('模拟返回成功响应');
|
||
|
||
setMessageType('success');
|
||
setMessage(`模拟发送成功!测试验证码: ${testCode}`);
|
||
} catch (error) {
|
||
addLog(`模拟异常: ${error instanceof Error ? error.message : String(error)}`);
|
||
setMessageType('error');
|
||
setMessage(`模拟失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
} finally {
|
||
setIsSending(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 根据后端代码分析,使用正确的API接口格式
|
||
const API_ENDPOINTS = [
|
||
{
|
||
name: 'API v1版本',
|
||
url: `${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/send-code`
|
||
}
|
||
];
|
||
|
||
// 测试多个可能的端点
|
||
for (const endpoint of API_ENDPOINTS) {
|
||
try {
|
||
// 根据后端代码分析,需要包含type参数
|
||
const requestBody = {
|
||
email,
|
||
type: 'register' // 默认使用register类型,可以根据需要修改为reset_password或change_email
|
||
};
|
||
|
||
addLog(`\n=== 测试 ${endpoint.name} ===`);
|
||
addLog(`请求URL: ${endpoint.url}`);
|
||
addLog(`请求参数: ${JSON.stringify(requestBody)}`);
|
||
|
||
// 发送请求
|
||
const startTime = Date.now();
|
||
const controller = new AbortController();
|
||
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8秒超时
|
||
|
||
const response = await fetch(endpoint.url, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify(requestBody),
|
||
signal: controller.signal,
|
||
credentials: 'include'
|
||
});
|
||
|
||
clearTimeout(timeoutId);
|
||
const endTime = Date.now();
|
||
|
||
addLog(`响应状态码: ${response.status}`);
|
||
addLog(`响应时间: ${endTime - startTime}ms`);
|
||
|
||
// 解析响应
|
||
const contentType = response.headers.get('content-type');
|
||
if (contentType && contentType.includes('application/json')) {
|
||
const data = await response.json();
|
||
addLog(`响应数据: ${JSON.stringify(data)}`);
|
||
|
||
if (response.ok) {
|
||
setMessageType('success');
|
||
setMessage(`验证码发送成功,请检查您的邮箱 (使用 ${endpoint.name})`);
|
||
setIsSending(false);
|
||
return;
|
||
} else {
|
||
addLog(`请求失败: ${data.message || response.status}`);
|
||
}
|
||
} else {
|
||
const text = await response.text();
|
||
addLog(`非JSON响应: ${text.substring(0, 150)}${text.length > 150 ? '...' : ''}`);
|
||
}
|
||
} catch (fetchError) {
|
||
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
||
addLog('请求超时 (8秒)');
|
||
} else {
|
||
addLog(`请求错误: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 诊断信息
|
||
addLog('\n=== 诊断信息 ===');
|
||
addLog('1. 后端API网关可能未启动或网络不可达');
|
||
addLog('2. 请检查API端点地址是否正确');
|
||
addLog('3. 根据架构文档,端点应位于auth_handler.go中');
|
||
addLog('4. 验证码服务使用Redis存储(10分钟有效期)');
|
||
addLog('5. 建议使用模拟模式测试前端功能');
|
||
|
||
setMessageType('error');
|
||
setMessage('所有API端点连接失败,请检查后端服务状态');
|
||
} catch (error) {
|
||
addLog(`系统异常: ${error instanceof Error ? error.message : String(error)}`);
|
||
setMessageType('error');
|
||
setMessage(`系统错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
} finally {
|
||
setIsSending(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="verify-code-container">
|
||
{/* 页面标题 */}
|
||
<div className="verify-code-header">
|
||
<h1>验证码测试工具</h1>
|
||
<p>测试验证码发送功能,基于提供的后端架构文档</p>
|
||
</div>
|
||
|
||
{/* 测试表单 */}
|
||
<div className="verify-code-card">
|
||
<h2>发送验证码</h2>
|
||
|
||
{/* 邮箱输入 */}
|
||
<div className="verify-code-form-group">
|
||
<label htmlFor="email">邮箱地址</label>
|
||
<input
|
||
id="email"
|
||
type="email"
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="请输入邮箱地址"
|
||
disabled={isSending}
|
||
/>
|
||
</div>
|
||
|
||
{/* 发送按钮 */}
|
||
<button
|
||
onClick={sendVerificationCode}
|
||
disabled={isSending || !email}
|
||
className="verify-code-btn verify-code-btn-primary"
|
||
>
|
||
{isSending ? '发送中...' : '发送验证码'}
|
||
</button>
|
||
|
||
{/* 消息显示 */}
|
||
{message && (
|
||
<div className={`verify-code-message verify-code-message-${messageType}`}>
|
||
{message}
|
||
</div>
|
||
)}
|
||
|
||
{/* 后端架构信息 */}
|
||
<div className="verify-code-info-box">
|
||
<h3>后端API接口信息</h3>
|
||
<ul>
|
||
<li>API端点: <code>POST /api/v1/auth/send-code</code></li>
|
||
<li>请求参数: <code>{'{"email": "user@example.com", "type": "register|reset_password|change_email"}'}</code></li>
|
||
<li>验证码规则: 6位数字,Redis存储(10分钟有效期)</li>
|
||
<li>限制: 每分钟只能发送1次</li>
|
||
<li>实现文件: internal/handler/auth_handler.go 和 internal/service/verification_service.go</li>
|
||
</ul>
|
||
</div>
|
||
|
||
{/* 测试模式选择 */}
|
||
<div className="verify-code-test-mode">
|
||
<label>测试模式</label>
|
||
<div className="verify-code-test-mode-options">
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="testMode"
|
||
value="real"
|
||
checked={testMode === 'real'}
|
||
onChange={() => setTestMode('real')}
|
||
/>
|
||
API调用
|
||
</label>
|
||
<label>
|
||
<input
|
||
type="radio"
|
||
name="testMode"
|
||
value="simulation"
|
||
checked={testMode === 'simulation'}
|
||
onChange={() => setTestMode('simulation')}
|
||
/>
|
||
本地模拟
|
||
</label>
|
||
</div>
|
||
<p className="verify-code-test-mode-description">
|
||
API调用: 真实调用后端API接口<br/>
|
||
本地模拟: 在前端模拟验证码发送流程
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 测试日志 */}
|
||
<div className="verify-code-logs-container">
|
||
<div className="verify-code-logs-header">
|
||
<h3>测试日志</h3>
|
||
{logs.length > 0 && (
|
||
<button
|
||
onClick={clearLogs}
|
||
className="verify-code-clear-logs-btn"
|
||
>
|
||
清除日志
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="verify-code-logs">
|
||
{logs.length === 0 ? (
|
||
<p className="verify-code-logs-empty">暂无日志</p>
|
||
) : (
|
||
logs.map((log, index) => (
|
||
<div key={index}>{log}</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |