blockmine 1.23.0 → 1.23.2
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/.claude/agents/code-architect.md +34 -0
- package/.claude/agents/code-explorer.md +51 -0
- package/.claude/agents/code-reviewer.md +46 -0
- package/.claude/commands/feature-dev.md +125 -0
- package/.claude/settings.json +1 -1
- package/.claude/settings.local.json +3 -1
- package/.claude/skills/frontend-design/SKILL.md +42 -0
- package/CHANGELOG.md +31 -16
- package/backend/prisma/migrations/20251116111851_add_execution_trace/migration.sql +22 -22
- package/backend/prisma/migrations/20251120154914_add_panel_api_keys/migration.sql +21 -21
- package/backend/prisma/migrations/20251121110241_add_proxy_table/migration.sql +45 -45
- package/backend/prisma/migrations/migration_lock.toml +2 -2
- package/backend/src/api/routes/auth.js +669 -669
- package/backend/src/api/routes/bots.js +2451 -2451
- package/backend/src/api/routes/panel.js +66 -66
- package/backend/src/api/routes/panelApiKeys.js +179 -179
- package/backend/src/api/routes/plugins.js +376 -376
- package/backend/src/api/routes/system.js +174 -174
- package/backend/src/core/EventGraphManager.js +194 -194
- package/backend/src/core/GraphExecutionEngine.js +28 -1
- package/backend/src/core/node-registries/actions.js +2 -2
- package/backend/src/core/nodes/actions/http_request.js +23 -4
- package/backend/src/core/nodes/actions/send_message.js +2 -12
- package/backend/src/core/nodes/data/string_literal.js +2 -13
- package/backend/src/core/services/BotLifecycleService.js +835 -835
- package/frontend/dist/assets/{index-B1serztM.js → index-DqzDkFsP.js} +185 -185
- package/frontend/dist/index.html +1 -1
- package/package.json +2 -1
- package/CLAUDE.md +0 -284
|
@@ -1,670 +1,670 @@
|
|
|
1
|
-
const express = require('express');
|
|
2
|
-
const bcrypt = require('bcryptjs');
|
|
3
|
-
const jwt = require('jsonwebtoken');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
|
-
const { PrismaClient } = require('@prisma/client');
|
|
6
|
-
const config = require('../../config');
|
|
7
|
-
const { authenticate, authorize } = require('../middleware/auth');
|
|
8
|
-
const { authenticatePanelApiKey } = require('../middleware/panelApiAuth');
|
|
9
|
-
const path = require('path');
|
|
10
|
-
const os = require('os');
|
|
11
|
-
|
|
12
|
-
const router = express.Router();
|
|
13
|
-
const prisma = new PrismaClient();
|
|
14
|
-
|
|
15
|
-
const JWT_SECRET = config.security.jwtSecret;
|
|
16
|
-
const JWT_EXPIRES_IN = '7d';
|
|
17
|
-
|
|
18
|
-
const activeResetTokens = new Map();
|
|
19
|
-
|
|
20
|
-
const ALL_PERMISSIONS = [
|
|
21
|
-
{ id: '*', label: 'Все права (Администратор)' },
|
|
22
|
-
{ id: 'bot:list', label: 'Просмотр ботов' },
|
|
23
|
-
{ id: 'bot:create', label: 'Создание ботов' },
|
|
24
|
-
{ id: 'bot:update', label: 'Редактирование ботов' },
|
|
25
|
-
{ id: 'bot:delete', label: 'Удаление ботов' },
|
|
26
|
-
{ id: 'bot:start_stop', label: 'Запуск/остановка ботов' },
|
|
27
|
-
{ id: 'bot:interact', label: 'Взаимодействие с ботом (консоль)' },
|
|
28
|
-
{ id: 'bot:export', label: 'Экспорт ботов' },
|
|
29
|
-
{ id: 'bot:import', label: 'Импорт ботов' },
|
|
30
|
-
{ id: 'bot:history:view', label: 'Просмотр истории чата и команд' },
|
|
31
|
-
{ id: 'bot:history:manage', label: 'Управление историей (очистка)' },
|
|
32
|
-
{ id: 'management:view', label: 'Просмотр вкладки "Управление" у бота' },
|
|
33
|
-
{ id: 'management:edit', label: 'Редактирование на вкладке "Управление" у бота' },
|
|
34
|
-
{ id: 'plugin:list', label: 'Просмотр плагинов' },
|
|
35
|
-
{ id: 'plugin:install', label: 'Установка плагинов' },
|
|
36
|
-
{ id: 'plugin:delete', label: 'Удаление плагинов' },
|
|
37
|
-
{ id: 'plugin:update', label: 'Обновление плагинов' },
|
|
38
|
-
{ id: 'plugin:settings:view', label: 'Просмотр настроек плагинов' },
|
|
39
|
-
{ id: 'plugin:settings:edit', label: 'Редактирование настроек плагинов' },
|
|
40
|
-
{ id: 'plugin:browse', label: 'Просмотр каталога плагинов' },
|
|
41
|
-
{ id: 'plugin:develop', label: 'Разработка и редактирование плагинов (IDE)' },
|
|
42
|
-
{ id: 'server:list', label: 'Просмотр серверов' },
|
|
43
|
-
{ id: 'server:create', label: 'Создание серверов' },
|
|
44
|
-
{ id: 'server:delete', label: 'Удаление серверов' },
|
|
45
|
-
{ id: 'proxy:list', label: 'Просмотр прокси' },
|
|
46
|
-
{ id: 'proxy:create', label: 'Создание и редактирование прокси' },
|
|
47
|
-
{ id: 'proxy:delete', label: 'Удаление прокси' },
|
|
48
|
-
{ id: 'task:list', label: 'Просмотр задач' },
|
|
49
|
-
{ id: 'task:create', label: 'Создание задач' },
|
|
50
|
-
{ id: 'task:edit', label: 'Редактирование задач' },
|
|
51
|
-
{ id: 'task:delete', label: 'Удаление задач' },
|
|
52
|
-
{ id: 'panel:user:list', label: 'Просмотр пользователей панели' },
|
|
53
|
-
{ id: 'panel:user:create', label: 'Создание пользователей панели' },
|
|
54
|
-
{ id: 'panel:user:edit', label: 'Редактирование пользователей панели' },
|
|
55
|
-
{ id: 'panel:user:delete', label: 'Удаление пользователей панели' },
|
|
56
|
-
{ id: 'panel:role:list', label: 'Просмотр ролей панели' },
|
|
57
|
-
{ id: 'panel:role:create', label: 'Создание ролей панели' },
|
|
58
|
-
{ id: 'panel:role:edit', label: 'Редактирование ролей панели' },
|
|
59
|
-
{ id: 'panel:role:delete', label: 'Удаление ролей панели' },
|
|
60
|
-
{ id: 'panel:settings:view', label: 'Просмотр глобальных настроек' },
|
|
61
|
-
{ id: 'panel:settings:edit', label: 'Редактирование глобальных настроек' },
|
|
62
|
-
{ id: 'graph:read', label: 'Просмотр магазина графов' },
|
|
63
|
-
{ id: 'graph:download', label: 'Скачивание графов из магазина' },
|
|
64
|
-
{ id: 'graph:like', label: 'Лайки графов в магазине' },
|
|
65
|
-
{ id: 'graph:publish', label: 'Публикация графов в магазин' },
|
|
66
|
-
];
|
|
67
|
-
|
|
68
|
-
const VIEWER_PERMISSIONS = [
|
|
69
|
-
'bot:list',
|
|
70
|
-
'bot:history:view',
|
|
71
|
-
'plugin:list',
|
|
72
|
-
'plugin:settings:view',
|
|
73
|
-
'management:view',
|
|
74
|
-
'server:list',
|
|
75
|
-
'proxy:list',
|
|
76
|
-
'task:list',
|
|
77
|
-
'graph:read',
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
function ownerOnly(req, res, next) {
|
|
81
|
-
if (req.user && req.user.userId === 1) return next();
|
|
82
|
-
return res.status(403).json({ error: 'Только владелец может изменять права пользователей и роли.' });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* @route GET /api/auth/status
|
|
87
|
-
* @desc Проверяет, была ли произведена первоначальная настройка (создан админ)
|
|
88
|
-
* @access Public
|
|
89
|
-
*/
|
|
90
|
-
router.get('/status', async (req, res) => {
|
|
91
|
-
try {
|
|
92
|
-
const userCount = await prisma.panelUser.count();
|
|
93
|
-
res.json({ needsSetup: userCount === 0 });
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error('[Auth Status Error]', error);
|
|
96
|
-
res.status(500).json({ error: 'Не удалось проверить статус сервера' });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* @route GET /api/auth/config-path
|
|
102
|
-
* @desc Получить путь к конфигурационному файлу
|
|
103
|
-
* @access Public
|
|
104
|
-
*/
|
|
105
|
-
router.get('/config-path', (req, res) => {
|
|
106
|
-
const configPath = path.join(os.homedir(), '.blockmine', 'config.json');
|
|
107
|
-
res.json({ configPath });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* @route POST /api/auth/setup
|
|
112
|
-
* @desc Создает первого пользователя с ролью администратора
|
|
113
|
-
* @access Public (только если нет других пользователей)
|
|
114
|
-
*/
|
|
115
|
-
router.post('/setup', async (req, res) => {
|
|
116
|
-
try {
|
|
117
|
-
const userCount = await prisma.panelUser.count();
|
|
118
|
-
if (userCount > 0) {
|
|
119
|
-
return res.status(403).json({ error: 'Настройка уже произведена.' });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const { username, password } = req.body;
|
|
123
|
-
if (!username || !password || password.length < 4) {
|
|
124
|
-
return res.status(400).json({ error: 'Имя пользователя и пароль (минимум 4 символа) обязательны.' });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const hashedPassword = await bcrypt.hash(password, 12);
|
|
128
|
-
|
|
129
|
-
let newUser;
|
|
130
|
-
|
|
131
|
-
await prisma.$transaction(async (tx) => {
|
|
132
|
-
const adminPermissions = ALL_PERMISSIONS
|
|
133
|
-
.map(p => p.id)
|
|
134
|
-
.filter(id => id !== '*' && id !== 'plugin:develop');
|
|
135
|
-
|
|
136
|
-
const adminRole = await tx.panelRole.upsert({
|
|
137
|
-
where: { name: 'Admin' },
|
|
138
|
-
update: {},
|
|
139
|
-
create: {
|
|
140
|
-
name: 'Admin',
|
|
141
|
-
permissions: JSON.stringify(adminPermissions)
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
newUser = await tx.panelUser.create({
|
|
146
|
-
data: {
|
|
147
|
-
username,
|
|
148
|
-
passwordHash: hashedPassword,
|
|
149
|
-
roleId: adminRole.id,
|
|
150
|
-
},
|
|
151
|
-
include: { role: true }
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const permissions = JSON.parse(newUser.role.permissions || '[]');
|
|
157
|
-
|
|
158
|
-
const payload = {
|
|
159
|
-
userId: newUser.id,
|
|
160
|
-
username: newUser.username,
|
|
161
|
-
permissions: permissions,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
165
|
-
|
|
166
|
-
res.status(201).json({
|
|
167
|
-
token,
|
|
168
|
-
user: {
|
|
169
|
-
id: newUser.id,
|
|
170
|
-
username: newUser.username,
|
|
171
|
-
permissions: permissions,
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
} catch (error) {
|
|
176
|
-
if (error.code === 'P2002') {
|
|
177
|
-
return res.status(409).json({ error: 'Пользователь с таким именем уже существует.' });
|
|
178
|
-
}
|
|
179
|
-
console.error('[Setup Error]', error);
|
|
180
|
-
res.status(500).json({ error: 'Не удалось создать администратора' });
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* @route POST /api/auth/recovery/verify
|
|
186
|
-
* @desc Проверка кода восстановления
|
|
187
|
-
* @access Public
|
|
188
|
-
*/
|
|
189
|
-
router.post('/recovery/verify', async (req, res) => {
|
|
190
|
-
try {
|
|
191
|
-
const { recoveryCode } = req.body;
|
|
192
|
-
|
|
193
|
-
if (!recoveryCode) {
|
|
194
|
-
return res.status(400).json({ error: 'Код восстановления обязателен' });
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (recoveryCode !== config.security.adminRecoveryCode) {
|
|
198
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
199
|
-
return res.status(401).json({ error: 'Неверный код восстановления' });
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const rootUser = await prisma.panelUser.findFirst({
|
|
203
|
-
orderBy: { id: 'asc' },
|
|
204
|
-
include: { role: true }
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (!rootUser) {
|
|
208
|
-
return res.status(404).json({ error: 'В системе нет ни одного пользователя. Выполните первоначальную настройку.' });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
const tokenId = crypto.randomBytes(16).toString('hex');
|
|
212
|
-
const resetToken = jwt.sign(
|
|
213
|
-
{
|
|
214
|
-
userId: rootUser.id,
|
|
215
|
-
type: 'password-reset',
|
|
216
|
-
tokenId: tokenId,
|
|
217
|
-
timestamp: Date.now()
|
|
218
|
-
},
|
|
219
|
-
JWT_SECRET,
|
|
220
|
-
{ expiresIn: '5m' }
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
activeResetTokens.set(tokenId, {
|
|
224
|
-
userId: rootUser.id,
|
|
225
|
-
createdAt: Date.now(),
|
|
226
|
-
used: false
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
setTimeout(() => {
|
|
230
|
-
activeResetTokens.delete(tokenId);
|
|
231
|
-
}, 5 * 60 * 1000);
|
|
232
|
-
|
|
233
|
-
res.json({
|
|
234
|
-
success: true,
|
|
235
|
-
username: rootUser.username,
|
|
236
|
-
resetToken
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
} catch (error) {
|
|
240
|
-
console.error('[Recovery Verify Error]', error);
|
|
241
|
-
res.status(500).json({ error: 'Ошибка при проверке кода восстановления' });
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* @route POST /api/auth/recovery/reset
|
|
247
|
-
* @desc Сброс пароля с использованием токена
|
|
248
|
-
* @access Public (с валидным токеном сброса)
|
|
249
|
-
*/
|
|
250
|
-
router.post('/recovery/reset', async (req, res) => {
|
|
251
|
-
try {
|
|
252
|
-
const { resetToken, newPassword } = req.body;
|
|
253
|
-
|
|
254
|
-
if (!resetToken || !newPassword) {
|
|
255
|
-
return res.status(400).json({ error: 'Токен и новый пароль обязательны' });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (newPassword.length < 4) {
|
|
259
|
-
return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let decoded;
|
|
263
|
-
try {
|
|
264
|
-
decoded = jwt.verify(resetToken, JWT_SECRET);
|
|
265
|
-
if (decoded.type !== 'password-reset') {
|
|
266
|
-
throw new Error('Invalid token type');
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const tokenInfo = activeResetTokens.get(decoded.tokenId);
|
|
270
|
-
if (!tokenInfo) {
|
|
271
|
-
throw new Error('Token not found in active tokens');
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (tokenInfo.used) {
|
|
275
|
-
throw new Error('Token already used');
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (tokenInfo.userId !== decoded.userId) {
|
|
279
|
-
throw new Error('Token userId mismatch');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
} catch (err) {
|
|
283
|
-
return res.status(401).json({ error: 'Недействительный или истекший токен' });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
|
287
|
-
const updatedUser = await prisma.panelUser.update({
|
|
288
|
-
where: { id: decoded.userId },
|
|
289
|
-
data: { passwordHash: hashedPassword },
|
|
290
|
-
select: { username: true }
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const tokenInfo = activeResetTokens.get(decoded.tokenId);
|
|
294
|
-
tokenInfo.used = true;
|
|
295
|
-
|
|
296
|
-
activeResetTokens.delete(decoded.tokenId);
|
|
297
|
-
|
|
298
|
-
res.json({
|
|
299
|
-
message: 'Пароль успешно сброшен',
|
|
300
|
-
username: updatedUser.username
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.error('[Recovery Reset Error]', error);
|
|
305
|
-
res.status(500).json({ error: 'Ошибка при сбросе пароля' });
|
|
306
|
-
}
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* @route POST /api/auth/login
|
|
311
|
-
* @desc Аутентифицирует пользователя и возвращает токен
|
|
312
|
-
* @access Public
|
|
313
|
-
*/
|
|
314
|
-
router.post('/login', async (req, res) => {
|
|
315
|
-
try {
|
|
316
|
-
const { username, password } = req.body;
|
|
317
|
-
if (!username || !password) {
|
|
318
|
-
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const user = await prisma.panelUser.findUnique({
|
|
322
|
-
where: { username },
|
|
323
|
-
include: { role: true },
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
if (!user) {
|
|
327
|
-
return res.status(401).json({ error: 'Неверные учетные данные' });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
|
331
|
-
if (!isMatch) {
|
|
332
|
-
return res.status(401).json({ error: 'Неверные учетные данные' });
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let permissions = JSON.parse(user.role.permissions || '[]');
|
|
336
|
-
|
|
337
|
-
// Владелец (первый пользователь) всегда получает все актуальные права
|
|
338
|
-
if (user.id === 1) {
|
|
339
|
-
permissions = ALL_PERMISSIONS.map(p => p.id).filter(id => id !== '*');
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const payload = {
|
|
343
|
-
userId: user.id,
|
|
344
|
-
username: user.username,
|
|
345
|
-
permissions: permissions,
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
349
|
-
|
|
350
|
-
res.json({
|
|
351
|
-
token,
|
|
352
|
-
user: {
|
|
353
|
-
id: user.id,
|
|
354
|
-
username: user.username,
|
|
355
|
-
permissions: permissions,
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
} catch (error) {
|
|
360
|
-
console.error('[Login Error]', error);
|
|
361
|
-
res.status(500).json({ error: 'Ошибка входа в систему' });
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* @route GET /api/auth/validate
|
|
368
|
-
* @desc Validate Panel API key
|
|
369
|
-
* @access Public (requires API key)
|
|
370
|
-
*/
|
|
371
|
-
router.get('/validate', authenticatePanelApiKey, async (req, res) => {
|
|
372
|
-
res.json({
|
|
373
|
-
valid: true,
|
|
374
|
-
user: {
|
|
375
|
-
id: req.user.id,
|
|
376
|
-
username: req.user.username,
|
|
377
|
-
role: req.user.roleName,
|
|
378
|
-
permissions: req.user.permissions
|
|
379
|
-
},
|
|
380
|
-
apiKey: {
|
|
381
|
-
id: req.apiKey.id,
|
|
382
|
-
name: req.apiKey.name,
|
|
383
|
-
prefix: req.apiKey.prefix
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* @route GET /api/auth/me
|
|
390
|
-
* @desc Получить данные текущего пользователя по токену
|
|
391
|
-
* @access Private
|
|
392
|
-
*/
|
|
393
|
-
router.get('/me', authenticate, async (req, res) => {
|
|
394
|
-
try {
|
|
395
|
-
const user = await prisma.panelUser.findUnique({
|
|
396
|
-
where: { id: req.user.userId },
|
|
397
|
-
select: { id: true, username: true, role: { select: { permissions: true } } }
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
if (!user) {
|
|
401
|
-
return res.status(404).json({ error: "Пользователь не найден." });
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
res.json({
|
|
405
|
-
id: user.id,
|
|
406
|
-
username: user.username,
|
|
407
|
-
permissions: JSON.parse(user.role.permissions || '[]')
|
|
408
|
-
});
|
|
409
|
-
} catch (error) {
|
|
410
|
-
res.status(500).json({ error: "Ошибка сервера" });
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* @route GET /api/auth/users
|
|
417
|
-
* @desc Получить всех пользователей и их роли
|
|
418
|
-
* @access Private (Admin only)
|
|
419
|
-
*/
|
|
420
|
-
router.get('/users', authenticate, authorize('panel:user:list'), async (req, res) => {
|
|
421
|
-
try {
|
|
422
|
-
const users = await prisma.panelUser.findMany({
|
|
423
|
-
include: { role: true, botAccess: { include: { bot: { select: { id: true, username: true } } } } },
|
|
424
|
-
orderBy: { username: 'asc' }
|
|
425
|
-
});
|
|
426
|
-
res.json(users.map(u => ({
|
|
427
|
-
...u,
|
|
428
|
-
botAccess: u.botAccess.map(a => ({ botId: a.botId, bot: a.bot }))
|
|
429
|
-
})));
|
|
430
|
-
} catch (error) {
|
|
431
|
-
res.status(500).json({ error: 'Не удалось получить пользователей' });
|
|
432
|
-
}
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* @route POST /api/auth/users
|
|
437
|
-
* @desc Создать нового пользователя
|
|
438
|
-
* @access Private (Admin only)
|
|
439
|
-
*/
|
|
440
|
-
router.post('/users', authenticate, ownerOnly, authorize('panel:user:create'), async (req, res) => {
|
|
441
|
-
const { username, password, roleId, allBots = true, botIds = [] } = req.body;
|
|
442
|
-
if (!username || !password || !roleId) {
|
|
443
|
-
return res.status(400).json({ error: 'Имя, пароль и роль обязательны' });
|
|
444
|
-
}
|
|
445
|
-
if (password.length < 4) {
|
|
446
|
-
return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
const hashedPassword = await bcrypt.hash(password, 12);
|
|
451
|
-
const newUser = await prisma.panelUser.create({
|
|
452
|
-
data: {
|
|
453
|
-
username,
|
|
454
|
-
passwordHash: hashedPassword,
|
|
455
|
-
roleId: parseInt(roleId, 10),
|
|
456
|
-
allBots: !!allBots,
|
|
457
|
-
botAccess: allBots ? undefined : {
|
|
458
|
-
create: (Array.isArray(botIds) ? botIds : []).map(id => ({ bot: { connect: { id: Number(id) } } }))
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
include: { role: true, botAccess: true }
|
|
462
|
-
});
|
|
463
|
-
const { passwordHash, ...userToReturn } = newUser;
|
|
464
|
-
res.status(201).json(userToReturn);
|
|
465
|
-
} catch (error) {
|
|
466
|
-
if (error.code === 'P2002') {
|
|
467
|
-
return res.status(409).json({ error: 'Пользователь с таким именем уже существует' });
|
|
468
|
-
}
|
|
469
|
-
res.status(500).json({ error: 'Не удалось создать пользователя' });
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* @route GET /api/auth/roles
|
|
476
|
-
* @desc Получить все роли
|
|
477
|
-
* @access Private (Admin only)
|
|
478
|
-
*/
|
|
479
|
-
router.get('/roles', authenticate, authorize('panel:role:list'), async (req, res) => {
|
|
480
|
-
try {
|
|
481
|
-
const roles = await prisma.panelRole.findMany();
|
|
482
|
-
const rolesWithParsedPermissions = roles.map(role => ({
|
|
483
|
-
...role,
|
|
484
|
-
permissions: JSON.parse(role.permissions || '[]')
|
|
485
|
-
}));
|
|
486
|
-
res.json(rolesWithParsedPermissions);
|
|
487
|
-
} catch (error) {
|
|
488
|
-
res.status(500).json({ error: 'Не удалось получить роли' });
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* @route GET /api/auth/permissions
|
|
495
|
-
* @desc Получить список всех возможных прав в системе
|
|
496
|
-
* @access Private
|
|
497
|
-
*/
|
|
498
|
-
router.get('/permissions', authenticate, (req, res) => {
|
|
499
|
-
res.json(ALL_PERMISSIONS);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
/**
|
|
504
|
-
* @route PUT /api/auth/users/:id
|
|
505
|
-
* @desc Обновить пользователя (роль, пароль)
|
|
506
|
-
* @access Private (Admin only)
|
|
507
|
-
*/
|
|
508
|
-
router.put('/users/:id', authenticate, ownerOnly, authorize('panel:user:edit'), async (req, res) => {
|
|
509
|
-
const userId = parseInt(req.params.id, 10);
|
|
510
|
-
const { password, roleId, allBots, botIds } = req.body;
|
|
511
|
-
|
|
512
|
-
try {
|
|
513
|
-
const owner = await prisma.panelUser.findFirst({ orderBy: { id: 'asc' } });
|
|
514
|
-
const isOwner = owner && owner.id === userId;
|
|
515
|
-
|
|
516
|
-
const updateData = {};
|
|
517
|
-
if (password) {
|
|
518
|
-
if (password.length < 4) return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
519
|
-
updateData.passwordHash = await bcrypt.hash(password, 12);
|
|
520
|
-
}
|
|
521
|
-
if (roleId) {
|
|
522
|
-
updateData.roleId = parseInt(roleId, 10);
|
|
523
|
-
}
|
|
524
|
-
if (!isOwner && typeof allBots === 'boolean') {
|
|
525
|
-
updateData.allBots = allBots;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const updatedUser = await prisma.panelUser.update({
|
|
529
|
-
where: { id: userId },
|
|
530
|
-
data: updateData,
|
|
531
|
-
include: { role: true }
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
if (Array.isArray(botIds)) {
|
|
535
|
-
await prisma.panelUserBotAccess.deleteMany({ where: { userId } });
|
|
536
|
-
if (!isOwner && !updatedUser.allBots && botIds.length > 0) {
|
|
537
|
-
await prisma.panelUserBotAccess.createMany({
|
|
538
|
-
data: botIds.map(id => ({ userId, botId: Number(id) }))
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const { passwordHash, ...userToReturn } = updatedUser;
|
|
544
|
-
res.json(userToReturn);
|
|
545
|
-
|
|
546
|
-
} catch (error) {
|
|
547
|
-
res.status(500).json({ error: 'Не удалось обновить пользователя' });
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* @route DELETE /api/auth/users/:id
|
|
553
|
-
* @desc Удалить пользователя
|
|
554
|
-
* @access Private (Admin only)
|
|
555
|
-
*/
|
|
556
|
-
router.delete('/users/:id', authenticate, ownerOnly, authorize('panel:user:delete'), async (req, res) => {
|
|
557
|
-
const userId = parseInt(req.params.id, 10);
|
|
558
|
-
|
|
559
|
-
if (req.user.userId === userId) {
|
|
560
|
-
return res.status(403).json({ error: 'Вы не можете удалить свою собственную учетную запись.' });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
try {
|
|
564
|
-
const owner = await prisma.panelUser.findFirst({ orderBy: { id: 'asc' } });
|
|
565
|
-
if (owner && owner.id === userId) {
|
|
566
|
-
return res.status(403).json({ error: 'Нельзя удалить владельца системы.' });
|
|
567
|
-
}
|
|
568
|
-
await prisma.panelUser.delete({ where: { id: userId } });
|
|
569
|
-
res.status(204).send();
|
|
570
|
-
} catch (error) {
|
|
571
|
-
if (error.code === 'P2025') {
|
|
572
|
-
return res.status(404).json({ error: 'Пользователь не найден' });
|
|
573
|
-
}
|
|
574
|
-
res.status(500).json({ error: 'Не удалось удалить пользователя' });
|
|
575
|
-
}
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* @route POST /api/auth/roles
|
|
580
|
-
* @desc Создать новую роль
|
|
581
|
-
* @access Private (Admin only)
|
|
582
|
-
*/
|
|
583
|
-
router.post('/roles', authenticate, ownerOnly, authorize('panel:role:create'), async (req, res) => {
|
|
584
|
-
const { name, permissions } = req.body;
|
|
585
|
-
if (!name || !Array.isArray(permissions)) {
|
|
586
|
-
return res.status(400).json({ error: 'Имя и массив прав обязательны' });
|
|
587
|
-
}
|
|
588
|
-
try {
|
|
589
|
-
const newRole = await prisma.panelRole.create({
|
|
590
|
-
data: {
|
|
591
|
-
name,
|
|
592
|
-
permissions: JSON.stringify(permissions)
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
res.status(201).json({
|
|
596
|
-
...newRole,
|
|
597
|
-
permissions: JSON.parse(newRole.permissions)
|
|
598
|
-
});
|
|
599
|
-
} catch (error) {
|
|
600
|
-
if (error.code === 'P2002') {
|
|
601
|
-
return res.status(409).json({ error: 'Роль с таким именем уже существует' });
|
|
602
|
-
}
|
|
603
|
-
res.status(500).json({ error: 'Не удалось создать роль' });
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* @route PUT /api/auth/roles/:id
|
|
609
|
-
* @desc Обновить роль (имя и права)
|
|
610
|
-
* @access Private (Admin only)
|
|
611
|
-
*/
|
|
612
|
-
router.put('/roles/:id', authenticate, ownerOnly, authorize('panel:role:edit'), async (req, res) => {
|
|
613
|
-
const roleId = parseInt(req.params.id, 10);
|
|
614
|
-
const { name, permissions } = req.body;
|
|
615
|
-
if (!name || !Array.isArray(permissions)) {
|
|
616
|
-
return res.status(400).json({ error: 'Имя и массив прав обязательны' });
|
|
617
|
-
}
|
|
618
|
-
try {
|
|
619
|
-
const role = await prisma.panelRole.findUnique({ where: { id: roleId } });
|
|
620
|
-
if (role && role.name === 'Admin') {
|
|
621
|
-
return res.status(403).json({ error: 'Редактирование роли "Admin" запрещено.' });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const updatedRole = await prisma.panelRole.update({
|
|
625
|
-
where: { id: roleId },
|
|
626
|
-
data: {
|
|
627
|
-
name,
|
|
628
|
-
permissions: JSON.stringify(permissions)
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
res.json({
|
|
632
|
-
...updatedRole,
|
|
633
|
-
permissions: JSON.parse(updatedRole.permissions)
|
|
634
|
-
});
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if (error.code === 'P2002') {
|
|
637
|
-
return res.status(409).json({ error: 'Роль с таким именем уже существует' });
|
|
638
|
-
}
|
|
639
|
-
res.status(500).json({ error: 'Не удалось обновить роль' });
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* @route DELETE /api/auth/roles/:id
|
|
645
|
-
* @desc Удалить роль
|
|
646
|
-
* @access Private (Admin only)
|
|
647
|
-
*/
|
|
648
|
-
router.delete('/roles/:id', authenticate, ownerOnly, authorize('panel:role:delete'), async (req, res) => {
|
|
649
|
-
const roleId = parseInt(req.params.id, 10);
|
|
650
|
-
try {
|
|
651
|
-
const role = await prisma.panelRole.findUnique({ where: { id: roleId }, include: { users: true } });
|
|
652
|
-
if (!role) return res.status(404).json({ error: 'Роль не найдена' });
|
|
653
|
-
if (role.name === 'Admin') {
|
|
654
|
-
return res.status(403).json({ error: 'Удаление роли "Admin" запрещено.' });
|
|
655
|
-
}
|
|
656
|
-
if (role.users.length > 0) {
|
|
657
|
-
return res.status(400).json({ error: 'Нельзя удалить роль, которая назначена пользователям.' });
|
|
658
|
-
}
|
|
659
|
-
await prisma.panelRole.delete({ where: { id: roleId } });
|
|
660
|
-
res.status(204).send();
|
|
661
|
-
} catch (error) {
|
|
662
|
-
res.status(500).json({ error: 'Не удалось удалить роль' });
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
module.exports = {
|
|
667
|
-
router,
|
|
668
|
-
ALL_PERMISSIONS,
|
|
669
|
-
VIEWER_PERMISSIONS,
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const bcrypt = require('bcryptjs');
|
|
3
|
+
const jwt = require('jsonwebtoken');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { PrismaClient } = require('@prisma/client');
|
|
6
|
+
const config = require('../../config');
|
|
7
|
+
const { authenticate, authorize } = require('../middleware/auth');
|
|
8
|
+
const { authenticatePanelApiKey } = require('../middleware/panelApiAuth');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
|
|
12
|
+
const router = express.Router();
|
|
13
|
+
const prisma = new PrismaClient();
|
|
14
|
+
|
|
15
|
+
const JWT_SECRET = config.security.jwtSecret;
|
|
16
|
+
const JWT_EXPIRES_IN = '7d';
|
|
17
|
+
|
|
18
|
+
const activeResetTokens = new Map();
|
|
19
|
+
|
|
20
|
+
const ALL_PERMISSIONS = [
|
|
21
|
+
{ id: '*', label: 'Все права (Администратор)' },
|
|
22
|
+
{ id: 'bot:list', label: 'Просмотр ботов' },
|
|
23
|
+
{ id: 'bot:create', label: 'Создание ботов' },
|
|
24
|
+
{ id: 'bot:update', label: 'Редактирование ботов' },
|
|
25
|
+
{ id: 'bot:delete', label: 'Удаление ботов' },
|
|
26
|
+
{ id: 'bot:start_stop', label: 'Запуск/остановка ботов' },
|
|
27
|
+
{ id: 'bot:interact', label: 'Взаимодействие с ботом (консоль)' },
|
|
28
|
+
{ id: 'bot:export', label: 'Экспорт ботов' },
|
|
29
|
+
{ id: 'bot:import', label: 'Импорт ботов' },
|
|
30
|
+
{ id: 'bot:history:view', label: 'Просмотр истории чата и команд' },
|
|
31
|
+
{ id: 'bot:history:manage', label: 'Управление историей (очистка)' },
|
|
32
|
+
{ id: 'management:view', label: 'Просмотр вкладки "Управление" у бота' },
|
|
33
|
+
{ id: 'management:edit', label: 'Редактирование на вкладке "Управление" у бота' },
|
|
34
|
+
{ id: 'plugin:list', label: 'Просмотр плагинов' },
|
|
35
|
+
{ id: 'plugin:install', label: 'Установка плагинов' },
|
|
36
|
+
{ id: 'plugin:delete', label: 'Удаление плагинов' },
|
|
37
|
+
{ id: 'plugin:update', label: 'Обновление плагинов' },
|
|
38
|
+
{ id: 'plugin:settings:view', label: 'Просмотр настроек плагинов' },
|
|
39
|
+
{ id: 'plugin:settings:edit', label: 'Редактирование настроек плагинов' },
|
|
40
|
+
{ id: 'plugin:browse', label: 'Просмотр каталога плагинов' },
|
|
41
|
+
{ id: 'plugin:develop', label: 'Разработка и редактирование плагинов (IDE)' },
|
|
42
|
+
{ id: 'server:list', label: 'Просмотр серверов' },
|
|
43
|
+
{ id: 'server:create', label: 'Создание серверов' },
|
|
44
|
+
{ id: 'server:delete', label: 'Удаление серверов' },
|
|
45
|
+
{ id: 'proxy:list', label: 'Просмотр прокси' },
|
|
46
|
+
{ id: 'proxy:create', label: 'Создание и редактирование прокси' },
|
|
47
|
+
{ id: 'proxy:delete', label: 'Удаление прокси' },
|
|
48
|
+
{ id: 'task:list', label: 'Просмотр задач' },
|
|
49
|
+
{ id: 'task:create', label: 'Создание задач' },
|
|
50
|
+
{ id: 'task:edit', label: 'Редактирование задач' },
|
|
51
|
+
{ id: 'task:delete', label: 'Удаление задач' },
|
|
52
|
+
{ id: 'panel:user:list', label: 'Просмотр пользователей панели' },
|
|
53
|
+
{ id: 'panel:user:create', label: 'Создание пользователей панели' },
|
|
54
|
+
{ id: 'panel:user:edit', label: 'Редактирование пользователей панели' },
|
|
55
|
+
{ id: 'panel:user:delete', label: 'Удаление пользователей панели' },
|
|
56
|
+
{ id: 'panel:role:list', label: 'Просмотр ролей панели' },
|
|
57
|
+
{ id: 'panel:role:create', label: 'Создание ролей панели' },
|
|
58
|
+
{ id: 'panel:role:edit', label: 'Редактирование ролей панели' },
|
|
59
|
+
{ id: 'panel:role:delete', label: 'Удаление ролей панели' },
|
|
60
|
+
{ id: 'panel:settings:view', label: 'Просмотр глобальных настроек' },
|
|
61
|
+
{ id: 'panel:settings:edit', label: 'Редактирование глобальных настроек' },
|
|
62
|
+
{ id: 'graph:read', label: 'Просмотр магазина графов' },
|
|
63
|
+
{ id: 'graph:download', label: 'Скачивание графов из магазина' },
|
|
64
|
+
{ id: 'graph:like', label: 'Лайки графов в магазине' },
|
|
65
|
+
{ id: 'graph:publish', label: 'Публикация графов в магазин' },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const VIEWER_PERMISSIONS = [
|
|
69
|
+
'bot:list',
|
|
70
|
+
'bot:history:view',
|
|
71
|
+
'plugin:list',
|
|
72
|
+
'plugin:settings:view',
|
|
73
|
+
'management:view',
|
|
74
|
+
'server:list',
|
|
75
|
+
'proxy:list',
|
|
76
|
+
'task:list',
|
|
77
|
+
'graph:read',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function ownerOnly(req, res, next) {
|
|
81
|
+
if (req.user && req.user.userId === 1) return next();
|
|
82
|
+
return res.status(403).json({ error: 'Только владелец может изменять права пользователей и роли.' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @route GET /api/auth/status
|
|
87
|
+
* @desc Проверяет, была ли произведена первоначальная настройка (создан админ)
|
|
88
|
+
* @access Public
|
|
89
|
+
*/
|
|
90
|
+
router.get('/status', async (req, res) => {
|
|
91
|
+
try {
|
|
92
|
+
const userCount = await prisma.panelUser.count();
|
|
93
|
+
res.json({ needsSetup: userCount === 0 });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('[Auth Status Error]', error);
|
|
96
|
+
res.status(500).json({ error: 'Не удалось проверить статус сервера' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @route GET /api/auth/config-path
|
|
102
|
+
* @desc Получить путь к конфигурационному файлу
|
|
103
|
+
* @access Public
|
|
104
|
+
*/
|
|
105
|
+
router.get('/config-path', (req, res) => {
|
|
106
|
+
const configPath = path.join(os.homedir(), '.blockmine', 'config.json');
|
|
107
|
+
res.json({ configPath });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @route POST /api/auth/setup
|
|
112
|
+
* @desc Создает первого пользователя с ролью администратора
|
|
113
|
+
* @access Public (только если нет других пользователей)
|
|
114
|
+
*/
|
|
115
|
+
router.post('/setup', async (req, res) => {
|
|
116
|
+
try {
|
|
117
|
+
const userCount = await prisma.panelUser.count();
|
|
118
|
+
if (userCount > 0) {
|
|
119
|
+
return res.status(403).json({ error: 'Настройка уже произведена.' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const { username, password } = req.body;
|
|
123
|
+
if (!username || !password || password.length < 4) {
|
|
124
|
+
return res.status(400).json({ error: 'Имя пользователя и пароль (минимум 4 символа) обязательны.' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
128
|
+
|
|
129
|
+
let newUser;
|
|
130
|
+
|
|
131
|
+
await prisma.$transaction(async (tx) => {
|
|
132
|
+
const adminPermissions = ALL_PERMISSIONS
|
|
133
|
+
.map(p => p.id)
|
|
134
|
+
.filter(id => id !== '*' && id !== 'plugin:develop');
|
|
135
|
+
|
|
136
|
+
const adminRole = await tx.panelRole.upsert({
|
|
137
|
+
where: { name: 'Admin' },
|
|
138
|
+
update: {},
|
|
139
|
+
create: {
|
|
140
|
+
name: 'Admin',
|
|
141
|
+
permissions: JSON.stringify(adminPermissions)
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
newUser = await tx.panelUser.create({
|
|
146
|
+
data: {
|
|
147
|
+
username,
|
|
148
|
+
passwordHash: hashedPassword,
|
|
149
|
+
roleId: adminRole.id,
|
|
150
|
+
},
|
|
151
|
+
include: { role: true }
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
const permissions = JSON.parse(newUser.role.permissions || '[]');
|
|
157
|
+
|
|
158
|
+
const payload = {
|
|
159
|
+
userId: newUser.id,
|
|
160
|
+
username: newUser.username,
|
|
161
|
+
permissions: permissions,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
165
|
+
|
|
166
|
+
res.status(201).json({
|
|
167
|
+
token,
|
|
168
|
+
user: {
|
|
169
|
+
id: newUser.id,
|
|
170
|
+
username: newUser.username,
|
|
171
|
+
permissions: permissions,
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error.code === 'P2002') {
|
|
177
|
+
return res.status(409).json({ error: 'Пользователь с таким именем уже существует.' });
|
|
178
|
+
}
|
|
179
|
+
console.error('[Setup Error]', error);
|
|
180
|
+
res.status(500).json({ error: 'Не удалось создать администратора' });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @route POST /api/auth/recovery/verify
|
|
186
|
+
* @desc Проверка кода восстановления
|
|
187
|
+
* @access Public
|
|
188
|
+
*/
|
|
189
|
+
router.post('/recovery/verify', async (req, res) => {
|
|
190
|
+
try {
|
|
191
|
+
const { recoveryCode } = req.body;
|
|
192
|
+
|
|
193
|
+
if (!recoveryCode) {
|
|
194
|
+
return res.status(400).json({ error: 'Код восстановления обязателен' });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (recoveryCode !== config.security.adminRecoveryCode) {
|
|
198
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
199
|
+
return res.status(401).json({ error: 'Неверный код восстановления' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rootUser = await prisma.panelUser.findFirst({
|
|
203
|
+
orderBy: { id: 'asc' },
|
|
204
|
+
include: { role: true }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!rootUser) {
|
|
208
|
+
return res.status(404).json({ error: 'В системе нет ни одного пользователя. Выполните первоначальную настройку.' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tokenId = crypto.randomBytes(16).toString('hex');
|
|
212
|
+
const resetToken = jwt.sign(
|
|
213
|
+
{
|
|
214
|
+
userId: rootUser.id,
|
|
215
|
+
type: 'password-reset',
|
|
216
|
+
tokenId: tokenId,
|
|
217
|
+
timestamp: Date.now()
|
|
218
|
+
},
|
|
219
|
+
JWT_SECRET,
|
|
220
|
+
{ expiresIn: '5m' }
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
activeResetTokens.set(tokenId, {
|
|
224
|
+
userId: rootUser.id,
|
|
225
|
+
createdAt: Date.now(),
|
|
226
|
+
used: false
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
activeResetTokens.delete(tokenId);
|
|
231
|
+
}, 5 * 60 * 1000);
|
|
232
|
+
|
|
233
|
+
res.json({
|
|
234
|
+
success: true,
|
|
235
|
+
username: rootUser.username,
|
|
236
|
+
resetToken
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('[Recovery Verify Error]', error);
|
|
241
|
+
res.status(500).json({ error: 'Ошибка при проверке кода восстановления' });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @route POST /api/auth/recovery/reset
|
|
247
|
+
* @desc Сброс пароля с использованием токена
|
|
248
|
+
* @access Public (с валидным токеном сброса)
|
|
249
|
+
*/
|
|
250
|
+
router.post('/recovery/reset', async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const { resetToken, newPassword } = req.body;
|
|
253
|
+
|
|
254
|
+
if (!resetToken || !newPassword) {
|
|
255
|
+
return res.status(400).json({ error: 'Токен и новый пароль обязательны' });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (newPassword.length < 4) {
|
|
259
|
+
return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
let decoded;
|
|
263
|
+
try {
|
|
264
|
+
decoded = jwt.verify(resetToken, JWT_SECRET);
|
|
265
|
+
if (decoded.type !== 'password-reset') {
|
|
266
|
+
throw new Error('Invalid token type');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tokenInfo = activeResetTokens.get(decoded.tokenId);
|
|
270
|
+
if (!tokenInfo) {
|
|
271
|
+
throw new Error('Token not found in active tokens');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (tokenInfo.used) {
|
|
275
|
+
throw new Error('Token already used');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (tokenInfo.userId !== decoded.userId) {
|
|
279
|
+
throw new Error('Token userId mismatch');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return res.status(401).json({ error: 'Недействительный или истекший токен' });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const hashedPassword = await bcrypt.hash(newPassword, 12);
|
|
287
|
+
const updatedUser = await prisma.panelUser.update({
|
|
288
|
+
where: { id: decoded.userId },
|
|
289
|
+
data: { passwordHash: hashedPassword },
|
|
290
|
+
select: { username: true }
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const tokenInfo = activeResetTokens.get(decoded.tokenId);
|
|
294
|
+
tokenInfo.used = true;
|
|
295
|
+
|
|
296
|
+
activeResetTokens.delete(decoded.tokenId);
|
|
297
|
+
|
|
298
|
+
res.json({
|
|
299
|
+
message: 'Пароль успешно сброшен',
|
|
300
|
+
username: updatedUser.username
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error('[Recovery Reset Error]', error);
|
|
305
|
+
res.status(500).json({ error: 'Ошибка при сбросе пароля' });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @route POST /api/auth/login
|
|
311
|
+
* @desc Аутентифицирует пользователя и возвращает токен
|
|
312
|
+
* @access Public
|
|
313
|
+
*/
|
|
314
|
+
router.post('/login', async (req, res) => {
|
|
315
|
+
try {
|
|
316
|
+
const { username, password } = req.body;
|
|
317
|
+
if (!username || !password) {
|
|
318
|
+
return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const user = await prisma.panelUser.findUnique({
|
|
322
|
+
where: { username },
|
|
323
|
+
include: { role: true },
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!user) {
|
|
327
|
+
return res.status(401).json({ error: 'Неверные учетные данные' });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const isMatch = await bcrypt.compare(password, user.passwordHash);
|
|
331
|
+
if (!isMatch) {
|
|
332
|
+
return res.status(401).json({ error: 'Неверные учетные данные' });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let permissions = JSON.parse(user.role.permissions || '[]');
|
|
336
|
+
|
|
337
|
+
// Владелец (первый пользователь) всегда получает все актуальные права
|
|
338
|
+
if (user.id === 1) {
|
|
339
|
+
permissions = ALL_PERMISSIONS.map(p => p.id).filter(id => id !== '*');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const payload = {
|
|
343
|
+
userId: user.id,
|
|
344
|
+
username: user.username,
|
|
345
|
+
permissions: permissions,
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
|
349
|
+
|
|
350
|
+
res.json({
|
|
351
|
+
token,
|
|
352
|
+
user: {
|
|
353
|
+
id: user.id,
|
|
354
|
+
username: user.username,
|
|
355
|
+
permissions: permissions,
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
} catch (error) {
|
|
360
|
+
console.error('[Login Error]', error);
|
|
361
|
+
res.status(500).json({ error: 'Ошибка входа в систему' });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @route GET /api/auth/validate
|
|
368
|
+
* @desc Validate Panel API key
|
|
369
|
+
* @access Public (requires API key)
|
|
370
|
+
*/
|
|
371
|
+
router.get('/validate', authenticatePanelApiKey, async (req, res) => {
|
|
372
|
+
res.json({
|
|
373
|
+
valid: true,
|
|
374
|
+
user: {
|
|
375
|
+
id: req.user.id,
|
|
376
|
+
username: req.user.username,
|
|
377
|
+
role: req.user.roleName,
|
|
378
|
+
permissions: req.user.permissions
|
|
379
|
+
},
|
|
380
|
+
apiKey: {
|
|
381
|
+
id: req.apiKey.id,
|
|
382
|
+
name: req.apiKey.name,
|
|
383
|
+
prefix: req.apiKey.prefix
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* @route GET /api/auth/me
|
|
390
|
+
* @desc Получить данные текущего пользователя по токену
|
|
391
|
+
* @access Private
|
|
392
|
+
*/
|
|
393
|
+
router.get('/me', authenticate, async (req, res) => {
|
|
394
|
+
try {
|
|
395
|
+
const user = await prisma.panelUser.findUnique({
|
|
396
|
+
where: { id: req.user.userId },
|
|
397
|
+
select: { id: true, username: true, role: { select: { permissions: true } } }
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
if (!user) {
|
|
401
|
+
return res.status(404).json({ error: "Пользователь не найден." });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
res.json({
|
|
405
|
+
id: user.id,
|
|
406
|
+
username: user.username,
|
|
407
|
+
permissions: JSON.parse(user.role.permissions || '[]')
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
res.status(500).json({ error: "Ошибка сервера" });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @route GET /api/auth/users
|
|
417
|
+
* @desc Получить всех пользователей и их роли
|
|
418
|
+
* @access Private (Admin only)
|
|
419
|
+
*/
|
|
420
|
+
router.get('/users', authenticate, authorize('panel:user:list'), async (req, res) => {
|
|
421
|
+
try {
|
|
422
|
+
const users = await prisma.panelUser.findMany({
|
|
423
|
+
include: { role: true, botAccess: { include: { bot: { select: { id: true, username: true } } } } },
|
|
424
|
+
orderBy: { username: 'asc' }
|
|
425
|
+
});
|
|
426
|
+
res.json(users.map(u => ({
|
|
427
|
+
...u,
|
|
428
|
+
botAccess: u.botAccess.map(a => ({ botId: a.botId, bot: a.bot }))
|
|
429
|
+
})));
|
|
430
|
+
} catch (error) {
|
|
431
|
+
res.status(500).json({ error: 'Не удалось получить пользователей' });
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* @route POST /api/auth/users
|
|
437
|
+
* @desc Создать нового пользователя
|
|
438
|
+
* @access Private (Admin only)
|
|
439
|
+
*/
|
|
440
|
+
router.post('/users', authenticate, ownerOnly, authorize('panel:user:create'), async (req, res) => {
|
|
441
|
+
const { username, password, roleId, allBots = true, botIds = [] } = req.body;
|
|
442
|
+
if (!username || !password || !roleId) {
|
|
443
|
+
return res.status(400).json({ error: 'Имя, пароль и роль обязательны' });
|
|
444
|
+
}
|
|
445
|
+
if (password.length < 4) {
|
|
446
|
+
return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const hashedPassword = await bcrypt.hash(password, 12);
|
|
451
|
+
const newUser = await prisma.panelUser.create({
|
|
452
|
+
data: {
|
|
453
|
+
username,
|
|
454
|
+
passwordHash: hashedPassword,
|
|
455
|
+
roleId: parseInt(roleId, 10),
|
|
456
|
+
allBots: !!allBots,
|
|
457
|
+
botAccess: allBots ? undefined : {
|
|
458
|
+
create: (Array.isArray(botIds) ? botIds : []).map(id => ({ bot: { connect: { id: Number(id) } } }))
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
include: { role: true, botAccess: true }
|
|
462
|
+
});
|
|
463
|
+
const { passwordHash, ...userToReturn } = newUser;
|
|
464
|
+
res.status(201).json(userToReturn);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error.code === 'P2002') {
|
|
467
|
+
return res.status(409).json({ error: 'Пользователь с таким именем уже существует' });
|
|
468
|
+
}
|
|
469
|
+
res.status(500).json({ error: 'Не удалось создать пользователя' });
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @route GET /api/auth/roles
|
|
476
|
+
* @desc Получить все роли
|
|
477
|
+
* @access Private (Admin only)
|
|
478
|
+
*/
|
|
479
|
+
router.get('/roles', authenticate, authorize('panel:role:list'), async (req, res) => {
|
|
480
|
+
try {
|
|
481
|
+
const roles = await prisma.panelRole.findMany();
|
|
482
|
+
const rolesWithParsedPermissions = roles.map(role => ({
|
|
483
|
+
...role,
|
|
484
|
+
permissions: JSON.parse(role.permissions || '[]')
|
|
485
|
+
}));
|
|
486
|
+
res.json(rolesWithParsedPermissions);
|
|
487
|
+
} catch (error) {
|
|
488
|
+
res.status(500).json({ error: 'Не удалось получить роли' });
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* @route GET /api/auth/permissions
|
|
495
|
+
* @desc Получить список всех возможных прав в системе
|
|
496
|
+
* @access Private
|
|
497
|
+
*/
|
|
498
|
+
router.get('/permissions', authenticate, (req, res) => {
|
|
499
|
+
res.json(ALL_PERMISSIONS);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @route PUT /api/auth/users/:id
|
|
505
|
+
* @desc Обновить пользователя (роль, пароль)
|
|
506
|
+
* @access Private (Admin only)
|
|
507
|
+
*/
|
|
508
|
+
router.put('/users/:id', authenticate, ownerOnly, authorize('panel:user:edit'), async (req, res) => {
|
|
509
|
+
const userId = parseInt(req.params.id, 10);
|
|
510
|
+
const { password, roleId, allBots, botIds } = req.body;
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const owner = await prisma.panelUser.findFirst({ orderBy: { id: 'asc' } });
|
|
514
|
+
const isOwner = owner && owner.id === userId;
|
|
515
|
+
|
|
516
|
+
const updateData = {};
|
|
517
|
+
if (password) {
|
|
518
|
+
if (password.length < 4) return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
|
|
519
|
+
updateData.passwordHash = await bcrypt.hash(password, 12);
|
|
520
|
+
}
|
|
521
|
+
if (roleId) {
|
|
522
|
+
updateData.roleId = parseInt(roleId, 10);
|
|
523
|
+
}
|
|
524
|
+
if (!isOwner && typeof allBots === 'boolean') {
|
|
525
|
+
updateData.allBots = allBots;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const updatedUser = await prisma.panelUser.update({
|
|
529
|
+
where: { id: userId },
|
|
530
|
+
data: updateData,
|
|
531
|
+
include: { role: true }
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
if (Array.isArray(botIds)) {
|
|
535
|
+
await prisma.panelUserBotAccess.deleteMany({ where: { userId } });
|
|
536
|
+
if (!isOwner && !updatedUser.allBots && botIds.length > 0) {
|
|
537
|
+
await prisma.panelUserBotAccess.createMany({
|
|
538
|
+
data: botIds.map(id => ({ userId, botId: Number(id) }))
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const { passwordHash, ...userToReturn } = updatedUser;
|
|
544
|
+
res.json(userToReturn);
|
|
545
|
+
|
|
546
|
+
} catch (error) {
|
|
547
|
+
res.status(500).json({ error: 'Не удалось обновить пользователя' });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* @route DELETE /api/auth/users/:id
|
|
553
|
+
* @desc Удалить пользователя
|
|
554
|
+
* @access Private (Admin only)
|
|
555
|
+
*/
|
|
556
|
+
router.delete('/users/:id', authenticate, ownerOnly, authorize('panel:user:delete'), async (req, res) => {
|
|
557
|
+
const userId = parseInt(req.params.id, 10);
|
|
558
|
+
|
|
559
|
+
if (req.user.userId === userId) {
|
|
560
|
+
return res.status(403).json({ error: 'Вы не можете удалить свою собственную учетную запись.' });
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const owner = await prisma.panelUser.findFirst({ orderBy: { id: 'asc' } });
|
|
565
|
+
if (owner && owner.id === userId) {
|
|
566
|
+
return res.status(403).json({ error: 'Нельзя удалить владельца системы.' });
|
|
567
|
+
}
|
|
568
|
+
await prisma.panelUser.delete({ where: { id: userId } });
|
|
569
|
+
res.status(204).send();
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code === 'P2025') {
|
|
572
|
+
return res.status(404).json({ error: 'Пользователь не найден' });
|
|
573
|
+
}
|
|
574
|
+
res.status(500).json({ error: 'Не удалось удалить пользователя' });
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @route POST /api/auth/roles
|
|
580
|
+
* @desc Создать новую роль
|
|
581
|
+
* @access Private (Admin only)
|
|
582
|
+
*/
|
|
583
|
+
router.post('/roles', authenticate, ownerOnly, authorize('panel:role:create'), async (req, res) => {
|
|
584
|
+
const { name, permissions } = req.body;
|
|
585
|
+
if (!name || !Array.isArray(permissions)) {
|
|
586
|
+
return res.status(400).json({ error: 'Имя и массив прав обязательны' });
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const newRole = await prisma.panelRole.create({
|
|
590
|
+
data: {
|
|
591
|
+
name,
|
|
592
|
+
permissions: JSON.stringify(permissions)
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
res.status(201).json({
|
|
596
|
+
...newRole,
|
|
597
|
+
permissions: JSON.parse(newRole.permissions)
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (error.code === 'P2002') {
|
|
601
|
+
return res.status(409).json({ error: 'Роль с таким именем уже существует' });
|
|
602
|
+
}
|
|
603
|
+
res.status(500).json({ error: 'Не удалось создать роль' });
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* @route PUT /api/auth/roles/:id
|
|
609
|
+
* @desc Обновить роль (имя и права)
|
|
610
|
+
* @access Private (Admin only)
|
|
611
|
+
*/
|
|
612
|
+
router.put('/roles/:id', authenticate, ownerOnly, authorize('panel:role:edit'), async (req, res) => {
|
|
613
|
+
const roleId = parseInt(req.params.id, 10);
|
|
614
|
+
const { name, permissions } = req.body;
|
|
615
|
+
if (!name || !Array.isArray(permissions)) {
|
|
616
|
+
return res.status(400).json({ error: 'Имя и массив прав обязательны' });
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
const role = await prisma.panelRole.findUnique({ where: { id: roleId } });
|
|
620
|
+
if (role && role.name === 'Admin') {
|
|
621
|
+
return res.status(403).json({ error: 'Редактирование роли "Admin" запрещено.' });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const updatedRole = await prisma.panelRole.update({
|
|
625
|
+
where: { id: roleId },
|
|
626
|
+
data: {
|
|
627
|
+
name,
|
|
628
|
+
permissions: JSON.stringify(permissions)
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
res.json({
|
|
632
|
+
...updatedRole,
|
|
633
|
+
permissions: JSON.parse(updatedRole.permissions)
|
|
634
|
+
});
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (error.code === 'P2002') {
|
|
637
|
+
return res.status(409).json({ error: 'Роль с таким именем уже существует' });
|
|
638
|
+
}
|
|
639
|
+
res.status(500).json({ error: 'Не удалось обновить роль' });
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* @route DELETE /api/auth/roles/:id
|
|
645
|
+
* @desc Удалить роль
|
|
646
|
+
* @access Private (Admin only)
|
|
647
|
+
*/
|
|
648
|
+
router.delete('/roles/:id', authenticate, ownerOnly, authorize('panel:role:delete'), async (req, res) => {
|
|
649
|
+
const roleId = parseInt(req.params.id, 10);
|
|
650
|
+
try {
|
|
651
|
+
const role = await prisma.panelRole.findUnique({ where: { id: roleId }, include: { users: true } });
|
|
652
|
+
if (!role) return res.status(404).json({ error: 'Роль не найдена' });
|
|
653
|
+
if (role.name === 'Admin') {
|
|
654
|
+
return res.status(403).json({ error: 'Удаление роли "Admin" запрещено.' });
|
|
655
|
+
}
|
|
656
|
+
if (role.users.length > 0) {
|
|
657
|
+
return res.status(400).json({ error: 'Нельзя удалить роль, которая назначена пользователям.' });
|
|
658
|
+
}
|
|
659
|
+
await prisma.panelRole.delete({ where: { id: roleId } });
|
|
660
|
+
res.status(204).send();
|
|
661
|
+
} catch (error) {
|
|
662
|
+
res.status(500).json({ error: 'Не удалось удалить роль' });
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
module.exports = {
|
|
667
|
+
router,
|
|
668
|
+
ALL_PERMISSIONS,
|
|
669
|
+
VIEWER_PERMISSIONS,
|
|
670
670
|
};
|