collabdocchat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +501 -0
- package/package.json +70 -0
- package/server/index.js +63 -0
- package/server/middleware/auth.js +26 -0
- package/server/models/AuditLog.js +90 -0
- package/server/models/Document.js +59 -0
- package/server/models/File.js +43 -0
- package/server/models/Group.js +61 -0
- package/server/models/Message.js +31 -0
- package/server/models/Task.js +55 -0
- package/server/models/User.js +60 -0
- package/server/routes/audit.js +210 -0
- package/server/routes/auth.js +125 -0
- package/server/routes/documents.js +254 -0
- package/server/routes/files.js +218 -0
- package/server/routes/groups.js +317 -0
- package/server/routes/tasks.js +110 -0
- package/server/utils/auditLogger.js +238 -0
- package/server/utils/initAdmin.js +51 -0
- package/server/websocket/index.js +228 -0
- package/src/main.js +53 -0
- package/src/pages/admin-dashboard.js +1493 -0
- package/src/pages/login.js +101 -0
- package/src/pages/user-dashboard.js +906 -0
- package/src/services/api.js +265 -0
- package/src/services/auth.js +54 -0
- package/src/services/websocket.js +80 -0
- package/src/styles/main.css +1421 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import User from '../models/User.js';
|
|
4
|
+
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
|
|
7
|
+
// 注册
|
|
8
|
+
router.post('/register', async (req, res) => {
|
|
9
|
+
try {
|
|
10
|
+
const { username, password } = req.body;
|
|
11
|
+
|
|
12
|
+
const existingUser = await User.findOne({ username });
|
|
13
|
+
if (existingUser) {
|
|
14
|
+
return res.status(400).json({ message: '用户名已存在' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 生成默认邮箱(保持数据库结构兼容性)
|
|
18
|
+
const email = `${username}@local.system`;
|
|
19
|
+
// 强制角色为普通用户,不允许注册管理员
|
|
20
|
+
const user = new User({ username, email, password, role: 'user' });
|
|
21
|
+
await user.save();
|
|
22
|
+
|
|
23
|
+
const token = jwt.sign(
|
|
24
|
+
{ userId: user._id, role: user.role },
|
|
25
|
+
process.env.JWT_SECRET,
|
|
26
|
+
{ expiresIn: '7d' }
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
res.status(201).json({
|
|
30
|
+
message: '注册成功',
|
|
31
|
+
token,
|
|
32
|
+
user: {
|
|
33
|
+
id: user._id,
|
|
34
|
+
username: user.username,
|
|
35
|
+
role: user.role
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
res.status(500).json({ message: '注册失败', error: error.message });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 登录
|
|
44
|
+
router.post('/login', async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const { username, password } = req.body;
|
|
47
|
+
|
|
48
|
+
const user = await User.findOne({ username });
|
|
49
|
+
if (!user) {
|
|
50
|
+
return res.status(401).json({ message: '用户名或密码错误' });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isMatch = await user.comparePassword(password);
|
|
54
|
+
if (!isMatch) {
|
|
55
|
+
return res.status(401).json({ message: '用户名或密码错误' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
user.isOnline = true;
|
|
59
|
+
user.lastSeen = new Date();
|
|
60
|
+
await user.save();
|
|
61
|
+
|
|
62
|
+
const token = jwt.sign(
|
|
63
|
+
{ userId: user._id, role: user.role },
|
|
64
|
+
process.env.JWT_SECRET,
|
|
65
|
+
{ expiresIn: '7d' }
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
res.json({
|
|
69
|
+
message: '登录成功',
|
|
70
|
+
token,
|
|
71
|
+
user: {
|
|
72
|
+
id: user._id,
|
|
73
|
+
username: user.username,
|
|
74
|
+
role: user.role,
|
|
75
|
+
avatar: user.avatar
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(500).json({ message: '登录失败', error: error.message });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 获取所有用户(仅管理员)
|
|
84
|
+
router.get('/users', async (req, res) => {
|
|
85
|
+
try {
|
|
86
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
87
|
+
if (!token) {
|
|
88
|
+
return res.status(401).json({ message: '未授权' });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
92
|
+
if (decoded.role !== 'admin') {
|
|
93
|
+
return res.status(403).json({ message: '权限不足' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const users = await User.find({}).select('-password');
|
|
97
|
+
res.json({ users });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
res.status(500).json({ message: '获取用户列表失败', error: error.message });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 获取当前用户信息
|
|
104
|
+
router.get('/me', async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const token = req.headers.authorization?.split(' ')[1];
|
|
107
|
+
if (!token) {
|
|
108
|
+
return res.status(401).json({ message: '未授权' });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
|
112
|
+
const user = await User.findById(decoded.userId).select('-password');
|
|
113
|
+
|
|
114
|
+
if (!user) {
|
|
115
|
+
return res.status(404).json({ message: '用户不存在' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
res.json({ user });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
res.status(401).json({ message: '无效的令牌' });
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export default router;
|
|
125
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import Document from '../models/Document.js';
|
|
3
|
+
import Group from '../models/Group.js';
|
|
4
|
+
import { authenticate, isAdmin } from '../middleware/auth.js';
|
|
5
|
+
import { logAuditAction } from '../utils/auditLogger.js';
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// 创建文档
|
|
10
|
+
router.post('/', authenticate, async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const { title, content, groupId, permission } = req.body;
|
|
13
|
+
|
|
14
|
+
const document = new Document({
|
|
15
|
+
title,
|
|
16
|
+
content,
|
|
17
|
+
group: groupId,
|
|
18
|
+
creator: req.user.userId,
|
|
19
|
+
permission: permission || 'editable'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await document.save();
|
|
23
|
+
|
|
24
|
+
// 更新群组的文档列表
|
|
25
|
+
await Group.findByIdAndUpdate(groupId, { $push: { documents: document._id } });
|
|
26
|
+
|
|
27
|
+
// 记录审计日志
|
|
28
|
+
await logAuditAction({
|
|
29
|
+
action: 'document_create',
|
|
30
|
+
userId: req.user.userId,
|
|
31
|
+
resourceType: 'document',
|
|
32
|
+
resourceId: document._id,
|
|
33
|
+
resourceTitle: document.title,
|
|
34
|
+
groupId: groupId,
|
|
35
|
+
description: `创建了文档 "${document.title}"`
|
|
36
|
+
}, req);
|
|
37
|
+
|
|
38
|
+
res.status(201).json({ message: '文档创建成功', document });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
res.status(500).json({ message: '创建文档失败', error: error.message });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 获取群组的所有文档
|
|
45
|
+
router.get('/group/:groupId', authenticate, async (req, res) => {
|
|
46
|
+
try {
|
|
47
|
+
const documents = await Document.find({ group: req.params.groupId })
|
|
48
|
+
.populate('creator', 'username avatar')
|
|
49
|
+
.populate('editors.user', 'username avatar')
|
|
50
|
+
.sort({ updatedAt: -1 });
|
|
51
|
+
|
|
52
|
+
res.json({ documents });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
res.status(500).json({ message: '获取文档失败', error: error.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 获取文档详情
|
|
59
|
+
router.get('/:id', authenticate, async (req, res) => {
|
|
60
|
+
try {
|
|
61
|
+
const document = await Document.findById(req.params.id)
|
|
62
|
+
.populate('creator', 'username avatar')
|
|
63
|
+
.populate('editors.user', 'username avatar');
|
|
64
|
+
|
|
65
|
+
if (!document) {
|
|
66
|
+
return res.status(404).json({ message: '文档不存在' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
res.json({ document });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
res.status(500).json({ message: '获取文档详情失败', error: error.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 更新文档内容
|
|
76
|
+
router.patch('/:id', authenticate, async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
const { content, title } = req.body;
|
|
79
|
+
const document = await Document.findById(req.params.id);
|
|
80
|
+
|
|
81
|
+
if (!document) {
|
|
82
|
+
return res.status(404).json({ message: '文档不存在' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (document.permission === 'readonly') {
|
|
86
|
+
return res.status(403).json({ message: '文档为只读模式' });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const oldContent = document.content;
|
|
90
|
+
const oldTitle = document.title;
|
|
91
|
+
|
|
92
|
+
// 保存版本历史
|
|
93
|
+
document.versions.push({
|
|
94
|
+
content: oldContent,
|
|
95
|
+
editor: req.user.userId,
|
|
96
|
+
timestamp: new Date()
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 更新内容
|
|
100
|
+
if (content !== undefined) {
|
|
101
|
+
document.content = content;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 更新标题
|
|
105
|
+
if (title !== undefined) {
|
|
106
|
+
document.title = title;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 更新编辑者列表
|
|
110
|
+
const editorIndex = document.editors.findIndex(
|
|
111
|
+
e => e.user.toString() === req.user.userId
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (editorIndex === -1) {
|
|
115
|
+
document.editors.push({ user: req.user.userId, lastEdit: new Date() });
|
|
116
|
+
} else {
|
|
117
|
+
document.editors[editorIndex].lastEdit = new Date();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await document.save();
|
|
121
|
+
|
|
122
|
+
// 记录审计日志
|
|
123
|
+
if (content !== undefined && content !== oldContent) {
|
|
124
|
+
await logAuditAction({
|
|
125
|
+
action: 'content_edit',
|
|
126
|
+
userId: req.user.userId,
|
|
127
|
+
resourceType: 'document',
|
|
128
|
+
resourceId: document._id,
|
|
129
|
+
resourceTitle: document.title,
|
|
130
|
+
groupId: document.group,
|
|
131
|
+
oldValue: oldContent,
|
|
132
|
+
newValue: content,
|
|
133
|
+
field: 'content',
|
|
134
|
+
description: `修改了文档 "${document.title}" 的内容`
|
|
135
|
+
}, req);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (title !== undefined && title !== oldTitle) {
|
|
139
|
+
await logAuditAction({
|
|
140
|
+
action: 'title_edit',
|
|
141
|
+
userId: req.user.userId,
|
|
142
|
+
resourceType: 'document',
|
|
143
|
+
resourceId: document._id,
|
|
144
|
+
resourceTitle: document.title,
|
|
145
|
+
groupId: document.group,
|
|
146
|
+
oldValue: oldTitle,
|
|
147
|
+
newValue: title,
|
|
148
|
+
field: 'title',
|
|
149
|
+
description: `将文档标题从 "${oldTitle}" 修改为 "${title}"`
|
|
150
|
+
}, req);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
res.json({ message: '文档更新成功', document });
|
|
154
|
+
} catch (error) {
|
|
155
|
+
res.status(500).json({ message: '更新文档失败', error: error.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 获取文档版本历史
|
|
160
|
+
router.get('/:id/versions', authenticate, async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const document = await Document.findById(req.params.id)
|
|
163
|
+
.populate('versions.editor', 'username avatar');
|
|
164
|
+
|
|
165
|
+
if (!document) {
|
|
166
|
+
return res.status(404).json({ message: '文档不存在' });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
res.json({ versions: document.versions });
|
|
170
|
+
} catch (error) {
|
|
171
|
+
res.status(500).json({ message: '获取版本历史失败', error: error.message });
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 更新文档权限(仅管理员)
|
|
176
|
+
router.patch('/:id/permission', authenticate, isAdmin, async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
const { permission } = req.body;
|
|
179
|
+
const oldDocument = await Document.findById(req.params.id);
|
|
180
|
+
|
|
181
|
+
if (!oldDocument) {
|
|
182
|
+
return res.status(404).json({ message: '文档不存在' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const oldPermission = oldDocument.permission;
|
|
186
|
+
|
|
187
|
+
const document = await Document.findByIdAndUpdate(
|
|
188
|
+
req.params.id,
|
|
189
|
+
{ permission },
|
|
190
|
+
{ new: true }
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// 记录审计日志
|
|
194
|
+
await logAuditAction({
|
|
195
|
+
action: 'document_permission_change',
|
|
196
|
+
userId: req.user.userId,
|
|
197
|
+
resourceType: 'document',
|
|
198
|
+
resourceId: document._id,
|
|
199
|
+
resourceTitle: document.title,
|
|
200
|
+
groupId: document.group,
|
|
201
|
+
oldValue: oldPermission,
|
|
202
|
+
newValue: permission,
|
|
203
|
+
field: 'permission',
|
|
204
|
+
description: `将文档 "${document.title}" 的权限从 "${oldPermission === 'readonly' ? '只读' : '可编辑'}" 修改为 "${permission === 'readonly' ? '只读' : '可编辑'}"`
|
|
205
|
+
}, req);
|
|
206
|
+
|
|
207
|
+
res.json({ message: '权限更新成功', document });
|
|
208
|
+
} catch (error) {
|
|
209
|
+
res.status(500).json({ message: '更新权限失败', error: error.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// 删除文档(仅管理员)
|
|
214
|
+
router.delete('/:id', authenticate, isAdmin, async (req, res) => {
|
|
215
|
+
try {
|
|
216
|
+
const document = await Document.findById(req.params.id);
|
|
217
|
+
|
|
218
|
+
if (!document) {
|
|
219
|
+
return res.status(404).json({ message: '文档不存在' });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 记录删除前的信息用于审计日志
|
|
223
|
+
const documentInfo = {
|
|
224
|
+
title: document.title,
|
|
225
|
+
group: document.group,
|
|
226
|
+
content: document.content
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
await Document.findByIdAndDelete(req.params.id);
|
|
230
|
+
|
|
231
|
+
// 从群组的文档列表中移除
|
|
232
|
+
await Group.findByIdAndUpdate(document.group, { $pull: { documents: document._id } });
|
|
233
|
+
|
|
234
|
+
// 记录审计日志
|
|
235
|
+
await logAuditAction({
|
|
236
|
+
action: 'document_delete',
|
|
237
|
+
userId: req.user.userId,
|
|
238
|
+
resourceType: 'document',
|
|
239
|
+
resourceId: document._id,
|
|
240
|
+
resourceTitle: documentInfo.title,
|
|
241
|
+
groupId: documentInfo.group,
|
|
242
|
+
oldValue: documentInfo.content,
|
|
243
|
+
description: `删除了文档 "${documentInfo.title}"`
|
|
244
|
+
}, req);
|
|
245
|
+
|
|
246
|
+
res.json({ message: '文档删除成功' });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
res.status(500).json({ message: '删除文档失败', error: error.message });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export default router;
|
|
253
|
+
|
|
254
|
+
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import multer from 'multer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import File from '../models/File.js';
|
|
8
|
+
import Group from '../models/Group.js';
|
|
9
|
+
import { authenticate } from '../middleware/auth.js';
|
|
10
|
+
import { logAuditAction } from '../utils/auditLogger.js';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const router = express.Router();
|
|
16
|
+
|
|
17
|
+
// 确保上传目录存在
|
|
18
|
+
const uploadsDir = path.join(__dirname, '../../uploads');
|
|
19
|
+
if (!fs.existsSync(uploadsDir)) {
|
|
20
|
+
fs.mkdirSync(uploadsDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 配置 multer
|
|
24
|
+
const storage = multer.diskStorage({
|
|
25
|
+
destination: (req, file, cb) => {
|
|
26
|
+
cb(null, uploadsDir);
|
|
27
|
+
},
|
|
28
|
+
filename: (req, file, cb) => {
|
|
29
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
30
|
+
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// 文件类型过滤
|
|
35
|
+
const fileFilter = (req, file, cb) => {
|
|
36
|
+
const allowedTypes = [
|
|
37
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
38
|
+
'application/pdf',
|
|
39
|
+
'application/msword',
|
|
40
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
41
|
+
'application/vnd.ms-excel',
|
|
42
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
43
|
+
'text/plain',
|
|
44
|
+
'application/zip',
|
|
45
|
+
'application/x-zip-compressed'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
if (allowedTypes.includes(file.mimetype)) {
|
|
49
|
+
cb(null, true);
|
|
50
|
+
} else {
|
|
51
|
+
cb(new Error('不支持的文件类型'), false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const upload = multer({
|
|
56
|
+
storage: storage,
|
|
57
|
+
limits: {
|
|
58
|
+
fileSize: 10 * 1024 * 1024 // 10MB
|
|
59
|
+
},
|
|
60
|
+
fileFilter: fileFilter
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 上传文件
|
|
64
|
+
router.post('/upload', authenticate, upload.single('file'), async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!req.file) {
|
|
67
|
+
return res.status(400).json({ message: '请选择要上传的文件' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { groupId, description } = req.body;
|
|
71
|
+
|
|
72
|
+
if (!groupId) {
|
|
73
|
+
// 删除已上传的文件
|
|
74
|
+
fs.unlinkSync(req.file.path);
|
|
75
|
+
return res.status(400).json({ message: '请指定群组' });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 检查用户是否在群组中
|
|
79
|
+
const group = await Group.findById(groupId);
|
|
80
|
+
if (!group) {
|
|
81
|
+
fs.unlinkSync(req.file.path);
|
|
82
|
+
return res.status(404).json({ message: '群组不存在' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const file = new File({
|
|
86
|
+
filename: req.file.filename,
|
|
87
|
+
originalName: req.file.originalname,
|
|
88
|
+
path: req.file.path,
|
|
89
|
+
mimetype: req.file.mimetype,
|
|
90
|
+
size: req.file.size,
|
|
91
|
+
group: groupId,
|
|
92
|
+
uploader: req.user.userId,
|
|
93
|
+
description: description || ''
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await file.save();
|
|
97
|
+
|
|
98
|
+
// 更新群组的文件列表
|
|
99
|
+
await Group.findByIdAndUpdate(groupId, { $push: { files: file._id } });
|
|
100
|
+
|
|
101
|
+
// 记录审计日志
|
|
102
|
+
await logAuditAction({
|
|
103
|
+
action: 'file_upload',
|
|
104
|
+
userId: req.user.userId,
|
|
105
|
+
resourceType: 'file',
|
|
106
|
+
resourceId: file._id,
|
|
107
|
+
resourceTitle: file.originalName,
|
|
108
|
+
groupId: groupId,
|
|
109
|
+
description: `上传了文件 "${file.originalName}"`
|
|
110
|
+
}, req);
|
|
111
|
+
|
|
112
|
+
res.status(201).json({ message: '文件上传成功', file });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (req.file) {
|
|
115
|
+
fs.unlinkSync(req.file.path);
|
|
116
|
+
}
|
|
117
|
+
res.status(500).json({ message: '文件上传失败', error: error.message });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 获取群组文件列表
|
|
122
|
+
router.get('/group/:groupId', authenticate, async (req, res) => {
|
|
123
|
+
try {
|
|
124
|
+
const files = await File.find({ group: req.params.groupId })
|
|
125
|
+
.populate('uploader', 'username avatar')
|
|
126
|
+
.sort({ createdAt: -1 });
|
|
127
|
+
|
|
128
|
+
res.json({ files });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
res.status(500).json({ message: '获取文件列表失败', error: error.message });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 下载文件
|
|
135
|
+
router.get('/:id/download', authenticate, async (req, res) => {
|
|
136
|
+
try {
|
|
137
|
+
const file = await File.findById(req.params.id);
|
|
138
|
+
|
|
139
|
+
if (!file) {
|
|
140
|
+
return res.status(404).json({ message: '文件不存在' });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 检查文件是否存在
|
|
144
|
+
if (!fs.existsSync(file.path)) {
|
|
145
|
+
return res.status(404).json({ message: '文件已被删除' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
res.download(file.path, file.originalName, (err) => {
|
|
149
|
+
if (err) {
|
|
150
|
+
console.error('文件下载错误:', err);
|
|
151
|
+
res.status(500).json({ message: '文件下载失败' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
res.status(500).json({ message: '文件下载失败', error: error.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// 删除文件
|
|
160
|
+
router.delete('/:id', authenticate, async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const file = await File.findById(req.params.id);
|
|
163
|
+
|
|
164
|
+
if (!file) {
|
|
165
|
+
return res.status(404).json({ message: '文件不存在' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 检查权限:只有上传者或管理员可以删除
|
|
169
|
+
if (file.uploader.toString() !== req.user.userId && req.user.role !== 'admin') {
|
|
170
|
+
return res.status(403).json({ message: '无权删除此文件' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 删除物理文件
|
|
174
|
+
if (fs.existsSync(file.path)) {
|
|
175
|
+
fs.unlinkSync(file.path);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 从群组中移除文件引用
|
|
179
|
+
await Group.findByIdAndUpdate(file.group, { $pull: { files: file._id } });
|
|
180
|
+
|
|
181
|
+
// 删除数据库记录
|
|
182
|
+
await File.findByIdAndDelete(req.params.id);
|
|
183
|
+
|
|
184
|
+
// 记录审计日志
|
|
185
|
+
await logAuditAction({
|
|
186
|
+
action: 'file_delete',
|
|
187
|
+
userId: req.user.userId,
|
|
188
|
+
resourceType: 'file',
|
|
189
|
+
resourceId: file._id,
|
|
190
|
+
resourceTitle: file.originalName,
|
|
191
|
+
groupId: file.group,
|
|
192
|
+
description: `删除了文件 "${file.originalName}"`
|
|
193
|
+
}, req);
|
|
194
|
+
|
|
195
|
+
res.json({ message: '文件删除成功' });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
res.status(500).json({ message: '删除文件失败', error: error.message });
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 获取文件信息
|
|
202
|
+
router.get('/:id', authenticate, async (req, res) => {
|
|
203
|
+
try {
|
|
204
|
+
const file = await File.findById(req.params.id)
|
|
205
|
+
.populate('uploader', 'username avatar');
|
|
206
|
+
|
|
207
|
+
if (!file) {
|
|
208
|
+
return res.status(404).json({ message: '文件不存在' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
res.json({ file });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
res.status(500).json({ message: '获取文件信息失败', error: error.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export default router;
|
|
218
|
+
|