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