diff --git a/README.md b/README.md index 102e00c..7bd7a0f 100644 --- a/README.md +++ b/README.md @@ -1,126 +1,117 @@ -# Minecraft 皮肤管理平台 +# CarrotSkin Web 应用 -一个基于 Next.js 构建的现代化 Minecraft 皮肤管理平台,支持皮肤上传、管理、角色创建和 Yggdrasil 认证。 +## 项目简介 -## 功能特性 - -- 📤 **皮肤上传与管理** - 支持上传、预览和管理多个 Minecraft 皮肤 -- 👤 **角色中心** - 创建和管理游戏角色,支持多角色切换 -- 🔐 **外置登录** - 通过拖拽功能实现与 PCL2 等启动器的 Yggdrasil 认证 -- 📚 **使用教程** - 提供基础教程、Yggdrasil 教程等帮助文档 -- 🎨 **响应式设计** - 适配桌面和移动设备的现代化界面 -- 🌙 **暗色模式** - 支持亮色/暗色主题切换 +CarrotSkin 是一个 Minecraft 皮肤站 Web 应用,提供皮肤上传、管理和分享功能。 ## 技术栈 -- **前端框架**: Next.js 14 + React 18 -- **类型系统**: TypeScript -- **样式方案**: Tailwind CSS 4 + Radix UI -- **认证系统**: NextAuth.js -- **API 调用**: Axios -- **图标库**: Lucide React +- **前端框架**: Next.js 14 +- **UI 组件**: React + Tailwind CSS +- **后端**: Go + Gin (CarrotSkin Backend) +- **API 网关**: APIgateway -## 安装与运行 +## 主要功能 -### 前置要求 +- 用户认证与授权 +- 邮箱验证码验证系统 +- 皮肤上传与预览 +- 角色中心管理 +- 自定义测试工具 -- Node.js 20+ -- npm 或 yarn +## API 接口信息 -### 安装步骤 +### 邮箱验证码接口 -1. 克隆项目 -```bash -git clone [仓库地址] -cd my-app +**正确的API接口格式(基于后端代码分析):** + +``` +POST /api/v1/auth/send-code +Content-Type: application/json ``` -2. 安装依赖 +**请求参数:** +```json +{ + "email": "user@example.com", + "type": "register" // 可选值: register, reset_password, change_email +} +``` + +**验证码规则:** +- 6位数字验证码 +- Redis存储,有效期10分钟 +- 限制:每分钟只能发送1次 +- 后端实现文件:internal/handler/auth_handler.go 和 internal/service/verification_service.go + +### 其他重要API接口 + +**认证相关:** +- `POST /api/v1/auth/register` - 用户注册(需邮箱验证码) +- `POST /api/v1/auth/login` - 用户登录(支持用户名/邮箱) +- `POST /api/v1/auth/reset-password` - 重置密码(需验证码) + +**用户相关(需认证):** +- `GET /api/v1/user/profile` - 获取用户信息 +- `PUT /api/v1/user/profile` - 更新用户信息(头像、密码) +- `POST /api/v1/user/change-email` - 更换邮箱(需验证码) + +**材质管理:** +- `GET /api/v1/texture` - 搜索材质 +- `GET /api/v1/texture/:id` - 获取材质详情 +- `POST /api/v1/texture` - 创建材质记录 +- `PUT /api/v1/texture/:id` - 更新材质 + +## 环境配置 + +### 环境变量 + +在项目根目录创建 `.env.local` 文件,配置以下环境变量: + +``` +NEXT_PUBLIC_API_URL=****** +``` + +## 开发指南 + +### 安装依赖 + ```bash npm install -# 或 -yarn install ``` -3. 启动开发服务器 +### 开发模式运行 + ```bash npm run dev -# 或 -yarn dev ``` -4. 构建生产版本 +### 构建生产版本 + ```bash npm run build npm start -# 或 -yarn build -yarn start ``` -## 项目结构 +## 测试工具 -``` -├── public/ # 静态资源文件 -│ ├── skins/ # 皮肤文件存储 -│ └── images/ # 其他图片资源 -├── src/ # 源代码目录 -│ ├── app/ # Next.js App Router -│ │ ├── (auth)/ # 认证相关页面 -│ │ ├── api/ # API 路由 -│ │ ├── character-center/ # 角色中心页面 -│ │ ├── dashboard/ # 皮肤管理仪表盘 -│ │ ├── help/ # 帮助文档页面 -│ │ ├── skins/ # 皮肤上传和管理 -│ │ └── user-home/ # 用户主页 -│ ├── components/ # 可复用组件 -│ │ ├── Navbar.tsx # 导航栏组件 -│ │ ├── auth/ # 认证相关组件 -│ │ ├── skins/ # 皮肤相关组件 -│ │ └── ui/ # UI 组件库 -│ ├── lib/ # 工具函数和 API 调用 -│ ├── styles/ # 全局样式和主题 -│ └── types/ # TypeScript 类型定义 -└── next.config.js # Next.js 配置文件 -``` +项目包含以下测试工具页面: -## 使用指南 +- **验证码测试工具**: `/verify-code-test` + - 支持API调用和本地模拟两种模式 + - 提供详细的请求日志和错误诊断 -### 皮肤上传 - -1. 登录平台后,在用户主页点击"上传皮肤"卡片 -2. 选择符合要求的 PNG 格式皮肤文件 -3. 上传成功后可在"我的皮肤"中查看和管理 - -### Yggdrasil 认证 - -1. 在用户主页找到"外置登录"卡片 -2. 拖拽此卡片到 PCL2 等支持 Yggdrasil 认证的启动器 -3. 启动器将自动完成认证流程 - -### 角色管理 - -1. 点击"角色中心"卡片进入角色管理界面 -2. 可以创建新角色、编辑现有角色信息 -3. 为不同角色分配不同的皮肤 +- **API测试工具**: `/api-tester` + - 用于测试自定义API端点 + - 支持多种HTTP方法 ## 注意事项 -- 皮肤文件必须为 PNG 格式,且符合 Minecraft 皮肤尺寸规范 -- 3D 预览功能尚未实现,目前使用 2D 预览 -- 部分功能可能需要后端 API 支持,请确保相关接口已正确配置 +1. 确保环境变量 `NEXT_PUBLIC_API_URL` 配置正确 +2. 验证码功能依赖后端服务正常运行 +3. 测试模式选择本地模拟可以在无后端服务的情况下进行前端功能测试 +4. 完整的后端API文档可在 `测试用/backed/backed/README_back.md` 文件中查看 ## 许可证 -[MIT](LICENSE) - -## 贡献指南 - -欢迎提交 Issue 和 Pull Request! - -## 联系方式 - -如有问题或建议,请通过以下方式联系我们: - -- Email: [项目邮箱] -- GitHub: [项目仓库地址] \ No newline at end of file +MIT \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index c1d0832..55a8ea9 100644 --- a/middleware.ts +++ b/middleware.ts @@ -9,10 +9,10 @@ export async function middleware(request: NextRequest) { // 定义需要检查登录状态的页面 const isRootPage = pathname === '/'; const isLoginPage = pathname === '/login'; - const isRegisterPage = pathname === '/register'; + // 移除对注册页面的登录状态检查,允许所有用户访问注册页面 - // 只对这三个页面进行检查 - const shouldCheckAuth = isRootPage || isLoginPage || isRegisterPage; + // 只对首页和登录页面进行检查 + const shouldCheckAuth = isRootPage || isLoginPage; if (!shouldCheckAuth) { return NextResponse.next(); @@ -42,8 +42,8 @@ export async function middleware(request: NextRequest) { // 配置中间件适用的路径 export const config = { - // 直接匹配三个目标页面 - matcher: ['/', '/login', '/register'], + // 只匹配首页和登录页面 + matcher: ['/', '/login'], }; // 重要提示:对于(auth)路由组中的页面,Next.js路由系统会自动将'/login'和'/register'映射到正确的物理路径 \ No newline at end of file diff --git a/src/app/api-test/page.tsx b/src/app/api-test/page.tsx index 5de3932..2c29f1e 100644 --- a/src/app/api-test/page.tsx +++ b/src/app/api-test/page.tsx @@ -11,9 +11,9 @@ import { getProfile, getProfilesByUserId, getProfileWithProperties } from '@/lib import { TextureType, searchTextures } from '@/lib/api/skins'; import { useSession } from 'next-auth/react'; -// Mock数据,用于演示 -const mockProfileId = 'b9b3c6d7-e8f9-4a5b-6c7d-8e9f4a5b6c7d'; // 示例UUID -const mockUserId = 1; // 示例用户ID +// 初始化使用的默认测试ID +const defaultProfileId = 'b9b3c6d7-e8f9-4a5b-6c7d-8e9f4a5b6c7d'; // 测试用UUID +const defaultUserId = '1'; // 测试用用户ID // 测试类型枚举 enum TestType { @@ -38,8 +38,8 @@ export default function APITestPage() { const [loading, setLoading] = useState(false); const [result, setResult] = useState(''); const [error, setError] = useState(''); - const [profileId, setProfileId] = useState(mockProfileId); - const [userId, setUserId] = useState(mockUserId.toString()); + const [profileId, setProfileId] = useState(defaultProfileId); + const [userId, setUserId] = useState(defaultUserId); const [selectedTest, setSelectedTest] = useState(TestType.PROFILE); // 测试获取单个角色信息 diff --git a/src/app/api-tester/page.tsx b/src/app/api-tester/page.tsx index 8fc261a..9ce14d9 100644 --- a/src/app/api-tester/page.tsx +++ b/src/app/api-tester/page.tsx @@ -1,7 +1,12 @@ 'use client'; -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; // 定义日志条目类型 interface LogEntry { @@ -13,15 +18,43 @@ const APITesterPage: React.FC = () => { const [logs, setLogs] = useState([]); const [isTesting, setIsTesting] = useState(false); const logsEndRef = useRef(null); + const [activeTab, setActiveTab] = useState('auth'); // 自定义测试参数 - const [customApiEndpoint, setCustomApiEndpoint] = useState('/api/textures'); + const [customApiEndpoint, setCustomApiEndpoint] = useState(`${process.env.NEXT_PUBLIC_API_URL || '******'}/textures`); const [customUserId, setCustomUserId] = useState('1'); const [customProfileId, setCustomProfileId] = useState('1'); // 验证码测试参数 const [emailForVerification, setEmailForVerification] = useState(''); const [testMethod, setTestMethod] = useState<'GET' | 'POST'>('GET'); + + // 认证诊断参数 + const [authTestEndpoint, setAuthTestEndpoint] = useState('/api/v1/auth/login'); + const [authRequestBody, setAuthRequestBody] = useState(JSON.stringify({ + "username": "test", + "password": "test", + "verificationCode": "123456" + }, null, 2)); + const [authMethod, setAuthMethod] = useState<'GET' | 'POST'>('POST'); + const [cookies, setCookies] = useState>({}); + const [apiBaseUrl, setApiBaseUrl] = useState(process.env.NEXT_PUBLIC_API_URL || '******'); /**等待替换 */ + + // 加载当前cookie信息 + useEffect(() => { + const loadCookies = () => { + const cookieObj: Record = {}; + document.cookie.split(';').forEach(cookie => { + const parts = cookie.split('='); + if (parts.length >= 2) { + cookieObj[decodeURIComponent(parts[0].trim())] = decodeURIComponent(parts.slice(1).join('=').trim()); + } + }); + setCookies(cookieObj); + }; + + loadCookies(); + }, []); // 添加日志条目 const addLog = (type: 'info' | 'success' | 'error', message: string) => { @@ -174,6 +207,156 @@ const APITesterPage: React.FC = () => { }, 100); }; + // 执行认证诊断测试 + const runAuthDiagnosticTest = async () => { + setLogs([{ type: 'info', message: `开始API网关认证诊断...` }]); + setIsTesting(true); + + try { + // 1. 测试API网关连接性 + addLog('info', `\n测试1: API网关连接性`); + addLog('info', `测试地址: ${apiBaseUrl}`); + + const gatewayStart = Date.now(); + const gatewayResponse = await fetch(apiBaseUrl, { + method: 'GET', + credentials: 'include', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Accept': 'application/json' + } + }); + const gatewayEnd = Date.now(); + + addLog('info', `状态码: ${gatewayResponse.status}`); + addLog('info', `响应时间: ${gatewayEnd - gatewayStart}ms`); + + // 检查响应内容类型 + const contentType = gatewayResponse.headers.get('content-type'); + addLog('info', `响应内容类型: ${contentType}`); + + // 检查是否是重定向到登录页面 + const responseText = await gatewayResponse.text(); + const isRedirectToLogin = responseText.includes('window.location.replace') && responseText.includes('/~login'); + + if (isRedirectToLogin) { + addLog('error', `API网关返回登录页面重定向,需要先通过Web界面登录`); + addLog('info', `解决方案: 请先在浏览器中打开 ${apiBaseUrl} 并完成登录,然后再尝试API调用`); + } else { + addLog('success', `API网关连接成功,但可能需要认证`); + } + + // 2. 测试认证API端点 + addLog('info', `\n测试2: 认证API端点测试`); + const fullAuthUrl = `${apiBaseUrl}${authTestEndpoint}`; + addLog('info', `测试地址: ${fullAuthUrl}`); + addLog('info', `请求方法: ${authMethod}`); + + const authStart = Date.now(); + const authOptions: RequestInit = { + method: authMethod, + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }; + + if (authMethod === 'POST' && authRequestBody.trim()) { + try { + JSON.parse(authRequestBody); // 验证JSON格式 + authOptions.body = authRequestBody; + addLog('info', `包含请求体: ${authRequestBody.length} 字符`); + } catch (jsonError) { + addLog('error', `请求体JSON格式错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`); + } + } + + try { + const authResponse = await fetch(fullAuthUrl, authOptions); + const authEnd = Date.now(); + + addLog('info', `状态码: ${authResponse.status}`); + addLog('info', `响应时间: ${authEnd - authStart}ms`); + + const authContentType = authResponse.headers.get('content-type'); + const authResponseText = await authResponse.text(); + + addLog('info', `响应内容类型: ${authContentType}`); + addLog('info', `响应大小: ${authResponseText.length} 字符`); + + // 检查是否又是登录重定向 + const isAuthRedirect = authResponseText.includes('window.location.replace') && authResponseText.includes('/~login'); + + if (isAuthRedirect) { + addLog('error', `认证API返回登录页面重定向,确认需要Web界面预认证`); + addLog('info', `请先在浏览器中访问 ${apiBaseUrl} 完成登录,然后重试`); + } else { + // 尝试解析JSON响应 + try { + const parsedData = JSON.parse(authResponseText); + addLog('success', `成功解析JSON响应`); + addLog('info', `响应包含字段: ${Object.keys(parsedData).join(', ')}`); + + // 检查是否有token字段 + if (parsedData.token || parsedData.access_token || parsedData.auth_token) { + addLog('success', `响应包含认证令牌!`); + } + } catch { + addLog('info', `响应不是有效的JSON,可能是HTML或其他格式`); + addLog('info', `响应预览: ${authResponseText.substring(0, 200)}...`); + } + } + } catch (authError) { + addLog('error', `认证API请求失败: ${authError instanceof Error ? authError.message : '未知错误'}`); + } + + // 3. 显示当前Cookie信息 + addLog('info', `\n测试3: 当前Cookie信息`); + const currentCookies: Record = {}; + document.cookie.split(';').forEach(cookie => { + const parts = cookie.split('='); + if (parts.length >= 2) { + currentCookies[decodeURIComponent(parts[0].trim())] = decodeURIComponent(parts.slice(1).join('=').trim()); + } + }); + + if (Object.keys(currentCookies).length > 0) { + addLog('info', `当前Cookie数量: ${Object.keys(currentCookies).length}`); + Object.keys(currentCookies).forEach(key => { + // 不显示敏感cookie的完整值 + const isSensitive = key.includes('token') || key.includes('session') || key.includes('secret'); + const displayValue = isSensitive ? '[已隐藏]' : currentCookies[key].substring(0, 20) + '...'; + addLog('info', `- ${key}: ${displayValue}`); + }); + } else { + addLog('info', `没有检测到Cookie`); + } + + // 4. 总结和建议 + addLog('info', `\n======== 认证诊断总结 ========`); + addLog('info', `API网关地址: ${apiBaseUrl}`); + addLog('info', `是否需要Web认证: ${isRedirectToLogin ? '是' : '否'}`); + + if (isRedirectToLogin) { + addLog('info', `\n建议解决方案:`); + addLog('info', `1. 直接在浏览器中打开: ${apiBaseUrl}`); + addLog('info', `2. 完成Web界面登录`); + addLog('info', `3. 登录成功后,返回此页面再次测试`); + addLog('info', `4. 如果仍有问题,可能需要联系API网关管理员获取访问凭证`); + } else { + addLog('success', `API网关似乎不需要额外的Web认证,可以直接调用API`); + addLog('info', `请检查请求参数和认证信息是否正确`); + } + + } catch (error) { + addLog('error', `诊断测试失败: ${error instanceof Error ? error.message : '未知错误'}`); + } finally { + setIsTesting(false); + setTimeout(() => scrollToBottom(), 100); + } + }; + // 执行单个自定义API测试 const runCustomAPITest = async () => { setLogs([{ type: 'info', message: `开始自定义API测试: ${customApiEndpoint}` }]); @@ -182,11 +365,8 @@ const APITesterPage: React.FC = () => { try { const startTime = Date.now(); - // API网关基础URL - const API_BASE_URL = 'https://code.littlelan.cn/CarrotSkin/APIgateway'; - - // 构建完整的请求URL - const fullUrl = customApiEndpoint.startsWith('http') ? customApiEndpoint : `${API_BASE_URL}${customApiEndpoint}`; + // 使用环境变量中的API基础URL + const fullUrl = customApiEndpoint.startsWith('http') ? customApiEndpoint : customApiEndpoint; addLog('info', `完整URL: ${fullUrl}`); // 构建请求选项 @@ -199,7 +379,7 @@ const APITesterPage: React.FC = () => { }; // 如果是POST请求且是验证码测试,添加请求体 - if (testMethod === 'POST' && fullUrl.includes('/auth/send-code')) { + if (testMethod === 'POST' && (fullUrl.includes('/auth/send-code') || fullUrl.includes('/v1/auth/send-code') || fullUrl.includes('/v2/auth/send-code'))) { requestOptions.body = JSON.stringify({ email: emailForVerification }); addLog('info', `请求体: {"email": "${emailForVerification}"}`); } @@ -240,160 +420,277 @@ const APITesterPage: React.FC = () => { return (

API连接测试工具

- - - - 验证码发送测试 - - 测试验证码发送API是否正常工作 - - - -
-
- - setEmailForVerification(e.target.value)} - placeholder="输入您的邮箱地址" - className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 hover:border-gray-400" - /> -

将向此邮箱发送验证码测试

-
- - - -
-

- 注意:此测试将调用后端API网关(https://code.littlelan.cn/CarrotSkin/APIgateway)发送验证码, - 请确保输入有效的邮箱地址以接收验证码。 -

-
-
-
-
-
- - - 测试所有API连接 - - 点击下方按钮运行全部API测试,查看系统与后端的连接状态 - - - -
-
- - setCustomUserId(e.target.value)} - placeholder="输入用户ID" - className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 hover:border-gray-400" - /> -
-
- - setCustomProfileId(e.target.value)} - placeholder="输入角色ID" - className="w-full px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 hover:border-gray-400" - /> -
-
- - -
-
-
- -
- - - 自定义API测试 - - 输入API端点路径,测试特定的API接口 - - - -
-
- - setCustomApiEndpoint(e.target.value)} - placeholder="例如: /api/textures" - className="md:col-span-3 px-3 py-2 border border-gray-300 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors duration-200 hover:border-gray-400" - /> + {/* 主标签页组件 */} + + + 认证诊断 + 自定义API测试 + 预设API测试 + + + {/* 认证诊断选项卡 */} + + + + API网关认证诊断 + + 诊断API网关认证问题,提供详细的连接和认证状态报告 + + + +
+

认证诊断说明

+

根据我们的测试,API网关可能需要通过Web界面预先登录才能正常工作。此工具将帮助您诊断认证问题并提供解决方案。

- -
- - -
- -
- - - 测试结果 - - API测试的详细日志和结果 - - - -
-
- {logs.map((log, index) => ( -
- {log.message} +
+
+ + setApiBaseUrl(e.target.value)} + placeholder="https://api.example.com" + /> +
+ +
+ +
+
+ setAuthTestEndpoint(e.target.value)} + placeholder="/api/v1/auth/login" + /> +
+
- ))} -
+
+ + {authMethod === 'POST' && ( +
+ +