Spaces:
Sleeping
Sleeping
Upload 19 files
Browse files- Dockerfile +23 -0
- config/auth.js +12 -0
- config/db.js +71 -0
- controllers/authController.js +83 -0
- controllers/categoryController.js +113 -0
- controllers/promptGroupController.js +246 -0
- middleware/auth.js +49 -0
- middleware/errorHandler.js +16 -0
- models/Category.js +23 -0
- models/PromptGroup.js +81 -0
- models/User.js +44 -0
- package.json +29 -0
- routes/authRoutes.js +15 -0
- routes/categoryRoutes.js +21 -0
- routes/index.js +12 -0
- routes/promptGroupRoutes.js +42 -0
- server.js +56 -0
- utils/logger.js +35 -0
- utils/validators.js +42 -0
Dockerfile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:18-alpine
|
| 2 |
+
|
| 3 |
+
# 创建工作目录
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# 复制 package.json 和 package-lock.json
|
| 7 |
+
COPY package*.json ./
|
| 8 |
+
|
| 9 |
+
# 安装依赖
|
| 10 |
+
RUN npm install --production
|
| 11 |
+
|
| 12 |
+
# 复制所有项目文件
|
| 13 |
+
COPY . .
|
| 14 |
+
|
| 15 |
+
# 暴露端口
|
| 16 |
+
EXPOSE 8080
|
| 17 |
+
|
| 18 |
+
# 设置环境变量
|
| 19 |
+
ENV PORT=8080
|
| 20 |
+
ENV NODE_ENV=production
|
| 21 |
+
|
| 22 |
+
# 启动应用
|
| 23 |
+
CMD ["npm", "start"]
|
config/auth.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const jwt = require('jsonwebtoken');
|
| 2 |
+
|
| 3 |
+
// 生成 JWT Token
|
| 4 |
+
const generateToken = (userId) => {
|
| 5 |
+
return jwt.sign(
|
| 6 |
+
{ id: userId },
|
| 7 |
+
process.env.JWT_SECRET,
|
| 8 |
+
{ expiresIn: process.env.JWT_EXPIRE || '24h' }
|
| 9 |
+
);
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
module.exports = { generateToken };
|
config/db.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mongoose = require('mongoose');
|
| 2 |
+
const logger = require('../utils/logger');
|
| 3 |
+
|
| 4 |
+
const connectDB = async () => {
|
| 5 |
+
try {
|
| 6 |
+
const conn = await mongoose.connect(process.env.MONGODB_URI, {
|
| 7 |
+
useNewUrlParser: true,
|
| 8 |
+
useUnifiedTopology: true,
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
logger.info(`MongoDB Connected: ${conn.connection.host}`);
|
| 12 |
+
|
| 13 |
+
// 创建默认管理员用户和默认分类
|
| 14 |
+
await createDefaultAdmin();
|
| 15 |
+
await createDefaultCategories();
|
| 16 |
+
} catch (error) {
|
| 17 |
+
logger.error(`Error connecting to MongoDB: ${error.message}`);
|
| 18 |
+
process.exit(1);
|
| 19 |
+
}
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const createDefaultAdmin = async () => {
|
| 23 |
+
try {
|
| 24 |
+
const User = require('../models/User');
|
| 25 |
+
const bcrypt = require('bcryptjs');
|
| 26 |
+
|
| 27 |
+
// 检查是否已存在管理员用户
|
| 28 |
+
const adminExists = await User.findOne({ username: process.env.ADMIN_USERNAME });
|
| 29 |
+
|
| 30 |
+
if (!adminExists && process.env.ADMIN_USERNAME && process.env.ADMIN_PASSWORD) {
|
| 31 |
+
// 创建管理员用户
|
| 32 |
+
const hashedPassword = await bcrypt.hash(process.env.ADMIN_PASSWORD, 10);
|
| 33 |
+
|
| 34 |
+
await User.create({
|
| 35 |
+
username: process.env.ADMIN_USERNAME,
|
| 36 |
+
password: hashedPassword,
|
| 37 |
+
isAdmin: true
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
logger.info('Default admin user created');
|
| 41 |
+
}
|
| 42 |
+
} catch (error) {
|
| 43 |
+
logger.error(`Error creating default admin: ${error.message}`);
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
const createDefaultCategories = async () => {
|
| 48 |
+
try {
|
| 49 |
+
const Category = require('../models/Category');
|
| 50 |
+
|
| 51 |
+
// 检查是否已有分类
|
| 52 |
+
const categoriesCount = await Category.countDocuments();
|
| 53 |
+
|
| 54 |
+
if (categoriesCount === 0) {
|
| 55 |
+
// 创建默认分类
|
| 56 |
+
const defaultCategories = [
|
| 57 |
+
{ name: '通用', color: '#007AFF' },
|
| 58 |
+
{ name: '工作', color: '#FF9500' },
|
| 59 |
+
{ name: '学习', color: '#4CD964' },
|
| 60 |
+
{ name: '创意', color: '#FF2D55' }
|
| 61 |
+
];
|
| 62 |
+
|
| 63 |
+
await Category.insertMany(defaultCategories);
|
| 64 |
+
logger.info('Default categories created');
|
| 65 |
+
}
|
| 66 |
+
} catch (error) {
|
| 67 |
+
logger.error(`Error creating default categories: ${error.message}`);
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
module.exports = { connectDB };
|
controllers/authController.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const asyncHandler = require('express-async-handler');
|
| 2 |
+
const User = require('../models/User');
|
| 3 |
+
const { generateToken } = require('../config/auth');
|
| 4 |
+
|
| 5 |
+
// @desc 用户注册
|
| 6 |
+
// @route POST /api/auth/register
|
| 7 |
+
// @access Public
|
| 8 |
+
const registerUser = asyncHandler(async (req, res) => {
|
| 9 |
+
const { username, password } = req.body;
|
| 10 |
+
|
| 11 |
+
// 检查用户是否已存在
|
| 12 |
+
const userExists = await User.findOne({ username });
|
| 13 |
+
|
| 14 |
+
if (userExists) {
|
| 15 |
+
res.status(400);
|
| 16 |
+
throw new Error('用户已存在');
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
// 创建用户
|
| 20 |
+
const user = await User.create({
|
| 21 |
+
username,
|
| 22 |
+
password,
|
| 23 |
+
});
|
| 24 |
+
|
| 25 |
+
if (user) {
|
| 26 |
+
res.status(201).json({
|
| 27 |
+
_id: user._id,
|
| 28 |
+
username: user.username,
|
| 29 |
+
isAdmin: user.isAdmin,
|
| 30 |
+
token: generateToken(user._id),
|
| 31 |
+
});
|
| 32 |
+
} else {
|
| 33 |
+
res.status(400);
|
| 34 |
+
throw new Error('无效的用户数据');
|
| 35 |
+
}
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
// @desc 用户登录
|
| 39 |
+
// @route POST /api/auth/login
|
| 40 |
+
// @access Public
|
| 41 |
+
const loginUser = asyncHandler(async (req, res) => {
|
| 42 |
+
const { username, password } = req.body;
|
| 43 |
+
|
| 44 |
+
// 查找用户
|
| 45 |
+
const user = await User.findOne({ username });
|
| 46 |
+
|
| 47 |
+
// 检查用户和密码
|
| 48 |
+
if (user && (await user.matchPassword(password))) {
|
| 49 |
+
res.json({
|
| 50 |
+
_id: user._id,
|
| 51 |
+
username: user.username,
|
| 52 |
+
isAdmin: user.isAdmin,
|
| 53 |
+
token: generateToken(user._id),
|
| 54 |
+
});
|
| 55 |
+
} else {
|
| 56 |
+
res.status(401);
|
| 57 |
+
throw new Error('用户名或密码错误');
|
| 58 |
+
}
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// @desc 获取当前用户资料
|
| 62 |
+
// @route GET /api/auth/profile
|
| 63 |
+
// @access Private
|
| 64 |
+
const getUserProfile = asyncHandler(async (req, res) => {
|
| 65 |
+
const user = await User.findById(req.user._id).select('-password');
|
| 66 |
+
|
| 67 |
+
if (user) {
|
| 68 |
+
res.json({
|
| 69 |
+
_id: user._id,
|
| 70 |
+
username: user.username,
|
| 71 |
+
isAdmin: user.isAdmin,
|
| 72 |
+
});
|
| 73 |
+
} else {
|
| 74 |
+
res.status(404);
|
| 75 |
+
throw new Error('用户未找到');
|
| 76 |
+
}
|
| 77 |
+
});
|
| 78 |
+
|
| 79 |
+
module.exports = {
|
| 80 |
+
registerUser,
|
| 81 |
+
loginUser,
|
| 82 |
+
getUserProfile,
|
| 83 |
+
};
|
controllers/categoryController.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const asyncHandler = require('express-async-handler');
|
| 2 |
+
const Category = require('../models/Category');
|
| 3 |
+
const PromptGroup = require('../models/PromptGroup');
|
| 4 |
+
|
| 5 |
+
// @desc 获取所有分类
|
| 6 |
+
// @route GET /api/categories
|
| 7 |
+
// @access Private
|
| 8 |
+
const getCategories = asyncHandler(async (req, res) => {
|
| 9 |
+
const categories = await Category.find({}).sort({ name: 1 });
|
| 10 |
+
res.json(categories);
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
// @desc 通过ID获取分类
|
| 14 |
+
// @route GET /api/categories/:id
|
| 15 |
+
// @access Private
|
| 16 |
+
const getCategoryById = asyncHandler(async (req, res) => {
|
| 17 |
+
const category = await Category.findById(req.params.id);
|
| 18 |
+
|
| 19 |
+
if (category) {
|
| 20 |
+
res.json(category);
|
| 21 |
+
} else {
|
| 22 |
+
res.status(404);
|
| 23 |
+
throw new Error('分类未找到');
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// @desc 创建分类
|
| 28 |
+
// @route POST /api/categories
|
| 29 |
+
// @access Private
|
| 30 |
+
const createCategory = asyncHandler(async (req, res) => {
|
| 31 |
+
const { name, color } = req.body;
|
| 32 |
+
|
| 33 |
+
// 检查是否分类名称已存在
|
| 34 |
+
const categoryExists = await Category.findOne({ name });
|
| 35 |
+
|
| 36 |
+
if (categoryExists) {
|
| 37 |
+
res.status(400);
|
| 38 |
+
throw new Error('分类名称已存在');
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
const category = await Category.create({
|
| 42 |
+
name,
|
| 43 |
+
color: color || '#007AFF',
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
if (category) {
|
| 47 |
+
res.status(201).json(category);
|
| 48 |
+
} else {
|
| 49 |
+
res.status(400);
|
| 50 |
+
throw new Error('无效的分类数据');
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// @desc 更新分类
|
| 55 |
+
// @route PUT /api/categories/:id
|
| 56 |
+
// @access Private
|
| 57 |
+
const updateCategory = asyncHandler(async (req, res) => {
|
| 58 |
+
const { name, color } = req.body;
|
| 59 |
+
|
| 60 |
+
const category = await Category.findById(req.params.id);
|
| 61 |
+
|
| 62 |
+
if (category) {
|
| 63 |
+
// 如果更改了名称,检查新名称是否已存在
|
| 64 |
+
if (name && name !== category.name) {
|
| 65 |
+
const categoryExists = await Category.findOne({ name });
|
| 66 |
+
|
| 67 |
+
if (categoryExists) {
|
| 68 |
+
res.status(400);
|
| 69 |
+
throw new Error('分类名称已存在');
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
category.name = name || category.name;
|
| 74 |
+
category.color = color || category.color;
|
| 75 |
+
|
| 76 |
+
const updatedCategory = await category.save();
|
| 77 |
+
res.json(updatedCategory);
|
| 78 |
+
} else {
|
| 79 |
+
res.status(404);
|
| 80 |
+
throw new Error('分类未找到');
|
| 81 |
+
}
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
// @desc 删除分类
|
| 85 |
+
// @route DELETE /api/categories/:id
|
| 86 |
+
// @access Private
|
| 87 |
+
const deleteCategory = asyncHandler(async (req, res) => {
|
| 88 |
+
const category = await Category.findById(req.params.id);
|
| 89 |
+
|
| 90 |
+
if (!category) {
|
| 91 |
+
res.status(404);
|
| 92 |
+
throw new Error('分类未找到');
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
// 检查是否有提示词组使用该分类
|
| 96 |
+
const groupsUsingCategory = await PromptGroup.countDocuments({ category: req.params.id });
|
| 97 |
+
|
| 98 |
+
if (groupsUsingCategory > 0) {
|
| 99 |
+
res.status(400);
|
| 100 |
+
throw new Error(`无法删除分类,有 ${groupsUsingCategory} 个提示词组正在使用它`);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
await category.deleteOne();
|
| 104 |
+
res.json({ message: '分类已删除' });
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
module.exports = {
|
| 108 |
+
getCategories,
|
| 109 |
+
getCategoryById,
|
| 110 |
+
createCategory,
|
| 111 |
+
updateCategory,
|
| 112 |
+
deleteCategory,
|
| 113 |
+
};
|
controllers/promptGroupController.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const asyncHandler = require('express-async-handler');
|
| 2 |
+
const PromptGroup = require('../models/PromptGroup');
|
| 3 |
+
|
| 4 |
+
// @desc 获取所有提示词组
|
| 5 |
+
// @route GET /api/prompt-groups
|
| 6 |
+
// @access Private
|
| 7 |
+
const getPromptGroups = asyncHandler(async (req, res) => {
|
| 8 |
+
const promptGroups = await PromptGroup.find({})
|
| 9 |
+
.populate('category', 'name color')
|
| 10 |
+
.sort({ updatedAt: -1 });
|
| 11 |
+
|
| 12 |
+
res.json(promptGroups);
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
// @desc 获取单个提示词组
|
| 16 |
+
// @route GET /api/prompt-groups/:id
|
| 17 |
+
// @access Private
|
| 18 |
+
const getPromptGroupById = asyncHandler(async (req, res) => {
|
| 19 |
+
const promptGroup = await PromptGroup.findById(req.params.id)
|
| 20 |
+
.populate('category', 'name color');
|
| 21 |
+
|
| 22 |
+
if (promptGroup) {
|
| 23 |
+
res.json(promptGroup);
|
| 24 |
+
} else {
|
| 25 |
+
res.status(404);
|
| 26 |
+
throw new Error('提示词组未找到');
|
| 27 |
+
}
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
// @desc 创建提示词组
|
| 31 |
+
// @route POST /api/prompt-groups
|
| 32 |
+
// @access Private
|
| 33 |
+
const createPromptGroup = asyncHandler(async (req, res) => {
|
| 34 |
+
const { name, description, category } = req.body;
|
| 35 |
+
|
| 36 |
+
const promptGroup = await PromptGroup.create({
|
| 37 |
+
name,
|
| 38 |
+
description,
|
| 39 |
+
category,
|
| 40 |
+
prompts: [],
|
| 41 |
+
workflows: [],
|
| 42 |
+
dslFiles: [],
|
| 43 |
+
createdBy: req.user._id,
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
if (promptGroup) {
|
| 47 |
+
res.status(201).json(promptGroup);
|
| 48 |
+
} else {
|
| 49 |
+
res.status(400);
|
| 50 |
+
throw new Error('无效的提示词组数据');
|
| 51 |
+
}
|
| 52 |
+
});
|
| 53 |
+
|
| 54 |
+
// @desc 更新提示词组
|
| 55 |
+
// @route PUT /api/prompt-groups/:id
|
| 56 |
+
// @access Private
|
| 57 |
+
const updatePromptGroup = asyncHandler(async (req, res) => {
|
| 58 |
+
const { name, description, category } = req.body;
|
| 59 |
+
|
| 60 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 61 |
+
|
| 62 |
+
if (promptGroup) {
|
| 63 |
+
promptGroup.name = name || promptGroup.name;
|
| 64 |
+
promptGroup.description = description || promptGroup.description;
|
| 65 |
+
promptGroup.category = category || promptGroup.category;
|
| 66 |
+
|
| 67 |
+
const updatedPromptGroup = await promptGroup.save();
|
| 68 |
+
res.json(updatedPromptGroup);
|
| 69 |
+
} else {
|
| 70 |
+
res.status(404);
|
| 71 |
+
throw new Error('提示词组未找到');
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
// @desc 删除提示词组
|
| 76 |
+
// @route DELETE /api/prompt-groups/:id
|
| 77 |
+
// @access Private
|
| 78 |
+
const deletePromptGroup = asyncHandler(async (req, res) => {
|
| 79 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 80 |
+
|
| 81 |
+
if (promptGroup) {
|
| 82 |
+
await promptGroup.deleteOne();
|
| 83 |
+
res.json({ message: '提示词组已删除' });
|
| 84 |
+
} else {
|
| 85 |
+
res.status(404);
|
| 86 |
+
throw new Error('提示词组未找到');
|
| 87 |
+
}
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
// @desc 添加提示词到提示词组
|
| 91 |
+
// @route POST /api/prompt-groups/:id/prompts
|
| 92 |
+
// @access Private
|
| 93 |
+
const addPromptToGroup = asyncHandler(async (req, res) => {
|
| 94 |
+
const { title, content, tags } = req.body;
|
| 95 |
+
|
| 96 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 97 |
+
|
| 98 |
+
if (promptGroup) {
|
| 99 |
+
const newPrompt = {
|
| 100 |
+
title,
|
| 101 |
+
content,
|
| 102 |
+
tags: tags || [],
|
| 103 |
+
createdAt: new Date(),
|
| 104 |
+
updatedAt: new Date(),
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
promptGroup.prompts.push(newPrompt);
|
| 108 |
+
promptGroup.updatedAt = new Date();
|
| 109 |
+
|
| 110 |
+
const updatedPromptGroup = await promptGroup.save();
|
| 111 |
+
res.status(201).json(updatedPromptGroup.prompts[updatedPromptGroup.prompts.length - 1]);
|
| 112 |
+
} else {
|
| 113 |
+
res.status(404);
|
| 114 |
+
throw new Error('提示词组未找到');
|
| 115 |
+
}
|
| 116 |
+
});
|
| 117 |
+
|
| 118 |
+
// @desc 更新提示词
|
| 119 |
+
// @route PUT /api/prompt-groups/:id/prompts/:promptId
|
| 120 |
+
// @access Private
|
| 121 |
+
const updatePrompt = asyncHandler(async (req, res) => {
|
| 122 |
+
const { title, content, tags } = req.body;
|
| 123 |
+
|
| 124 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 125 |
+
|
| 126 |
+
if (promptGroup) {
|
| 127 |
+
const promptIndex = promptGroup.prompts.findIndex(
|
| 128 |
+
(p) => p._id.toString() === req.params.promptId
|
| 129 |
+
);
|
| 130 |
+
|
| 131 |
+
if (promptIndex !== -1) {
|
| 132 |
+
promptGroup.prompts[promptIndex].title = title || promptGroup.prompts[promptIndex].title;
|
| 133 |
+
promptGroup.prompts[promptIndex].content = content || promptGroup.prompts[promptIndex].content;
|
| 134 |
+
promptGroup.prompts[promptIndex].tags = tags || promptGroup.prompts[promptIndex].tags;
|
| 135 |
+
promptGroup.prompts[promptIndex].updatedAt = new Date();
|
| 136 |
+
promptGroup.updatedAt = new Date();
|
| 137 |
+
|
| 138 |
+
const updatedPromptGroup = await promptGroup.save();
|
| 139 |
+
res.json(updatedPromptGroup.prompts[promptIndex]);
|
| 140 |
+
} else {
|
| 141 |
+
res.status(404);
|
| 142 |
+
throw new Error('提示词未找到');
|
| 143 |
+
}
|
| 144 |
+
} else {
|
| 145 |
+
res.status(404);
|
| 146 |
+
throw new Error('提示词组未找到');
|
| 147 |
+
}
|
| 148 |
+
});
|
| 149 |
+
|
| 150 |
+
// @desc 删除提示词
|
| 151 |
+
// @route DELETE /api/prompt-groups/:id/prompts/:promptId
|
| 152 |
+
// @access Private
|
| 153 |
+
const deletePrompt = asyncHandler(async (req, res) => {
|
| 154 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 155 |
+
|
| 156 |
+
if (promptGroup) {
|
| 157 |
+
const promptExists = promptGroup.prompts.some(
|
| 158 |
+
(p) => p._id.toString() === req.params.promptId
|
| 159 |
+
);
|
| 160 |
+
|
| 161 |
+
if (promptExists) {
|
| 162 |
+
promptGroup.prompts = promptGroup.prompts.filter(
|
| 163 |
+
(p) => p._id.toString() !== req.params.promptId
|
| 164 |
+
);
|
| 165 |
+
promptGroup.updatedAt = new Date();
|
| 166 |
+
|
| 167 |
+
await promptGroup.save();
|
| 168 |
+
res.json({ message: '提示词已删除' });
|
| 169 |
+
} else {
|
| 170 |
+
res.status(404);
|
| 171 |
+
throw new Error('提示词未找到');
|
| 172 |
+
}
|
| 173 |
+
} else {
|
| 174 |
+
res.status(404);
|
| 175 |
+
throw new Error('提示词组未找到');
|
| 176 |
+
}
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
// @desc 添加DSL文件到提示词组
|
| 180 |
+
// @route POST /api/prompt-groups/:id/dsl-files
|
| 181 |
+
// @access Private
|
| 182 |
+
const addDslFileToGroup = asyncHandler(async (req, res) => {
|
| 183 |
+
const { name, fileData, mimeType } = req.body;
|
| 184 |
+
|
| 185 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 186 |
+
|
| 187 |
+
if (promptGroup) {
|
| 188 |
+
const newDslFile = {
|
| 189 |
+
name,
|
| 190 |
+
fileData,
|
| 191 |
+
mimeType: mimeType || 'application/octet-stream',
|
| 192 |
+
uploadedAt: new Date(),
|
| 193 |
+
};
|
| 194 |
+
|
| 195 |
+
promptGroup.dslFiles.push(newDslFile);
|
| 196 |
+
promptGroup.updatedAt = new Date();
|
| 197 |
+
|
| 198 |
+
const updatedPromptGroup = await promptGroup.save();
|
| 199 |
+
res.status(201).json(updatedPromptGroup.dslFiles[updatedPromptGroup.dslFiles.length - 1]);
|
| 200 |
+
} else {
|
| 201 |
+
res.status(404);
|
| 202 |
+
throw new Error('提示词组未找到');
|
| 203 |
+
}
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
// @desc 删除DSL文件
|
| 207 |
+
// @route DELETE /api/prompt-groups/:id/dsl-files/:fileId
|
| 208 |
+
// @access Private
|
| 209 |
+
const deleteDslFile = asyncHandler(async (req, res) => {
|
| 210 |
+
const promptGroup = await PromptGroup.findById(req.params.id);
|
| 211 |
+
|
| 212 |
+
if (promptGroup) {
|
| 213 |
+
const fileExists = promptGroup.dslFiles.some(
|
| 214 |
+
(f) => f._id.toString() === req.params.fileId
|
| 215 |
+
);
|
| 216 |
+
|
| 217 |
+
if (fileExists) {
|
| 218 |
+
promptGroup.dslFiles = promptGroup.dslFiles.filter(
|
| 219 |
+
(f) => f._id.toString() !== req.params.fileId
|
| 220 |
+
);
|
| 221 |
+
promptGroup.updatedAt = new Date();
|
| 222 |
+
|
| 223 |
+
await promptGroup.save();
|
| 224 |
+
res.json({ message: 'DSL文件已删除' });
|
| 225 |
+
} else {
|
| 226 |
+
res.status(404);
|
| 227 |
+
throw new Error('DSL文件未找到');
|
| 228 |
+
}
|
| 229 |
+
} else {
|
| 230 |
+
res.status(404);
|
| 231 |
+
throw new Error('提示词组未找到');
|
| 232 |
+
}
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
module.exports = {
|
| 236 |
+
getPromptGroups,
|
| 237 |
+
getPromptGroupById,
|
| 238 |
+
createPromptGroup,
|
| 239 |
+
updatePromptGroup,
|
| 240 |
+
deletePromptGroup,
|
| 241 |
+
addPromptToGroup,
|
| 242 |
+
updatePrompt,
|
| 243 |
+
deletePrompt,
|
| 244 |
+
addDslFileToGroup,
|
| 245 |
+
deleteDslFile,
|
| 246 |
+
};
|
middleware/auth.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const jwt = require('jsonwebtoken');
|
| 2 |
+
const asyncHandler = require('express-async-handler');
|
| 3 |
+
const User = require('../models/User');
|
| 4 |
+
|
| 5 |
+
// 保护路由 - 验证 JWT Token
|
| 6 |
+
const protect = asyncHandler(async (req, res, next) => {
|
| 7 |
+
let token;
|
| 8 |
+
|
| 9 |
+
// 从 Authorization 头获取 token
|
| 10 |
+
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
|
| 11 |
+
try {
|
| 12 |
+
// 获取 token
|
| 13 |
+
token = req.headers.authorization.split(' ')[1];
|
| 14 |
+
|
| 15 |
+
// 验证 token
|
| 16 |
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
| 17 |
+
|
| 18 |
+
// 获取用户并添加到请求对象中,不包含密码
|
| 19 |
+
req.user = await User.findById(decoded.id).select('-password');
|
| 20 |
+
|
| 21 |
+
if (!req.user) {
|
| 22 |
+
res.status(401);
|
| 23 |
+
throw new Error('未授权,用户不存在');
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
next();
|
| 27 |
+
} catch (error) {
|
| 28 |
+
res.status(401);
|
| 29 |
+
throw new Error('未授权,token 无效');
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
if (!token) {
|
| 34 |
+
res.status(401);
|
| 35 |
+
throw new Error('未授权,未提供 token');
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// 限制仅管理员访问
|
| 40 |
+
const admin = (req, res, next) => {
|
| 41 |
+
if (req.user && req.user.isAdmin) {
|
| 42 |
+
next();
|
| 43 |
+
} else {
|
| 44 |
+
res.status(403);
|
| 45 |
+
throw new Error('未授权,仅管理员可访问');
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
module.exports = { protect, admin };
|
middleware/errorHandler.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const logger = require('../utils/logger');
|
| 2 |
+
|
| 3 |
+
const errorHandler = (err, req, res, next) => {
|
| 4 |
+
// 日志记录错误
|
| 5 |
+
logger.error(`${err.message} - ${req.originalUrl} - ${req.method}`);
|
| 6 |
+
|
| 7 |
+
const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
|
| 8 |
+
|
| 9 |
+
res.status(statusCode);
|
| 10 |
+
res.json({
|
| 11 |
+
message: err.message,
|
| 12 |
+
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
|
| 13 |
+
});
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
module.exports = { errorHandler };
|
models/Category.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mongoose = require('mongoose');
|
| 2 |
+
|
| 3 |
+
const categorySchema = new mongoose.Schema(
|
| 4 |
+
{
|
| 5 |
+
name: {
|
| 6 |
+
type: String,
|
| 7 |
+
required: [true, '请输入分类名称'],
|
| 8 |
+
trim: true,
|
| 9 |
+
unique: true,
|
| 10 |
+
},
|
| 11 |
+
color: {
|
| 12 |
+
type: String,
|
| 13 |
+
default: '#007AFF',
|
| 14 |
+
},
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
timestamps: true,
|
| 18 |
+
}
|
| 19 |
+
);
|
| 20 |
+
|
| 21 |
+
const Category = mongoose.model('Category', categorySchema);
|
| 22 |
+
|
| 23 |
+
module.exports = Category;
|
models/PromptGroup.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mongoose = require('mongoose');
|
| 2 |
+
|
| 3 |
+
// 提示词 Schema
|
| 4 |
+
const promptSchema = new mongoose.Schema(
|
| 5 |
+
{
|
| 6 |
+
title: {
|
| 7 |
+
type: String,
|
| 8 |
+
required: [true, '请输入提示词标题'],
|
| 9 |
+
trim: true,
|
| 10 |
+
},
|
| 11 |
+
content: {
|
| 12 |
+
type: String,
|
| 13 |
+
required: [true, '请输入提示词内容'],
|
| 14 |
+
},
|
| 15 |
+
tags: [String],
|
| 16 |
+
createdAt: {
|
| 17 |
+
type: Date,
|
| 18 |
+
default: Date.now,
|
| 19 |
+
},
|
| 20 |
+
updatedAt: {
|
| 21 |
+
type: Date,
|
| 22 |
+
default: Date.now,
|
| 23 |
+
},
|
| 24 |
+
}
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
// DSL 文件 Schema
|
| 28 |
+
const dslFileSchema = new mongoose.Schema(
|
| 29 |
+
{
|
| 30 |
+
name: {
|
| 31 |
+
type: String,
|
| 32 |
+
required: [true, '请输入文件名称'],
|
| 33 |
+
trim: true,
|
| 34 |
+
},
|
| 35 |
+
fileData: {
|
| 36 |
+
type: String,
|
| 37 |
+
required: [true, '请提供文件数据'],
|
| 38 |
+
},
|
| 39 |
+
mimeType: {
|
| 40 |
+
type: String,
|
| 41 |
+
default: 'application/octet-stream',
|
| 42 |
+
},
|
| 43 |
+
uploadedAt: {
|
| 44 |
+
type: Date,
|
| 45 |
+
default: Date.now,
|
| 46 |
+
},
|
| 47 |
+
}
|
| 48 |
+
);
|
| 49 |
+
|
| 50 |
+
// 提示词组 Schema
|
| 51 |
+
const promptGroupSchema = new mongoose.Schema(
|
| 52 |
+
{
|
| 53 |
+
name: {
|
| 54 |
+
type: String,
|
| 55 |
+
required: [true, '请输入提示词组名称'],
|
| 56 |
+
trim: true,
|
| 57 |
+
},
|
| 58 |
+
description: {
|
| 59 |
+
type: String,
|
| 60 |
+
trim: true,
|
| 61 |
+
},
|
| 62 |
+
category: {
|
| 63 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 64 |
+
ref: 'Category',
|
| 65 |
+
},
|
| 66 |
+
prompts: [promptSchema],
|
| 67 |
+
workflows: [String],
|
| 68 |
+
dslFiles: [dslFileSchema],
|
| 69 |
+
createdBy: {
|
| 70 |
+
type: mongoose.Schema.Types.ObjectId,
|
| 71 |
+
ref: 'User',
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
timestamps: true,
|
| 76 |
+
}
|
| 77 |
+
);
|
| 78 |
+
|
| 79 |
+
const PromptGroup = mongoose.model('PromptGroup', promptGroupSchema);
|
| 80 |
+
|
| 81 |
+
module.exports = PromptGroup;
|
models/User.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mongoose = require('mongoose');
|
| 2 |
+
const bcrypt = require('bcryptjs');
|
| 3 |
+
|
| 4 |
+
const userSchema = new mongoose.Schema(
|
| 5 |
+
{
|
| 6 |
+
username: {
|
| 7 |
+
type: String,
|
| 8 |
+
required: [true, '请输入用户名'],
|
| 9 |
+
unique: true,
|
| 10 |
+
trim: true,
|
| 11 |
+
},
|
| 12 |
+
password: {
|
| 13 |
+
type: String,
|
| 14 |
+
required: [true, '请输入密码'],
|
| 15 |
+
minlength: [6, '密码长度不能小于6个字符'],
|
| 16 |
+
},
|
| 17 |
+
isAdmin: {
|
| 18 |
+
type: Boolean,
|
| 19 |
+
default: false,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
timestamps: true,
|
| 24 |
+
}
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
// 密码匹配方法
|
| 28 |
+
userSchema.methods.matchPassword = async function (enteredPassword) {
|
| 29 |
+
return await bcrypt.compare(enteredPassword, this.password);
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// 保存前加密密码
|
| 33 |
+
userSchema.pre('save', async function (next) {
|
| 34 |
+
if (!this.isModified('password')) {
|
| 35 |
+
next();
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const salt = await bcrypt.genSalt(10);
|
| 39 |
+
this.password = await bcrypt.hash(this.password, salt);
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
const User = mongoose.model('User', userSchema);
|
| 43 |
+
|
| 44 |
+
module.exports = User;
|
package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "prompt-manager-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Backend for Prompt Manager application",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js",
|
| 8 |
+
"dev": "nodemon server.js",
|
| 9 |
+
"test": "echo \"Error: no test specified\" && exit 1"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"bcryptjs": "^2.4.3",
|
| 13 |
+
"cors": "^2.8.5",
|
| 14 |
+
"dotenv": "^16.0.3",
|
| 15 |
+
"express": "^4.18.2",
|
| 16 |
+
"express-async-handler": "^1.2.0",
|
| 17 |
+
"joi": "^17.9.2",
|
| 18 |
+
"jsonwebtoken": "^9.0.0",
|
| 19 |
+
"mongoose": "^7.2.0",
|
| 20 |
+
"morgan": "^1.10.0",
|
| 21 |
+
"winston": "^3.8.2"
|
| 22 |
+
},
|
| 23 |
+
"devDependencies": {
|
| 24 |
+
"nodemon": "^2.0.22"
|
| 25 |
+
},
|
| 26 |
+
"engines": {
|
| 27 |
+
"node": ">=14.0.0"
|
| 28 |
+
}
|
| 29 |
+
}
|
routes/authRoutes.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const router = express.Router();
|
| 3 |
+
const { registerUser, loginUser, getUserProfile } = require('../controllers/authController');
|
| 4 |
+
const { protect, admin } = require('../middleware/auth');
|
| 5 |
+
|
| 6 |
+
// 用户注册
|
| 7 |
+
router.post('/register', protect, admin, registerUser);
|
| 8 |
+
|
| 9 |
+
// 用户登录
|
| 10 |
+
router.post('/login', loginUser);
|
| 11 |
+
|
| 12 |
+
// 获取用户资料
|
| 13 |
+
router.get('/profile', protect, getUserProfile);
|
| 14 |
+
|
| 15 |
+
module.exports = router;
|
routes/categoryRoutes.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const router = express.Router();
|
| 3 |
+
const {
|
| 4 |
+
getCategories,
|
| 5 |
+
getCategoryById,
|
| 6 |
+
createCategory,
|
| 7 |
+
updateCategory,
|
| 8 |
+
deleteCategory,
|
| 9 |
+
} = require('../controllers/categoryController');
|
| 10 |
+
const { protect } = require('../middleware/auth');
|
| 11 |
+
|
| 12 |
+
router.route('/')
|
| 13 |
+
.get(protect, getCategories)
|
| 14 |
+
.post(protect, createCategory);
|
| 15 |
+
|
| 16 |
+
router.route('/:id')
|
| 17 |
+
.get(protect, getCategoryById)
|
| 18 |
+
.put(protect, updateCategory)
|
| 19 |
+
.delete(protect, deleteCategory);
|
| 20 |
+
|
| 21 |
+
module.exports = router;
|
routes/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const router = express.Router();
|
| 3 |
+
const authRoutes = require('./authRoutes');
|
| 4 |
+
const promptGroupRoutes = require('./promptGroupRoutes');
|
| 5 |
+
const categoryRoutes = require('./categoryRoutes');
|
| 6 |
+
|
| 7 |
+
// 注册路由
|
| 8 |
+
router.use('/auth', authRoutes);
|
| 9 |
+
router.use('/prompt-groups', promptGroupRoutes);
|
| 10 |
+
router.use('/categories', categoryRoutes);
|
| 11 |
+
|
| 12 |
+
module.exports = router;
|
routes/promptGroupRoutes.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const router = express.Router();
|
| 3 |
+
const {
|
| 4 |
+
getPromptGroups,
|
| 5 |
+
getPromptGroupById,
|
| 6 |
+
createPromptGroup,
|
| 7 |
+
updatePromptGroup,
|
| 8 |
+
deletePromptGroup,
|
| 9 |
+
addPromptToGroup,
|
| 10 |
+
updatePrompt,
|
| 11 |
+
deletePrompt,
|
| 12 |
+
addDslFileToGroup,
|
| 13 |
+
deleteDslFile,
|
| 14 |
+
} = require('../controllers/promptGroupController');
|
| 15 |
+
const { protect } = require('../middleware/auth');
|
| 16 |
+
|
| 17 |
+
// 提示词组路由
|
| 18 |
+
router.route('/')
|
| 19 |
+
.get(protect, getPromptGroups)
|
| 20 |
+
.post(protect, createPromptGroup);
|
| 21 |
+
|
| 22 |
+
router.route('/:id')
|
| 23 |
+
.get(protect, getPromptGroupById)
|
| 24 |
+
.put(protect, updatePromptGroup)
|
| 25 |
+
.delete(protect, deletePromptGroup);
|
| 26 |
+
|
| 27 |
+
// 提示词路由
|
| 28 |
+
router.route('/:id/prompts')
|
| 29 |
+
.post(protect, addPromptToGroup);
|
| 30 |
+
|
| 31 |
+
router.route('/:id/prompts/:promptId')
|
| 32 |
+
.put(protect, updatePrompt)
|
| 33 |
+
.delete(protect, deletePrompt);
|
| 34 |
+
|
| 35 |
+
// DSL文件路由
|
| 36 |
+
router.route('/:id/dsl-files')
|
| 37 |
+
.post(protect, addDslFileToGroup);
|
| 38 |
+
|
| 39 |
+
router.route('/:id/dsl-files/:fileId')
|
| 40 |
+
.delete(protect, deleteDslFile);
|
| 41 |
+
|
| 42 |
+
module.exports = router;
|
server.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const dotenv = require('dotenv');
|
| 3 |
+
const cors = require('cors');
|
| 4 |
+
const morgan = require('morgan');
|
| 5 |
+
const { connectDB } = require('./config/db');
|
| 6 |
+
const { errorHandler } = require('./middleware/errorHandler');
|
| 7 |
+
const routes = require('./routes');
|
| 8 |
+
const logger = require('./utils/logger');
|
| 9 |
+
|
| 10 |
+
// 加载环境变量
|
| 11 |
+
dotenv.config();
|
| 12 |
+
|
| 13 |
+
// 初始化 Express 应用
|
| 14 |
+
const app = express();
|
| 15 |
+
|
| 16 |
+
// 中间件
|
| 17 |
+
app.use(cors());
|
| 18 |
+
app.use(express.json());
|
| 19 |
+
app.use(express.urlencoded({ extended: true }));
|
| 20 |
+
|
| 21 |
+
// 日志中间件
|
| 22 |
+
if (process.env.NODE_ENV === 'development') {
|
| 23 |
+
app.use(morgan('dev'));
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// 连接数据库
|
| 27 |
+
connectDB();
|
| 28 |
+
|
| 29 |
+
// 路由
|
| 30 |
+
app.use('/api', routes);
|
| 31 |
+
|
| 32 |
+
// 健康检查端点
|
| 33 |
+
app.get('/health', (req, res) => {
|
| 34 |
+
res.status(200).json({ status: 'ok', message: 'Server is running' });
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// 错误处理中间件
|
| 38 |
+
app.use(errorHandler);
|
| 39 |
+
|
| 40 |
+
// 启动服务器
|
| 41 |
+
const PORT = process.env.PORT || 8080;
|
| 42 |
+
app.listen(PORT, () => {
|
| 43 |
+
logger.info(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
// 处理未捕获的异常
|
| 47 |
+
process.on('uncaughtException', (err) => {
|
| 48 |
+
logger.error('Uncaught Exception:', err);
|
| 49 |
+
process.exit(1);
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// 处理未处理的 Promise 拒绝
|
| 53 |
+
process.on('unhandledRejection', (err) => {
|
| 54 |
+
logger.error('Unhandled Rejection:', err);
|
| 55 |
+
process.exit(1);
|
| 56 |
+
});
|
utils/logger.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const winston = require('winston');
|
| 2 |
+
|
| 3 |
+
// 定义日志格式
|
| 4 |
+
const logger = winston.createLogger({
|
| 5 |
+
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
| 6 |
+
format: winston.format.combine(
|
| 7 |
+
winston.format.timestamp({
|
| 8 |
+
format: 'YYYY-MM-DD HH:mm:ss',
|
| 9 |
+
}),
|
| 10 |
+
winston.format.errors({ stack: true }),
|
| 11 |
+
winston.format.splat(),
|
| 12 |
+
winston.format.json()
|
| 13 |
+
),
|
| 14 |
+
defaultMeta: { service: 'prompt-manager-api' },
|
| 15 |
+
transports: [
|
| 16 |
+
new winston.transports.Console({
|
| 17 |
+
format: winston.format.combine(
|
| 18 |
+
winston.format.colorize(),
|
| 19 |
+
winston.format.printf(
|
| 20 |
+
(info) => `${info.timestamp} ${info.level}: ${info.message}`
|
| 21 |
+
)
|
| 22 |
+
),
|
| 23 |
+
}),
|
| 24 |
+
],
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// 如果是生产环境,记录到文件
|
| 28 |
+
if (process.env.NODE_ENV === 'production') {
|
| 29 |
+
logger.add(
|
| 30 |
+
new winston.transports.File({ filename: 'logs/error.log', level: 'error' })
|
| 31 |
+
);
|
| 32 |
+
logger.add(new winston.transports.File({ filename: 'logs/combined.log' }));
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
module.exports = logger;
|
utils/validators.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Joi = require('joi');
|
| 2 |
+
|
| 3 |
+
// 用户验证模式
|
| 4 |
+
const userSchema = Joi.object({
|
| 5 |
+
username: Joi.string().required().min(3).max(30),
|
| 6 |
+
password: Joi.string().required().min(6),
|
| 7 |
+
});
|
| 8 |
+
|
| 9 |
+
// 分类验证模式
|
| 10 |
+
const categorySchema = Joi.object({
|
| 11 |
+
name: Joi.string().required().min(1).max(30),
|
| 12 |
+
color: Joi.string().pattern(new RegExp('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')),
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
// 提示词验证模式
|
| 16 |
+
const promptSchema = Joi.object({
|
| 17 |
+
title: Joi.string().required().min(1).max(100),
|
| 18 |
+
content: Joi.string().required(),
|
| 19 |
+
tags: Joi.array().items(Joi.string()),
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
// 提示词组验证模式
|
| 23 |
+
const promptGroupSchema = Joi.object({
|
| 24 |
+
name: Joi.string().required().min(1).max(100),
|
| 25 |
+
description: Joi.string().allow('', null),
|
| 26 |
+
category: Joi.string().pattern(new RegExp('^[0-9a-fA-F]{24}$')),
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
// DSL文件验证模式
|
| 30 |
+
const dslFileSchema = Joi.object({
|
| 31 |
+
name: Joi.string().required(),
|
| 32 |
+
fileData: Joi.string().required(),
|
| 33 |
+
mimeType: Joi.string(),
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
module.exports = {
|
| 37 |
+
userSchema,
|
| 38 |
+
categorySchema,
|
| 39 |
+
promptSchema,
|
| 40 |
+
promptGroupSchema,
|
| 41 |
+
dslFileSchema,
|
| 42 |
+
};
|