first commit
This commit is contained in:
50
.gitignore
vendored
Normal file
50
.gitignore
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
frontend/node_modules/
|
||||||
|
package-lock.json
|
||||||
|
frontend/package-lock.json
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
frontend/dist/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Database
|
||||||
|
prisma/dev.db
|
||||||
|
prisma/dev.db-journal
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
175
README.md
Normal file
175
README.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# SiliconFlow API Key 验证器
|
||||||
|
|
||||||
|
这是一个全栈应用程序,用于验证和管理 SiliconFlow API Key,实时监控账户余额状态。
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
- 🔐 **API Key 管理**: 安全地存储和管理多个API Key
|
||||||
|
- ✅ **实时验证**: 批量或单独验证API Key有效性
|
||||||
|
- 💰 **余额监控**: 查看实时余额信息
|
||||||
|
- 📊 **统计面板**: 一目了然的账户状态统计
|
||||||
|
- 🎨 **现代界面**: 美观易用的用户界面
|
||||||
|
- 🔒 **数据安全**: 本地数据库存储,确保数据私密性
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 1️⃣ 克隆和安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆仓库
|
||||||
|
git clone [your-repo-url]
|
||||||
|
cd siliconflow-api-key-validator
|
||||||
|
|
||||||
|
# 安装后端依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 安装前端依赖
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 数据库初始化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建数据库
|
||||||
|
npx prisma generate
|
||||||
|
npx prisma migrate dev --name init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ 启动应用
|
||||||
|
|
||||||
|
#### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 同时启动后端和前端开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 或者分别启动
|
||||||
|
npm run server:dev # 后端 (端口3000)
|
||||||
|
cd frontend && npm run dev # 前端 (端口3001)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 生产模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 启动生产服务器
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 访问应用
|
||||||
|
|
||||||
|
- **首页**: http://localhost:3001
|
||||||
|
- **API 地址**: http://localhost:3000/api
|
||||||
|
- **健康检查**: http://localhost:3000/health
|
||||||
|
|
||||||
|
## 📋 API 端点
|
||||||
|
|
||||||
|
### API Keys 管理
|
||||||
|
|
||||||
|
- `GET /api/api-keys` - 获取所有API Key
|
||||||
|
- `POST /api/api-keys` - 添加新API Key
|
||||||
|
- `POST /api/api-keys/:id/validate` - 验证单个API Key
|
||||||
|
- `POST /api/api-keys/validate-all` - 批量验证所有API Key
|
||||||
|
- `DELETE /api/api-keys/:id` - 删除API Key
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **Node.js** + **Express**
|
||||||
|
- **TypeScript** - 类型安全
|
||||||
|
- **Prisma** - 数据库ORM
|
||||||
|
- **SQLite** - 轻量级数据库
|
||||||
|
- **Axios** - HTTP客户端
|
||||||
|
- **CORS** + **Helmet** + **Compression** - 安全与性能优化
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **React 18** + **TypeScript**
|
||||||
|
- **Vite** - 现代构建工具
|
||||||
|
- **TailwindCSS** - 样式框架
|
||||||
|
- **Axios** - HTTP客户端
|
||||||
|
- **Lucide React** - 图标库
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
siliconflow-api-key-validator/
|
||||||
|
├── src/
|
||||||
|
│ ├── config/
|
||||||
|
│ │ └── siliconflow.ts # SiliconFlow API 配置和验证逻辑
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── apiKeyService.ts # 业务逻辑服务
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ └── apiKeys.ts # API 路由定义
|
||||||
|
│ ├── utils/
|
||||||
|
│ │ └── errors.ts # 自定义错误类
|
||||||
|
│ └── server.ts # Express 服务器入口
|
||||||
|
├── frontend/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React 组件
|
||||||
|
│ │ ├── services/ # 前端 API 服务
|
||||||
|
│ │ ├── types/ # TypeScript 类型定义
|
||||||
|
│ │ ├── utils/ # 工具函数
|
||||||
|
│ │ └── App.tsx # 主应用组件
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── vite.config.ts
|
||||||
|
│ └── tailwind.config.js
|
||||||
|
├── prisma/
|
||||||
|
│ └── schema.prisma # 数据模型定义
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 环境变量配置
|
||||||
|
|
||||||
|
### .env
|
||||||
|
```env
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
API_TIMEOUT=10000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 使用说明
|
||||||
|
|
||||||
|
### 添加API Key
|
||||||
|
1. 在主页的"添加新API Key"表单中输入:
|
||||||
|
- API Key名称(如"生产环境")
|
||||||
|
- 实际的API Key
|
||||||
|
- 可选描述信息
|
||||||
|
|
||||||
|
### 验证API Key
|
||||||
|
- **单独验证**: 点击卡片右上角的刷新图标
|
||||||
|
- **批量验证**: 使用页面顶部"批量验证"按钮
|
||||||
|
|
||||||
|
### 查看账户信息
|
||||||
|
- **状态**: 绿色表示有效,红色表示无效
|
||||||
|
- **余额**: 显示实时账户余额
|
||||||
|
- **用户信息**: 显示关联的账户邮箱和名称
|
||||||
|
- **验证统计**: 显示验证次数和最后验证时间
|
||||||
|
|
||||||
|
## 🐛 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **端口占用**: 确保3000和3001端口未被占用
|
||||||
|
2. **数据库连接**: 检查`prisma/dev.db`文件是否存在
|
||||||
|
3. **构建错误**: 确保所有依赖已安装: `npm install` 和 `cd frontend && npm install`
|
||||||
|
4. **API验证失败**: 检查网络连接和API Key格式
|
||||||
|
|
||||||
|
### 重置数据
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除数据库重新初始化
|
||||||
|
rm prisma/dev.db
|
||||||
|
npx prisma migrate dev --name init
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 贡献
|
||||||
|
|
||||||
|
欢迎提交Issue和Pull Request!
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License - 详见 LICENSE 文件
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>SiliconFlow API Key 验证器</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
frontend/package.json
Normal file
31
frontend/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "siliconflow-api-key-validator-frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"lucide-react": "^0.292.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"tailwind-merge": "^2.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.24",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
|
"autoprefixer": "^10.4.16",
|
||||||
|
"postcss": "^8.4.32",
|
||||||
|
"tailwindcss": "^3.3.6",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.4.19"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
145
frontend/src/App.tsx
Normal file
145
frontend/src/App.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
|
import { ApiKeyForm } from './components/ApiKeyForm';
|
||||||
|
import { ApiKeyCard } from './components/ApiKeyCard';
|
||||||
|
import { Stats } from './components/Stats';
|
||||||
|
import { apiKeyService } from './services/api';
|
||||||
|
import type { ApiKey } from './types/api';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isValidatingAll, setIsValidatingAll] = useState(false);
|
||||||
|
|
||||||
|
const loadApiKeys = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await apiKeyService.getAllApiKeys();
|
||||||
|
setApiKeys(data);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '加载失败');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadApiKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddSuccess = async () => {
|
||||||
|
await loadApiKeys();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = (id: string, updated: ApiKey) => {
|
||||||
|
setApiKeys(prev => prev.map(key => key.id === id ? updated : key));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
setApiKeys(prev => prev.filter(key => key.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidateAll = async () => {
|
||||||
|
setIsValidatingAll(true);
|
||||||
|
try {
|
||||||
|
const results = await apiKeyService.validateAllApiKeys();
|
||||||
|
setApiKeys(results);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '批量验证失败');
|
||||||
|
} finally {
|
||||||
|
setIsValidatingAll(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
|
SiliconFlow API Key 管理器
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
管理和验证您的 SiliconFlow API Key,实时监控账户状态和余额。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||||
|
<p className="mt-2 text-gray-600">加载中...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
|
||||||
|
<div className="flex">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-400 mr-3" />
|
||||||
|
<div className="text-sm text-red-700">
|
||||||
|
<p className="font-medium">加载失败</p>
|
||||||
|
<p>{error}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Stats apiKeys={apiKeys} />
|
||||||
|
|
||||||
|
<div className="mb-6 flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
API Keys ({apiKeys.length})
|
||||||
|
</h2>
|
||||||
|
{apiKeys.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={handleValidateAll}
|
||||||
|
disabled={isValidatingAll}
|
||||||
|
className="flex items-center px-3 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${isValidatingAll ? 'animate-spin' : ''}`} />
|
||||||
|
批量验证
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apiKeys.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="text-gray-400 mb-4">
|
||||||
|
<svg className="h-12 w-12 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无 API Key</h3>
|
||||||
|
<p className="text-gray-600 mb-4">添加您的第一个API Key开始使用</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4 sm:gap-6 grid-cols-1 lg:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{apiKeys.map((apiKey) => (
|
||||||
|
<ApiKeyCard
|
||||||
|
key={apiKey.id}
|
||||||
|
apiKey={apiKey}
|
||||||
|
onUpdate={handleUpdate}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<div className="px-4 py-5 sm:p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
||||||
|
添加新 API Key
|
||||||
|
</h3>
|
||||||
|
<ApiKeyForm onSuccess={handleAddSuccess} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
147
frontend/src/components/ApiKeyCard.tsx
Normal file
147
frontend/src/components/ApiKeyCard.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { CheckCircle2, XCircle, Loader2, Trash2, RefreshCw } from 'lucide-react';
|
||||||
|
import { apiKeyService } from '../services/api';
|
||||||
|
import type { ApiKey } from '../types/api';
|
||||||
|
|
||||||
|
interface ApiKeyCardProps {
|
||||||
|
apiKey: ApiKey;
|
||||||
|
onUpdate: (id: string, updated: ApiKey) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyCard({ apiKey, onUpdate, onDelete }: ApiKeyCardProps) {
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const handleValidate = async () => {
|
||||||
|
setIsValidating(true);
|
||||||
|
try {
|
||||||
|
const updated = await apiKeyService.validateApiKey(apiKey.id);
|
||||||
|
onUpdate(apiKey.id, updated);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证失败:', error);
|
||||||
|
} finally {
|
||||||
|
setIsValidating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!confirm("确定要删除这个API Key吗?此操作不可恢复。")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await apiKeyService.deleteApiKey(apiKey.id);
|
||||||
|
onDelete(apiKey.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number) => {
|
||||||
|
return new Intl.NumberFormat('zh-CN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'CNY',
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleString('zh-CN');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-gray-200 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-start mb-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 truncate">
|
||||||
|
{apiKey.name}
|
||||||
|
</h3>
|
||||||
|
{apiKey.description && (
|
||||||
|
<p className="mt-1 text-sm text-gray-600 line-clamp-2">
|
||||||
|
{apiKey.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center ml-4">
|
||||||
|
<button
|
||||||
|
onClick={handleValidate}
|
||||||
|
disabled={isValidating}
|
||||||
|
className="p-2 mr-2 text-blue-600 hover:bg-blue-50 rounded-md disabled:opacity-50"
|
||||||
|
title="验证API Key"
|
||||||
|
>
|
||||||
|
<RefreshCw className={`w-4 h-4 ${isValidating ? 'animate-spin' : ''}`} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="p-2 text-red-600 hover:bg-red-50 rounded-md disabled:opacity-50"
|
||||||
|
title="删除API Key"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center mb-3">
|
||||||
|
{apiKey.isValid ? (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||||
|
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||||
|
有效
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||||
|
<XCircle className="w-3 h-3 mr-1" />
|
||||||
|
无效
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 直接显示完整的API Key */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">API Key:</label>
|
||||||
|
<div className="p-2 bg-gray-50 border border-gray-200 rounded-md text-xs font-mono text-gray-600 break-all">
|
||||||
|
{apiKey.key}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{apiKey.error ? (
|
||||||
|
<div className="mb-3 p-2 bg-red-50 border border-red-200 rounded-md">
|
||||||
|
<p className="text-sm text-red-600">{apiKey.error}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{apiKey.balance !== undefined && (
|
||||||
|
<div className="mb-3 p-3 bg-gray-50 rounded-md">
|
||||||
|
<p className="text-sm text-gray-600 mb-1">
|
||||||
|
账户余额:
|
||||||
|
<span className="font-semibold text-green-600 ml-1">
|
||||||
|
{formatCurrency(apiKey.balance)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{apiKey.user && (
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
用户: {apiKey.user.name} ({apiKey.user.email})
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500">
|
||||||
|
<span>
|
||||||
|
最后验证: {formatDate(apiKey.lastChecked)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
验证次数: {apiKey.checkCount}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
frontend/src/components/ApiKeyForm.tsx
Normal file
105
frontend/src/components/ApiKeyForm.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Plus, Loader2 } from 'lucide-react';
|
||||||
|
import { apiKeyService } from '../services/api';
|
||||||
|
import { cn } from '../utils/cn';
|
||||||
|
|
||||||
|
interface ApiKeyFormProps {
|
||||||
|
onSuccess: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApiKeyForm({ onSuccess }: ApiKeyFormProps) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
key: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiKeyService.createApiKey(formData);
|
||||||
|
setFormData({ name: '', key: '', description: '' });
|
||||||
|
onSuccess();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : '未知错误');
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
API Key 名称 *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="例如: 生产环境 API"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
API Key *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={formData.key}
|
||||||
|
onChange={(e) => setFormData({ ...formData, key: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm"
|
||||||
|
placeholder="sk-... 或sf-xxxxx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-2">
|
||||||
|
描述 (可选)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
rows={2}
|
||||||
|
placeholder="用于管理此API Key的描述信息"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="px-3 py-2 bg-red-100 border border-red-300 rounded-md text-red-700 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={cn(
|
||||||
|
"w-full flex items-center justify-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
添加中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
添加 API Key
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
frontend/src/components/Stats.tsx
Normal file
72
frontend/src/components/Stats.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { CheckCircle, XCircle, DollarSign, Zap } from 'lucide-react';
|
||||||
|
import type { ApiKey } from '../types/api';
|
||||||
|
|
||||||
|
interface StatsProps {
|
||||||
|
apiKeys: ApiKey[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Stats({ apiKeys }: StatsProps) {
|
||||||
|
const totalCount = apiKeys.length;
|
||||||
|
const validCount = apiKeys.filter(key => key.isValid).length;
|
||||||
|
const invalidCount = totalCount - validCount;
|
||||||
|
const totalBalance = apiKeys.reduce((sum, key) => sum + (key.balance || 0), 0);
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
name: '总 API Key',
|
||||||
|
value: totalCount,
|
||||||
|
icon: Zap,
|
||||||
|
color: 'text-gray-600',
|
||||||
|
bgColor: 'bg-gray-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '有效',
|
||||||
|
value: validCount,
|
||||||
|
icon: CheckCircle,
|
||||||
|
color: 'text-green-600',
|
||||||
|
bgColor: 'bg-green-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '无效',
|
||||||
|
value: invalidCount,
|
||||||
|
icon: XCircle,
|
||||||
|
color: 'text-red-600',
|
||||||
|
bgColor: 'bg-red-50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '总余额',
|
||||||
|
value: `$${totalBalance.toFixed(2)}`,
|
||||||
|
icon: DollarSign,
|
||||||
|
color: 'text-blue-600',
|
||||||
|
bgColor: 'bg-blue-50',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-8">
|
||||||
|
{stats.map((stat) => {
|
||||||
|
const Icon = stat.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={stat.name}
|
||||||
|
className="relative overflow-hidden rounded-lg bg-white px-4 py-5 shadow border-l-4 hover:shadow-md transition-shadow"
|
||||||
|
style={{ borderColor: stat.color.replace('text-', '').split('-')[1] ? `#${stat.color.replace('text-', '').split('-')[1]}-500` : 'gray' }}
|
||||||
|
>
|
||||||
|
<dt>
|
||||||
|
<div className={`absolute rounded-md p-3 ${stat.bgColor}`}>
|
||||||
|
<Icon className={`h-6 w-6 ${stat.color}`} aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<p className="ml-16 truncate text-sm font-medium text-gray-500">
|
||||||
|
{stat.name}
|
||||||
|
</p>
|
||||||
|
</dt>
|
||||||
|
<dd className="ml-16 flex items-baseline">
|
||||||
|
<p className="text-2xl font-semibold text-gray-900">{stat.value}</p>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
frontend/src/index.css
Normal file
53
frontend/src/index.css
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--primary: 221.2 83.2% 53.3%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96%;
|
||||||
|
--secondary-foreground: 222.2 84% 4.9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--muted: 210 40% 96%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96%;
|
||||||
|
--accent-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--primary: 217.2 91.2% 59.8%;
|
||||||
|
--primary-foreground: 222.2 84% 4.9%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.tsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
56
frontend/src/services/api.ts
Normal file
56
frontend/src/services/api.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import type { ApiKey, ApiResponse, CreateApiKeyRequest } from '../types/api';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: '/api',
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
const message = error.response?.data?.error || error.message || '未知错误';
|
||||||
|
return Promise.reject(new Error(message));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const apiKeyService = {
|
||||||
|
async getAllApiKeys(): Promise<ApiKey[]> {
|
||||||
|
const response = await api.get<ApiResponse<ApiKey[]>>('/api-keys');
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.error || '获取API Key失败');
|
||||||
|
}
|
||||||
|
return response.data.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async createApiKey(data: CreateApiKeyRequest): Promise<ApiKey> {
|
||||||
|
const response = await api.post<ApiResponse<ApiKey>>('/api-keys', data);
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.error || '创建API Key失败');
|
||||||
|
}
|
||||||
|
return response.data.data!;
|
||||||
|
},
|
||||||
|
|
||||||
|
async validateApiKey(id: string): Promise<ApiKey> {
|
||||||
|
const response = await api.post<ApiResponse<ApiKey>>(`/api-keys/${id}/validate`);
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.error || '验证API Key失败');
|
||||||
|
}
|
||||||
|
return response.data.data!;
|
||||||
|
},
|
||||||
|
|
||||||
|
async validateAllApiKeys(): Promise<ApiKey[]> {
|
||||||
|
const response = await api.post<ApiResponse<ApiKey[]>>('/api-keys/validate-all');
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.error || '批量验证失败');
|
||||||
|
}
|
||||||
|
return response.data.data || [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteApiKey(id: string): Promise<void> {
|
||||||
|
const response = await api.delete<ApiResponse<never>>(`/api-keys/${id}`);
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw new Error(response.data.error || '删除API Key失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
29
frontend/src/types/api.ts
Normal file
29
frontend/src/types/api.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface ApiKey {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description?: string;
|
||||||
|
isValid: boolean;
|
||||||
|
balance?: number;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
lastChecked: string;
|
||||||
|
checkCount: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateApiKeyRequest {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
6
frontend/src/utils/cn.ts
Normal file
6
frontend/src/utils/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
45
frontend/tailwind.config.js
Normal file
45
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
21
frontend/tsconfig.json
Normal file
21
frontend/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
19
frontend/vite.config.ts
Normal file
19
frontend/vite.config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
outDir: 'dist'
|
||||||
|
}
|
||||||
|
})
|
||||||
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "siliconflow-api-key-validator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A full-stack application to validate and manage SiliconFlow API keys",
|
||||||
|
"main": "dist/server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently \"npm run server:dev\" \"npm run client:dev\"",
|
||||||
|
"server:dev": "npx tsx src/server.ts",
|
||||||
|
"client:dev": "cd frontend && npm run dev",
|
||||||
|
"build": "npm run build:server && npm run build:client",
|
||||||
|
"build:server": "tsc -p tsconfig.server.json",
|
||||||
|
"build:client": "cd frontend && npm run build",
|
||||||
|
"start": "node dist/server.js",
|
||||||
|
"migrate": "prisma migrate deploy"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"api-key",
|
||||||
|
"validator",
|
||||||
|
"siliconflow"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.15.0",
|
||||||
|
"axios": "^1.6.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"helmet": "^7.1.0",
|
||||||
|
"http-status-codes": "^2.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"prisma": "^6.15.0",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
prisma/migrations/20250905094912_init/migration.sql
Normal file
17
prisma/migrations/20250905094912_init/migration.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "api_keys" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"key" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"isValid" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"balance" REAL,
|
||||||
|
"currency" TEXT,
|
||||||
|
"lastChecked" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"checkCount" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "api_keys_key_key" ON "api_keys"("key");
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "api_keys" DROP COLUMN "currency";
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "sqlite"
|
||||||
27
prisma/schema.prisma
Normal file
27
prisma/schema.prisma
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// This is your Prisma schema file,
|
||||||
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "sqlite"
|
||||||
|
url = "file:./dev.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
model ApiKey {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String
|
||||||
|
key String @unique
|
||||||
|
description String?
|
||||||
|
isValid Boolean @default(true)
|
||||||
|
balance Float? // 当前余额
|
||||||
|
// 删除 currency 字段
|
||||||
|
lastChecked DateTime @default(now())
|
||||||
|
checkCount Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@map("api_keys")
|
||||||
|
}
|
||||||
22
quick-start.bat
Normal file
22
quick-start.bat
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@echo off
|
||||||
|
echo 🚀 SiliconFlow API Key 验证器快速启动...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM 启动后端
|
||||||
|
start cmd /k "echo 启动后端服务器... && npx tsx src/server.ts"
|
||||||
|
|
||||||
|
REM 等待2秒让后端启动
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
|
||||||
|
REM 启动前端
|
||||||
|
cd frontend
|
||||||
|
start cmd /k "echo 启动前端服务器... && npm run dev"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 🌐 应用已启动:
|
||||||
|
echo Frontend: http://localhost:3001
|
||||||
|
echo Backend API: http://localhost:3000/api
|
||||||
|
echo Health Check: http://localhost:3000/health
|
||||||
|
echo.
|
||||||
|
echo 按任意键退出...
|
||||||
|
pause >nul
|
||||||
115
src/config/siliconflow.ts
Normal file
115
src/config/siliconflow.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
export interface SiliconFlowResponse {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
status: boolean;
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
email: string;
|
||||||
|
isAdmin: boolean;
|
||||||
|
balance: string;
|
||||||
|
status: string;
|
||||||
|
introduction: string;
|
||||||
|
role: string;
|
||||||
|
chargeBalance: string;
|
||||||
|
totalBalance: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
isValid: boolean;
|
||||||
|
balance?: number | null;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SiliconFlowValidator {
|
||||||
|
private static readonly BASE_URL = 'https://api.siliconflow.cn/v1';
|
||||||
|
private static readonly ENDPOINT = '/user/info';
|
||||||
|
|
||||||
|
static async validateApiKey(apiKey: string): Promise<ValidationResult> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.BASE_URL}${this.ENDPOINT}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: '无效的 API Key'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorText = await response.text();
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: `API 错误: ${response.status} - ${errorText}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json() as SiliconFlowResponse;
|
||||||
|
|
||||||
|
// 验证响应结构
|
||||||
|
if (!responseData || typeof responseData !== 'object') {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: '无效的响应格式'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 API 返回状态
|
||||||
|
if (responseData.code !== 20000 || !responseData.status) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: responseData.message || '验证失败'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数据结构
|
||||||
|
if (!responseData.data || typeof responseData.data !== 'object') {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: '缺失用户数据'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = responseData;
|
||||||
|
|
||||||
|
// 将字符串余额转换为数字
|
||||||
|
const balance = parseFloat(data.balance);
|
||||||
|
if (isNaN(balance)) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: '无效的用户余额格式'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
balance: balance,
|
||||||
|
user: {
|
||||||
|
id: data.id,
|
||||||
|
email: data.email,
|
||||||
|
name: data.name,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
error: `网络连接失败: ${errorMessage}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/routes/apiKeys.ts
Normal file
113
src/routes/apiKeys.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { StatusCodes } from 'http-status-codes';
|
||||||
|
import { ApiKeyService } from '../services/apiKeyService.js';
|
||||||
|
import { ApiValidationError } from '../utils/errors.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
// 获取所有API Keys
|
||||||
|
router.get('/api-keys', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const apiKeys = await ApiKeyService.getAllApiKeys();
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: apiKeys,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '获取API Key列表失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建新的API Key
|
||||||
|
router.post('/api-keys', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, key, description } = req.body;
|
||||||
|
|
||||||
|
if (!name || !key) {
|
||||||
|
throw new ApiValidationError('名称和API Key是必需字段');
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKey = await ApiKeyService.createApiKey({
|
||||||
|
name,
|
||||||
|
key,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(StatusCodes.CREATED).json({
|
||||||
|
success: true,
|
||||||
|
data: apiKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiValidationError) {
|
||||||
|
res.status(StatusCodes.BAD_REQUEST).json({
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '创建API Key失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证单个API Key
|
||||||
|
router.post('/api-keys/:id/validate', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const apiKey = await ApiKeyService.validateApiKey(id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: apiKey,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(error instanceof Error && error.message === 'API Key 未找到'
|
||||||
|
? StatusCodes.NOT_FOUND
|
||||||
|
: StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '验证API Key失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证所有API Keys
|
||||||
|
router.post('/api-keys/validate-all', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const results = await ApiKeyService.validateAll();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: results,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '批量验证失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 删除API Key
|
||||||
|
router.delete('/api-keys/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
await ApiKeyService.deleteApiKey(id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'API Key 删除成功',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '删除API Key失败',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
55
src/server.ts
Normal file
55
src/server.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import compression from 'compression';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import apiKeysRouter from './routes/apiKeys.js';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// 中间件
|
||||||
|
app.use(helmet());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(compression());
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// API路由
|
||||||
|
app.use('/api', apiKeysRouter);
|
||||||
|
|
||||||
|
// 健康检查
|
||||||
|
app.get('/health', (req, res) => {
|
||||||
|
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// 静态文件服务 (用于生产环境)
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
app.use(express.static(join(__dirname, '../frontend/dist')));
|
||||||
|
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(join(__dirname, '../frontend/dist/index.html'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局错误处理
|
||||||
|
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: '服务器内部错误',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 服务器运行在端口 ${PORT}`);
|
||||||
|
console.log(`📡 API地址: http://localhost:${PORT}/api`);
|
||||||
|
console.log(`🔍 健康检查: http://localhost:${PORT}/health`);
|
||||||
|
});
|
||||||
140
src/services/apiKeyService.ts
Normal file
140
src/services/apiKeyService.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { SiliconFlowValidator } from '../config/siliconflow.js';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export interface ApiKeyInput {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiKeyWithValidation extends ApiKeyInput {
|
||||||
|
id: string;
|
||||||
|
isValid: boolean;
|
||||||
|
balance?: number | null;
|
||||||
|
user?: {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
lastChecked: Date;
|
||||||
|
checkCount: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiKeyService {
|
||||||
|
static async createApiKey(input: ApiKeyInput): Promise<ApiKeyWithValidation> {
|
||||||
|
// 验证API Key
|
||||||
|
const validation = await SiliconFlowValidator.validateApiKey(input.key);
|
||||||
|
|
||||||
|
const apiKey = await prisma.apiKey.create({
|
||||||
|
data: {
|
||||||
|
name: input.name,
|
||||||
|
key: input.key,
|
||||||
|
description: input.description ?? '',
|
||||||
|
isValid: validation.isValid,
|
||||||
|
balance: validation.balance ?? 0,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
checkCount: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...apiKey,
|
||||||
|
|
||||||
|
key: apiKey.key, // 直接传输真实key
|
||||||
|
error: validation.error || undefined,
|
||||||
|
user: validation.user || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getAllApiKeys(): Promise<ApiKeyWithValidation[]> {
|
||||||
|
const apiKeys = await prisma.apiKey.findMany({
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 直接传输原始key值,不再隐藏
|
||||||
|
return apiKeys.map((key) => ({
|
||||||
|
id: key.id,
|
||||||
|
name: key.name,
|
||||||
|
description: key.description,
|
||||||
|
isValid: key.isValid,
|
||||||
|
balance: key.balance,
|
||||||
|
lastChecked: key.lastChecked,
|
||||||
|
checkCount: key.checkCount,
|
||||||
|
|
||||||
|
key: key.key, // 直接传输真实key
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validateApiKey(id: string): Promise<ApiKeyWithValidation> {
|
||||||
|
const apiKey = await prisma.apiKey.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error('API Key 未找到');
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await SiliconFlowValidator.validateApiKey(apiKey.key);
|
||||||
|
|
||||||
|
const updated = await prisma.apiKey.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
isValid: validation.isValid,
|
||||||
|
balance: validation.balance ?? 0,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
checkCount: { increment: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updated,
|
||||||
|
|
||||||
|
key: updated.key, // 直接传输真实key
|
||||||
|
error: validation.error || undefined,
|
||||||
|
user: validation.user || null,
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static async deleteApiKey(id: string): Promise<void> {
|
||||||
|
await prisma.apiKey.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async validateAll(): Promise<ApiKeyWithValidation[]> {
|
||||||
|
const apiKeys = await prisma.apiKey.findMany();
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
|
||||||
|
apiKeys.map(async (key) => {
|
||||||
|
const validation = await SiliconFlowValidator.validateApiKey(key.key);
|
||||||
|
|
||||||
|
const updated = await prisma.apiKey.update({
|
||||||
|
where: { id: key.id },
|
||||||
|
data: {
|
||||||
|
isValid: validation.isValid,
|
||||||
|
balance: validation.balance ?? 0,
|
||||||
|
lastChecked: new Date(),
|
||||||
|
checkCount: { increment: 1 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...updated,
|
||||||
|
key: updated.key, // 直接传输真实key
|
||||||
|
error: validation.error || undefined,
|
||||||
|
user: validation.user || null,
|
||||||
|
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/utils/errors.ts
Normal file
13
src/utils/errors.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export class ApiValidationError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApiNotFoundError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiNotFoundError';
|
||||||
|
}
|
||||||
|
}
|
||||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "frontend"]
|
||||||
|
}
|
||||||
9
tsconfig.server.json
Normal file
9
tsconfig.server.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"declaration": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["frontend/**/*"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user