我忘了改啥了
This commit is contained in:
175
README.md
175
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: [项目仓库地址]
|
||||
MIT
|
||||
@@ -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'映射到正确的物理路径
|
||||
@@ -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<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
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);
|
||||
|
||||
// 测试获取单个角色信息
|
||||
|
||||
@@ -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,9 +18,10 @@ const APITesterPage: React.FC = () => {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [isTesting, setIsTesting] = useState<boolean>(false);
|
||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>('auth');
|
||||
|
||||
// 自定义测试参数
|
||||
const [customApiEndpoint, setCustomApiEndpoint] = useState<string>('/api/textures');
|
||||
const [customApiEndpoint, setCustomApiEndpoint] = useState<string>(`${process.env.NEXT_PUBLIC_API_URL || '******'}/textures`);
|
||||
const [customUserId, setCustomUserId] = useState<string>('1');
|
||||
const [customProfileId, setCustomProfileId] = useState<string>('1');
|
||||
|
||||
@@ -23,6 +29,33 @@ const APITesterPage: React.FC = () => {
|
||||
const [emailForVerification, setEmailForVerification] = useState<string>('');
|
||||
const [testMethod, setTestMethod] = useState<'GET' | 'POST'>('GET');
|
||||
|
||||
// 认证诊断参数
|
||||
const [authTestEndpoint, setAuthTestEndpoint] = useState<string>('/api/v1/auth/login');
|
||||
const [authRequestBody, setAuthRequestBody] = useState<string>(JSON.stringify({
|
||||
"username": "test",
|
||||
"password": "test",
|
||||
"verificationCode": "123456"
|
||||
}, null, 2));
|
||||
const [authMethod, setAuthMethod] = useState<'GET' | 'POST'>('POST');
|
||||
const [cookies, setCookies] = useState<Record<string, string>>({});
|
||||
const [apiBaseUrl, setApiBaseUrl] = useState<string>(process.env.NEXT_PUBLIC_API_URL || '******'); /**等待替换 */
|
||||
|
||||
// 加载当前cookie信息
|
||||
useEffect(() => {
|
||||
const loadCookies = () => {
|
||||
const cookieObj: Record<string, string> = {};
|
||||
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) => {
|
||||
setLogs(prevLogs => [...prevLogs, { type, message }]);
|
||||
@@ -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<string, string> = {};
|
||||
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}"}`);
|
||||
}
|
||||
@@ -241,14 +421,129 @@ const APITesterPage: React.FC = () => {
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">API连接测试工具</h1>
|
||||
|
||||
{/* 主标签页组件 */}
|
||||
<Tabs defaultValue="auth" className="w-full">
|
||||
<TabsList className="grid grid-cols-3">
|
||||
<TabsTrigger value="auth">认证诊断</TabsTrigger>
|
||||
<TabsTrigger value="custom">自定义API测试</TabsTrigger>
|
||||
<TabsTrigger value="preset">预设API测试</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 认证诊断选项卡 */}
|
||||
<TabsContent value="auth" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>验证码发送测试</CardTitle>
|
||||
<CardTitle>API网关认证诊断</CardTitle>
|
||||
<CardDescription>
|
||||
测试验证码发送API是否正常工作
|
||||
诊断API网关认证问题,提供详细的连接和认证状态报告
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg mb-6">
|
||||
<h3 className="font-semibold text-blue-800 mb-2">认证诊断说明</h3>
|
||||
<p className="text-sm text-blue-700">根据我们的测试,API网关可能需要通过Web界面预先登录才能正常工作。此工具将帮助您诊断认证问题并提供解决方案。</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="authBaseUrl">API网关地址</Label>
|
||||
<Input
|
||||
id="authBaseUrl"
|
||||
value={apiBaseUrl}
|
||||
onChange={(e) => setApiBaseUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="authEndpoint">认证测试端点</Label>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
id="authEndpoint"
|
||||
value={authTestEndpoint}
|
||||
onChange={(e) => setAuthTestEndpoint(e.target.value)}
|
||||
placeholder="/api/v1/auth/login"
|
||||
/>
|
||||
</div>
|
||||
<Select value={authMethod} onValueChange={(value) => setAuthMethod(value as 'GET' | 'POST')}>
|
||||
<SelectTrigger className="w-[120px]">
|
||||
<SelectValue placeholder="方法" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="GET">GET</SelectItem>
|
||||
<SelectItem value="POST">POST</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{authMethod === 'POST' && (
|
||||
<div>
|
||||
<Label htmlFor="authRequestBody">请求体 (JSON)</Label>
|
||||
<Textarea
|
||||
id="authRequestBody"
|
||||
value={authRequestBody}
|
||||
onChange={(e) => setAuthRequestBody(e.target.value)}
|
||||
placeholder='{"username": "test", "password": "test", "verificationCode": "123456"}'
|
||||
className="min-h-[120px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
onClick={runAuthDiagnosticTest}
|
||||
disabled={isTesting}
|
||||
className="bg-indigo-600 hover:bg-indigo-700"
|
||||
>
|
||||
{isTesting ? '诊断中...' : '开始诊断'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setLogs([])}
|
||||
variant="secondary"
|
||||
>
|
||||
清空日志
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 日志显示区域 */}
|
||||
<div className="mt-6 border rounded-lg max-h-[400px] overflow-y-auto">
|
||||
<div className="p-4 font-mono text-sm space-y-1">
|
||||
{logs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={
|
||||
log.type === 'error' ? 'text-red-600' :
|
||||
log.type === 'success' ? 'text-green-600' :
|
||||
'text-gray-700'
|
||||
}
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 自定义API测试选项卡 */}
|
||||
<TabsContent value="custom" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>自定义API测试</CardTitle>
|
||||
<CardDescription>
|
||||
输入API端点路径,测试特定的API接口
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{/* 验证码测试 */}
|
||||
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-lg">
|
||||
<h3 className="font-semibold text-yellow-800 mb-2">验证码发送测试</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">邮箱地址</label>
|
||||
@@ -259,12 +554,11 @@ const APITesterPage: React.FC = () => {
|
||||
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"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">将向此邮箱发送验证码测试</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
setCustomApiEndpoint('/api/v1/auth/send-code');
|
||||
setCustomApiEndpoint(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/auth/send-code`);
|
||||
setTestMethod('POST');
|
||||
runCustomAPITest();
|
||||
}}
|
||||
@@ -273,27 +567,77 @@ const APITesterPage: React.FC = () => {
|
||||
>
|
||||
{isTesting ? '发送中...' : '发送验证码测试'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-yellow-50 border border-yellow-200 p-3 rounded-md">
|
||||
<p className="text-sm text-yellow-700">
|
||||
注意:此测试将调用后端API网关(https://code.littlelan.cn/CarrotSkin/APIgateway)发送验证码,
|
||||
请确保输入有效的邮箱地址以接收验证码。
|
||||
</p>
|
||||
{/* 自定义端点测试 */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">自定义端点测试</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||
<select
|
||||
title="选择API请求方法"
|
||||
value={testMethod}
|
||||
onChange={(e) => setTestMethod(e.target.value as 'GET' | 'POST')}
|
||||
className="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"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={customApiEndpoint}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={runCustomAPITest}
|
||||
disabled={isTesting}
|
||||
className="px-6 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isTesting ? '测试中...' : '运行自定义测试'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 测试结果 */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">测试结果</h3>
|
||||
<div className="border rounded-md h-[300px] overflow-hidden bg-gray-50">
|
||||
<div className="h-full p-4 text-sm overflow-auto">
|
||||
{logs.map((log, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`whitespace-pre-wrap mb-1 ${log.type === 'error' ? 'text-red-600' :
|
||||
log.type === 'success' ? 'text-green-600' : 'text-gray-800'}`}
|
||||
>
|
||||
{log.message}
|
||||
</div>
|
||||
))}
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<div className="mt-8">
|
||||
{/* 预设API测试选项卡 */}
|
||||
<TabsContent value="preset" className="mt-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>测试所有API连接</CardTitle>
|
||||
<CardTitle>预设API测试</CardTitle>
|
||||
<CardDescription>
|
||||
点击下方按钮运行全部API测试,查看系统与后端的连接状态
|
||||
运行预定义的API测试集,全面检查系统连接状态
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">用户ID</label>
|
||||
<input
|
||||
@@ -323,61 +667,11 @@ const APITesterPage: React.FC = () => {
|
||||
>
|
||||
{isTesting ? '测试中...' : '运行所有API测试'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>自定义API测试</CardTitle>
|
||||
<CardDescription>
|
||||
输入API端点路径,测试特定的API接口
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
|
||||
<select
|
||||
title="选择API请求方法"
|
||||
value={testMethod}
|
||||
onChange={(e) => setTestMethod(e.target.value as 'GET' | 'POST')}
|
||||
className="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"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={customApiEndpoint}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={runCustomAPITest}
|
||||
disabled={isTesting}
|
||||
className="px-6 py-2 rounded-md bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isTesting ? '测试中...' : '运行自定义测试'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>测试结果</CardTitle>
|
||||
<CardDescription>
|
||||
API测试的详细日志和结果
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border rounded-md h-[400px] overflow-hidden bg-gray-50">
|
||||
{/* 测试结果 */}
|
||||
<div>
|
||||
<h3 className="font-semibold mb-3">测试结果</h3>
|
||||
<div className="border rounded-md h-[300px] overflow-hidden bg-gray-50">
|
||||
<div className="h-full p-4 text-sm overflow-auto">
|
||||
{logs.map((log, index) => (
|
||||
<div
|
||||
@@ -391,9 +685,12 @@ const APITesterPage: React.FC = () => {
|
||||
<div ref={logsEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -142,6 +142,7 @@ function AddCharacterCard({ onAddClick }: { onAddClick: () => void }) {
|
||||
);
|
||||
}
|
||||
|
||||
// 客户端组件,处理角色管理的交互逻辑
|
||||
export default function CharacterCenterClient({ userName }: { userName: string }) {
|
||||
// 使用简单的状态管理来模拟标签页
|
||||
const [activeTab, setActiveTab] = useState('my-characters');
|
||||
@@ -296,10 +297,8 @@ export default function CharacterCenterClient({ userName }: { userName: string }
|
||||
// 调用API更新角色
|
||||
const updatedProfile = await updateProfile(editingCharacterId, {
|
||||
name: characterForm.name.trim(),
|
||||
description: characterForm.description,
|
||||
skinId: characterForm.skinId != null && characterForm.skinId !== '' ? String(characterForm.skinId) : null,
|
||||
capeId: characterForm.capeId != null && characterForm.capeId !== '' ? String(characterForm.capeId) : null,
|
||||
isActive: characterForm.isActive
|
||||
skinId: characterForm.skinId != null && characterForm.skinId !== '' ? parseInt(characterForm.skinId) : null,
|
||||
capeId: characterForm.capeId != null && characterForm.capeId !== '' ? parseInt(characterForm.capeId) : null
|
||||
});
|
||||
|
||||
// 更新本地角色列表
|
||||
|
||||
@@ -6,14 +6,15 @@ import { redirect } from 'next/navigation';
|
||||
import CharacterCenterClient from './CharacterCenterClient';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getProfilesByUserId } from '@/lib/api/profiles';
|
||||
|
||||
// 角色类型定义
|
||||
interface Character {
|
||||
id: string;
|
||||
name: string;
|
||||
skinId: string;
|
||||
skinId?: string;
|
||||
created: string;
|
||||
level: number;
|
||||
level?: number;
|
||||
description?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
@@ -30,11 +31,30 @@ export default async function CharacterCenter() {
|
||||
// 安全地获取用户信息
|
||||
const userName = session.user?.name || '玩家';
|
||||
|
||||
// 模拟的角色数据
|
||||
const characters: Character[] = [];
|
||||
// 安全地获取用户ID
|
||||
const userId = session.user?.id;
|
||||
|
||||
// 调用实际API获取用户角色列表
|
||||
let characters: Character[] = [];
|
||||
if (userId) {
|
||||
try {
|
||||
const profiles = await getProfilesByUserId(Number(userId));
|
||||
// 转换为Character类型
|
||||
characters = profiles.map(profile => ({
|
||||
id: profile.uuid,
|
||||
name: profile.name,
|
||||
skinId: profile.skinId?.toString(),
|
||||
created: profile.createdAt,
|
||||
isActive: false // 需要根据实际API返回确定活跃状态
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error);
|
||||
// 错误时保持空数组,客户端会显示错误状态
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染客户端组件
|
||||
return <CharacterCenterClient userName={userName} characters={characters} />;
|
||||
return <CharacterCenterClient userName={userName} />;
|
||||
}
|
||||
|
||||
// 角色卡片组件
|
||||
|
||||
@@ -5,14 +5,20 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { redirect } from 'next/navigation';
|
||||
import Canvas2DSkinPreview from '@/components/skins/Canvas2DSkinPreview';
|
||||
import { TextureInfo, TextureType, getUserTextures, getUserFavorites, addFavorite, removeFavorite, checkFavoriteStatus } from '@/lib/api/skins';
|
||||
|
||||
export default function Dashboard() {
|
||||
// 由于这是客户端组件,我们不能在这里使用getServerSession
|
||||
// 在实际应用中,你应该使用useSession钩子或在服务器端获取会话
|
||||
// 这里我们模拟一个已登录的状态
|
||||
const session = { user: { id: 'test_user_1', name: '测试玩家', email: 'test@test.com' } };
|
||||
// 使用NextAuth.js的useSession钩子获取实际的用户会话
|
||||
const { data: session, status } = useSession({
|
||||
required: true,
|
||||
onUnauthenticated() {
|
||||
// 未认证时重定向到登录页面
|
||||
redirect('/login');
|
||||
}
|
||||
});
|
||||
|
||||
// 安全地获取用户ID
|
||||
const userId = session?.user?.id || 'unknown';
|
||||
@@ -188,7 +194,7 @@ export default function Dashboard() {
|
||||
<div className="aspect-square bg-gray-100/95 dark:bg-gray-900/95 flex items-center justify-center p-4 relative overflow-hidden group">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-50/50 to-teal-50/50 dark:from-emerald-900/10 dark:to-teal-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||
<Canvas2DSkinPreview
|
||||
skinUrl={skin.url || `/test-skin.png`}
|
||||
skinUrl={skin.url}
|
||||
size={128}
|
||||
className="max-w-full max-h-full relative z-10 transition-transform duration-500 group-hover:scale-110"
|
||||
/>
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function CustomSkinLoaderTutorialPage() {
|
||||
{
|
||||
"name": "OurSkin",
|
||||
"type": "CustomSkinAPI",
|
||||
"root": "https://code.littlelan.cn/CarrotSkin/APIgateway/csl/"
|
||||
"root": "******/csl/"
|
||||
},
|
||||
{
|
||||
"name": "Mojang",
|
||||
|
||||
@@ -169,7 +169,7 @@ name: 'LittleSkin'
|
||||
# Don't change it unless you really want to.
|
||||
serviceType: BLESSING_SKIN
|
||||
# Fill in the API root like \`https://skin.example.com/api/yggdrasil\`
|
||||
apiRoot: 'https://code.littlelan.cn/CarrotSkin/APIgateway/yggdrasil'`}</pre>
|
||||
apiRoot: '******/yggdrasil'`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function YggdrasilTutorialPage() {
|
||||
|
||||
{/* 拖放配置卡片 */}
|
||||
<DragConfigCard
|
||||
authUrl="https://code.littlelan.cn/CarrotSkin/APIgateway/yggdrasil"
|
||||
authUrl={`${process.env.NEXT_PUBLIC_API_URL}/yggdrasil`}
|
||||
serviceName="HITWH.GAMES 皮肤系统"
|
||||
/>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function YggdrasilTutorialPage() {
|
||||
<li>点击"添加认证服务器"</li>
|
||||
<li>
|
||||
在"服务器地址"中输入:<br/>
|
||||
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">https://code.littlelan.cn/CarrotSkin/APIgateway/yggdrasil</code>
|
||||
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">{process.env.NEXT_PUBLIC_API_URL}/yggdrasil</code>
|
||||
</li>
|
||||
<li>点击"下一步",然后输入您在HITWH.GAMES的用户名和密码</li>
|
||||
<li>完成后点击"登录"即可使用您的HITWH.GAMES账号</li>
|
||||
@@ -105,7 +105,7 @@ export default function YggdrasilTutorialPage() {
|
||||
<li>选择"外置登录"</li>
|
||||
<li>
|
||||
在"认证服务器"中输入:<br/>
|
||||
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">https://code.littlelan.cn/CarrotSkin/APIgateway/yggdrasil</code>
|
||||
<code className="bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded block mt-2">{process.env.NEXT_PUBLIC_API_URL}/yggdrasil</code>
|
||||
</li>
|
||||
<li>点击"保存"</li>
|
||||
<li>返回主界面,输入您在HITWH.GAMES的用户名和密码进行登录</li>
|
||||
|
||||
@@ -7,19 +7,42 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { getSession } from 'next-auth/react';
|
||||
import { Session } from 'next-auth';
|
||||
import axios from 'axios';
|
||||
|
||||
export default function UserHome() {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
totalSkins: 0,
|
||||
activeUsers: 0,
|
||||
downloadCount: 0,
|
||||
todayNew: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// 获取用户会话
|
||||
const sessionData = await getSession();
|
||||
if (!sessionData) {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
setSession(sessionData);
|
||||
|
||||
// 获取统计数据
|
||||
try {
|
||||
const statsResponse = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/stats`);
|
||||
setStats({
|
||||
totalSkins: statsResponse.data.totalSkins || 0,
|
||||
activeUsers: statsResponse.data.activeUsers || 0,
|
||||
downloadCount: statsResponse.data.downloadCount || 0,
|
||||
todayNew: statsResponse.data.todayNew || 0
|
||||
});
|
||||
} catch (statsError) {
|
||||
console.warn('获取统计数据失败,使用默认值:', statsError);
|
||||
// 保留默认值,不做处理
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取会话失败:', error);
|
||||
window.location.href = '/';
|
||||
@@ -28,7 +51,7 @@ export default function UserHome() {
|
||||
}
|
||||
};
|
||||
|
||||
fetchSession();
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
@@ -99,16 +122,16 @@ export default function UserHome() {
|
||||
|
||||
{/* 外置登录卡片 */}
|
||||
<div className="transform transition-all duration-300 hover:-translate-y-1" draggable="true" onDragStart={(e) => {
|
||||
// 为拖拽提供Yggdrasil认证信息
|
||||
// 为拖拽提供Yggdrasil认证信息,使用本地后端地址
|
||||
const yggdrasilData = JSON.stringify({
|
||||
name: "外置登录",
|
||||
uuid: session?.user?.id || "user_uuid",
|
||||
accessToken: "demo_token",
|
||||
validateUrl: "https://code.littlelan.cn/CarrotSkin/APIgateway/auth/validate",
|
||||
refreshUrl: "https://code.littlelan.cn/CarrotSkin/APIgateway/auth/refresh",
|
||||
invalidateUrl: "https://code.littlelan.cn/CarrotSkin/APIgateway/auth/invalidate",
|
||||
userInfoUrl: "https://code.littlelan.cn/CarrotSkin/APIgateway/auth/user",
|
||||
authUrl: "https://code.littlelan.cn/CarrotSkin/APIgateway/auth/authenticate"
|
||||
validateUrl: `${process.env.NEXT_PUBLIC_API_URL}/auth/validate`,
|
||||
refreshUrl: `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`,
|
||||
invalidateUrl: `${process.env.NEXT_PUBLIC_API_URL}/auth/invalidate`,
|
||||
userInfoUrl: `${process.env.NEXT_PUBLIC_API_URL}/auth/user`,
|
||||
authUrl: `${process.env.NEXT_PUBLIC_API_URL}/auth/authenticate`
|
||||
});
|
||||
e.dataTransfer.setData('text/plain', yggdrasilData);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
@@ -207,19 +230,19 @@ export default function UserHome() {
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="text-center p-4 rounded-xl bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
|
||||
<div className="text-4xl font-bold text-emerald-600 dark:text-emerald-400">1,256</div>
|
||||
<div className="text-4xl font-bold text-emerald-600 dark:text-emerald-400">{stats.totalSkins.toLocaleString()}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">总皮肤数</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-xl bg-teal-50 dark:bg-teal-900/20 border border-teal-100 dark:border-teal-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
|
||||
<div className="text-4xl font-bold text-teal-600 dark:text-teal-400">342</div>
|
||||
<div className="text-4xl font-bold text-teal-600 dark:text-teal-400">{stats.activeUsers.toLocaleString()}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">活跃用户</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-xl bg-cyan-50 dark:bg-cyan-900/20 border border-cyan-100 dark:border-cyan-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
|
||||
<div className="text-4xl font-bold text-cyan-600 dark:text-cyan-400">12,890</div>
|
||||
<div className="text-4xl font-bold text-cyan-600 dark:text-cyan-400">{stats.downloadCount.toLocaleString()}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">下载次数</div>
|
||||
</div>
|
||||
<div className="text-center p-4 rounded-xl bg-lime-50 dark:bg-lime-900/20 border border-lime-100 dark:border-lime-900/30 transition-all duration-300 hover:shadow-md hover:-translate-y-1">
|
||||
<div className="text-4xl font-bold text-lime-600 dark:text-lime-400">89</div>
|
||||
<div className="text-4xl font-bold text-lime-600 dark:text-lime-400">{stats.todayNew.toLocaleString()}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">今日新增</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -269,31 +269,28 @@ export default function VerifyCodeTest() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 真实API调用模式
|
||||
try {
|
||||
// 根据后端架构文档中的API网关信息
|
||||
// 根据后端代码分析,使用正确的API接口格式
|
||||
const API_ENDPOINTS = [
|
||||
{
|
||||
name: 'API网关v1版本',
|
||||
url: 'https://code.littlelan.cn/CarrotSkin/APIgateway/api/v1/auth/send-code'
|
||||
},
|
||||
{
|
||||
name: 'API网关v2版本',
|
||||
url: 'https://code.littlelan.cn/CarrotSkin/APIgateway/api/v2/auth/send-code'
|
||||
},
|
||||
{
|
||||
name: '直接API',
|
||||
url: 'https://code.littlelan.cn/CarrotSkin/api/auth/send-code'
|
||||
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(`请求参数: {"email": "${email}"}`);
|
||||
addLog(`请求参数: ${JSON.stringify(requestBody)}`);
|
||||
|
||||
try {
|
||||
// 发送请求
|
||||
const startTime = Date.now();
|
||||
const controller = new AbortController();
|
||||
@@ -304,7 +301,7 @@ export default function VerifyCodeTest() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
credentials: 'include'
|
||||
});
|
||||
@@ -334,7 +331,7 @@ export default function VerifyCodeTest() {
|
||||
addLog(`非JSON响应: ${text.substring(0, 150)}${text.length > 150 ? '...' : ''}`);
|
||||
}
|
||||
} catch (fetchError) {
|
||||
if (fetchError.name === 'AbortError') {
|
||||
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
||||
addLog('请求超时 (8秒)');
|
||||
} else {
|
||||
addLog(`请求错误: ${fetchError instanceof Error ? fetchError.message : String(fetchError)}`);
|
||||
@@ -352,7 +349,6 @@ export default function VerifyCodeTest() {
|
||||
|
||||
setMessageType('error');
|
||||
setMessage('所有API端点连接失败,请检查后端服务状态');
|
||||
|
||||
} catch (error) {
|
||||
addLog(`系统异常: ${error instanceof Error ? error.message : String(error)}`);
|
||||
setMessageType('error');
|
||||
@@ -405,14 +401,13 @@ export default function VerifyCodeTest() {
|
||||
|
||||
{/* 后端架构信息 */}
|
||||
<div className="verify-code-info-box">
|
||||
<h3>后端架构信息</h3>
|
||||
<p>基于提供的后端架构文档:</p>
|
||||
<h3>后端API接口信息</h3>
|
||||
<ul>
|
||||
<li>认证模块: internal/handler/auth_handler.go</li>
|
||||
<li>邮箱验证码模块: internal/service/verification_service.go</li>
|
||||
<li>验证码: 6位数字,Redis存储(10分钟有效期)</li>
|
||||
<li>发送频率: 限制1分钟</li>
|
||||
<li>邮件格式: HTML格式</li>
|
||||
<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>
|
||||
|
||||
@@ -428,7 +423,7 @@ export default function VerifyCodeTest() {
|
||||
checked={testMode === 'real'}
|
||||
onChange={() => setTestMode('real')}
|
||||
/>
|
||||
真实API调用
|
||||
API调用
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
@@ -442,9 +437,8 @@ export default function VerifyCodeTest() {
|
||||
</label>
|
||||
</div>
|
||||
<p className="verify-code-test-mode-description">
|
||||
{testMode === 'real'
|
||||
? '将实际调用后端API发送验证码'
|
||||
: '模拟验证码发送过程,生成测试验证码'}
|
||||
API调用: 真实调用后端API接口<br/>
|
||||
本地模拟: 在前端模拟验证码发送流程
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// src/components/auth/AuthForm.tsx
|
||||
// 认证表单组件 - 包含登录和注册功能,同时提供测试账号信息
|
||||
'use client';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { register, getVerificationCode } from '@/lib/api/actions';
|
||||
import { login, register, getVerificationCode } from '@/lib/api/actions';
|
||||
// 由于找不到 '@/components/ui/alert' 模块,推测可能路径有误,这里使用实际路径代替,需根据项目实际情况调整
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
@@ -16,98 +16,163 @@ interface AuthFormProps {
|
||||
}
|
||||
|
||||
export default function AuthForm({ type }: AuthFormProps) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// 状态管理
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [minecraftUsername, setMinecraftUsername] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [verificationCode, setVerificationCode] = useState('');
|
||||
const [isSendingCode, setIsSendingCode] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [codeSuccessMessage, setCodeSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
// 倒计时引用
|
||||
const countdownRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const timerFinishedRef = useRef(false); // 用于跟踪定时器是否已完成
|
||||
|
||||
// 组件卸载时清除定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 处理倒计时
|
||||
useEffect(() => {
|
||||
if (countdown > 0 && !timerFinishedRef.current) {
|
||||
countdownRef.current = setInterval(() => {
|
||||
setCountdown((prevCountdown) => {
|
||||
if (prevCountdown <= 1) {
|
||||
if (countdownRef.current) {
|
||||
clearInterval(countdownRef.current);
|
||||
countdownRef.current = null;
|
||||
timerFinishedRef.current = true;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return prevCountdown - 1;
|
||||
});
|
||||
}, 1000);
|
||||
} else if (countdown === 0) {
|
||||
// 重置定时器状态
|
||||
timerFinishedRef.current = false;
|
||||
}
|
||||
}, [countdown]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
if (type === 'login') {
|
||||
// 基本验证
|
||||
// 重置消息状态
|
||||
resetMessages();
|
||||
|
||||
// 基础验证
|
||||
if (!password) {
|
||||
setError('请输入密码');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const usernameField = username || email;
|
||||
if (!usernameField) {
|
||||
setError('请输入用户名或邮箱');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查测试账号情况
|
||||
const isTestAccount = usernameField === 'test' && password === 'test';
|
||||
const isTestAccount = type === 'login' && username === 'test' && password === 'test';
|
||||
|
||||
// 对于非测试账号,验证验证码是否输入
|
||||
if (!isTestAccount && !verificationCode) {
|
||||
setError('请输入验证码');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用next-auth的signIn功能进行登录,设置redirect为false以便处理错误
|
||||
const result = await signIn('credentials', {
|
||||
username: usernameField, // 使用username或email作为用户名字段
|
||||
email: email, // 传递email字段
|
||||
password,
|
||||
verificationCode, // 传递验证码
|
||||
redirect: false, // 不自动重定向,以便处理错误
|
||||
callbackUrl: '/user-home' // 指定重定向目标
|
||||
});
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (result?.error) {
|
||||
// 处理登录错误 - 根据错误类型显示不同的错误信息
|
||||
console.error('登录失败:', result.error);
|
||||
if (result.error.includes('CredentialsSignin')) {
|
||||
setError('用户名或密码错误,请重试');
|
||||
} else if (result.error.includes('Network')) {
|
||||
setError('网络连接失败,请检查您的网络设置');
|
||||
} else {
|
||||
setError('登录失败,请稍后再试');
|
||||
try {
|
||||
if (type === 'login') {
|
||||
// 登录表单验证
|
||||
const usernameField = username || email;
|
||||
if (!usernameField) {
|
||||
setError('请输入用户名或邮箱');
|
||||
return;
|
||||
}
|
||||
} else if (result?.ok) {
|
||||
// 登录成功,手动重定向
|
||||
window.location.href = '/user-home';
|
||||
|
||||
// 确定使用的登录凭证
|
||||
let credentials;
|
||||
if (username.includes('@')) {
|
||||
// 用户名输入框包含@,作为邮箱登录
|
||||
credentials = { email: username, password, verificationCode };
|
||||
} else if (username) {
|
||||
// 使用用户名登录,可能附带邮箱
|
||||
credentials = {
|
||||
username,
|
||||
password,
|
||||
verificationCode,
|
||||
email: email || undefined
|
||||
};
|
||||
} else {
|
||||
setError('登录过程中发生未知错误');
|
||||
// 使用邮箱登录
|
||||
credentials = { email, password, verificationCode };
|
||||
}
|
||||
|
||||
// 使用新的login API函数
|
||||
const result = await login(credentials);
|
||||
|
||||
if (result.success) {
|
||||
// 登录成功,服务器应该已经处理了认证状态
|
||||
setSuccessMessage('登录成功!正在跳转...');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/user-home'; // 重定向到用户主页
|
||||
}, 1500);
|
||||
} else {
|
||||
setError(result.error || '登录失败,请稍后再试');
|
||||
}
|
||||
} else {
|
||||
// 验证验证码是否输入
|
||||
if (!verificationCode) {
|
||||
setError('请输入验证码');
|
||||
setIsLoading(false);
|
||||
// 注册表单验证
|
||||
if (!username) {
|
||||
setError('请输入用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
setError('请输入邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!minecraftUsername) {
|
||||
setError('请输入Minecraft用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
// 密码强度检查
|
||||
if (password.length < 6) {
|
||||
setError('密码长度至少为6位');
|
||||
return;
|
||||
}
|
||||
|
||||
// 邮箱格式检查
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
setError('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用注册API
|
||||
const result = await register({
|
||||
const userData = {
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
minecraftUsername,
|
||||
verificationCode
|
||||
});
|
||||
};
|
||||
|
||||
const result = await register(userData);
|
||||
|
||||
if (result.success) {
|
||||
setSuccess('注册成功!即将跳转到登录页面...');
|
||||
setSuccessMessage('注册成功!正在跳转到登录页面...');
|
||||
// 注册成功后重定向到登录页面
|
||||
setTimeout(() => {
|
||||
window.location.href = '/login';
|
||||
}, 2000);
|
||||
}, 1500);
|
||||
} else {
|
||||
setError(result.error || '注册失败,请稍后再试');
|
||||
}
|
||||
@@ -116,15 +181,14 @@ export default function AuthForm({ type }: AuthFormProps) {
|
||||
console.error('认证失败:', error);
|
||||
setError('网络错误,请检查您的连接');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置错误和成功消息
|
||||
const resetMessages = () => {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setCodeSuccessMessage(null);
|
||||
setSuccessMessage(null);
|
||||
};
|
||||
|
||||
// 处理获取验证码
|
||||
@@ -132,34 +196,40 @@ export default function AuthForm({ type }: AuthFormProps) {
|
||||
// 检查是否是测试环境使用测试账号
|
||||
const isTestAccount = type === 'login' && username === 'test' && password === 'test';
|
||||
if (isTestAccount) {
|
||||
setCodeSuccessMessage('测试账号无需验证码');
|
||||
setSuccessMessage('测试账号无需验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetEmail = type === 'login' ? (email || username) : email;
|
||||
// 重置消息状态
|
||||
resetMessages();
|
||||
|
||||
if (!targetEmail) {
|
||||
// 确定要使用的邮箱地址
|
||||
const emailToUse = type === 'login' ? (username.includes('@') ? username : email) : email;
|
||||
|
||||
if (!emailToUse) {
|
||||
setError(type === 'login' ? '请输入用户名/邮箱' : '请输入邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单的邮箱格式验证
|
||||
// 邮箱格式验证
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (type === 'register' || (type === 'login' && targetEmail.includes('@'))) {
|
||||
if (!emailRegex.test(targetEmail)) {
|
||||
if (type === 'register' || (type === 'login' && emailToUse.includes('@'))) {
|
||||
if (!emailRegex.test(emailToUse)) {
|
||||
setError('请输入有效的邮箱地址');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setIsSendingCode(true);
|
||||
|
||||
try {
|
||||
const result = await getVerificationCode(targetEmail);
|
||||
const result = await getVerificationCode(emailToUse);
|
||||
|
||||
if (result.success) {
|
||||
setCodeSuccessMessage('验证码已发送,请注意查收');
|
||||
setCountdown(60); // 设置60秒倒计时
|
||||
setSuccessMessage('验证码已发送到您的邮箱,请查收');
|
||||
setCountdown(60); // 60秒倒计时
|
||||
} else {
|
||||
setError(result.error);
|
||||
setError(result.error || '获取验证码失败,请稍后再试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error);
|
||||
@@ -187,6 +257,7 @@ export default function AuthForm({ type }: AuthFormProps) {
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* 消息提示区域 */}
|
||||
{error && (
|
||||
<Alert variant="destructive" className="bg-red-50 border-red-200 text-red-800">
|
||||
<AlertTitle>错误</AlertTitle>
|
||||
@@ -194,18 +265,10 @@ export default function AuthForm({ type }: AuthFormProps) {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
{successMessage && (
|
||||
<Alert className="bg-green-50 border-green-200 text-green-800">
|
||||
<AlertTitle>成功</AlertTitle>
|
||||
<AlertDescription>{success}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 验证码发送成功消息 */}
|
||||
{codeSuccessMessage && (
|
||||
<Alert className="bg-blue-50 border-blue-200 text-blue-800">
|
||||
<AlertTitle>提示</AlertTitle>
|
||||
<AlertDescription>{codeSuccessMessage}</AlertDescription>
|
||||
<AlertDescription>{successMessage}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div>
|
||||
@@ -329,8 +392,8 @@ export default function AuthForm({ type }: AuthFormProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? '处理中...' : type === 'login' ? '登录' : '注册'}
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? '处理中...' : type === 'login' ? '登录' : '注册'}
|
||||
</Button>
|
||||
|
||||
{type === 'login' && (
|
||||
|
||||
20
src/components/ui/textarea.tsx
Normal file
20
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex min-h-[100px] w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm resize-y",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -2,71 +2,214 @@
|
||||
'use server';
|
||||
import axios from 'axios';
|
||||
|
||||
// 是否启用调试模式
|
||||
const debugMode = process.env.NEXT_PUBLIC_DEBUG_MODE === 'true';
|
||||
|
||||
// 配置axios实例,统一处理API请求
|
||||
const apiClient = axios.create({
|
||||
// 使用配置的API基础URL
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'https://code.littlelan.cn/CarrotSkin/APIgateway',
|
||||
timeout: 10000, // 设置10秒超时
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
|
||||
timeout: parseInt(process.env.NEXT_PUBLIC_API_TIMEOUT || '8000'), // 从环境变量读取超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
// 添加API网关可能需要的额外头部
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
withCredentials: true, // 确保跨域请求时携带cookie和认证信息
|
||||
});
|
||||
|
||||
// 添加请求拦截器处理认证
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
// 可以在这里添加认证token
|
||||
// 调试模式下记录请求信息
|
||||
if (debugMode) {
|
||||
console.log(`API请求: ${config.method?.toUpperCase()} ${config.baseURL}${config.url}`);
|
||||
}
|
||||
|
||||
// 从Next.js的cookie或session中获取认证token
|
||||
// 注意:在服务器端组件中,我们需要使用cookies()来获取cookie
|
||||
try {
|
||||
import('next/headers').then(({ cookies }) => {
|
||||
const cookieStore = cookies();
|
||||
const authToken = cookieStore.get('auth_token')?.value ||
|
||||
cookieStore.get('access_token')?.value ||
|
||||
cookieStore.get('token')?.value;
|
||||
|
||||
if (authToken && config.headers) {
|
||||
config.headers.Authorization = `Bearer ${authToken}`;
|
||||
if (debugMode) {
|
||||
console.log('添加Bearer认证token');
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (debugMode) {
|
||||
console.log('无法访问cookie存储,这在某些上下文中是正常的');
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
if (debugMode) {
|
||||
console.error('请求配置错误:', error);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 添加响应拦截器统一处理错误
|
||||
// 添加响应拦截器统一处理错误和认证
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(response) => {
|
||||
// 调试模式下记录响应信息
|
||||
if (debugMode) {
|
||||
console.log(`API响应: ${response.status} ${response.config?.url}`);
|
||||
// 避免记录敏感信息
|
||||
if (response.config?.url?.includes('/auth/')) {
|
||||
console.log('响应数据: [包含敏感信息,已隐藏]');
|
||||
} else if (response.data && typeof response.data === 'object') {
|
||||
console.log('响应数据大小:', JSON.stringify(response.data).length, '字节');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应中是否包含新的认证token
|
||||
if (response.headers?.['authorization'] || response.headers?.['Authorization']) {
|
||||
const token = response.headers['authorization'] || response.headers['Authorization'];
|
||||
// 这里可以保存token到cookie或session
|
||||
if (debugMode) {
|
||||
console.log('收到新的认证token');
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 调试模式下记录详细错误信息
|
||||
if (debugMode) {
|
||||
console.error('API请求失败:', error);
|
||||
if (error.response) {
|
||||
console.error('响应状态:', error.response.status);
|
||||
console.error('响应数据:', error.response.data);
|
||||
console.error('响应头:', error.response.headers);
|
||||
} else if (error.request) {
|
||||
console.error('请求已发送但未收到响应');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理常见错误状态码
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 未授权,可能需要重新登录
|
||||
console.error('认证失败,需要重新登录');
|
||||
if (debugMode) {
|
||||
console.error('认证失败 (401),可能需要重新登录或刷新token');
|
||||
}
|
||||
// 这里可以触发重新登录流程
|
||||
break;
|
||||
case 403:
|
||||
// 禁止访问
|
||||
console.error('权限不足');
|
||||
if (debugMode) {
|
||||
console.error('权限不足 (403)');
|
||||
}
|
||||
break;
|
||||
case 404:
|
||||
// 资源不存在
|
||||
console.error('请求的资源不存在');
|
||||
if (debugMode) {
|
||||
console.error('请求的资源不存在 (404)');
|
||||
}
|
||||
break;
|
||||
case 500:
|
||||
// 服务器错误
|
||||
console.error('服务器内部错误');
|
||||
if (debugMode) {
|
||||
console.error('服务器内部错误 (500)');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (debugMode) {
|
||||
console.error(`API请求错误: ${error.response.status}`);
|
||||
}
|
||||
}
|
||||
} else if (error.request) {
|
||||
// 没有收到响应
|
||||
console.error('网络错误,无法连接到服务器');
|
||||
// 没有收到响应 - 可能是网络问题或API网关问题
|
||||
if (debugMode) {
|
||||
console.error('网络错误,无法连接到服务器或API网关');
|
||||
console.error('请检查网络连接和API地址配置');
|
||||
}
|
||||
} else {
|
||||
// 请求配置错误
|
||||
if (debugMode) {
|
||||
console.error('请求配置错误:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// API网关认证辅助函数
|
||||
export const authenticateWithApiGateway = async () => {
|
||||
if (debugMode) {
|
||||
console.log('尝试通过API网关认证...');
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 首先尝试直接访问API网关,看是否需要重定向登录
|
||||
const gatewayResponse = await fetch(process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080', {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
}
|
||||
});
|
||||
|
||||
if (debugMode) {
|
||||
console.log('API网关响应状态:', gatewayResponse.status);
|
||||
console.log('API网关响应头:', gatewayResponse.headers.get('content-type'));
|
||||
}
|
||||
|
||||
// 2. 检查是否需要特殊的认证令牌
|
||||
// 这是一个示例实现,实际需要根据API网关的要求调整
|
||||
const { cookies } = await import('next/headers');
|
||||
const cookieStore = cookies();
|
||||
|
||||
// 3. 检查是否已经有有效的认证cookie
|
||||
const hasAuthCookie = cookieStore.has('JSESSIONID') ||
|
||||
cookieStore.has('auth_session') ||
|
||||
cookieStore.has('gateway_token');
|
||||
|
||||
if (hasAuthCookie && debugMode) {
|
||||
console.log('检测到可能的认证cookie,尝试使用现有认证');
|
||||
}
|
||||
|
||||
return {
|
||||
authenticated: gatewayResponse.status === 200,
|
||||
requiresLogin: gatewayResponse.status === 302 || gatewayResponse.status === 401,
|
||||
cookies: hasAuthCookie
|
||||
};
|
||||
} catch (error) {
|
||||
if (debugMode) {
|
||||
console.error('API网关认证检查失败:', error);
|
||||
}
|
||||
return {
|
||||
authenticated: false,
|
||||
requiresLogin: true,
|
||||
error: error instanceof Error ? error.message : '未知错误'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 导出退出登录函数 - 供服务器端使用
|
||||
export const serverSignOut = async () => {
|
||||
console.log('serverSignOut函数执行开始');
|
||||
|
||||
// 调用API退出登录
|
||||
try {
|
||||
await apiClient.post('/api/v1/auth/logout');
|
||||
await apiClient.post('/api/v1/auth/logout', {}, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('API退出登录失败,继续执行本地清理:', error);
|
||||
}
|
||||
@@ -80,10 +223,10 @@ export const serverSignOut = async () => {
|
||||
const allCookies = cookieStore.getAll();
|
||||
console.log('当前cookies:', allCookies.map(c => c.name));
|
||||
|
||||
// 清除所有可能的NextAuth相关cookie
|
||||
// 清除所有可能的认证相关cookie
|
||||
let deletedCookies = 0;
|
||||
allCookies.forEach(cookie => {
|
||||
if (cookie.name.includes('next-auth')) {
|
||||
if (cookie.name.includes('next-auth') || cookie.name.includes('session') || cookie.name.includes('token')) {
|
||||
console.log('删除cookie:', cookie.name);
|
||||
cookieStore.delete(cookie.name);
|
||||
deletedCookies++;
|
||||
@@ -93,7 +236,6 @@ export const serverSignOut = async () => {
|
||||
console.log(`总共删除了${deletedCookies}个cookie`);
|
||||
|
||||
// 强制重定向到登录页面
|
||||
// 确保页面完全刷新而不是客户端导航
|
||||
console.log('执行重定向到登录页面');
|
||||
const { redirect } = await import('next/navigation');
|
||||
redirect('/login');
|
||||
@@ -107,10 +249,6 @@ export const login = async (credentials: {
|
||||
verificationCode: string;
|
||||
}) => {
|
||||
try {
|
||||
// 对于测试环境,可以直接验证测试账号
|
||||
const TEST_USERNAME = process.env.TEST_USERNAME || 'test';
|
||||
const TEST_PASSWORD = process.env.TEST_PASSWORD || 'test';
|
||||
|
||||
// 验证是否为空
|
||||
if (!credentials.password) {
|
||||
return { success: false, error: '请输入密码' };
|
||||
@@ -122,42 +260,41 @@ export const login = async (credentials: {
|
||||
return { success: false, error: '请输入用户名或邮箱' };
|
||||
}
|
||||
|
||||
// 支持通过username或email字段登录测试账号
|
||||
if (process.env.NODE_ENV !== 'production' &&
|
||||
usernameField === TEST_USERNAME &&
|
||||
credentials.password === TEST_PASSWORD) {
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id: 'test_user_1',
|
||||
name: '测试玩家',
|
||||
email: 'test@test.com',
|
||||
minecraftUsername: 'SteveTest'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 实际环境中调用API
|
||||
// 验证码验证 - 测试环境下使用测试账号时可以跳过验证
|
||||
if (!(process.env.NODE_ENV !== 'production' &&
|
||||
usernameField === TEST_USERNAME &&
|
||||
credentials.password === TEST_PASSWORD)) {
|
||||
|
||||
// 非测试账号需要验证验证码
|
||||
// 验证验证码
|
||||
const verifyResult = await verifyCode(usernameField, credentials.verificationCode);
|
||||
if (!verifyResult.success) {
|
||||
return verifyResult;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiClient.post('/api/v1/auth/login', credentials);
|
||||
// 准备登录请求参数,确保格式符合API要求
|
||||
const loginData = {
|
||||
username: credentials.username,
|
||||
email: credentials.email,
|
||||
password: credentials.password,
|
||||
verificationCode: credentials.verificationCode
|
||||
};
|
||||
|
||||
const response = await apiClient.post('/api/v1/auth/login', loginData, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// 确保返回格式一致
|
||||
return { success: true, ...response.data };
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
// 根据错误类型提供更具体的错误信息
|
||||
|
||||
// 更详细的错误处理
|
||||
if (error instanceof axios.AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { success: false, error: '服务器响应超时,请稍后再试' };
|
||||
}
|
||||
if (error.response?.status === 401) {
|
||||
return { success: false, error: '用户名或密码错误,请重试' };
|
||||
return { success: false, error: error.response.data?.error || '用户名或密码错误,请重试' };
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
return { success: false, error: error.response.data?.error || '请求参数错误,请检查输入' };
|
||||
}
|
||||
if (error.request) {
|
||||
return { success: false, error: '网络连接失败,请检查您的网络设置' };
|
||||
@@ -177,16 +314,27 @@ export const getVerificationCode = async (email: string) => {
|
||||
}
|
||||
|
||||
// 调用API获取验证码
|
||||
const response = await apiClient.post('/api/v1/auth/send-code', { email });
|
||||
const response = await apiClient.post('/api/v1/auth/send-code', { email }, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, ...response.data };
|
||||
} catch (error) {
|
||||
console.error('获取验证码失败:', error);
|
||||
if (error instanceof axios.AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { success: false, error: '服务器响应超时,请稍后再试' };
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
return { success: false, error: error.response.data?.error || '获取验证码失败,请检查邮箱格式' };
|
||||
}
|
||||
if (error.response?.status === 429) {
|
||||
return { success: false, error: '验证码发送过于频繁,请稍后再试' };
|
||||
}
|
||||
if (error.request) {
|
||||
return { success: false, error: '网络连接失败,请检查您的网络设置' };
|
||||
return { success: false, error: '无法连接到服务器,请检查网络连接或稍后再试' };
|
||||
}
|
||||
}
|
||||
return { success: false, error: '获取验证码失败,请稍后再试' };
|
||||
@@ -195,14 +343,6 @@ export const getVerificationCode = async (email: string) => {
|
||||
|
||||
// 导出验证验证码函数
|
||||
export const verifyCode = async (email: string, code: string) => {
|
||||
// 对于测试环境,可以直接验证测试账号的验证码
|
||||
const TEST_EMAIL = process.env.TEST_EMAIL || 'test@test.com';
|
||||
const TEST_VERIFICATION_CODE = process.env.TEST_VERIFICATION_CODE || '123456';
|
||||
|
||||
// 测试环境下的特殊处理
|
||||
if (process.env.NODE_ENV !== 'production' && email === TEST_EMAIL && code === TEST_VERIFICATION_CODE) {
|
||||
return { success: true };
|
||||
}
|
||||
try {
|
||||
// 验证输入
|
||||
if (!email?.trim()) {
|
||||
@@ -213,16 +353,24 @@ export const verifyCode = async (email: string, code: string) => {
|
||||
}
|
||||
|
||||
// 调用API验证验证码
|
||||
const response = await apiClient.post('/api/v1/auth/verify-code', { email, code });
|
||||
const response = await apiClient.post('/api/v1/auth/verify-code', { email, code }, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
return { success: true, ...response.data };
|
||||
} catch (error) {
|
||||
console.error('验证码验证失败:', error);
|
||||
if (error instanceof axios.AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { success: false, error: '服务器响应超时,请稍后再试' };
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
return { success: false, error: error.response.data?.error || '验证码错误或已过期' };
|
||||
}
|
||||
if (error.request) {
|
||||
return { success: false, error: '网络连接失败,请检查您的网络设置' };
|
||||
return { success: false, error: '无法连接到服务器,请检查网络连接或稍后再试' };
|
||||
}
|
||||
}
|
||||
return { success: false, error: '验证码验证失败,请稍后再试' };
|
||||
@@ -273,11 +421,14 @@ export const resetPassword = async (email: string, username: string, newPassword
|
||||
} catch (error) {
|
||||
console.error('重置密码失败:', error);
|
||||
if (error instanceof axios.AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { success: false, error: '服务器响应超时,请稍后再试' };
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
return { success: false, error: error.response.data?.error || '重置密码失败,请检查信息' };
|
||||
}
|
||||
if (error.request) {
|
||||
return { success: false, error: '网络连接失败,请检查您的网络设置' };
|
||||
return { success: false, error: '无法连接到服务器,请检查网络连接或稍后再试' };
|
||||
}
|
||||
}
|
||||
return { success: false, error: '重置密码失败,请稍后再试' };
|
||||
@@ -307,7 +458,7 @@ export const register = async (userData: {
|
||||
return { success: false, error: 'Minecraft用户名不能为空' };
|
||||
}
|
||||
|
||||
// 密码强度检查(简单示例)
|
||||
// 密码强度检查
|
||||
if (userData.password.length < 6) {
|
||||
return { success: false, error: '密码长度至少为6位' };
|
||||
}
|
||||
@@ -324,15 +475,33 @@ export const register = async (userData: {
|
||||
return verifyResult;
|
||||
}
|
||||
|
||||
// 实际环境中调用API
|
||||
const response = await apiClient.post('/api/v1/auth/register', userData);
|
||||
// 准备注册数据,确保格式符合API要求
|
||||
const registerData = {
|
||||
username: userData.username,
|
||||
password: userData.password,
|
||||
email: userData.email,
|
||||
minecraftUsername: userData.minecraftUsername,
|
||||
verificationCode: userData.verificationCode
|
||||
};
|
||||
|
||||
// 调用注册API
|
||||
const response = await apiClient.post('/api/v1/auth/register', registerData, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
timeout: 10000, // 注册请求可能需要更长时间
|
||||
});
|
||||
|
||||
return { success: true, ...response.data };
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
// 根据错误类型提供更具体的错误信息
|
||||
|
||||
// 详细的错误处理,提供更具体的错误信息
|
||||
if (error instanceof axios.AxiosError) {
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
return { success: false, error: '服务器响应超时,请稍后再试' };
|
||||
}
|
||||
if (error.response?.status === 400) {
|
||||
// 从服务器获取具体错误信息
|
||||
return {
|
||||
success: false,
|
||||
error: error.response.data?.error || '注册信息有误,请检查后重试'
|
||||
@@ -342,11 +511,18 @@ export const register = async (userData: {
|
||||
// 冲突,可能是用户名或邮箱已存在
|
||||
return {
|
||||
success: false,
|
||||
error: '用户名或邮箱已被注册'
|
||||
error: error.response.data?.error || '用户名或邮箱已被注册'
|
||||
};
|
||||
}
|
||||
if (error.response?.status === 422) {
|
||||
// 数据验证错误
|
||||
return {
|
||||
success: false,
|
||||
error: error.response.data?.error || '输入数据格式不正确,请检查后重试'
|
||||
};
|
||||
}
|
||||
if (error.request) {
|
||||
return { success: false, error: '网络连接失败,请检查您的网络设置' };
|
||||
return { success: false, error: '无法连接到服务器,请检查网络连接或稍后再试' };
|
||||
}
|
||||
}
|
||||
return { success: false, error: '注册失败,请稍后再试' };
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import NextAuth, { AuthOptions } from 'next-auth';
|
||||
import CredentialsProvider from 'next-auth/providers/credentials';
|
||||
import axios from 'axios';
|
||||
import { login as apiLogin } from '@/lib/api/actions';
|
||||
|
||||
// 配置axios实例,与actions.ts保持一致
|
||||
const apiClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'https://code.littlelan.cn/CarrotSkin/APIgateway',
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080',
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -42,9 +43,7 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
);
|
||||
|
||||
// 测试账号配置 - 从环境变量获取
|
||||
const TEST_USERNAME = process.env.TEST_USERNAME || 'test';
|
||||
const TEST_PASSWORD = process.env.TEST_PASSWORD || 'test';
|
||||
// 生产环境中移除测试账号配置
|
||||
|
||||
// 定义authOptions配置
|
||||
const authOptions: AuthOptions = {
|
||||
@@ -78,20 +77,6 @@ const authOptions: AuthOptions = {
|
||||
// 认证逻辑处理函数
|
||||
async authorize(credentials) {
|
||||
try {
|
||||
// 检查是否是测试账号 - 支持通过username或email字段登录
|
||||
const usernameField = credentials?.username || credentials?.email;
|
||||
if (process.env.NODE_ENV !== 'production' &&
|
||||
usernameField === TEST_USERNAME &&
|
||||
credentials?.password === TEST_PASSWORD) {
|
||||
// 返回模拟的测试用户数据
|
||||
return {
|
||||
id: 'test_user_1',
|
||||
name: '测试玩家',
|
||||
email: 'test@test.com',
|
||||
minecraftUsername: 'SteveTest'
|
||||
};
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if (!credentials?.password) {
|
||||
throw new Error('请输入密码');
|
||||
@@ -102,17 +87,17 @@ const authOptions: AuthOptions = {
|
||||
throw new Error('请输入用户名或邮箱');
|
||||
}
|
||||
|
||||
// 对于非测试账号,调用实际API进行认证
|
||||
const response = await apiClient.post('/api/v1/auth/login', credentials);
|
||||
// 使用统一的API登录函数
|
||||
const response = await apiLogin(credentials);
|
||||
|
||||
if (response.data && response.data.user) {
|
||||
if (response.success && response.user) {
|
||||
// 返回认证成功的用户信息
|
||||
return response.data.user;
|
||||
return response.user;
|
||||
} else {
|
||||
// 认证失败,抛出错误
|
||||
console.error('登录失败:', response.error);
|
||||
throw new Error(response.error || '无效的凭据');
|
||||
}
|
||||
|
||||
// 认证失败,返回null
|
||||
console.error('登录失败: 无效的凭据');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
// 转换错误对象为字符串,确保错误信息可以被正确传递
|
||||
@@ -153,8 +138,11 @@ const authOptions: AuthOptions = {
|
||||
token.id = user.id;
|
||||
token.name = user.name;
|
||||
token.email = user.email;
|
||||
// 安全地处理minecraftUsername属性
|
||||
if ('minecraftUsername' in user) {
|
||||
token.minecraftUsername = user.minecraftUsername;
|
||||
}
|
||||
}
|
||||
return token;
|
||||
},
|
||||
// 会话回调 - 构建会话对象
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// 根据ProfileService RPC接口规范实现前端API调用
|
||||
|
||||
// API基础配置
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://code.littlelan.cn/CarrotSkin/APIgateway';
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
// 角色信息接口
|
||||
interface ProfileInfo {
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface FavoriteTextureInfo {
|
||||
}
|
||||
|
||||
// API基础配置
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://code.littlelan.cn/CarrotSkin/APIgateway';
|
||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
|
||||
|
||||
/**
|
||||
* 生成材质上传预签名URL
|
||||
|
||||
Reference in New Issue
Block a user