Upload 42 files
Browse files- .env.example +15 -0
- .gitignore +3 -1
- Dockerfile +3 -0
- README.md +231 -49
- cmd/server/main.go +80 -1
- go.mod +50 -37
- go.sum +118 -92
- internal/handler/auth.go +125 -0
- internal/handler/cookies.go +320 -0
- internal/handler/messages.go +34 -0
- internal/middleware/auth.go +53 -0
- internal/model/cookie.go +64 -0
- internal/model/db.go +69 -0
- internal/model/user.go +92 -0
- internal/service/auth_service.go +190 -0
- internal/service/cookie_service.go +125 -0
- internal/service/rotator.go +167 -0
- internal/service/validator.go +141 -0
- internal/types/common.go +55 -4
- web/static/app.js +484 -0
- web/static/dashboard.html +196 -0
- web/static/index.html +27 -0
- web/static/styles.css +447 -0
.env.example
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PostgreSQL 数据库连接(必需,Hugging Face Spaces 中通过 Secrets 配置)
|
| 2 |
+
DATABASE_URL=postgresql://user:password@host:5432/dbname
|
| 3 |
+
|
| 4 |
+
# JWT 签名密钥(必需,生产环境请使用强密码)
|
| 5 |
+
JWT_SECRET=your-secret-key-change-in-production
|
| 6 |
+
|
| 7 |
+
# 默认管理员账号(首次启动时创建)
|
| 8 |
+
DEFAULT_ADMIN_USERNAME=admin
|
| 9 |
+
DEFAULT_ADMIN_PASSWORD=admin
|
| 10 |
+
|
| 11 |
+
# Cookie 验证配置
|
| 12 |
+
COOKIE_MAX_ERROR_COUNT=3
|
| 13 |
+
|
| 14 |
+
# 轮询策略:round_robin, priority, least_used
|
| 15 |
+
ROTATION_STRATEGY=priority
|
.gitignore
CHANGED
|
@@ -39,4 +39,6 @@ temp/
|
|
| 39 |
|
| 40 |
# Build artifacts
|
| 41 |
dist/
|
| 42 |
-
build/
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
# Build artifacts
|
| 41 |
dist/
|
| 42 |
+
build/
|
| 43 |
+
|
| 44 |
+
.env
|
Dockerfile
CHANGED
|
@@ -35,6 +35,9 @@ COPY --from=builder /app/server .
|
|
| 35 |
# Copy Python startup script
|
| 36 |
COPY app.py .
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
# Create logs directory
|
| 39 |
RUN mkdir -p /app/logs
|
| 40 |
|
|
|
|
| 35 |
# Copy Python startup script
|
| 36 |
COPY app.py .
|
| 37 |
|
| 38 |
+
# Copy web static files
|
| 39 |
+
COPY --from=builder /app/web ./web
|
| 40 |
+
|
| 41 |
# Create logs directory
|
| 42 |
RUN mkdir -p /app/logs
|
| 43 |
|
README.md
CHANGED
|
@@ -7,28 +7,98 @@ sdk: docker
|
|
| 7 |
app_port: 7860
|
| 8 |
---
|
| 9 |
|
| 10 |
-
# Opus API
|
| 11 |
|
| 12 |
-
一个用于 API 消息格式转换的服务,将 Claude API 格式转换为其他格式。
|
| 13 |
|
| 14 |
-
## 功能特性
|
| 15 |
|
| 16 |
-
-
|
| 17 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
- 🛠️ 工具调用处理
|
| 19 |
-
- 📊 Token 计数
|
| 20 |
- 💾 请求/响应日志记录(调试模式)
|
|
|
|
| 21 |
|
| 22 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
### 1. 消息转换接口
|
| 25 |
```
|
| 26 |
-
POST /
|
|
|
|
|
|
|
| 27 |
```
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
```bash
|
| 33 |
curl -X POST https://your-space.hf.space/v1/messages \
|
| 34 |
-H "Content-Type: application/json" \
|
|
@@ -41,70 +111,182 @@ curl -X POST https://your-space.hf.space/v1/messages \
|
|
| 41 |
}'
|
| 42 |
```
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
```
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
```
|
| 48 |
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
| 57 |
```
|
| 58 |
|
| 59 |
-
##
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
## 环境变量
|
| 68 |
|
| 69 |
-
|
| 70 |
-
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
## 本地开发
|
| 74 |
|
| 75 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
```bash
|
| 77 |
-
go
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
```
|
| 79 |
|
| 80 |
-
###
|
| 81 |
```bash
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
docker build -t opus-api .
|
| 83 |
-
docker run -p 7860:7860
|
|
|
|
|
|
|
|
|
|
| 84 |
```
|
| 85 |
|
| 86 |
-
## 项目结构
|
| 87 |
|
| 88 |
```
|
| 89 |
opus-api/
|
| 90 |
-
├── cmd/server/
|
|
|
|
| 91 |
├── internal/
|
| 92 |
-
│ ├── converter/
|
| 93 |
-
│ ├── handler/
|
| 94 |
-
│ ├──
|
| 95 |
-
│ ├──
|
| 96 |
-
│ ├──
|
| 97 |
-
│
|
| 98 |
-
│
|
| 99 |
-
|
| 100 |
-
├──
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
```
|
| 103 |
|
| 104 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
本项目遵循开源协议。
|
| 107 |
|
| 108 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
如有问题或建议,欢迎提交 Issue。
|
|
|
|
| 7 |
app_port: 7860
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# Opus API - 账号管理系统
|
| 11 |
|
| 12 |
+
一个用于 API 消息格式转换的服务,将 Claude API 格式转换为其他格式,支持多账号 Cookie 管理和轮询。
|
| 13 |
|
| 14 |
+
## ✨ 功能特性
|
| 15 |
|
| 16 |
+
- 🔄 Claude API 消息格式转换
|
| 17 |
+
- 📊 Token 计数统计
|
| 18 |
+
- 🎯 **账号管理系统**
|
| 19 |
+
- 用户登录认证(JWT)
|
| 20 |
+
- 多 Morph 账号 Cookie 管理
|
| 21 |
+
- Cookie 有效性检测(手动/自动)
|
| 22 |
+
- Cookie 轮询策略(轮询/优先级/最少使用)
|
| 23 |
+
- Web 管理界面
|
| 24 |
- 🛠️ 工具调用处理
|
|
|
|
| 25 |
- 💾 请求/响应日志记录(调试模式)
|
| 26 |
+
- 🔌 支持客户端 Cookie/Authorization 请求头覆盖
|
| 27 |
|
| 28 |
+
## 🚀 快速开始
|
| 29 |
+
|
| 30 |
+
### 部署到 Hugging Face Spaces
|
| 31 |
+
|
| 32 |
+
1. **创建 Space**
|
| 33 |
+
- 在 Hugging Face 上创建新 Space
|
| 34 |
+
- 选择 **Docker SDK**
|
| 35 |
+
- Space 名称例如:`your-username/opus-api`
|
| 36 |
+
|
| 37 |
+
2. **配置环境变量**
|
| 38 |
+
|
| 39 |
+
在 Space 的 **Settings → Repository secrets** 中添加:
|
| 40 |
+
|
| 41 |
+
| Secret 名称 | 说明 | 示例值 |
|
| 42 |
+
|-------------|------|--------|
|
| 43 |
+
| `DATABASE_URL` | PostgreSQL 数据库连接 | `postgresql://user:password@host:5432/dbname` |
|
| 44 |
+
| `JWT_SECRET` | JWT 签名密钥 | `your-random-secret-key-here` |
|
| 45 |
+
| `DEFAULT_ADMIN_PASSWORD` | 管理员密码 | `changeme123` |
|
| 46 |
+
|
| 47 |
+
3. **推送代码**
|
| 48 |
+
```bash
|
| 49 |
+
git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 50 |
+
git push hf main
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
4. **访问管理界面**
|
| 54 |
+
- 访问 `https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space`
|
| 55 |
+
- 使用默认账号登录:
|
| 56 |
+
- 用户名:`admin`
|
| 57 |
+
- 密码:你设置的 `DEFAULT_ADMIN_PASSWORD`
|
| 58 |
+
|
| 59 |
+
## 📡 API 端点
|
| 60 |
+
|
| 61 |
+
### 认证 API
|
| 62 |
|
|
|
|
| 63 |
```
|
| 64 |
+
POST /api/auth/login # 用户登录
|
| 65 |
+
POST /api/auth/logout # 用户登出
|
| 66 |
+
GET /api/auth/me # 获取当前用户信息
|
| 67 |
```
|
| 68 |
|
| 69 |
+
### Cookie 管理 API(需要认证)
|
| 70 |
|
| 71 |
+
```
|
| 72 |
+
GET /api/cookies # 获取 Cookie 列表
|
| 73 |
+
POST /api/cookies # 添加 Cookie
|
| 74 |
+
GET /api/cookies/:id # 获取单个 Cookie
|
| 75 |
+
PUT /api/cookies/:id # 更新 Cookie
|
| 76 |
+
DELETE /api/cookies/:id # 删除 Cookie
|
| 77 |
+
POST /api/cookies/:id/validate # 验证单个 Cookie
|
| 78 |
+
POST /api/cookies/validate/all # 批量验证所有 Cookie
|
| 79 |
+
GET /api/cookies/stats # 获取统计信息
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### 消息转换 API
|
| 83 |
+
|
| 84 |
+
```
|
| 85 |
+
POST /v1/messages # 消息转换接口(支持客户端 Cookie 覆盖)
|
| 86 |
+
GET /health # 健康检查接口
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## 🔧 使用方法
|
| 90 |
+
|
| 91 |
+
### 1. Web 管理界面
|
| 92 |
+
|
| 93 |
+
访问 `/dashboard` 或 `/static/dashboard.html`,登录后可以:
|
| 94 |
+
- 查看 Cookie 统计信息
|
| 95 |
+
- 添加/编辑/删除 Cookie
|
| 96 |
+
- 手动或批量验证 Cookie 有效性
|
| 97 |
+
- 设置 Cookie 优先级
|
| 98 |
+
|
| 99 |
+
### 2. 调用消息 API
|
| 100 |
+
|
| 101 |
+
**基础请求:**
|
| 102 |
```bash
|
| 103 |
curl -X POST https://your-space.hf.space/v1/messages \
|
| 104 |
-H "Content-Type: application/json" \
|
|
|
|
| 111 |
}'
|
| 112 |
```
|
| 113 |
|
| 114 |
+
**使用自定义 Cookie(覆盖轮询):**
|
| 115 |
+
```bash
|
| 116 |
+
curl -X POST https://your-space.hf.space/v1/messages \
|
| 117 |
+
-H "Content-Type: application/json" \
|
| 118 |
+
-H "Cookie: _gcl_aw=GCL.17692..." \
|
| 119 |
+
-d '{
|
| 120 |
+
"model": "claude-opus-4-20250514",
|
| 121 |
+
"max_tokens": 1024,
|
| 122 |
+
"messages": [
|
| 123 |
+
{"role": "user", "content": "Hello!"}
|
| 124 |
+
]
|
| 125 |
+
}'
|
| 126 |
```
|
| 127 |
+
|
| 128 |
+
## 🗄️ 数据库结构
|
| 129 |
+
|
| 130 |
+
### users 表
|
| 131 |
+
```sql
|
| 132 |
+
CREATE TABLE users (
|
| 133 |
+
id SERIAL PRIMARY KEY,
|
| 134 |
+
username VARCHAR(50) UNIQUE NOT NULL,
|
| 135 |
+
password_hash VARCHAR(255) NOT NULL,
|
| 136 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 137 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 138 |
+
);
|
| 139 |
```
|
| 140 |
|
| 141 |
+
### morph_cookies 表
|
| 142 |
+
```sql
|
| 143 |
+
CREATE TABLE morph_cookies (
|
| 144 |
+
id SERIAL PRIMARY KEY,
|
| 145 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 146 |
+
name VARCHAR(100) NOT NULL,
|
| 147 |
+
api_key TEXT NOT NULL,
|
| 148 |
+
session_key TEXT,
|
| 149 |
+
is_valid BOOLEAN DEFAULT true,
|
| 150 |
+
last_validated TIMESTAMP,
|
| 151 |
+
last_used TIMESTAMP,
|
| 152 |
+
priority INTEGER DEFAULT 0,
|
| 153 |
+
usage_count BIGINT DEFAULT 0,
|
| 154 |
+
error_count INTEGER DEFAULT 0,
|
| 155 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 156 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 157 |
+
);
|
| 158 |
+
```
|
| 159 |
|
| 160 |
+
### user_sessions 表
|
| 161 |
+
```sql
|
| 162 |
+
CREATE TABLE user_sessions (
|
| 163 |
+
id SERIAL PRIMARY KEY,
|
| 164 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 165 |
+
token_hash VARCHAR(255) UNIQUE NOT NULL,
|
| 166 |
+
expires_at TIMESTAMP NOT NULL,
|
| 167 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 168 |
+
);
|
| 169 |
```
|
| 170 |
|
| 171 |
+
## 🔄 Cookie 轮询策略
|
| 172 |
|
| 173 |
+
系统支持三种轮询策略,通过环境变量 `ROTATION_STRATEGY` 配置:
|
| 174 |
+
|
| 175 |
+
| 策略 | 说明 |
|
| 176 |
+
|------|------|
|
| 177 |
+
| `priority` | 按优先级(数字越大越优先,默认) |
|
| 178 |
+
| `round_robin` | 轮询(按顺序循环使用) |
|
| 179 |
+
| `least_used` | 使用次数最少的优先 |
|
| 180 |
|
| 181 |
+
## 🔐 环境变量配置
|
| 182 |
|
| 183 |
+
| 变量名 | 说明 | 默认值 | 必需 |
|
| 184 |
+
|--------|------|--------|------|
|
| 185 |
+
| `DATABASE_URL` | PostgreSQL 连接字符串 | - | ✅ |
|
| 186 |
+
| `JWT_SECRET` | JWT 签名密钥 | - | ✅ |
|
| 187 |
+
| `DEFAULT_ADMIN_USERNAME` | 默认管理员用户名 | `admin` | ❌ |
|
| 188 |
+
| `DEFAULT_ADMIN_PASSWORD` | 默认管理员密码 | `changeme123` | ❌ |
|
| 189 |
+
| `COOKIE_MAX_ERROR_COUNT` | Cookie 失败阈值 | `3` | ❌ |
|
| 190 |
+
| `ROTATION_STRATEGY` | 轮询策略 | `priority` | ❌ |
|
| 191 |
+
| `DEBUG_MODE` | 调试模式 | `false` | ❌ |
|
| 192 |
|
| 193 |
+
## 🛠️ 本地开发
|
| 194 |
|
| 195 |
+
### 前置要求
|
| 196 |
+
- Go 1.21+
|
| 197 |
+
- PostgreSQL 14+
|
| 198 |
+
- Python 3.9+(用于 Hugging Face 部署)
|
| 199 |
+
|
| 200 |
+
### 安装依赖
|
| 201 |
```bash
|
| 202 |
+
go mod download
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
### 配置环境变量
|
| 206 |
+
```bash
|
| 207 |
+
cp .env.example .env
|
| 208 |
+
# 编辑 .env 文件,设置数据库连接等
|
| 209 |
```
|
| 210 |
|
| 211 |
+
### 运行服务
|
| 212 |
```bash
|
| 213 |
+
# 使用 Go 直接运行
|
| 214 |
+
go run cmd/server/main.go
|
| 215 |
+
|
| 216 |
+
# 或使用 Docker
|
| 217 |
docker build -t opus-api .
|
| 218 |
+
docker run -p 7860:7860 \
|
| 219 |
+
-e DATABASE_URL="postgresql://..." \
|
| 220 |
+
-e JWT_SECRET="your-secret" \
|
| 221 |
+
opus-api
|
| 222 |
```
|
| 223 |
|
| 224 |
+
## 📁 项目结构
|
| 225 |
|
| 226 |
```
|
| 227 |
opus-api/
|
| 228 |
+
├── cmd/server/ # 主程序入口
|
| 229 |
+
│ └── main.go
|
| 230 |
├── internal/
|
| 231 |
+
│ ├── converter/ # 格式转换逻辑
|
| 232 |
+
│ ├── handler/ # HTTP 处理器
|
| 233 |
+
│ │ ├── messages.go # 消息处理
|
| 234 |
+
│ │ ├── health.go # 健康检查
|
| 235 |
+
│ │ ├── auth.go # 认证处理
|
| 236 |
+
│ │ └── cookies.go # Cookie 管理
|
| 237 |
+
│ ├── middleware/ # 中间件
|
| 238 |
+
│ │ └── auth.go # JWT 认证
|
| 239 |
+
│ ├── model/ # 数据模型
|
| 240 |
+
│ │ ├── db.go # 数据库连接
|
| 241 |
+
│ │ ├── user.go # 用户模型
|
| 242 |
+
│ │ └── cookie.go # Cookie 模型
|
| 243 |
+
│ ├── service/ # 业务逻辑
|
| 244 |
+
│ │ ├── auth_service.go # 认证服务
|
| 245 |
+
│ │ ├── cookie_service.go# Cookie 服务
|
| 246 |
+
│ │ ├── validator.go # Cookie 验证
|
| 247 |
+
│ │ └── rotator.go # Cookie 轮询
|
| 248 |
+
│ ├── logger/ # 日志管理
|
| 249 |
+
│ ├── parser/ # 消息解析
|
| 250 |
+
│ ├── stream/ # 流式处理
|
| 251 |
+
│ ├── tokenizer/ # Token 计数
|
| 252 |
+
│ ├── types/ # 类型定义
|
| 253 |
+
│ └── converter/ # 格式转换
|
| 254 |
+
├── web/static/ # 前端静态文件
|
| 255 |
+
│ ├── index.html # 登录页
|
| 256 |
+
│ ├── dashboard.html # 管理面板
|
| 257 |
+
│ ├── styles.css # 样式
|
| 258 |
+
│ └── app.js # 前端逻辑
|
| 259 |
+
├── migrations/ # 数据库迁移(可选)
|
| 260 |
+
├── .env.example # 环境变量示例
|
| 261 |
+
├── app.py # Python 启动脚本
|
| 262 |
+
├── Dockerfile # Docker 构建文件
|
| 263 |
+
└── go.mod # Go 依赖管理
|
| 264 |
```
|
| 265 |
|
| 266 |
+
## 📝 技术栈
|
| 267 |
+
|
| 268 |
+
- **后端**: Go 1.21, Gin
|
| 269 |
+
- **数据库**: PostgreSQL, GORM
|
| 270 |
+
- **认证**: JWT
|
| 271 |
+
- **前端**: HTML, CSS, Vanilla JavaScript
|
| 272 |
+
- **容器化**: Docker
|
| 273 |
+
- **部署平台**: Hugging Face Spaces
|
| 274 |
+
|
| 275 |
+
## 🔐 安全建议
|
| 276 |
+
|
| 277 |
+
1. **首次登录后立即修改默认密码**
|
| 278 |
+
2. **生产环境使用强 JWT_SECRET**
|
| 279 |
+
3. **定期更新 Cookie**(Morph Cookie 可能会过期)
|
| 280 |
+
4. **使用 HTTPS**(Hugging Face Spaces 自动提供)
|
| 281 |
+
|
| 282 |
+
## 📄 许可证
|
| 283 |
|
| 284 |
本项目遵循开源协议。
|
| 285 |
|
| 286 |
+
## ���� 贡献
|
| 287 |
+
|
| 288 |
+
欢迎提交 Issue 和 Pull Request。
|
| 289 |
+
|
| 290 |
+
## 📞 联系方式
|
| 291 |
|
| 292 |
如有问题或建议,欢迎提交 Issue。
|
cmd/server/main.go
CHANGED
|
@@ -5,14 +5,25 @@ import (
|
|
| 5 |
"log"
|
| 6 |
"opus-api/internal/handler"
|
| 7 |
"opus-api/internal/logger"
|
|
|
|
|
|
|
|
|
|
| 8 |
"opus-api/internal/tokenizer"
|
| 9 |
"opus-api/internal/types"
|
| 10 |
"os"
|
| 11 |
|
| 12 |
"github.com/gin-gonic/gin"
|
|
|
|
| 13 |
)
|
| 14 |
|
| 15 |
func main() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
// Create logs directory
|
| 17 |
if err := os.MkdirAll(types.LogDir, 0755); err != nil {
|
| 18 |
log.Fatalf("Failed to create logs directory: %v", err)
|
|
@@ -28,16 +39,83 @@ func main() {
|
|
| 28 |
log.Printf("[WARN] Failed to initialize tokenizer: %v (will use fallback)", err)
|
| 29 |
}
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
// Set Gin mode
|
| 32 |
gin.SetMode(gin.ReleaseMode)
|
| 33 |
|
| 34 |
// Create Gin router
|
| 35 |
router := gin.Default()
|
| 36 |
|
| 37 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
router.POST("/v1/messages", handler.HandleMessages)
|
| 39 |
router.GET("/health", handler.HandleHealth)
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
// Start server
|
| 42 |
// Hugging Face Spaces uses port 7860
|
| 43 |
port := 7860
|
|
@@ -48,6 +126,7 @@ func main() {
|
|
| 48 |
log.Printf("Server running on http://0.0.0.0:%d", port)
|
| 49 |
log.Printf("Debug mode: %v", types.DebugMode)
|
| 50 |
log.Printf("Log directory: %s", types.LogDir)
|
|
|
|
| 51 |
|
| 52 |
if err := router.Run(addr); err != nil {
|
| 53 |
log.Fatalf("Failed to start server: %v", err)
|
|
|
|
| 5 |
"log"
|
| 6 |
"opus-api/internal/handler"
|
| 7 |
"opus-api/internal/logger"
|
| 8 |
+
"opus-api/internal/middleware"
|
| 9 |
+
"opus-api/internal/model"
|
| 10 |
+
"opus-api/internal/service"
|
| 11 |
"opus-api/internal/tokenizer"
|
| 12 |
"opus-api/internal/types"
|
| 13 |
"os"
|
| 14 |
|
| 15 |
"github.com/gin-gonic/gin"
|
| 16 |
+
"github.com/joho/godotenv"
|
| 17 |
)
|
| 18 |
|
| 19 |
func main() {
|
| 20 |
+
// Load .env file
|
| 21 |
+
if err := godotenv.Load(); err != nil {
|
| 22 |
+
log.Printf("[INFO] No .env file found or error loading it: %v", err)
|
| 23 |
+
} else {
|
| 24 |
+
log.Printf("[INFO] .env file loaded successfully")
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
// Create logs directory
|
| 28 |
if err := os.MkdirAll(types.LogDir, 0755); err != nil {
|
| 29 |
log.Fatalf("Failed to create logs directory: %v", err)
|
|
|
|
| 39 |
log.Printf("[WARN] Failed to initialize tokenizer: %v (will use fallback)", err)
|
| 40 |
}
|
| 41 |
|
| 42 |
+
// Initialize database
|
| 43 |
+
if err := model.InitDB(); err != nil {
|
| 44 |
+
log.Printf("[WARN] Failed to initialize database: %v (running without database)", err)
|
| 45 |
+
} else {
|
| 46 |
+
// Create default admin user
|
| 47 |
+
if err := model.CreateDefaultAdmin(model.DB); err != nil {
|
| 48 |
+
log.Printf("[WARN] Failed to create default admin: %v", err)
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Initialize services
|
| 53 |
+
var authService *service.AuthService
|
| 54 |
+
var cookieService *service.CookieService
|
| 55 |
+
var cookieValidator *service.CookieValidator
|
| 56 |
+
var cookieRotator *service.CookieRotator
|
| 57 |
+
|
| 58 |
+
if model.DB != nil {
|
| 59 |
+
authService = service.NewAuthService(model.DB)
|
| 60 |
+
cookieService = service.NewCookieService(model.DB)
|
| 61 |
+
cookieValidator = service.NewCookieValidator(cookieService)
|
| 62 |
+
cookieRotator = service.NewCookieRotator(cookieService, service.StrategyRoundRobin)
|
| 63 |
+
|
| 64 |
+
// Store rotator in types for use in messages handler
|
| 65 |
+
types.CookieRotatorInstance = cookieRotator
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
// Set Gin mode
|
| 69 |
gin.SetMode(gin.ReleaseMode)
|
| 70 |
|
| 71 |
// Create Gin router
|
| 72 |
router := gin.Default()
|
| 73 |
|
| 74 |
+
// Serve static files
|
| 75 |
+
router.Static("/static", "./web/static")
|
| 76 |
+
|
| 77 |
+
// Redirect root to login page or dashboard
|
| 78 |
+
router.GET("/", func(c *gin.Context) {
|
| 79 |
+
c.Redirect(302, "/static/index.html")
|
| 80 |
+
})
|
| 81 |
+
|
| 82 |
+
// Dashboard redirect
|
| 83 |
+
router.GET("/dashboard", func(c *gin.Context) {
|
| 84 |
+
c.Redirect(302, "/static/dashboard.html")
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
// Register API routes
|
| 88 |
router.POST("/v1/messages", handler.HandleMessages)
|
| 89 |
router.GET("/health", handler.HandleHealth)
|
| 90 |
|
| 91 |
+
// Auth routes (only if database is available)
|
| 92 |
+
if authService != nil {
|
| 93 |
+
authHandler := handler.NewAuthHandler(authService)
|
| 94 |
+
router.POST("/api/auth/login", authHandler.Login)
|
| 95 |
+
router.POST("/api/auth/logout", authHandler.Logout)
|
| 96 |
+
|
| 97 |
+
// Protected routes
|
| 98 |
+
authGroup := router.Group("/api")
|
| 99 |
+
authGroup.Use(middleware.AuthMiddleware(authService))
|
| 100 |
+
{
|
| 101 |
+
authGroup.GET("/auth/me", authHandler.Me)
|
| 102 |
+
authGroup.PUT("/auth/password", authHandler.ChangePassword)
|
| 103 |
+
|
| 104 |
+
// Cookie management routes
|
| 105 |
+
if cookieService != nil && cookieValidator != nil {
|
| 106 |
+
cookieHandler := handler.NewCookieHandler(cookieService, cookieValidator)
|
| 107 |
+
authGroup.GET("/cookies", cookieHandler.ListCookies)
|
| 108 |
+
authGroup.GET("/cookies/stats", cookieHandler.GetStats)
|
| 109 |
+
authGroup.POST("/cookies", cookieHandler.CreateCookie)
|
| 110 |
+
authGroup.GET("/cookies/:id", cookieHandler.GetCookie)
|
| 111 |
+
authGroup.PUT("/cookies/:id", cookieHandler.UpdateCookie)
|
| 112 |
+
authGroup.DELETE("/cookies/:id", cookieHandler.DeleteCookie)
|
| 113 |
+
authGroup.POST("/cookies/:id/validate", cookieHandler.ValidateCookie)
|
| 114 |
+
authGroup.POST("/cookies/validate/all", cookieHandler.ValidateAllCookies)
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
// Start server
|
| 120 |
// Hugging Face Spaces uses port 7860
|
| 121 |
port := 7860
|
|
|
|
| 126 |
log.Printf("Server running on http://0.0.0.0:%d", port)
|
| 127 |
log.Printf("Debug mode: %v", types.DebugMode)
|
| 128 |
log.Printf("Log directory: %s", types.LogDir)
|
| 129 |
+
log.Printf("Database connected: %v", model.DB != nil)
|
| 130 |
|
| 131 |
if err := router.Run(addr); err != nil {
|
| 132 |
log.Fatalf("Failed to start server: %v", err)
|
go.mod
CHANGED
|
@@ -1,37 +1,50 @@
|
|
| 1 |
-
module opus-api
|
| 2 |
-
|
| 3 |
-
go 1.
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
github.com/
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
github.com/
|
| 19 |
-
github.com/
|
| 20 |
-
github.com/
|
| 21 |
-
github.com/
|
| 22 |
-
github.com/
|
| 23 |
-
github.com/
|
| 24 |
-
github.com/
|
| 25 |
-
github.com/
|
| 26 |
-
github.com/
|
| 27 |
-
github.com/
|
| 28 |
-
github.com/
|
| 29 |
-
github.com/
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module opus-api
|
| 2 |
+
|
| 3 |
+
go 1.23
|
| 4 |
+
|
| 5 |
+
toolchain go1.24.5
|
| 6 |
+
|
| 7 |
+
require (
|
| 8 |
+
github.com/gin-gonic/gin v1.9.1
|
| 9 |
+
github.com/golang-jwt/jwt/v5 v5.2.0
|
| 10 |
+
github.com/google/uuid v1.6.0
|
| 11 |
+
github.com/pkoukk/tiktoken-go v0.1.8
|
| 12 |
+
golang.org/x/crypto v0.18.0
|
| 13 |
+
gorm.io/driver/postgres v1.5.4
|
| 14 |
+
gorm.io/gorm v1.25.5
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
require (
|
| 18 |
+
github.com/bytedance/sonic v1.9.1 // indirect
|
| 19 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
| 20 |
+
github.com/dlclark/regexp2 v1.10.0 // indirect
|
| 21 |
+
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
| 22 |
+
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 23 |
+
github.com/go-playground/locales v0.14.1 // indirect
|
| 24 |
+
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 25 |
+
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
| 26 |
+
github.com/goccy/go-json v0.10.2 // indirect
|
| 27 |
+
github.com/jackc/pgpassfile v1.0.0 // indirect
|
| 28 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
| 29 |
+
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
| 30 |
+
github.com/jinzhu/inflection v1.0.0 // indirect
|
| 31 |
+
github.com/jinzhu/now v1.1.5 // indirect
|
| 32 |
+
github.com/joho/godotenv v1.5.1 // indirect
|
| 33 |
+
github.com/json-iterator/go v1.1.12 // indirect
|
| 34 |
+
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
| 35 |
+
github.com/kr/text v0.2.0 // indirect
|
| 36 |
+
github.com/leodido/go-urn v1.2.4 // indirect
|
| 37 |
+
github.com/mattn/go-isatty v0.0.19 // indirect
|
| 38 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 39 |
+
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 40 |
+
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
| 41 |
+
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
| 42 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 43 |
+
github.com/ugorji/go/codec v1.2.11 // indirect
|
| 44 |
+
golang.org/x/arch v0.3.0 // indirect
|
| 45 |
+
golang.org/x/net v0.10.0 // indirect
|
| 46 |
+
golang.org/x/sys v0.26.0 // indirect
|
| 47 |
+
golang.org/x/text v0.14.0 // indirect
|
| 48 |
+
google.golang.org/protobuf v1.30.0 // indirect
|
| 49 |
+
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 50 |
+
)
|
go.sum
CHANGED
|
@@ -1,92 +1,118 @@
|
|
| 1 |
-
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
| 2 |
-
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
| 3 |
-
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
| 4 |
-
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
| 5 |
-
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
| 6 |
-
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
| 7 |
-
github.com/
|
| 8 |
-
github.com/davecgh/go-spew v1.1.
|
| 9 |
-
github.com/davecgh/go-spew v1.1.1
|
| 10 |
-
github.com/
|
| 11 |
-
github.com/dlclark/regexp2 v1.10.0
|
| 12 |
-
github.com/
|
| 13 |
-
github.com/gabriel-vasile/mimetype v1.4.2
|
| 14 |
-
github.com/
|
| 15 |
-
github.com/gin-contrib/sse v0.1.0
|
| 16 |
-
github.com/gin-
|
| 17 |
-
github.com/gin-gonic/gin v1.9.1
|
| 18 |
-
github.com/
|
| 19 |
-
github.com/go-playground/assert/v2 v2.2.0
|
| 20 |
-
github.com/go-playground/
|
| 21 |
-
github.com/go-playground/locales v0.14.1
|
| 22 |
-
github.com/go-playground/
|
| 23 |
-
github.com/go-playground/universal-translator v0.18.1
|
| 24 |
-
github.com/go-playground/
|
| 25 |
-
github.com/go-playground/validator/v10 v10.14.0
|
| 26 |
-
github.com/
|
| 27 |
-
github.com/goccy/go-json v0.10.2
|
| 28 |
-
github.com/
|
| 29 |
-
github.com/
|
| 30 |
-
github.com/
|
| 31 |
-
github.com/
|
| 32 |
-
github.com/google/
|
| 33 |
-
github.com/google/
|
| 34 |
-
github.com/
|
| 35 |
-
github.com/
|
| 36 |
-
github.com/
|
| 37 |
-
github.com/
|
| 38 |
-
github.com/
|
| 39 |
-
github.com/
|
| 40 |
-
github.com/
|
| 41 |
-
github.com/
|
| 42 |
-
github.com/
|
| 43 |
-
github.com/
|
| 44 |
-
github.com/
|
| 45 |
-
github.com/
|
| 46 |
-
github.com/
|
| 47 |
-
github.com/
|
| 48 |
-
github.com/
|
| 49 |
-
github.com/
|
| 50 |
-
github.com/
|
| 51 |
-
github.com/
|
| 52 |
-
github.com/
|
| 53 |
-
github.com/
|
| 54 |
-
github.com/
|
| 55 |
-
github.com/
|
| 56 |
-
github.com/
|
| 57 |
-
github.com/
|
| 58 |
-
github.com/
|
| 59 |
-
github.com/
|
| 60 |
-
github.com/
|
| 61 |
-
github.com/
|
| 62 |
-
github.com/
|
| 63 |
-
github.com/
|
| 64 |
-
github.com/
|
| 65 |
-
github.com/
|
| 66 |
-
github.com/
|
| 67 |
-
github.com/
|
| 68 |
-
github.com/
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
| 2 |
+
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
| 3 |
+
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
| 4 |
+
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
| 5 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
| 6 |
+
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
| 7 |
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
| 8 |
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 9 |
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 10 |
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 11 |
+
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
| 12 |
+
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
| 13 |
+
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
| 14 |
+
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
| 15 |
+
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 16 |
+
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 17 |
+
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
| 18 |
+
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
| 19 |
+
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 20 |
+
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 21 |
+
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 22 |
+
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 23 |
+
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 24 |
+
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 25 |
+
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
| 26 |
+
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
| 27 |
+
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
| 28 |
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
| 29 |
+
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
| 30 |
+
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
| 31 |
+
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
| 32 |
+
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
| 33 |
+
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 34 |
+
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 35 |
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 36 |
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 37 |
+
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
| 38 |
+
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
| 39 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
| 40 |
+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
| 41 |
+
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
| 42 |
+
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
| 43 |
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
| 44 |
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
| 45 |
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
| 46 |
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
| 47 |
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
| 48 |
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
| 49 |
+
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 50 |
+
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 51 |
+
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 52 |
+
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
| 53 |
+
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
| 54 |
+
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
| 55 |
+
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
| 56 |
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
| 57 |
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
| 58 |
+
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
| 59 |
+
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
| 60 |
+
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
| 61 |
+
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 62 |
+
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 63 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 64 |
+
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 65 |
+
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 66 |
+
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 67 |
+
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
| 68 |
+
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
| 69 |
+
github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo=
|
| 70 |
+
github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
|
| 71 |
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 72 |
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 73 |
+
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
| 74 |
+
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
| 75 |
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 76 |
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 77 |
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 78 |
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 79 |
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 80 |
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 81 |
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 82 |
+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 83 |
+
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 84 |
+
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
| 85 |
+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 86 |
+
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 87 |
+
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 88 |
+
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
| 89 |
+
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 90 |
+
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 91 |
+
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
| 92 |
+
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 93 |
+
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
| 94 |
+
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
| 95 |
+
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
| 96 |
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
| 97 |
+
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 98 |
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 99 |
+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
| 100 |
+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
| 101 |
+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
| 102 |
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
| 103 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
| 104 |
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 105 |
+
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
| 106 |
+
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
| 107 |
+
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
| 108 |
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 109 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
| 110 |
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
| 111 |
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 112 |
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 113 |
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 114 |
+
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
| 115 |
+
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
| 116 |
+
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
| 117 |
+
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
| 118 |
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
internal/handler/auth.go
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"opus-api/internal/middleware"
|
| 6 |
+
"opus-api/internal/service"
|
| 7 |
+
"opus-api/internal/types"
|
| 8 |
+
|
| 9 |
+
"github.com/gin-gonic/gin"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// AuthHandler 认证处理器
|
| 13 |
+
type AuthHandler struct {
|
| 14 |
+
authService *service.AuthService
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// NewAuthHandler 创建认证处理器
|
| 18 |
+
func NewAuthHandler(authService *service.AuthService) *AuthHandler {
|
| 19 |
+
return &AuthHandler{authService: authService}
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// LoginRequest 登录请求
|
| 23 |
+
type LoginRequest struct {
|
| 24 |
+
Username string `json:"username" binding:"required"`
|
| 25 |
+
Password string `json:"password" binding:"required"`
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
// LoginResponse 登录响应
|
| 29 |
+
type LoginResponse struct {
|
| 30 |
+
Token string `json:"token"`
|
| 31 |
+
User User `json:"user"`
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// User 用户信息
|
| 35 |
+
type User struct {
|
| 36 |
+
ID uint `json:"id"`
|
| 37 |
+
Username string `json:"username"`
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Login 登录
|
| 41 |
+
func (h *AuthHandler) Login(c *gin.Context) {
|
| 42 |
+
var req LoginRequest
|
| 43 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 44 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 45 |
+
return
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
user, token, err := h.authService.Login(req.Username, req.Password)
|
| 49 |
+
if err != nil {
|
| 50 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
c.JSON(http.StatusOK, LoginResponse{
|
| 55 |
+
Token: token,
|
| 56 |
+
User: User{
|
| 57 |
+
ID: user.ID,
|
| 58 |
+
Username: user.Username,
|
| 59 |
+
},
|
| 60 |
+
})
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Logout 登出
|
| 64 |
+
func (h *AuthHandler) Logout(c *gin.Context) {
|
| 65 |
+
userID, ok := middleware.GetUserID(c)
|
| 66 |
+
if !ok {
|
| 67 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 68 |
+
return
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
if err := h.authService.Logout(userID); err != nil {
|
| 72 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to logout"})
|
| 73 |
+
return
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Me 获取当前用户信息
|
| 80 |
+
func (h *AuthHandler) Me(c *gin.Context) {
|
| 81 |
+
userID, ok := middleware.GetUserID(c)
|
| 82 |
+
if !ok {
|
| 83 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
user, err := h.authService.GetUserByID(userID)
|
| 88 |
+
if err != nil {
|
| 89 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
| 90 |
+
return
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
c.JSON(http.StatusOK, User{
|
| 94 |
+
ID: user.ID,
|
| 95 |
+
Username: user.Username,
|
| 96 |
+
})
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// ChangePassword 修改密码
|
| 100 |
+
func (h *AuthHandler) ChangePassword(c *gin.Context) {
|
| 101 |
+
var req types.ChangePasswordRequest
|
| 102 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 103 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
// 从上下文获取当前用户ID
|
| 108 |
+
userID, ok := middleware.GetUserID(c)
|
| 109 |
+
if !ok {
|
| 110 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 111 |
+
return
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
// 修改密码
|
| 115 |
+
if err := h.authService.ChangePassword(userID, req.OldPassword, req.NewPassword); err != nil {
|
| 116 |
+
if err == service.ErrInvalidCredentials {
|
| 117 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "原密码错误"})
|
| 118 |
+
return
|
| 119 |
+
}
|
| 120 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码修改失败"})
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
|
| 125 |
+
}
|
internal/handler/cookies.go
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package handler
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"opus-api/internal/middleware"
|
| 6 |
+
"opus-api/internal/model"
|
| 7 |
+
"opus-api/internal/service"
|
| 8 |
+
"strconv"
|
| 9 |
+
|
| 10 |
+
"github.com/gin-gonic/gin"
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
// CookieHandler Cookie 管理处理器
|
| 14 |
+
type CookieHandler struct {
|
| 15 |
+
cookieService *service.CookieService
|
| 16 |
+
validator *service.CookieValidator
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// NewCookieHandler 创建 Cookie 处理器
|
| 20 |
+
func NewCookieHandler(cookieService *service.CookieService, validator *service.CookieValidator) *CookieHandler {
|
| 21 |
+
return &CookieHandler{
|
| 22 |
+
cookieService: cookieService,
|
| 23 |
+
validator: validator,
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// CreateCookieRequest 创建 Cookie 请求
|
| 28 |
+
type CreateCookieRequest struct {
|
| 29 |
+
Name string `json:"name" binding:"required"`
|
| 30 |
+
APIKey string `json:"api_key" binding:"required"`
|
| 31 |
+
SessionKey string `json:"session_key"`
|
| 32 |
+
Priority int `json:"priority"`
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
// UpdateCookieRequest 更新 Cookie 请求
|
| 36 |
+
type UpdateCookieRequest struct {
|
| 37 |
+
Name string `json:"name"`
|
| 38 |
+
APIKey string `json:"api_key"`
|
| 39 |
+
SessionKey string `json:"session_key"`
|
| 40 |
+
Priority *int `json:"priority"`
|
| 41 |
+
IsValid *bool `json:"is_valid"`
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// CookieResponse Cookie 响应
|
| 45 |
+
type CookieResponse struct {
|
| 46 |
+
ID uint `json:"id"`
|
| 47 |
+
Name string `json:"name"`
|
| 48 |
+
APIKey string `json:"api_key"`
|
| 49 |
+
SessionKey string `json:"session_key"`
|
| 50 |
+
IsValid bool `json:"is_valid"`
|
| 51 |
+
Priority int `json:"priority"`
|
| 52 |
+
UsageCount int64 `json:"usage_count"`
|
| 53 |
+
ErrorCount int `json:"error_count"`
|
| 54 |
+
LastUsed string `json:"last_used,omitempty"`
|
| 55 |
+
LastValidated string `json:"last_validated,omitempty"`
|
| 56 |
+
CreatedAt string `json:"created_at"`
|
| 57 |
+
UpdatedAt string `json:"updated_at"`
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// ListCookies 获取 Cookie 列表
|
| 61 |
+
func (h *CookieHandler) ListCookies(c *gin.Context) {
|
| 62 |
+
userID, ok := middleware.GetUserID(c)
|
| 63 |
+
if !ok {
|
| 64 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 65 |
+
return
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
cookies, err := h.cookieService.ListCookies(userID)
|
| 69 |
+
if err != nil {
|
| 70 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list cookies"})
|
| 71 |
+
return
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
responses := make([]CookieResponse, len(cookies))
|
| 75 |
+
for i, cookie := range cookies {
|
| 76 |
+
responses[i] = toCookieResponse(&cookie)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
c.JSON(http.StatusOK, responses)
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// GetCookie 获取单个 Cookie
|
| 83 |
+
func (h *CookieHandler) GetCookie(c *gin.Context) {
|
| 84 |
+
userID, ok := middleware.GetUserID(c)
|
| 85 |
+
if !ok {
|
| 86 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 87 |
+
return
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
idStr := c.Param("id")
|
| 91 |
+
id, err := strconv.ParseUint(idStr, 10, 32)
|
| 92 |
+
if err != nil {
|
| 93 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
cookie, err := h.cookieService.GetCookie(uint(id), userID)
|
| 98 |
+
if err != nil {
|
| 99 |
+
if err == service.ErrCookieNotFound {
|
| 100 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
|
| 101 |
+
return
|
| 102 |
+
}
|
| 103 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
|
| 104 |
+
return
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
c.JSON(http.StatusOK, toCookieResponse(cookie))
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
// CreateCookie 创建 Cookie
|
| 111 |
+
func (h *CookieHandler) CreateCookie(c *gin.Context) {
|
| 112 |
+
userID, ok := middleware.GetUserID(c)
|
| 113 |
+
if !ok {
|
| 114 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 115 |
+
return
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
var req CreateCookieRequest
|
| 119 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 120 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 121 |
+
return
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
cookie := &model.MorphCookie{
|
| 125 |
+
UserID: userID,
|
| 126 |
+
Name: req.Name,
|
| 127 |
+
APIKey: req.APIKey,
|
| 128 |
+
SessionKey: req.SessionKey,
|
| 129 |
+
Priority: req.Priority,
|
| 130 |
+
IsValid: true, // 默认有效,可以通过验证接口验证
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if err := h.cookieService.CreateCookie(cookie); err != nil {
|
| 134 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create cookie"})
|
| 135 |
+
return
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
c.JSON(http.StatusCreated, toCookieResponse(cookie))
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// UpdateCookie 更新 Cookie
|
| 142 |
+
func (h *CookieHandler) UpdateCookie(c *gin.Context) {
|
| 143 |
+
userID, ok := middleware.GetUserID(c)
|
| 144 |
+
if !ok {
|
| 145 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 146 |
+
return
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
idStr := c.Param("id")
|
| 150 |
+
id, err := strconv.ParseUint(idStr, 10, 32)
|
| 151 |
+
if err != nil {
|
| 152 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
| 153 |
+
return
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
cookie, err := h.cookieService.GetCookie(uint(id), userID)
|
| 157 |
+
if err != nil {
|
| 158 |
+
if err == service.ErrCookieNotFound {
|
| 159 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
|
| 160 |
+
return
|
| 161 |
+
}
|
| 162 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
var req UpdateCookieRequest
|
| 167 |
+
if err := c.ShouldBindJSON(&req); err != nil {
|
| 168 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 169 |
+
return
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
// 更新字段
|
| 173 |
+
if req.Name != "" {
|
| 174 |
+
cookie.Name = req.Name
|
| 175 |
+
}
|
| 176 |
+
if req.APIKey != "" {
|
| 177 |
+
cookie.APIKey = req.APIKey
|
| 178 |
+
}
|
| 179 |
+
if req.SessionKey != "" {
|
| 180 |
+
cookie.SessionKey = req.SessionKey
|
| 181 |
+
}
|
| 182 |
+
if req.Priority != nil {
|
| 183 |
+
cookie.Priority = *req.Priority
|
| 184 |
+
}
|
| 185 |
+
if req.IsValid != nil {
|
| 186 |
+
cookie.IsValid = *req.IsValid
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
if err := h.cookieService.UpdateCookie(cookie); err != nil {
|
| 190 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update cookie"})
|
| 191 |
+
return
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
c.JSON(http.StatusOK, toCookieResponse(cookie))
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
// DeleteCookie 删除 Cookie
|
| 198 |
+
func (h *CookieHandler) DeleteCookie(c *gin.Context) {
|
| 199 |
+
userID, ok := middleware.GetUserID(c)
|
| 200 |
+
if !ok {
|
| 201 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 202 |
+
return
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
idStr := c.Param("id")
|
| 206 |
+
id, err := strconv.ParseUint(idStr, 10, 32)
|
| 207 |
+
if err != nil {
|
| 208 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
| 209 |
+
return
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
if err := h.cookieService.DeleteCookie(uint(id), userID); err != nil {
|
| 213 |
+
if err == service.ErrCookieNotFound {
|
| 214 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
|
| 215 |
+
return
|
| 216 |
+
}
|
| 217 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete cookie"})
|
| 218 |
+
return
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
c.JSON(http.StatusOK, gin.H{"message": "cookie deleted successfully"})
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// ValidateCookie 验证单个 Cookie
|
| 225 |
+
func (h *CookieHandler) ValidateCookie(c *gin.Context) {
|
| 226 |
+
userID, ok := middleware.GetUserID(c)
|
| 227 |
+
if !ok {
|
| 228 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 229 |
+
return
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
idStr := c.Param("id")
|
| 233 |
+
id, err := strconv.ParseUint(idStr, 10, 32)
|
| 234 |
+
if err != nil {
|
| 235 |
+
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
| 236 |
+
return
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
cookie, err := h.cookieService.GetCookie(uint(id), userID)
|
| 240 |
+
if err != nil {
|
| 241 |
+
if err == service.ErrCookieNotFound {
|
| 242 |
+
c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
|
| 243 |
+
return
|
| 244 |
+
}
|
| 245 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
|
| 246 |
+
return
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
isValid := h.validator.ValidateCookie(cookie)
|
| 250 |
+
|
| 251 |
+
c.JSON(http.StatusOK, gin.H{
|
| 252 |
+
"id": cookie.ID,
|
| 253 |
+
"is_valid": isValid,
|
| 254 |
+
})
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// ValidateAllCookies 验证所有 Cookie
|
| 258 |
+
func (h *CookieHandler) ValidateAllCookies(c *gin.Context) {
|
| 259 |
+
userID, ok := middleware.GetUserID(c)
|
| 260 |
+
if !ok {
|
| 261 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 262 |
+
return
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
results := h.validator.ValidateAllCookies(userID)
|
| 266 |
+
|
| 267 |
+
c.JSON(http.StatusOK, gin.H{
|
| 268 |
+
"results": results,
|
| 269 |
+
})
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// GetStats 获取统计信息
|
| 273 |
+
func (h *CookieHandler) GetStats(c *gin.Context) {
|
| 274 |
+
userID, ok := middleware.GetUserID(c)
|
| 275 |
+
if !ok {
|
| 276 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
| 277 |
+
return
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
stats, err := h.cookieService.GetStats(userID)
|
| 281 |
+
if err != nil {
|
| 282 |
+
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get stats"})
|
| 283 |
+
return
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
c.JSON(http.StatusOK, stats)
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
// toCookieResponse 转换为响应格式
|
| 290 |
+
func toCookieResponse(cookie *model.MorphCookie) CookieResponse {
|
| 291 |
+
resp := CookieResponse{
|
| 292 |
+
ID: cookie.ID,
|
| 293 |
+
Name: cookie.Name,
|
| 294 |
+
APIKey: maskAPIKey(cookie.APIKey),
|
| 295 |
+
SessionKey: cookie.SessionKey,
|
| 296 |
+
IsValid: cookie.IsValid,
|
| 297 |
+
Priority: cookie.Priority,
|
| 298 |
+
UsageCount: cookie.UsageCount,
|
| 299 |
+
ErrorCount: cookie.ErrorCount,
|
| 300 |
+
CreatedAt: cookie.CreatedAt.Format("2006-01-02 15:04:05"),
|
| 301 |
+
UpdatedAt: cookie.UpdatedAt.Format("2006-01-02 15:04:05"),
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
if cookie.LastUsed != nil {
|
| 305 |
+
resp.LastUsed = cookie.LastUsed.Format("2006-01-02 15:04:05")
|
| 306 |
+
}
|
| 307 |
+
if cookie.LastValidated != nil {
|
| 308 |
+
resp.LastValidated = cookie.LastValidated.Format("2006-01-02 15:04:05")
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
return resp
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
// maskAPIKey 隐藏 API Key 中间部分
|
| 315 |
+
func maskAPIKey(apiKey string) string {
|
| 316 |
+
if len(apiKey) <= 8 {
|
| 317 |
+
return "****"
|
| 318 |
+
}
|
| 319 |
+
return apiKey[:4] + "****" + apiKey[len(apiKey)-4:]
|
| 320 |
+
}
|
internal/handler/messages.go
CHANGED
|
@@ -9,6 +9,7 @@ import (
|
|
| 9 |
"net/http"
|
| 10 |
"opus-api/internal/converter"
|
| 11 |
"opus-api/internal/logger"
|
|
|
|
| 12 |
"opus-api/internal/stream"
|
| 13 |
"opus-api/internal/tokenizer"
|
| 14 |
"opus-api/internal/types"
|
|
@@ -35,6 +36,17 @@ func HandleMessages(c *gin.Context) {
|
|
| 35 |
return
|
| 36 |
}
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
// Log Point 1: Claude request
|
| 39 |
var logFolder string
|
| 40 |
if types.DebugMode {
|
|
@@ -64,7 +76,29 @@ func HandleMessages(c *gin.Context) {
|
|
| 64 |
}
|
| 65 |
|
| 66 |
// Set headers
|
|
|
|
| 67 |
for key, value := range types.MorphHeaders {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
req.Header.Set(key, value)
|
| 69 |
}
|
| 70 |
|
|
|
|
| 9 |
"net/http"
|
| 10 |
"opus-api/internal/converter"
|
| 11 |
"opus-api/internal/logger"
|
| 12 |
+
"opus-api/internal/model"
|
| 13 |
"opus-api/internal/stream"
|
| 14 |
"opus-api/internal/tokenizer"
|
| 15 |
"opus-api/internal/types"
|
|
|
|
| 36 |
return
|
| 37 |
}
|
| 38 |
|
| 39 |
+
// 验证模型是否支持
|
| 40 |
+
if claudeReq.Model == "" {
|
| 41 |
+
claudeReq.Model = types.DefaultModel
|
| 42 |
+
}
|
| 43 |
+
if !types.IsModelSupported(claudeReq.Model) {
|
| 44 |
+
c.JSON(http.StatusBadRequest, gin.H{
|
| 45 |
+
"error": fmt.Sprintf("Model '%s' is not supported. Supported models: %v", claudeReq.Model, types.SupportedModels),
|
| 46 |
+
})
|
| 47 |
+
return
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
// Log Point 1: Claude request
|
| 51 |
var logFolder string
|
| 52 |
if types.DebugMode {
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
// Set headers
|
| 79 |
+
headers := make(map[string]string)
|
| 80 |
for key, value := range types.MorphHeaders {
|
| 81 |
+
headers[key] = value
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// 如果启用了 Cookie 轮询器,使用轮询的 Cookie
|
| 85 |
+
if types.CookieRotatorInstance != nil {
|
| 86 |
+
cookieInterface, err := types.CookieRotatorInstance.NextCookie()
|
| 87 |
+
if err == nil && cookieInterface != nil {
|
| 88 |
+
// 类型断言为 *model.MorphCookie
|
| 89 |
+
if cookie, ok := cookieInterface.(*model.MorphCookie); ok {
|
| 90 |
+
headers["cookie"] = cookie.APIKey
|
| 91 |
+
log.Printf("[INFO] Using rotated cookie (ID: %d, Priority: %d)", cookie.ID, cookie.Priority)
|
| 92 |
+
} else {
|
| 93 |
+
log.Printf("[WARN] Cookie type assertion failed, using default")
|
| 94 |
+
}
|
| 95 |
+
} else {
|
| 96 |
+
log.Printf("[WARN] Failed to get rotated cookie: %v, using default", err)
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 应用所有请求头
|
| 101 |
+
for key, value := range headers {
|
| 102 |
req.Header.Set(key, value)
|
| 103 |
}
|
| 104 |
|
internal/middleware/auth.go
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package middleware
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"net/http"
|
| 5 |
+
"opus-api/internal/service"
|
| 6 |
+
"strings"
|
| 7 |
+
|
| 8 |
+
"github.com/gin-gonic/gin"
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
// AuthMiddleware JWT 认证中间件
|
| 12 |
+
func AuthMiddleware(authService *service.AuthService) gin.HandlerFunc {
|
| 13 |
+
return func(c *gin.Context) {
|
| 14 |
+
authHeader := c.GetHeader("Authorization")
|
| 15 |
+
if authHeader == "" {
|
| 16 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
|
| 17 |
+
c.Abort()
|
| 18 |
+
return
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// 解析 Bearer token
|
| 22 |
+
parts := strings.SplitN(authHeader, " ", 2)
|
| 23 |
+
if len(parts) != 2 || parts[0] != "Bearer" {
|
| 24 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"})
|
| 25 |
+
c.Abort()
|
| 26 |
+
return
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
token := parts[1]
|
| 30 |
+
|
| 31 |
+
// 验证 token
|
| 32 |
+
userID, err := authService.ValidateToken(token)
|
| 33 |
+
if err != nil {
|
| 34 |
+
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
|
| 35 |
+
c.Abort()
|
| 36 |
+
return
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// 将用户 ID 存储到上下文
|
| 40 |
+
c.Set("user_id", userID)
|
| 41 |
+
c.Next()
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// GetUserID 从上下文获取用户 ID
|
| 46 |
+
func GetUserID(c *gin.Context) (uint, bool) {
|
| 47 |
+
userID, exists := c.Get("user_id")
|
| 48 |
+
if !exists {
|
| 49 |
+
return 0, false
|
| 50 |
+
}
|
| 51 |
+
id, ok := userID.(uint)
|
| 52 |
+
return id, ok
|
| 53 |
+
}
|
internal/model/cookie.go
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package model
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"time"
|
| 5 |
+
)
|
| 6 |
+
|
| 7 |
+
// MorphCookie Morph Cookie 模型
|
| 8 |
+
type MorphCookie struct {
|
| 9 |
+
ID uint `gorm:"primaryKey" json:"id"`
|
| 10 |
+
UserID uint `gorm:"not null;index" json:"user_id"`
|
| 11 |
+
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
| 12 |
+
Name string `gorm:"size:100;not null" json:"name"`
|
| 13 |
+
APIKey string `gorm:"column:api_key;type:text;not null" json:"api_key"`
|
| 14 |
+
SessionKey string `gorm:"column:session_key;type:text" json:"session_key"`
|
| 15 |
+
IsValid bool `gorm:"default:true;index" json:"is_valid"`
|
| 16 |
+
LastValidated *time.Time `gorm:"column:last_validated" json:"last_validated"`
|
| 17 |
+
LastUsed *time.Time `gorm:"column:last_used" json:"last_used"`
|
| 18 |
+
Priority int `gorm:"default:0;index" json:"priority"`
|
| 19 |
+
UsageCount int64 `gorm:"default:0" json:"usage_count"`
|
| 20 |
+
ErrorCount int `gorm:"default:0" json:"error_count"`
|
| 21 |
+
CreatedAt time.Time `json:"created_at"`
|
| 22 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// CookieStats Cookie 统计信息
|
| 26 |
+
type CookieStats struct {
|
| 27 |
+
TotalCount int64 `json:"total_count"`
|
| 28 |
+
ValidCount int64 `json:"valid_count"`
|
| 29 |
+
InvalidCount int64 `json:"invalid_count"`
|
| 30 |
+
TotalUsage int64 `json:"total_usage"`
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// TableName 指定表名
|
| 34 |
+
func (MorphCookie) TableName() string {
|
| 35 |
+
return "morph_cookies"
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// MarkUsed 标记 Cookie 已使用
|
| 39 |
+
func (c *MorphCookie) MarkUsed() {
|
| 40 |
+
now := time.Now()
|
| 41 |
+
c.LastUsed = &now
|
| 42 |
+
c.UsageCount++
|
| 43 |
+
c.ErrorCount = 0 // 成功使用后重置错误计数
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// MarkError 标记 Cookie 错误
|
| 47 |
+
func (c *MorphCookie) MarkError() {
|
| 48 |
+
c.ErrorCount++
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// MarkInvalid 标记 Cookie 无效
|
| 52 |
+
func (c *MorphCookie) MarkInvalid() {
|
| 53 |
+
c.IsValid = false
|
| 54 |
+
now := time.Now()
|
| 55 |
+
c.LastValidated = &now
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// MarkValid 标记 Cookie 有效
|
| 59 |
+
func (c *MorphCookie) MarkValid() {
|
| 60 |
+
c.IsValid = true
|
| 61 |
+
c.ErrorCount = 0
|
| 62 |
+
now := time.Now()
|
| 63 |
+
c.LastValidated = &now
|
| 64 |
+
}
|
internal/model/db.go
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package model
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"fmt"
|
| 5 |
+
"log"
|
| 6 |
+
"os"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"gorm.io/driver/postgres"
|
| 10 |
+
"gorm.io/gorm"
|
| 11 |
+
"gorm.io/gorm/logger"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
var DB *gorm.DB
|
| 15 |
+
|
| 16 |
+
// InitDB 初始化数据库连接
|
| 17 |
+
func InitDB() error {
|
| 18 |
+
databaseURL := os.Getenv("DATABASE_URL")
|
| 19 |
+
if databaseURL == "" {
|
| 20 |
+
return fmt.Errorf("DATABASE_URL environment variable is not set")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
var err error
|
| 24 |
+
DB, err = gorm.Open(postgres.Open(databaseURL), &gorm.Config{
|
| 25 |
+
Logger: logger.Default.LogMode(logger.Info),
|
| 26 |
+
NowFunc: func() time.Time {
|
| 27 |
+
return time.Now().UTC()
|
| 28 |
+
},
|
| 29 |
+
})
|
| 30 |
+
if err != nil {
|
| 31 |
+
return fmt.Errorf("failed to connect to database: %w", err)
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
// 配置连接池
|
| 35 |
+
sqlDB, err := DB.DB()
|
| 36 |
+
if err != nil {
|
| 37 |
+
return fmt.Errorf("failed to get database instance: %w", err)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
sqlDB.SetMaxIdleConns(10)
|
| 41 |
+
sqlDB.SetMaxOpenConns(100)
|
| 42 |
+
sqlDB.SetConnMaxLifetime(time.Hour)
|
| 43 |
+
|
| 44 |
+
// 自动迁移
|
| 45 |
+
if err := autoMigrate(); err != nil {
|
| 46 |
+
return fmt.Errorf("failed to migrate database: %w", err)
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
log.Println("Database connected and migrated successfully")
|
| 50 |
+
return nil
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// autoMigrate 自动迁移数据库表结构
|
| 54 |
+
func autoMigrate() error {
|
| 55 |
+
return DB.AutoMigrate(
|
| 56 |
+
&User{},
|
| 57 |
+
&MorphCookie{},
|
| 58 |
+
&UserSession{},
|
| 59 |
+
)
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// CloseDB 关闭数据库连接
|
| 63 |
+
func CloseDB() error {
|
| 64 |
+
sqlDB, err := DB.DB()
|
| 65 |
+
if err != nil {
|
| 66 |
+
return err
|
| 67 |
+
}
|
| 68 |
+
return sqlDB.Close()
|
| 69 |
+
}
|
internal/model/user.go
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package model
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"log"
|
| 5 |
+
"os"
|
| 6 |
+
"time"
|
| 7 |
+
|
| 8 |
+
"golang.org/x/crypto/bcrypt"
|
| 9 |
+
"gorm.io/gorm"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// User 用户模型
|
| 13 |
+
type User struct {
|
| 14 |
+
ID uint `gorm:"primaryKey" json:"id"`
|
| 15 |
+
Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
|
| 16 |
+
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
| 17 |
+
CreatedAt time.Time `json:"created_at"`
|
| 18 |
+
UpdatedAt time.Time `json:"updated_at"`
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// UserSession 用户会话模型
|
| 22 |
+
type UserSession struct {
|
| 23 |
+
ID uint `gorm:"primaryKey" json:"id"`
|
| 24 |
+
UserID uint `gorm:"not null;index" json:"user_id"`
|
| 25 |
+
User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
|
| 26 |
+
TokenHash string `gorm:"uniqueIndex;size:255;not null" json:"-"`
|
| 27 |
+
ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"`
|
| 28 |
+
CreatedAt time.Time `json:"created_at"`
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// SetPassword 设置用户密码(加密)
|
| 32 |
+
func (u *User) SetPassword(password string) error {
|
| 33 |
+
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
| 34 |
+
if err != nil {
|
| 35 |
+
return err
|
| 36 |
+
}
|
| 37 |
+
u.PasswordHash = string(hash)
|
| 38 |
+
return nil
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// CheckPassword 验证密码
|
| 42 |
+
func (u *User) CheckPassword(password string) bool {
|
| 43 |
+
err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
|
| 44 |
+
return err == nil
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// TableName 指定表名
|
| 48 |
+
func (User) TableName() string {
|
| 49 |
+
return "users"
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// TableName 指定表名
|
| 53 |
+
func (UserSession) TableName() string {
|
| 54 |
+
return "user_sessions"
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
// CreateDefaultAdmin 创建默认管理员用户
|
| 58 |
+
func CreateDefaultAdmin(db *gorm.DB) error {
|
| 59 |
+
username := os.Getenv("DEFAULT_ADMIN_USERNAME")
|
| 60 |
+
if username == "" {
|
| 61 |
+
username = "admin"
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
password := os.Getenv("DEFAULT_ADMIN_PASSWORD")
|
| 65 |
+
if password == "" {
|
| 66 |
+
password = "changeme123"
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// 检查用户是否已存在
|
| 70 |
+
var existingUser User
|
| 71 |
+
result := db.Where("username = ?", username).First(&existingUser)
|
| 72 |
+
if result.Error == nil {
|
| 73 |
+
// 用户已存在
|
| 74 |
+
log.Printf("Default admin user '%s' already exists", username)
|
| 75 |
+
return nil
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// 创建新用户
|
| 79 |
+
user := &User{
|
| 80 |
+
Username: username,
|
| 81 |
+
}
|
| 82 |
+
if err := user.SetPassword(password); err != nil {
|
| 83 |
+
return err
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
if err := db.Create(user).Error; err != nil {
|
| 87 |
+
return err
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
log.Printf("Default admin user '%s' created successfully", username)
|
| 91 |
+
return nil
|
| 92 |
+
}
|
internal/service/auth_service.go
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"crypto/sha256"
|
| 5 |
+
"encoding/hex"
|
| 6 |
+
"errors"
|
| 7 |
+
"fmt"
|
| 8 |
+
"os"
|
| 9 |
+
"time"
|
| 10 |
+
|
| 11 |
+
"opus-api/internal/model"
|
| 12 |
+
|
| 13 |
+
"github.com/golang-jwt/jwt/v5"
|
| 14 |
+
"gorm.io/gorm"
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
var (
|
| 18 |
+
ErrInvalidCredentials = errors.New("invalid username or password")
|
| 19 |
+
ErrUserNotFound = errors.New("user not found")
|
| 20 |
+
ErrInvalidToken = errors.New("invalid token")
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
// AuthService 认证服务
|
| 24 |
+
type AuthService struct {
|
| 25 |
+
db *gorm.DB
|
| 26 |
+
jwtSecret []byte
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// NewAuthService 创建认证服务
|
| 30 |
+
func NewAuthService(db *gorm.DB) *AuthService {
|
| 31 |
+
secret := os.Getenv("JWT_SECRET")
|
| 32 |
+
if secret == "" {
|
| 33 |
+
secret = "default-secret-change-me-in-production"
|
| 34 |
+
}
|
| 35 |
+
return &AuthService{
|
| 36 |
+
db: db,
|
| 37 |
+
jwtSecret: []byte(secret),
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Claims JWT 声明
|
| 42 |
+
type Claims struct {
|
| 43 |
+
UserID uint `json:"user_id"`
|
| 44 |
+
Username string `json:"username"`
|
| 45 |
+
jwt.RegisteredClaims
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Login 用户登录
|
| 49 |
+
func (s *AuthService) Login(username, password string) (*model.User, string, error) {
|
| 50 |
+
var user model.User
|
| 51 |
+
if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
|
| 52 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 53 |
+
return nil, "", ErrInvalidCredentials
|
| 54 |
+
}
|
| 55 |
+
return nil, "", err
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if !user.CheckPassword(password) {
|
| 59 |
+
return nil, "", ErrInvalidCredentials
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// 生成 JWT token
|
| 63 |
+
token, err := s.generateToken(&user)
|
| 64 |
+
if err != nil {
|
| 65 |
+
return nil, "", err
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
// 保存会话
|
| 69 |
+
if err := s.saveSession(&user, token); err != nil {
|
| 70 |
+
return nil, "", err
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return &user, token, nil
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// generateToken 生成 JWT token
|
| 77 |
+
func (s *AuthService) generateToken(user *model.User) (string, error) {
|
| 78 |
+
expirationTime := time.Now().Add(24 * time.Hour)
|
| 79 |
+
claims := &Claims{
|
| 80 |
+
UserID: user.ID,
|
| 81 |
+
Username: user.Username,
|
| 82 |
+
RegisteredClaims: jwt.RegisteredClaims{
|
| 83 |
+
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
| 84 |
+
IssuedAt: jwt.NewNumericDate(time.Now()),
|
| 85 |
+
},
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
| 89 |
+
return token.SignedString(s.jwtSecret)
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// saveSession 保存会话
|
| 93 |
+
func (s *AuthService) saveSession(user *model.User, token string) error {
|
| 94 |
+
tokenHash := hashToken(token)
|
| 95 |
+
session := &model.UserSession{
|
| 96 |
+
UserID: user.ID,
|
| 97 |
+
TokenHash: tokenHash,
|
| 98 |
+
ExpiresAt: time.Now().Add(24 * time.Hour),
|
| 99 |
+
}
|
| 100 |
+
return s.db.Create(session).Error
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// ValidateToken 验证 token,返回用户 ID
|
| 104 |
+
func (s *AuthService) ValidateToken(tokenString string) (uint, error) {
|
| 105 |
+
claims := &Claims{}
|
| 106 |
+
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
| 107 |
+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
| 108 |
+
return 0, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
| 109 |
+
}
|
| 110 |
+
return s.jwtSecret, nil
|
| 111 |
+
})
|
| 112 |
+
|
| 113 |
+
if err != nil {
|
| 114 |
+
return 0, ErrInvalidToken
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
if !token.Valid {
|
| 118 |
+
return 0, ErrInvalidToken
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// 检查会话是否存在且未过期
|
| 122 |
+
tokenHash := hashToken(tokenString)
|
| 123 |
+
var session model.UserSession
|
| 124 |
+
if err := s.db.Where("token_hash = ? AND expires_at > ?", tokenHash, time.Now()).First(&session).Error; err != nil {
|
| 125 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 126 |
+
return 0, ErrInvalidToken
|
| 127 |
+
}
|
| 128 |
+
return 0, err
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
return claims.UserID, nil
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Logout 用户登出(删除用户的所有会话)
|
| 135 |
+
func (s *AuthService) Logout(userID uint) error {
|
| 136 |
+
return s.db.Where("user_id = ?", userID).Delete(&model.UserSession{}).Error
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// GetUserByID 根据 ID 获取用户
|
| 140 |
+
func (s *AuthService) GetUserByID(userID uint) (*model.User, error) {
|
| 141 |
+
var user model.User
|
| 142 |
+
if err := s.db.First(&user, userID).Error; err != nil {
|
| 143 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 144 |
+
return nil, ErrUserNotFound
|
| 145 |
+
}
|
| 146 |
+
return nil, err
|
| 147 |
+
}
|
| 148 |
+
return &user, nil
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// hashToken 对 token 进行哈希
|
| 152 |
+
func hashToken(token string) string {
|
| 153 |
+
hash := sha256.Sum256([]byte(token))
|
| 154 |
+
return hex.EncodeToString(hash[:])
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
// CleanExpiredSessions 清理过期会话
|
| 158 |
+
func (s *AuthService) CleanExpiredSessions() error {
|
| 159 |
+
return s.db.Where("expires_at < ?", time.Now()).Delete(&model.UserSession{}).Error
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// ChangePassword 修改用户密码
|
| 163 |
+
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
|
| 164 |
+
var user model.User
|
| 165 |
+
|
| 166 |
+
// 查找用户
|
| 167 |
+
if err := s.db.First(&user, userID).Error; err != nil {
|
| 168 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 169 |
+
return ErrUserNotFound
|
| 170 |
+
}
|
| 171 |
+
return err
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
// 验证旧密码
|
| 175 |
+
if !user.CheckPassword(oldPassword) {
|
| 176 |
+
return ErrInvalidCredentials
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
// 设置新密码
|
| 180 |
+
if err := user.SetPassword(newPassword); err != nil {
|
| 181 |
+
return fmt.Errorf("failed to hash password: %w", err)
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// 保存到数据库
|
| 185 |
+
if err := s.db.Save(&user).Error; err != nil {
|
| 186 |
+
return fmt.Errorf("failed to update password: %w", err)
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
return nil
|
| 190 |
+
}
|
internal/service/cookie_service.go
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"opus-api/internal/model"
|
| 6 |
+
|
| 7 |
+
"gorm.io/gorm"
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
var (
|
| 11 |
+
ErrCookieNotFound = errors.New("cookie not found")
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
// CookieService Cookie 管理服务
|
| 15 |
+
type CookieService struct {
|
| 16 |
+
db *gorm.DB
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// NewCookieService 创建 Cookie 服务
|
| 20 |
+
func NewCookieService(db *gorm.DB) *CookieService {
|
| 21 |
+
return &CookieService{db: db}
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
// GetDB 获取数据库连接
|
| 25 |
+
func (s *CookieService) GetDB() *gorm.DB {
|
| 26 |
+
return s.db
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
// ListCookies 获取用户的所有 Cookie
|
| 30 |
+
func (s *CookieService) ListCookies(userID uint) ([]model.MorphCookie, error) {
|
| 31 |
+
var cookies []model.MorphCookie
|
| 32 |
+
err := s.db.Where("user_id = ?", userID).
|
| 33 |
+
Order("priority DESC, created_at DESC").
|
| 34 |
+
Find(&cookies).Error
|
| 35 |
+
return cookies, err
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// GetCookie 获取单个 Cookie
|
| 39 |
+
func (s *CookieService) GetCookie(id, userID uint) (*model.MorphCookie, error) {
|
| 40 |
+
var cookie model.MorphCookie
|
| 41 |
+
err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&cookie).Error
|
| 42 |
+
if err != nil {
|
| 43 |
+
if errors.Is(err, gorm.ErrRecordNotFound) {
|
| 44 |
+
return nil, ErrCookieNotFound
|
| 45 |
+
}
|
| 46 |
+
return nil, err
|
| 47 |
+
}
|
| 48 |
+
return &cookie, nil
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// CreateCookie 创建 Cookie
|
| 52 |
+
func (s *CookieService) CreateCookie(cookie *model.MorphCookie) error {
|
| 53 |
+
return s.db.Create(cookie).Error
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// UpdateCookie 更新 Cookie
|
| 57 |
+
func (s *CookieService) UpdateCookie(cookie *model.MorphCookie) error {
|
| 58 |
+
return s.db.Save(cookie).Error
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// DeleteCookie 删除 Cookie
|
| 62 |
+
func (s *CookieService) DeleteCookie(id, userID uint) error {
|
| 63 |
+
result := s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.MorphCookie{})
|
| 64 |
+
if result.Error != nil {
|
| 65 |
+
return result.Error
|
| 66 |
+
}
|
| 67 |
+
if result.RowsAffected == 0 {
|
| 68 |
+
return ErrCookieNotFound
|
| 69 |
+
}
|
| 70 |
+
return nil
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// GetStats 获取统计信息
|
| 74 |
+
func (s *CookieService) GetStats(userID uint) (*model.CookieStats, error) {
|
| 75 |
+
stats := &model.CookieStats{}
|
| 76 |
+
|
| 77 |
+
// 总数
|
| 78 |
+
if err := s.db.Model(&model.MorphCookie{}).
|
| 79 |
+
Where("user_id = ?", userID).
|
| 80 |
+
Count(&stats.TotalCount).Error; err != nil {
|
| 81 |
+
return nil, err
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// 有效数量
|
| 85 |
+
if err := s.db.Model(&model.MorphCookie{}).
|
| 86 |
+
Where("user_id = ? AND is_valid = ?", userID, true).
|
| 87 |
+
Count(&stats.ValidCount).Error; err != nil {
|
| 88 |
+
return nil, err
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// 无效数量
|
| 92 |
+
stats.InvalidCount = stats.TotalCount - stats.ValidCount
|
| 93 |
+
|
| 94 |
+
// 总使用次数
|
| 95 |
+
var totalUsage *int64
|
| 96 |
+
if err := s.db.Model(&model.MorphCookie{}).
|
| 97 |
+
Where("user_id = ?", userID).
|
| 98 |
+
Select("COALESCE(SUM(usage_count), 0)").
|
| 99 |
+
Scan(&totalUsage).Error; err != nil {
|
| 100 |
+
return nil, err
|
| 101 |
+
}
|
| 102 |
+
if totalUsage != nil {
|
| 103 |
+
stats.TotalUsage = *totalUsage
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return stats, nil
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// GetValidCookies 获取所有有效的 Cookie
|
| 110 |
+
func (s *CookieService) GetValidCookies(userID uint) ([]model.MorphCookie, error) {
|
| 111 |
+
var cookies []model.MorphCookie
|
| 112 |
+
err := s.db.Where("user_id = ? AND is_valid = ?", userID, true).
|
| 113 |
+
Order("priority DESC, usage_count ASC").
|
| 114 |
+
Find(&cookies).Error
|
| 115 |
+
return cookies, err
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// GetAllValidCookies 获取系统中所有有效的 Cookie(用于轮询)
|
| 119 |
+
func (s *CookieService) GetAllValidCookies() ([]model.MorphCookie, error) {
|
| 120 |
+
var cookies []model.MorphCookie
|
| 121 |
+
err := s.db.Where("is_valid = ?", true).
|
| 122 |
+
Order("priority DESC, usage_count ASC").
|
| 123 |
+
Find(&cookies).Error
|
| 124 |
+
return cookies, err
|
| 125 |
+
}
|
internal/service/rotator.go
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"opus-api/internal/model"
|
| 6 |
+
"sync"
|
| 7 |
+
"time"
|
| 8 |
+
|
| 9 |
+
"gorm.io/gorm"
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
// RotationStrategy Cookie 轮询策略
|
| 13 |
+
type RotationStrategy string
|
| 14 |
+
|
| 15 |
+
const (
|
| 16 |
+
StrategyRoundRobin RotationStrategy = "round_robin" // 轮询
|
| 17 |
+
StrategyPriority RotationStrategy = "priority" // 优先级
|
| 18 |
+
StrategyLeastUsed RotationStrategy = "least_used" // 最少使用
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
var (
|
| 22 |
+
ErrNoCookiesAvailable = errors.New("no valid cookies available")
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
// CookieRotator Cookie 轮询器
|
| 26 |
+
type CookieRotator struct {
|
| 27 |
+
service *CookieService
|
| 28 |
+
strategy RotationStrategy
|
| 29 |
+
mu sync.RWMutex
|
| 30 |
+
index int // 用于 round_robin 策略
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
// NewCookieRotator 创建轮询器
|
| 34 |
+
func NewCookieRotator(service *CookieService, strategy RotationStrategy) *CookieRotator {
|
| 35 |
+
if strategy == "" {
|
| 36 |
+
strategy = StrategyRoundRobin
|
| 37 |
+
}
|
| 38 |
+
return &CookieRotator{
|
| 39 |
+
service: service,
|
| 40 |
+
strategy: strategy,
|
| 41 |
+
index: 0,
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// NextCookie 获取下一个可用的 Cookie
|
| 46 |
+
func (r *CookieRotator) NextCookie() (interface{}, error) {
|
| 47 |
+
r.mu.Lock()
|
| 48 |
+
defer r.mu.Unlock()
|
| 49 |
+
|
| 50 |
+
cookies, err := r.service.GetAllValidCookies()
|
| 51 |
+
if err != nil {
|
| 52 |
+
return nil, err
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
if len(cookies) == 0 {
|
| 56 |
+
return nil, ErrNoCookiesAvailable
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
var selected *model.MorphCookie
|
| 60 |
+
|
| 61 |
+
switch r.strategy {
|
| 62 |
+
case StrategyRoundRobin:
|
| 63 |
+
selected = r.roundRobin(cookies)
|
| 64 |
+
case StrategyPriority:
|
| 65 |
+
selected = r.priority(cookies)
|
| 66 |
+
case StrategyLeastUsed:
|
| 67 |
+
selected = r.leastUsed(cookies)
|
| 68 |
+
default:
|
| 69 |
+
selected = r.roundRobin(cookies)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
if selected == nil {
|
| 73 |
+
return nil, ErrNoCookiesAvailable
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
return selected, nil
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// roundRobin 轮询策略
|
| 80 |
+
func (r *CookieRotator) roundRobin(cookies []model.MorphCookie) *model.MorphCookie {
|
| 81 |
+
if len(cookies) == 0 {
|
| 82 |
+
return nil
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
r.index = r.index % len(cookies)
|
| 86 |
+
selected := &cookies[r.index]
|
| 87 |
+
r.index++
|
| 88 |
+
|
| 89 |
+
return selected
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// priority 优先级策略(优先级高的优先使用)
|
| 93 |
+
func (r *CookieRotator) priority(cookies []model.MorphCookie) *model.MorphCookie {
|
| 94 |
+
if len(cookies) == 0 {
|
| 95 |
+
return nil
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
// cookies 已经按照 priority DESC 排序
|
| 99 |
+
return &cookies[0]
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// leastUsed 最少使用策略
|
| 103 |
+
func (r *CookieRotator) leastUsed(cookies []model.MorphCookie) *model.MorphCookie {
|
| 104 |
+
if len(cookies) == 0 {
|
| 105 |
+
return nil
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// cookies 已经按照 usage_count ASC 排序
|
| 109 |
+
return &cookies[0]
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// MarkUsed 标记 Cookie 已使用
|
| 113 |
+
func (r *CookieRotator) MarkUsed(cookieID uint) error {
|
| 114 |
+
db := r.service.GetDB()
|
| 115 |
+
return db.Model(&model.MorphCookie{}).
|
| 116 |
+
Where("id = ?", cookieID).
|
| 117 |
+
Updates(map[string]interface{}{
|
| 118 |
+
"usage_count": gorm.Expr("usage_count + ?", 1),
|
| 119 |
+
"last_used": time.Now(),
|
| 120 |
+
}).Error
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// MarkInvalid 标记 Cookie 无效
|
| 124 |
+
func (r *CookieRotator) MarkInvalid(cookieID uint) error {
|
| 125 |
+
db := r.service.GetDB()
|
| 126 |
+
return db.Model(&model.MorphCookie{}).
|
| 127 |
+
Where("id = ?", cookieID).
|
| 128 |
+
Updates(map[string]interface{}{
|
| 129 |
+
"is_valid": false,
|
| 130 |
+
}).Error
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// MarkError 标记 Cookie 错误
|
| 134 |
+
func (r *CookieRotator) MarkError(cookieID uint) error {
|
| 135 |
+
db := r.service.GetDB()
|
| 136 |
+
|
| 137 |
+
var cookie model.MorphCookie
|
| 138 |
+
if err := db.First(&cookie, cookieID).Error; err != nil {
|
| 139 |
+
return err
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
updates := map[string]interface{}{
|
| 143 |
+
"error_count": gorm.Expr("error_count + ?", 1),
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
// 如果错误次数超过阈值,标记为无效
|
| 147 |
+
if cookie.ErrorCount >= 5 {
|
| 148 |
+
updates["is_valid"] = false
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
return db.Model(&model.MorphCookie{}).
|
| 152 |
+
Where("id = ?", cookieID).
|
| 153 |
+
Updates(updates).Error
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// GetStrategy 获取当前策略
|
| 157 |
+
func (r *CookieRotator) GetStrategy() RotationStrategy {
|
| 158 |
+
return r.strategy
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
// SetStrategy 设置轮询策略
|
| 162 |
+
func (r *CookieRotator) SetStrategy(strategy RotationStrategy) {
|
| 163 |
+
r.mu.Lock()
|
| 164 |
+
defer r.mu.Unlock()
|
| 165 |
+
r.strategy = strategy
|
| 166 |
+
r.index = 0 // 重置索引
|
| 167 |
+
}
|
internal/service/validator.go
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package service
|
| 2 |
+
|
| 3 |
+
import (
|
| 4 |
+
"bytes"
|
| 5 |
+
"encoding/json"
|
| 6 |
+
"io"
|
| 7 |
+
"net/http"
|
| 8 |
+
"opus-api/internal/converter"
|
| 9 |
+
"opus-api/internal/model"
|
| 10 |
+
"opus-api/internal/types"
|
| 11 |
+
"time"
|
| 12 |
+
|
| 13 |
+
"gorm.io/gorm"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
// CookieValidator Cookie 验证器
|
| 17 |
+
type CookieValidator struct {
|
| 18 |
+
service *CookieService
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// NewCookieValidator 创建验证器
|
| 22 |
+
func NewCookieValidator(service *CookieService) *CookieValidator {
|
| 23 |
+
return &CookieValidator{service: service}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// ValidateCookie 验证单个 Cookie
|
| 27 |
+
func (v *CookieValidator) ValidateCookie(cookie *model.MorphCookie) bool {
|
| 28 |
+
result := v.testCookie(cookie)
|
| 29 |
+
|
| 30 |
+
db := v.service.GetDB()
|
| 31 |
+
if result {
|
| 32 |
+
db.Model(cookie).Updates(map[string]interface{}{
|
| 33 |
+
"is_valid": true,
|
| 34 |
+
"last_validated": time.Now(),
|
| 35 |
+
"error_count": 0,
|
| 36 |
+
})
|
| 37 |
+
} else {
|
| 38 |
+
db.Model(cookie).Updates(map[string]interface{}{
|
| 39 |
+
"is_valid": false,
|
| 40 |
+
"last_validated": time.Now(),
|
| 41 |
+
"error_count": gorm.Expr("error_count + ?", 1),
|
| 42 |
+
})
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return result
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// ValidateAllCookies 验证用户的所有 Cookie
|
| 49 |
+
func (v *CookieValidator) ValidateAllCookies(userID uint) map[uint]bool {
|
| 50 |
+
cookies, err := v.service.ListCookies(userID)
|
| 51 |
+
if err != nil {
|
| 52 |
+
return nil
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
results := make(map[uint]bool)
|
| 56 |
+
for _, cookie := range cookies {
|
| 57 |
+
results[cookie.ID] = v.ValidateCookie(&cookie)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
return results
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// testCookie 测试 Cookie 是否有效(与 /v1/messages 逻辑完全一致)
|
| 64 |
+
func (v *CookieValidator) testCookie(cookie *model.MorphCookie) bool {
|
| 65 |
+
client := &http.Client{
|
| 66 |
+
Timeout: 30 * time.Second,
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// 创建 Claude 格式的测试请求
|
| 70 |
+
claudeReq := types.ClaudeRequest{
|
| 71 |
+
Model: types.DefaultModel,
|
| 72 |
+
MaxTokens: 1024,
|
| 73 |
+
Messages: []types.ClaudeMessage{
|
| 74 |
+
{
|
| 75 |
+
Role: "user",
|
| 76 |
+
Content: "Hello!",
|
| 77 |
+
},
|
| 78 |
+
},
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// 使用与 /v1/messages 相同的转换逻辑,将 Claude 格式转换为 Morph 格式
|
| 82 |
+
morphReq := converter.ClaudeToMorph(claudeReq)
|
| 83 |
+
|
| 84 |
+
reqBody, _ := json.Marshal(morphReq)
|
| 85 |
+
req, err := http.NewRequest("POST", types.MorphAPIURL, bytes.NewReader(reqBody))
|
| 86 |
+
if err != nil {
|
| 87 |
+
return false
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// 使用与 /v1/messages 相同的请求头
|
| 91 |
+
for key, value := range types.MorphHeaders {
|
| 92 |
+
req.Header.Set(key, value)
|
| 93 |
+
}
|
| 94 |
+
// 覆盖 Cookie
|
| 95 |
+
req.Header.Set("cookie", cookie.APIKey)
|
| 96 |
+
|
| 97 |
+
resp, err := client.Do(req)
|
| 98 |
+
if err != nil {
|
| 99 |
+
return false
|
| 100 |
+
}
|
| 101 |
+
defer resp.Body.Close()
|
| 102 |
+
|
| 103 |
+
// 检查响应状态码
|
| 104 |
+
// 200 OK 表示成功,401/403 表示认证失败
|
| 105 |
+
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
|
| 106 |
+
return false
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// 尝试读取响应体
|
| 110 |
+
body, _ := io.ReadAll(resp.Body)
|
| 111 |
+
|
| 112 |
+
// 检查是否是有效的 API 响应(SSE 流格式)
|
| 113 |
+
bodyStr := string(body)
|
| 114 |
+
|
| 115 |
+
// Morph API 返回 SSE 流,有效的响应会包含 "data:" 前缀
|
| 116 |
+
if !containsPrefix(bodyStr, "data:") {
|
| 117 |
+
return false
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
return true
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
// containsPrefix 检查字符串是否包含指定前缀(忽略空白字符)
|
| 124 |
+
func containsPrefix(s, prefix string) bool {
|
| 125 |
+
// 去除前导空白字符
|
| 126 |
+
start := 0
|
| 127 |
+
for start < len(s) && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n' || s[start] == '\r') {
|
| 128 |
+
start++
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
if start >= len(s) {
|
| 132 |
+
return false
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
return len(s[start:]) >= len(prefix) && s[start:start+len(prefix)] == prefix
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
// validateCookieQuiet 静默验证 Cookie(不更新数据库)
|
| 139 |
+
func (v *CookieValidator) validateCookieQuiet(cookie *model.MorphCookie) bool {
|
| 140 |
+
return v.testCookie(cookie)
|
| 141 |
+
}
|
internal/types/common.go
CHANGED
|
@@ -1,9 +1,13 @@
|
|
| 1 |
package types
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
const (
|
| 4 |
-
MorphAPIURL
|
| 5 |
-
|
| 6 |
-
LogDir = "./logs"
|
| 7 |
)
|
| 8 |
|
| 9 |
var MorphHeaders = map[string]string{
|
|
@@ -22,12 +26,59 @@ var MorphHeaders = map[string]string{
|
|
| 22 |
"sec-fetch-mode": "cors",
|
| 23 |
"sec-fetch-site": "same-origin",
|
| 24 |
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
| 25 |
-
"cookie": MorphCookies,
|
| 26 |
}
|
| 27 |
|
| 28 |
var DebugMode = true
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
type ParsedToolCall struct {
|
| 31 |
Name string `json:"name"`
|
| 32 |
Input map[string]interface{} `json:"input"`
|
| 33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
package types
|
| 2 |
|
| 3 |
+
import (
|
| 4 |
+
"errors"
|
| 5 |
+
"strings"
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
const (
|
| 9 |
+
MorphAPIURL = "https://www.morphllm.com/api/warpgrep-chat"
|
| 10 |
+
LogDir = "./logs"
|
|
|
|
| 11 |
)
|
| 12 |
|
| 13 |
var MorphHeaders = map[string]string{
|
|
|
|
| 26 |
"sec-fetch-mode": "cors",
|
| 27 |
"sec-fetch-site": "same-origin",
|
| 28 |
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
var DebugMode = true
|
| 32 |
|
| 33 |
+
// CookieRotatorInstance is a global reference to the cookie rotator service
|
| 34 |
+
// It's set in main.go after initialization
|
| 35 |
+
var CookieRotatorInstance interface {
|
| 36 |
+
NextCookie() (cookie interface{}, err error)
|
| 37 |
+
MarkUsed(cookieID uint) error
|
| 38 |
+
MarkError(cookieID uint) error
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// GetNextCookieFromRotator 从轮询器获取下一个 Cookie
|
| 42 |
+
// 这是一个辅助函数,用于从全局轮询器实例获取 Cookie
|
| 43 |
+
func GetNextCookieFromRotator() (interface{}, error) {
|
| 44 |
+
if CookieRotatorInstance == nil {
|
| 45 |
+
return nil, errors.New("cookie rotator not initialized")
|
| 46 |
+
}
|
| 47 |
+
return CookieRotatorInstance.NextCookie()
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
type ParsedToolCall struct {
|
| 51 |
Name string `json:"name"`
|
| 52 |
Input map[string]interface{} `json:"input"`
|
| 53 |
}
|
| 54 |
+
|
| 55 |
+
// ========== 模型配置 ==========
|
| 56 |
+
|
| 57 |
+
// DefaultModel 默认使用的模型
|
| 58 |
+
const DefaultModel = "claude-opus-4-5-20251101"
|
| 59 |
+
|
| 60 |
+
// SupportedModels 支持的模型列表
|
| 61 |
+
var SupportedModels = []string{
|
| 62 |
+
"claude-opus-4-5-20251101",
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// IsModelSupported 检查模型是否在支持列表中
|
| 66 |
+
func IsModelSupported(model string) bool {
|
| 67 |
+
if model == "" {
|
| 68 |
+
return false
|
| 69 |
+
}
|
| 70 |
+
for _, supported := range SupportedModels {
|
| 71 |
+
if strings.EqualFold(supported, model) {
|
| 72 |
+
return true
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
return false
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
// ========== 认证请求/响应类型 ==========
|
| 79 |
+
|
| 80 |
+
// ChangePasswordRequest 修改密码请求
|
| 81 |
+
type ChangePasswordRequest struct {
|
| 82 |
+
OldPassword string `json:"old_password" binding:"required"`
|
| 83 |
+
NewPassword string `json:"new_password" binding:"required,min=6"`
|
| 84 |
+
}
|
web/static/app.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// API 基础路径
|
| 2 |
+
const API_BASE = window.location.origin;
|
| 3 |
+
|
| 4 |
+
// 存储认证 token
|
| 5 |
+
let authToken = localStorage.getItem('auth_token');
|
| 6 |
+
|
| 7 |
+
// 页面加载时检查认证状态
|
| 8 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 9 |
+
const currentPage = window.location.pathname;
|
| 10 |
+
console.log('[DEBUG] Current page:', currentPage);
|
| 11 |
+
console.log('[DEBUG] Auth token:', authToken ? 'exists' : 'none');
|
| 12 |
+
|
| 13 |
+
if (currentPage.includes('dashboard')) {
|
| 14 |
+
console.log('[DEBUG] Loading dashboard...');
|
| 15 |
+
if (!authToken) {
|
| 16 |
+
window.location.href = '/static/index.html';
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
loadDashboard();
|
| 20 |
+
} else {
|
| 21 |
+
// 登录页面(包括 /static/index.html, /, 或其他)
|
| 22 |
+
console.log('[DEBUG] Loading login page...');
|
| 23 |
+
if (authToken) {
|
| 24 |
+
console.log('[DEBUG] Already logged in, redirecting to dashboard...');
|
| 25 |
+
window.location.href = '/dashboard';
|
| 26 |
+
return;
|
| 27 |
+
}
|
| 28 |
+
setupLoginForm();
|
| 29 |
+
}
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
+
// ========== 认证功能 ==========
|
| 33 |
+
|
| 34 |
+
// 设置登录表单
|
| 35 |
+
function setupLoginForm() {
|
| 36 |
+
console.log('[DEBUG] setupLoginForm called');
|
| 37 |
+
const loginForm = document.getElementById('loginForm');
|
| 38 |
+
console.log('[DEBUG] loginForm element:', loginForm);
|
| 39 |
+
if (loginForm) {
|
| 40 |
+
console.log('[DEBUG] Adding submit event listener to loginForm');
|
| 41 |
+
loginForm.addEventListener('submit', async (e) => {
|
| 42 |
+
e.preventDefault();
|
| 43 |
+
console.log('[DEBUG] Form submitted!');
|
| 44 |
+
|
| 45 |
+
const username = document.getElementById('username').value;
|
| 46 |
+
const password = document.getElementById('password').value;
|
| 47 |
+
const errorDiv = document.getElementById('loginError');
|
| 48 |
+
|
| 49 |
+
console.log('[DEBUG] Attempting login with username:', username);
|
| 50 |
+
|
| 51 |
+
try {
|
| 52 |
+
console.log('[DEBUG] Sending request to:', `${API_BASE}/api/auth/login`);
|
| 53 |
+
const response = await fetch(`${API_BASE}/api/auth/login`, {
|
| 54 |
+
method: 'POST',
|
| 55 |
+
headers: {
|
| 56 |
+
'Content-Type': 'application/json'
|
| 57 |
+
},
|
| 58 |
+
body: JSON.stringify({ username, password })
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
const data = await response.json();
|
| 62 |
+
|
| 63 |
+
if (response.ok) {
|
| 64 |
+
authToken = data.token;
|
| 65 |
+
localStorage.setItem('auth_token', authToken);
|
| 66 |
+
window.location.href = '/dashboard';
|
| 67 |
+
} else {
|
| 68 |
+
errorDiv.textContent = data.error || '登录失败';
|
| 69 |
+
errorDiv.style.display = 'block';
|
| 70 |
+
}
|
| 71 |
+
} catch (error) {
|
| 72 |
+
errorDiv.textContent = '网络错误,请稍后重试';
|
| 73 |
+
errorDiv.style.display = 'block';
|
| 74 |
+
}
|
| 75 |
+
});
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// 登出
|
| 80 |
+
function logout() {
|
| 81 |
+
if (confirm('确定要退出吗?')) {
|
| 82 |
+
localStorage.removeItem('auth_token');
|
| 83 |
+
authToken = null;
|
| 84 |
+
window.location.href = '/';
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// 显示修改密码弹窗
|
| 89 |
+
function showChangePasswordModal() {
|
| 90 |
+
document.getElementById('changePasswordModal').style.display = 'flex';
|
| 91 |
+
document.getElementById('changePasswordForm').reset();
|
| 92 |
+
document.getElementById('changePasswordError').style.display = 'none';
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 关闭修改密码弹窗
|
| 96 |
+
function closeChangePasswordModal() {
|
| 97 |
+
document.getElementById('changePasswordModal').style.display = 'none';
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// 修改密码
|
| 101 |
+
async function changePassword(oldPassword, newPassword) {
|
| 102 |
+
try {
|
| 103 |
+
const response = await apiRequest('/api/auth/password', {
|
| 104 |
+
method: 'PUT',
|
| 105 |
+
body: JSON.stringify({
|
| 106 |
+
old_password: oldPassword,
|
| 107 |
+
new_password: newPassword
|
| 108 |
+
})
|
| 109 |
+
});
|
| 110 |
+
|
| 111 |
+
if (response.ok) {
|
| 112 |
+
showToast('密码修改成功', 'success');
|
| 113 |
+
closeChangePasswordModal();
|
| 114 |
+
return true;
|
| 115 |
+
} else {
|
| 116 |
+
const data = await response.json();
|
| 117 |
+
return { error: data.error || '密码修改失败' };
|
| 118 |
+
}
|
| 119 |
+
} catch (error) {
|
| 120 |
+
console.error('修改密码错误:', error);
|
| 121 |
+
return { error: '网络错误,请重试' };
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
// 设置修改密码表单
|
| 126 |
+
function setupChangePasswordForm() {
|
| 127 |
+
const form = document.getElementById('changePasswordForm');
|
| 128 |
+
if (form) {
|
| 129 |
+
form.addEventListener('submit', async (e) => {
|
| 130 |
+
e.preventDefault();
|
| 131 |
+
|
| 132 |
+
const oldPassword = document.getElementById('oldPassword').value;
|
| 133 |
+
const newPassword = document.getElementById('newPassword').value;
|
| 134 |
+
const confirmPassword = document.getElementById('confirmPassword').value;
|
| 135 |
+
const errorDiv = document.getElementById('changePasswordError');
|
| 136 |
+
|
| 137 |
+
// 验证新密码长度
|
| 138 |
+
if (newPassword.length < 6) {
|
| 139 |
+
errorDiv.textContent = '新密码至少需要6位字符';
|
| 140 |
+
errorDiv.style.display = 'block';
|
| 141 |
+
return;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// 验证两次密码是否一致
|
| 145 |
+
if (newPassword !== confirmPassword) {
|
| 146 |
+
errorDiv.textContent = '两次输入的密码不一致';
|
| 147 |
+
errorDiv.style.display = 'block';
|
| 148 |
+
return;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
const result = await changePassword(oldPassword, newPassword);
|
| 152 |
+
if (result === true) {
|
| 153 |
+
// 成功
|
| 154 |
+
} else if (result && result.error) {
|
| 155 |
+
errorDiv.textContent = result.error;
|
| 156 |
+
errorDiv.style.display = 'block';
|
| 157 |
+
}
|
| 158 |
+
});
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// API 请求封装
|
| 163 |
+
async function apiRequest(url, options = {}) {
|
| 164 |
+
const headers = {
|
| 165 |
+
'Content-Type': 'application/json',
|
| 166 |
+
...options.headers
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
if (authToken) {
|
| 170 |
+
headers['Authorization'] = `Bearer ${authToken}`;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
const response = await fetch(`${API_BASE}${url}`, {
|
| 174 |
+
...options,
|
| 175 |
+
headers
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
// 如果 401,跳转到登录页
|
| 179 |
+
if (response.status === 401) {
|
| 180 |
+
localStorage.removeItem('auth_token');
|
| 181 |
+
authToken = null;
|
| 182 |
+
window.location.href = '/';
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
return response;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// ========== Dashboard 功能 ==========
|
| 190 |
+
|
| 191 |
+
// 加载 Dashboard
|
| 192 |
+
async function loadDashboard() {
|
| 193 |
+
setupChangePasswordForm();
|
| 194 |
+
await loadUserInfo();
|
| 195 |
+
await loadStats();
|
| 196 |
+
await loadCookies();
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
// 加载用户信息
|
| 200 |
+
async function loadUserInfo() {
|
| 201 |
+
try {
|
| 202 |
+
const response = await apiRequest('/api/auth/me');
|
| 203 |
+
if (response.ok) {
|
| 204 |
+
const user = await response.json();
|
| 205 |
+
document.getElementById('currentUser').textContent = user.username;
|
| 206 |
+
}
|
| 207 |
+
} catch (error) {
|
| 208 |
+
console.error('加载用户信息失败:', error);
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
// 加载统计信息
|
| 213 |
+
async function loadStats() {
|
| 214 |
+
try {
|
| 215 |
+
const response = await apiRequest('/api/cookies/stats');
|
| 216 |
+
if (response.ok) {
|
| 217 |
+
const stats = await response.json();
|
| 218 |
+
document.getElementById('totalCount').textContent = stats.total_count;
|
| 219 |
+
document.getElementById('validCount').textContent = stats.valid_count;
|
| 220 |
+
document.getElementById('invalidCount').textContent = stats.invalid_count;
|
| 221 |
+
document.getElementById('totalUsage').textContent = stats.total_usage.toLocaleString();
|
| 222 |
+
}
|
| 223 |
+
} catch (error) {
|
| 224 |
+
console.error('加载统计信息失败:', error);
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
// 加载 Cookie 列表
|
| 229 |
+
async function loadCookies() {
|
| 230 |
+
try {
|
| 231 |
+
const response = await apiRequest('/api/cookies');
|
| 232 |
+
if (response.ok) {
|
| 233 |
+
const data = await response.json();
|
| 234 |
+
// API 返回的是数组而不是对象
|
| 235 |
+
renderCookieTable(Array.isArray(data) ? data : (data.cookies || []));
|
| 236 |
+
}
|
| 237 |
+
} catch (error) {
|
| 238 |
+
console.error('加载 Cookie 列表失败:', error);
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// 渲染 Cookie 表格
|
| 243 |
+
function renderCookieTable(cookies) {
|
| 244 |
+
const tbody = document.getElementById('cookieTableBody');
|
| 245 |
+
const emptyState = document.getElementById('emptyState');
|
| 246 |
+
|
| 247 |
+
if (cookies.length === 0) {
|
| 248 |
+
tbody.innerHTML = '';
|
| 249 |
+
emptyState.style.display = 'block';
|
| 250 |
+
return;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
emptyState.style.display = 'none';
|
| 254 |
+
|
| 255 |
+
tbody.innerHTML = cookies.map((cookie, index) => `
|
| 256 |
+
<tr>
|
| 257 |
+
<td>${index + 1}</td>
|
| 258 |
+
<td>${escapeHtml(cookie.name)}</td>
|
| 259 |
+
<td>
|
| 260 |
+
<span class="status-badge ${cookie.is_valid ? 'status-valid' : 'status-invalid'}">
|
| 261 |
+
${cookie.is_valid ? '✅ 有效' : '❌ 无效'}
|
| 262 |
+
</span>
|
| 263 |
+
</td>
|
| 264 |
+
<td>${(cookie.usage_count || 0).toLocaleString()}</td>
|
| 265 |
+
<td>${cookie.priority || 0}</td>
|
| 266 |
+
<td>${formatTime(cookie.last_validated)}</td>
|
| 267 |
+
<td>
|
| 268 |
+
<div class="action-buttons">
|
| 269 |
+
<button class="btn btn-secondary btn-sm" onclick="validateCookie(${cookie.id})">🔄</button>
|
| 270 |
+
<button class="btn btn-secondary btn-sm" onclick="editCookie(${cookie.id})">⚙️</button>
|
| 271 |
+
<button class="btn btn-danger btn-sm" onclick="deleteCookie(${cookie.id})">🗑️</button>
|
| 272 |
+
</div>
|
| 273 |
+
</td>
|
| 274 |
+
</tr>
|
| 275 |
+
`).join('');
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
// 刷新 Cookie 列表
|
| 279 |
+
function refreshCookies() {
|
| 280 |
+
loadCookies();
|
| 281 |
+
loadStats();
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
// ========== Cookie 操作 ==========
|
| 285 |
+
|
| 286 |
+
// 显示添加弹窗
|
| 287 |
+
function showAddModal() {
|
| 288 |
+
document.getElementById('addModal').style.display = 'flex';
|
| 289 |
+
document.getElementById('addCookieForm').reset();
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
// 关闭添加弹窗
|
| 293 |
+
function closeAddModal() {
|
| 294 |
+
document.getElementById('addModal').style.display = 'none';
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// 添加 Cookie
|
| 298 |
+
document.getElementById('addCookieForm')?.addEventListener('submit', async (e) => {
|
| 299 |
+
e.preventDefault();
|
| 300 |
+
|
| 301 |
+
const formData = new FormData(e.target);
|
| 302 |
+
const data = {
|
| 303 |
+
name: formData.get('name'),
|
| 304 |
+
api_key: formData.get('api_key'),
|
| 305 |
+
session_key: formData.get('session_key') || '',
|
| 306 |
+
priority: parseInt(formData.get('priority') || '0')
|
| 307 |
+
};
|
| 308 |
+
|
| 309 |
+
try {
|
| 310 |
+
const response = await apiRequest('/api/cookies', {
|
| 311 |
+
method: 'POST',
|
| 312 |
+
body: JSON.stringify(data)
|
| 313 |
+
});
|
| 314 |
+
|
| 315 |
+
if (response.ok) {
|
| 316 |
+
showToast('Cookie 添加成功', 'success');
|
| 317 |
+
closeAddModal();
|
| 318 |
+
refreshCookies();
|
| 319 |
+
} else {
|
| 320 |
+
const error = await response.json();
|
| 321 |
+
showToast(error.error || '添加失败', 'error');
|
| 322 |
+
}
|
| 323 |
+
} catch (error) {
|
| 324 |
+
showToast('网络错误', 'error');
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
// 显示编辑弹窗
|
| 329 |
+
async function editCookie(id) {
|
| 330 |
+
try {
|
| 331 |
+
const response = await apiRequest(`/api/cookies/${id}`);
|
| 332 |
+
if (response.ok) {
|
| 333 |
+
const cookie = await response.json();
|
| 334 |
+
document.getElementById('editCookieId').value = cookie.id;
|
| 335 |
+
document.getElementById('editCookieName').value = cookie.name;
|
| 336 |
+
document.getElementById('editApiKey').value = cookie.api_key || '';
|
| 337 |
+
document.getElementById('editSessionKey').value = cookie.session_key || '';
|
| 338 |
+
document.getElementById('editCookiePriority').value = cookie.priority || 0;
|
| 339 |
+
document.getElementById('editModal').style.display = 'flex';
|
| 340 |
+
}
|
| 341 |
+
} catch (error) {
|
| 342 |
+
showToast('加载失败', 'error');
|
| 343 |
+
}
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
// 关闭编辑弹窗
|
| 347 |
+
function closeEditModal() {
|
| 348 |
+
document.getElementById('editModal').style.display = 'none';
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
// 更新 Cookie
|
| 352 |
+
document.getElementById('editCookieForm')?.addEventListener('submit', async (e) => {
|
| 353 |
+
e.preventDefault();
|
| 354 |
+
|
| 355 |
+
const id = document.getElementById('editCookieId').value;
|
| 356 |
+
const formData = new FormData(e.target);
|
| 357 |
+
const data = {
|
| 358 |
+
name: formData.get('name'),
|
| 359 |
+
api_key: formData.get('api_key'),
|
| 360 |
+
session_key: formData.get('session_key') || '',
|
| 361 |
+
priority: parseInt(formData.get('priority') || '0')
|
| 362 |
+
};
|
| 363 |
+
|
| 364 |
+
try {
|
| 365 |
+
const response = await apiRequest(`/api/cookies/${id}`, {
|
| 366 |
+
method: 'PUT',
|
| 367 |
+
body: JSON.stringify(data)
|
| 368 |
+
});
|
| 369 |
+
|
| 370 |
+
if (response.ok) {
|
| 371 |
+
showToast('Cookie 更新成功', 'success');
|
| 372 |
+
closeEditModal();
|
| 373 |
+
refreshCookies();
|
| 374 |
+
} else {
|
| 375 |
+
const error = await response.json();
|
| 376 |
+
showToast(error.error || '更新失败', 'error');
|
| 377 |
+
}
|
| 378 |
+
} catch (error) {
|
| 379 |
+
showToast('网络错误', 'error');
|
| 380 |
+
}
|
| 381 |
+
});
|
| 382 |
+
|
| 383 |
+
// 删除 Cookie
|
| 384 |
+
async function deleteCookie(id) {
|
| 385 |
+
if (!confirm('确定要删除这个 Cookie 吗?')) {
|
| 386 |
+
return;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
try {
|
| 390 |
+
const response = await apiRequest(`/api/cookies/${id}`, {
|
| 391 |
+
method: 'DELETE'
|
| 392 |
+
});
|
| 393 |
+
|
| 394 |
+
if (response.ok) {
|
| 395 |
+
showToast('Cookie 删除成功', 'success');
|
| 396 |
+
refreshCookies();
|
| 397 |
+
} else {
|
| 398 |
+
const error = await response.json();
|
| 399 |
+
showToast(error.error || '删除失败', 'error');
|
| 400 |
+
}
|
| 401 |
+
} catch (error) {
|
| 402 |
+
showToast('网络错误', 'error');
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// 验证单个 Cookie
|
| 407 |
+
async function validateCookie(id) {
|
| 408 |
+
try {
|
| 409 |
+
showToast('正在验证...', 'success');
|
| 410 |
+
const response = await apiRequest(`/api/cookies/${id}/validate`, {
|
| 411 |
+
method: 'POST'
|
| 412 |
+
});
|
| 413 |
+
|
| 414 |
+
if (response.ok) {
|
| 415 |
+
const result = await response.json();
|
| 416 |
+
showToast(result.is_valid ? '✅ Cookie 有效' : '❌ Cookie 无效', result.is_valid ? 'success' : 'error');
|
| 417 |
+
refreshCookies();
|
| 418 |
+
} else {
|
| 419 |
+
const error = await response.json();
|
| 420 |
+
showToast(error.error || '验证失败', 'error');
|
| 421 |
+
}
|
| 422 |
+
} catch (error) {
|
| 423 |
+
showToast('网络错误', 'error');
|
| 424 |
+
}
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
// 批量验证所有 Cookie
|
| 428 |
+
async function validateAll() {
|
| 429 |
+
if (!confirm('确定要验证所有 Cookie 吗?这可能需要一些时间。')) {
|
| 430 |
+
return;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
try {
|
| 434 |
+
showToast('正在批量验证...', 'success');
|
| 435 |
+
const response = await apiRequest('/api/cookies/validate/all', {
|
| 436 |
+
method: 'POST'
|
| 437 |
+
});
|
| 438 |
+
|
| 439 |
+
if (response.ok) {
|
| 440 |
+
const result = await response.json();
|
| 441 |
+
showToast(`验证完成:${result.valid_count} 个有效,${result.invalid_count} 个无效`, 'success');
|
| 442 |
+
refreshCookies();
|
| 443 |
+
} else {
|
| 444 |
+
const error = await response.json();
|
| 445 |
+
showToast(error.error || '验证失败', 'error');
|
| 446 |
+
}
|
| 447 |
+
} catch (error) {
|
| 448 |
+
showToast('网络错误', 'error');
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
// ========== 工具函数 ==========
|
| 453 |
+
|
| 454 |
+
// 显示 Toast 通知
|
| 455 |
+
function showToast(message, type = 'success') {
|
| 456 |
+
const toast = document.getElementById('toast');
|
| 457 |
+
toast.textContent = message;
|
| 458 |
+
toast.className = `toast ${type}`;
|
| 459 |
+
toast.style.display = 'block';
|
| 460 |
+
|
| 461 |
+
setTimeout(() => {
|
| 462 |
+
toast.style.display = 'none';
|
| 463 |
+
}, 3000);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
// 格式化时间
|
| 467 |
+
function formatTime(timeStr) {
|
| 468 |
+
if (!timeStr) return '-';
|
| 469 |
+
const date = new Date(timeStr);
|
| 470 |
+
const now = new Date();
|
| 471 |
+
const diff = Math.floor((now - date) / 1000);
|
| 472 |
+
|
| 473 |
+
if (diff < 60) return '刚刚';
|
| 474 |
+
if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
|
| 475 |
+
if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
|
| 476 |
+
return `${Math.floor(diff / 86400)} 天前`;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
// HTML 转义
|
| 480 |
+
function escapeHtml(text) {
|
| 481 |
+
const div = document.createElement('div');
|
| 482 |
+
div.textContent = text;
|
| 483 |
+
return div.innerHTML;
|
| 484 |
+
}
|
web/static/dashboard.html
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Opus API 管理系统 - Cookie 管理</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="dashboard-container">
|
| 11 |
+
<!-- 顶部导航 -->
|
| 12 |
+
<header class="header">
|
| 13 |
+
<h1>🚀 Opus API 管理系统</h1>
|
| 14 |
+
<div class="user-menu">
|
| 15 |
+
<span id="currentUser">admin</span>
|
| 16 |
+
<button class="btn btn-secondary" onclick="showChangePasswordModal()">修改密码</button>
|
| 17 |
+
<button class="btn btn-secondary" onclick="logout()">退出</button>
|
| 18 |
+
</div>
|
| 19 |
+
</header>
|
| 20 |
+
|
| 21 |
+
<!-- 主内容区 -->
|
| 22 |
+
<main class="main-content">
|
| 23 |
+
<!-- 统计卡片 -->
|
| 24 |
+
<section class="stats-section">
|
| 25 |
+
<div class="stats-grid">
|
| 26 |
+
<div class="stat-card">
|
| 27 |
+
<div class="stat-icon">📊</div>
|
| 28 |
+
<div class="stat-info">
|
| 29 |
+
<h3 id="totalCount">0</h3>
|
| 30 |
+
<p>总账号数</p>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="stat-card valid">
|
| 34 |
+
<div class="stat-icon">✅</div>
|
| 35 |
+
<div class="stat-info">
|
| 36 |
+
<h3 id="validCount">0</h3>
|
| 37 |
+
<p>有效账号</p>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
<div class="stat-card invalid">
|
| 41 |
+
<div class="stat-icon">❌</div>
|
| 42 |
+
<div class="stat-info">
|
| 43 |
+
<h3 id="invalidCount">0</h3>
|
| 44 |
+
<p>无效账号</p>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
<div class="stat-card">
|
| 48 |
+
<div class="stat-icon">📈</div>
|
| 49 |
+
<div class="stat-info">
|
| 50 |
+
<h3 id="totalUsage">0</h3>
|
| 51 |
+
<p>总请求数</p>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</section>
|
| 56 |
+
|
| 57 |
+
<!-- 操作按钮 -->
|
| 58 |
+
<section class="actions-section">
|
| 59 |
+
<button class="btn btn-primary" onclick="showAddModal()">
|
| 60 |
+
➕ 添加 Cookie
|
| 61 |
+
</button>
|
| 62 |
+
<button class="btn btn-secondary" onclick="validateAll()">
|
| 63 |
+
🔄 批量验证
|
| 64 |
+
</button>
|
| 65 |
+
<button class="btn btn-secondary" onclick="refreshCookies()">
|
| 66 |
+
🔃 刷新列表
|
| 67 |
+
</button>
|
| 68 |
+
</section>
|
| 69 |
+
|
| 70 |
+
<!-- Cookie 列表 -->
|
| 71 |
+
<section class="table-section">
|
| 72 |
+
<h2>Cookie 列表</h2>
|
| 73 |
+
<div class="table-container">
|
| 74 |
+
<table id="cookieTable">
|
| 75 |
+
<thead>
|
| 76 |
+
<tr>
|
| 77 |
+
<th>#</th>
|
| 78 |
+
<th>名称</th>
|
| 79 |
+
<th>状态</th>
|
| 80 |
+
<th>使用次数</th>
|
| 81 |
+
<th>优先级</th>
|
| 82 |
+
<th>最后验证</th>
|
| 83 |
+
<th>操作</th>
|
| 84 |
+
</tr>
|
| 85 |
+
</thead>
|
| 86 |
+
<tbody id="cookieTableBody">
|
| 87 |
+
<!-- 动态填充 -->
|
| 88 |
+
</tbody>
|
| 89 |
+
</table>
|
| 90 |
+
<div id="emptyState" class="empty-state" style="display: none;">
|
| 91 |
+
<p>暂无 Cookie,点击上方按钮添加</p>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</section>
|
| 95 |
+
</main>
|
| 96 |
+
|
| 97 |
+
<!-- 添加 Cookie 弹窗 -->
|
| 98 |
+
<div id="addModal" class="modal" style="display: none;">
|
| 99 |
+
<div class="modal-content">
|
| 100 |
+
<div class="modal-header">
|
| 101 |
+
<h2>添加 Morph Cookie</h2>
|
| 102 |
+
<button class="modal-close" onclick="closeAddModal()">×</button>
|
| 103 |
+
</div>
|
| 104 |
+
<form id="addCookieForm">
|
| 105 |
+
<div class="form-group">
|
| 106 |
+
<label for="cookieName">账号名称</label>
|
| 107 |
+
<input type="text" id="cookieName" name="name" placeholder="例如:主账号" required>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="form-group">
|
| 110 |
+
<label for="apiKey">API Key</label>
|
| 111 |
+
<input type="text" id="apiKey" name="api_key" placeholder="输入 API Key..." required>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="form-group">
|
| 114 |
+
<label for="sessionKey">Session Key (可选)</label>
|
| 115 |
+
<input type="text" id="sessionKey" name="session_key" placeholder="输入 Session Key...">
|
| 116 |
+
</div>
|
| 117 |
+
<div class="form-group">
|
| 118 |
+
<label for="cookiePriority">优先级 (0-100)</label>
|
| 119 |
+
<input type="number" id="cookiePriority" name="priority" value="0" min="0" max="100">
|
| 120 |
+
</div>
|
| 121 |
+
<div class="modal-footer">
|
| 122 |
+
<button type="button" class="btn btn-secondary" onclick="closeAddModal()">取消</button>
|
| 123 |
+
<button type="submit" class="btn btn-primary">确定</button>
|
| 124 |
+
</div>
|
| 125 |
+
</form>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
|
| 129 |
+
<!-- 编辑 Cookie 弹窗 -->
|
| 130 |
+
<div id="editModal" class="modal" style="display: none;">
|
| 131 |
+
<div class="modal-content">
|
| 132 |
+
<div class="modal-header">
|
| 133 |
+
<h2>编辑 Cookie</h2>
|
| 134 |
+
<button class="modal-close" onclick="closeEditModal()">×</button>
|
| 135 |
+
</div>
|
| 136 |
+
<form id="editCookieForm">
|
| 137 |
+
<input type="hidden" id="editCookieId">
|
| 138 |
+
<div class="form-group">
|
| 139 |
+
<label for="editCookieName">账号名称</label>
|
| 140 |
+
<input type="text" id="editCookieName" name="name" placeholder="例如:主账号">
|
| 141 |
+
</div>
|
| 142 |
+
<div class="form-group">
|
| 143 |
+
<label for="editApiKey">API Key</label>
|
| 144 |
+
<input type="text" id="editApiKey" name="api_key" placeholder="输入 API Key...">
|
| 145 |
+
</div>
|
| 146 |
+
<div class="form-group">
|
| 147 |
+
<label for="editSessionKey">Session Key (可选)</label>
|
| 148 |
+
<input type="text" id="editSessionKey" name="session_key" placeholder="输入 Session Key...">
|
| 149 |
+
</div>
|
| 150 |
+
<div class="form-group">
|
| 151 |
+
<label for="editCookiePriority">优先级 (0-100)</label>
|
| 152 |
+
<input type="number" id="editCookiePriority" name="priority" min="0" max="100">
|
| 153 |
+
</div>
|
| 154 |
+
<div class="modal-footer">
|
| 155 |
+
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">取消</button>
|
| 156 |
+
<button type="submit" class="btn btn-primary">保存</button>
|
| 157 |
+
</div>
|
| 158 |
+
</form>
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<!-- 修改密码弹窗 -->
|
| 163 |
+
<div id="changePasswordModal" class="modal" style="display: none;">
|
| 164 |
+
<div class="modal-content">
|
| 165 |
+
<div class="modal-header">
|
| 166 |
+
<h2>修改密码</h2>
|
| 167 |
+
<button class="modal-close" onclick="closeChangePasswordModal()">×</button>
|
| 168 |
+
</div>
|
| 169 |
+
<form id="changePasswordForm">
|
| 170 |
+
<div class="form-group">
|
| 171 |
+
<label for="oldPassword">原密码</label>
|
| 172 |
+
<input type="password" id="oldPassword" name="old_password" placeholder="请输入原密码" required>
|
| 173 |
+
</div>
|
| 174 |
+
<div class="form-group">
|
| 175 |
+
<label for="newPassword">新密码</label>
|
| 176 |
+
<input type="password" id="newPassword" name="new_password" placeholder="请输入新密码 (至少6位)" required minlength="6">
|
| 177 |
+
</div>
|
| 178 |
+
<div class="form-group">
|
| 179 |
+
<label for="confirmPassword">确认新密码</label>
|
| 180 |
+
<input type="password" id="confirmPassword" name="confirm_password" placeholder="请再次输入新密码" required>
|
| 181 |
+
</div>
|
| 182 |
+
<div class="error" id="changePasswordError" style="display: none;"></div>
|
| 183 |
+
<div class="modal-footer">
|
| 184 |
+
<button type="button" class="btn btn-secondary" onclick="closeChangePasswordModal()">取消</button>
|
| 185 |
+
<button type="submit" class="btn btn-primary">确定</button>
|
| 186 |
+
</div>
|
| 187 |
+
</form>
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
<!-- Toast 通知 -->
|
| 192 |
+
<div id="toast" class="toast" style="display: none;"></div>
|
| 193 |
+
</div>
|
| 194 |
+
<script src="/static/app.js"></script>
|
| 195 |
+
</body>
|
| 196 |
+
</html>
|
web/static/index.html
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Opus API 管理系统 - 登录</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css">
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div class="login-container">
|
| 11 |
+
<div class="login-box">
|
| 12 |
+
<h1>🚀 Opus API 管理系统</h1>
|
| 13 |
+
<form id="loginForm">
|
| 14 |
+
<div class="form-group">
|
| 15 |
+
<input type="text" id="username" name="username" placeholder="用户名" required autocomplete="username">
|
| 16 |
+
</div>
|
| 17 |
+
<div class="form-group">
|
| 18 |
+
<input type="password" id="password" name="password" placeholder="密码" required autocomplete="current-password">
|
| 19 |
+
</div>
|
| 20 |
+
<button type="submit" class="btn btn-primary btn-block">登录</button>
|
| 21 |
+
</form>
|
| 22 |
+
<div class="error" id="loginError"></div>
|
| 23 |
+
</div>
|
| 24 |
+
</div>
|
| 25 |
+
<script src="/static/app.js"></script>
|
| 26 |
+
</body>
|
| 27 |
+
</html>
|
web/static/styles.css
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* 全局样式 */
|
| 2 |
+
* {
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
box-sizing: border-box;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
body {
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
| 10 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 11 |
+
min-height: 100vh;
|
| 12 |
+
color: #333;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
/* 登录页面 */
|
| 16 |
+
.login-container {
|
| 17 |
+
display: flex;
|
| 18 |
+
justify-content: center;
|
| 19 |
+
align-items: center;
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
padding: 20px;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.login-box {
|
| 25 |
+
background: white;
|
| 26 |
+
border-radius: 12px;
|
| 27 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
| 28 |
+
padding: 40px;
|
| 29 |
+
width: 100%;
|
| 30 |
+
max-width: 400px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.login-box h1 {
|
| 34 |
+
text-align: center;
|
| 35 |
+
margin-bottom: 30px;
|
| 36 |
+
color: #667eea;
|
| 37 |
+
font-size: 28px;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* 表单样式 */
|
| 41 |
+
.form-group {
|
| 42 |
+
margin-bottom: 20px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.form-group label {
|
| 46 |
+
display: block;
|
| 47 |
+
margin-bottom: 8px;
|
| 48 |
+
font-weight: 500;
|
| 49 |
+
color: #555;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.form-group input,
|
| 53 |
+
.form-group textarea,
|
| 54 |
+
.form-group select {
|
| 55 |
+
width: 100%;
|
| 56 |
+
padding: 12px 16px;
|
| 57 |
+
border: 2px solid #e0e0e0;
|
| 58 |
+
border-radius: 8px;
|
| 59 |
+
font-size: 14px;
|
| 60 |
+
transition: border-color 0.3s;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.form-group input:focus,
|
| 64 |
+
.form-group textarea:focus,
|
| 65 |
+
.form-group select:focus {
|
| 66 |
+
outline: none;
|
| 67 |
+
border-color: #667eea;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.form-group.checkbox {
|
| 71 |
+
display: flex;
|
| 72 |
+
align-items: center;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.form-group.checkbox label {
|
| 76 |
+
margin: 0;
|
| 77 |
+
margin-left: 8px;
|
| 78 |
+
font-weight: normal;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.form-group.checkbox input {
|
| 82 |
+
width: auto;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/* 按钮样式 */
|
| 86 |
+
.btn {
|
| 87 |
+
padding: 12px 24px;
|
| 88 |
+
border: none;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
font-size: 14px;
|
| 91 |
+
font-weight: 600;
|
| 92 |
+
cursor: pointer;
|
| 93 |
+
transition: all 0.3s;
|
| 94 |
+
text-decoration: none;
|
| 95 |
+
display: inline-block;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.btn-primary {
|
| 99 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 100 |
+
color: white;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.btn-primary:hover {
|
| 104 |
+
transform: translateY(-2px);
|
| 105 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.btn-secondary {
|
| 109 |
+
background: #f5f5f5;
|
| 110 |
+
color: #333;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.btn-secondary:hover {
|
| 114 |
+
background: #e0e0e0;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.btn-danger {
|
| 118 |
+
background: #ff4757;
|
| 119 |
+
color: white;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn-danger:hover {
|
| 123 |
+
background: #ee5a6f;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.btn-block {
|
| 127 |
+
width: 100%;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.btn:disabled {
|
| 131 |
+
opacity: 0.6;
|
| 132 |
+
cursor: not-allowed;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* 错误提示 */
|
| 136 |
+
.error {
|
| 137 |
+
color: #ff4757;
|
| 138 |
+
font-size: 14px;
|
| 139 |
+
margin-top: 10px;
|
| 140 |
+
text-align: center;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/* Dashboard 样式 */
|
| 144 |
+
.dashboard-container {
|
| 145 |
+
min-height: 100vh;
|
| 146 |
+
background: #f5f7fa;
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.header {
|
| 150 |
+
background: white;
|
| 151 |
+
padding: 20px 40px;
|
| 152 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 153 |
+
display: flex;
|
| 154 |
+
justify-content: space-between;
|
| 155 |
+
align-items: center;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.header h1 {
|
| 159 |
+
color: #667eea;
|
| 160 |
+
font-size: 24px;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.user-menu {
|
| 164 |
+
display: flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
gap: 15px;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.user-menu span {
|
| 170 |
+
font-weight: 500;
|
| 171 |
+
color: #555;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.main-content {
|
| 175 |
+
max-width: 1200px;
|
| 176 |
+
margin: 0 auto;
|
| 177 |
+
padding: 40px 20px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
/* 统计卡片 */
|
| 181 |
+
.stats-section {
|
| 182 |
+
margin-bottom: 30px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.stats-grid {
|
| 186 |
+
display: grid;
|
| 187 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 188 |
+
gap: 20px;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.stat-card {
|
| 192 |
+
background: white;
|
| 193 |
+
border-radius: 12px;
|
| 194 |
+
padding: 24px;
|
| 195 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 196 |
+
display: flex;
|
| 197 |
+
align-items: center;
|
| 198 |
+
gap: 20px;
|
| 199 |
+
transition: transform 0.3s;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.stat-card:hover {
|
| 203 |
+
transform: translateY(-4px);
|
| 204 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.stat-card.valid {
|
| 208 |
+
border-left: 4px solid #2ecc71;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.stat-card.invalid {
|
| 212 |
+
border-left: 4px solid #ff4757;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.stat-icon {
|
| 216 |
+
font-size: 40px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.stat-info h3 {
|
| 220 |
+
font-size: 32px;
|
| 221 |
+
color: #333;
|
| 222 |
+
margin-bottom: 4px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.stat-info p {
|
| 226 |
+
color: #888;
|
| 227 |
+
font-size: 14px;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* 操作按钮区 */
|
| 231 |
+
.actions-section {
|
| 232 |
+
margin-bottom: 30px;
|
| 233 |
+
display: flex;
|
| 234 |
+
gap: 15px;
|
| 235 |
+
flex-wrap: wrap;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/* 表格样式 */
|
| 239 |
+
.table-section {
|
| 240 |
+
background: white;
|
| 241 |
+
border-radius: 12px;
|
| 242 |
+
padding: 24px;
|
| 243 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.table-section h2 {
|
| 247 |
+
margin-bottom: 20px;
|
| 248 |
+
color: #333;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.table-container {
|
| 252 |
+
overflow-x: auto;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
table {
|
| 256 |
+
width: 100%;
|
| 257 |
+
border-collapse: collapse;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
thead {
|
| 261 |
+
background: #f8f9fa;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
th {
|
| 265 |
+
padding: 12px;
|
| 266 |
+
text-align: left;
|
| 267 |
+
font-weight: 600;
|
| 268 |
+
color: #555;
|
| 269 |
+
border-bottom: 2px solid #e0e0e0;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
td {
|
| 273 |
+
padding: 12px;
|
| 274 |
+
border-bottom: 1px solid #f0f0f0;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
tr:hover {
|
| 278 |
+
background: #f8f9fa;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.status-badge {
|
| 282 |
+
display: inline-block;
|
| 283 |
+
padding: 4px 12px;
|
| 284 |
+
border-radius: 12px;
|
| 285 |
+
font-size: 12px;
|
| 286 |
+
font-weight: 600;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.status-valid {
|
| 290 |
+
background: #d4edda;
|
| 291 |
+
color: #155724;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.status-invalid {
|
| 295 |
+
background: #f8d7da;
|
| 296 |
+
color: #721c24;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.action-buttons {
|
| 300 |
+
display: flex;
|
| 301 |
+
gap: 8px;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.btn-sm {
|
| 305 |
+
padding: 6px 12px;
|
| 306 |
+
font-size: 12px;
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.empty-state {
|
| 310 |
+
text-align: center;
|
| 311 |
+
padding: 60px 20px;
|
| 312 |
+
color: #999;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
/* 模态框 */
|
| 316 |
+
.modal {
|
| 317 |
+
position: fixed;
|
| 318 |
+
top: 0;
|
| 319 |
+
left: 0;
|
| 320 |
+
width: 100%;
|
| 321 |
+
height: 100%;
|
| 322 |
+
background: rgba(0, 0, 0, 0.5);
|
| 323 |
+
display: flex;
|
| 324 |
+
justify-content: center;
|
| 325 |
+
align-items: center;
|
| 326 |
+
z-index: 1000;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.modal-content {
|
| 330 |
+
background: white;
|
| 331 |
+
border-radius: 12px;
|
| 332 |
+
width: 90%;
|
| 333 |
+
max-width: 600px;
|
| 334 |
+
max-height: 90vh;
|
| 335 |
+
overflow-y: auto;
|
| 336 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.modal-header {
|
| 340 |
+
padding: 24px;
|
| 341 |
+
border-bottom: 1px solid #e0e0e0;
|
| 342 |
+
display: flex;
|
| 343 |
+
justify-content: space-between;
|
| 344 |
+
align-items: center;
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
.modal-header h2 {
|
| 348 |
+
color: #333;
|
| 349 |
+
font-size: 20px;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.modal-close {
|
| 353 |
+
background: none;
|
| 354 |
+
border: none;
|
| 355 |
+
font-size: 28px;
|
| 356 |
+
color: #999;
|
| 357 |
+
cursor: pointer;
|
| 358 |
+
padding: 0;
|
| 359 |
+
width: 32px;
|
| 360 |
+
height: 32px;
|
| 361 |
+
display: flex;
|
| 362 |
+
align-items: center;
|
| 363 |
+
justify-content: center;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.modal-close:hover {
|
| 367 |
+
color: #333;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.modal-content form {
|
| 371 |
+
padding: 24px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.modal-footer {
|
| 375 |
+
display: flex;
|
| 376 |
+
justify-content: flex-end;
|
| 377 |
+
gap: 12px;
|
| 378 |
+
margin-top: 24px;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* Toast 通知 */
|
| 382 |
+
.toast {
|
| 383 |
+
position: fixed;
|
| 384 |
+
bottom: 30px;
|
| 385 |
+
right: 30px;
|
| 386 |
+
background: #333;
|
| 387 |
+
color: white;
|
| 388 |
+
padding: 16px 24px;
|
| 389 |
+
border-radius: 8px;
|
| 390 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 391 |
+
z-index: 2000;
|
| 392 |
+
animation: slideIn 0.3s ease-out;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.toast.success {
|
| 396 |
+
background: #2ecc71;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.toast.error {
|
| 400 |
+
background: #ff4757;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
@keyframes slideIn {
|
| 404 |
+
from {
|
| 405 |
+
transform: translateX(400px);
|
| 406 |
+
opacity: 0;
|
| 407 |
+
}
|
| 408 |
+
to {
|
| 409 |
+
transform: translateX(0);
|
| 410 |
+
opacity: 1;
|
| 411 |
+
}
|
| 412 |
+
}
|
| 413 |
+
|
| 414 |
+
/* 响应式设计 */
|
| 415 |
+
@media (max-width: 768px) {
|
| 416 |
+
.header {
|
| 417 |
+
padding: 15px 20px;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.header h1 {
|
| 421 |
+
font-size: 20px;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.main-content {
|
| 425 |
+
padding: 20px 15px;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.stats-grid {
|
| 429 |
+
grid-template-columns: 1fr;
|
| 430 |
+
}
|
| 431 |
+
|
| 432 |
+
.actions-section {
|
| 433 |
+
flex-direction: column;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.actions-section .btn {
|
| 437 |
+
width: 100%;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
table {
|
| 441 |
+
font-size: 14px;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
th, td {
|
| 445 |
+
padding: 8px;
|
| 446 |
+
}
|
| 447 |
+
}
|