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,265 @@
|
|
|
1
|
+
const API_URL = 'http://localhost:8765/api';
|
|
2
|
+
|
|
3
|
+
export class ApiService {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.token = localStorage.getItem('token');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async request(endpoint, options = {}) {
|
|
9
|
+
const response = await fetch(`${API_URL}${endpoint}`, {
|
|
10
|
+
...options,
|
|
11
|
+
headers: {
|
|
12
|
+
'Content-Type': 'application/json',
|
|
13
|
+
'Authorization': `Bearer ${this.token}`,
|
|
14
|
+
...options.headers
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const error = await response.json().catch(() => ({ message: '请求失败' }));
|
|
20
|
+
console.error('API 错误:', {
|
|
21
|
+
endpoint,
|
|
22
|
+
status: response.status,
|
|
23
|
+
error
|
|
24
|
+
});
|
|
25
|
+
throw new Error(error.message || `请求失败: ${response.status}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return await response.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 群组相关
|
|
32
|
+
async getGroups() {
|
|
33
|
+
return await this.request('/groups');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getAllGroups() {
|
|
37
|
+
return await this.request('/groups/all');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getGroup(groupId) {
|
|
41
|
+
return await this.request(`/groups/${groupId}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async createGroup(name, description, members) {
|
|
45
|
+
return await this.request('/groups', {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: JSON.stringify({ name, description, members })
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async joinGroup(groupId) {
|
|
52
|
+
return await this.request(`/groups/${groupId}/join`, {
|
|
53
|
+
method: 'POST'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async leaveGroup(groupId) {
|
|
58
|
+
return await this.request(`/groups/${groupId}/leave`, {
|
|
59
|
+
method: 'POST'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async addMember(groupId, userId) {
|
|
64
|
+
return await this.request(`/groups/${groupId}/members`, {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
body: JSON.stringify({ userId })
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async removeMember(groupId, userId) {
|
|
71
|
+
return await this.request(`/groups/${groupId}/members/${userId}`, {
|
|
72
|
+
method: 'DELETE'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async setMuteAll(groupId, enabled) {
|
|
77
|
+
return await this.request(`/groups/${groupId}/mute/all`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: JSON.stringify({ enabled })
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async setUserMute(groupId, userId, muted) {
|
|
84
|
+
return await this.request(`/groups/${groupId}/mute/users/${userId}`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify({ muted })
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getAllUsers() {
|
|
91
|
+
return await this.request('/auth/users');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getGroupMessages(groupId) {
|
|
95
|
+
return await this.request(`/groups/${groupId}/messages`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async randomCall(groupId, count = 1) {
|
|
99
|
+
return await this.request(`/groups/${groupId}/call`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
body: JSON.stringify({ count })
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 任务相关
|
|
106
|
+
async getTasks(groupId) {
|
|
107
|
+
return await this.request(`/tasks/group/${groupId}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async getMyTasks() {
|
|
111
|
+
return await this.request('/tasks/my');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async createTask(taskData) {
|
|
115
|
+
return await this.request('/tasks', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
body: JSON.stringify(taskData)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async updateTaskStatus(taskId, status) {
|
|
122
|
+
return await this.request(`/tasks/${taskId}/status`, {
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
body: JSON.stringify({ status })
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async deleteTask(taskId) {
|
|
129
|
+
return await this.request(`/tasks/${taskId}`, {
|
|
130
|
+
method: 'DELETE'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 文档相关
|
|
135
|
+
async getDocuments(groupId) {
|
|
136
|
+
return await this.request(`/documents/group/${groupId}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getDocument(documentId) {
|
|
140
|
+
return await this.request(`/documents/${documentId}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async createDocument(title, content, groupId, permission = 'editable') {
|
|
144
|
+
return await this.request('/documents', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
body: JSON.stringify({ title, content, groupId, permission })
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async updateDocument(documentId, content) {
|
|
151
|
+
return await this.request(`/documents/${documentId}`, {
|
|
152
|
+
method: 'PATCH',
|
|
153
|
+
body: JSON.stringify({ content })
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getDocumentVersions(documentId) {
|
|
158
|
+
return await this.request(`/documents/${documentId}/versions`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async deleteDocument(documentId) {
|
|
162
|
+
return await this.request(`/documents/${documentId}`, {
|
|
163
|
+
method: 'DELETE'
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 审计日志相关
|
|
168
|
+
async getAuditLogs(filters = {}, options = {}) {
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
|
|
171
|
+
// 添加过滤条件
|
|
172
|
+
Object.keys(filters).forEach(key => {
|
|
173
|
+
if (filters[key]) {
|
|
174
|
+
params.append(key, filters[key]);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 添加分页和排序选项
|
|
179
|
+
Object.keys(options).forEach(key => {
|
|
180
|
+
if (options[key]) {
|
|
181
|
+
params.append(key, options[key]);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const queryString = params.toString();
|
|
186
|
+
return await this.request(`/audit${queryString ? '?' + queryString : ''}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async getUserActivityStats(userId, timeRange = {}) {
|
|
190
|
+
const params = new URLSearchParams(timeRange);
|
|
191
|
+
const queryString = params.toString();
|
|
192
|
+
return await this.request(`/audit/user-stats/${userId}${queryString ? '?' + queryString : ''}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getDocumentEditHistory(documentId, limit = 20) {
|
|
196
|
+
return await this.request(`/audit/document-history/${documentId}?limit=${limit}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getGroupAuditLogs(groupId, filters = {}, options = {}) {
|
|
200
|
+
const params = new URLSearchParams();
|
|
201
|
+
|
|
202
|
+
// 添加过滤条件
|
|
203
|
+
Object.keys(filters).forEach(key => {
|
|
204
|
+
if (filters[key]) {
|
|
205
|
+
params.append(key, filters[key]);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 添加分页和排序选项
|
|
210
|
+
Object.keys(options).forEach(key => {
|
|
211
|
+
if (options[key]) {
|
|
212
|
+
params.append(key, options[key]);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const queryString = params.toString();
|
|
217
|
+
return await this.request(`/audit/group/${groupId}${queryString ? '?' + queryString : ''}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async getAuditSummary(filters = {}) {
|
|
221
|
+
const params = new URLSearchParams(filters);
|
|
222
|
+
const queryString = params.toString();
|
|
223
|
+
return await this.request(`/audit/stats/summary${queryString ? '?' + queryString : ''}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// 文件相关
|
|
227
|
+
async uploadFile(groupId, file, description = '') {
|
|
228
|
+
const formData = new FormData();
|
|
229
|
+
formData.append('file', file);
|
|
230
|
+
formData.append('groupId', groupId);
|
|
231
|
+
if (description) {
|
|
232
|
+
formData.append('description', description);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const response = await fetch(`${API_URL}/files/upload`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
'Authorization': `Bearer ${this.token}`
|
|
239
|
+
},
|
|
240
|
+
body: formData
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
const error = await response.json().catch(() => ({ message: '上传失败' }));
|
|
245
|
+
throw new Error(error.message || '上传失败');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return await response.json();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async getGroupFiles(groupId) {
|
|
252
|
+
return await this.request(`/files/group/${groupId}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async deleteFile(fileId) {
|
|
256
|
+
return await this.request(`/files/${fileId}`, {
|
|
257
|
+
method: 'DELETE'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
getFileDownloadUrl(fileId) {
|
|
262
|
+
return `${API_URL}/files/${fileId}/download?token=${this.token}`;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const API_URL = 'http://localhost:8765/api';
|
|
2
|
+
|
|
3
|
+
export class AuthService {
|
|
4
|
+
async login(username, password) {
|
|
5
|
+
const response = await fetch(`${API_URL}/auth/login`, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
headers: { 'Content-Type': 'application/json' },
|
|
8
|
+
body: JSON.stringify({ username, password })
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
const error = await response.json();
|
|
13
|
+
throw new Error(error.message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return await response.json();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async register(username, password) {
|
|
20
|
+
const response = await fetch(`${API_URL}/auth/register`, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
body: JSON.stringify({ username, password })
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.json();
|
|
28
|
+
throw new Error(error.message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return await response.json();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getCurrentUser() {
|
|
35
|
+
const token = localStorage.getItem('token');
|
|
36
|
+
const response = await fetch(`${API_URL}/auth/me`, {
|
|
37
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error('获取用户信息失败');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return data.user;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logout() {
|
|
49
|
+
localStorage.removeItem('token');
|
|
50
|
+
window.location.reload();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export class WebSocketService {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.ws = null;
|
|
4
|
+
this.listeners = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
connect(token) {
|
|
8
|
+
this.ws = new WebSocket('ws://localhost:3000');
|
|
9
|
+
|
|
10
|
+
this.ws.onopen = () => {
|
|
11
|
+
console.log('✅ WebSocket 连接成功');
|
|
12
|
+
this.send({ type: 'auth', token });
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
this.ws.onmessage = (event) => {
|
|
16
|
+
const data = JSON.parse(event.data);
|
|
17
|
+
this.notifyListeners(data.type, data);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
this.ws.onerror = (error) => {
|
|
21
|
+
console.error('❌ WebSocket 错误:', error);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
this.ws.onclose = () => {
|
|
25
|
+
console.log('🔌 WebSocket 连接关闭');
|
|
26
|
+
setTimeout(() => this.connect(token), 3000);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
send(data) {
|
|
31
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
32
|
+
this.ws.send(JSON.stringify(data));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
on(event, callback) {
|
|
37
|
+
if (!this.listeners.has(event)) {
|
|
38
|
+
this.listeners.set(event, []);
|
|
39
|
+
}
|
|
40
|
+
this.listeners.get(event).push(callback);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
off(event, callback) {
|
|
44
|
+
if (this.listeners.has(event)) {
|
|
45
|
+
const callbacks = this.listeners.get(event);
|
|
46
|
+
const index = callbacks.indexOf(callback);
|
|
47
|
+
if (index > -1) {
|
|
48
|
+
callbacks.splice(index, 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
notifyListeners(event, data) {
|
|
54
|
+
if (this.listeners.has(event)) {
|
|
55
|
+
this.listeners.get(event).forEach(callback => callback(data));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
joinGroup(groupId) {
|
|
60
|
+
this.send({ type: 'join_group', groupId });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
sendChatMessage(groupId, username, content) {
|
|
64
|
+
this.send({ type: 'chat_message', groupId, username, content });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
syncDocument(documentId, content, cursorPosition) {
|
|
68
|
+
this.send({ type: 'document_sync', documentId, content, cursorPosition });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
respondToCall(groupId, username) {
|
|
72
|
+
this.send({ type: 'call_response', groupId, username });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
sendTyping(documentId, username, isTyping) {
|
|
76
|
+
this.send({ type: 'typing', documentId, username, isTyping });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|