blockmine 1.3.22 → 1.4.1

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/backend/cli.js CHANGED
@@ -1,25 +1,23 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const { execSync } = require('child_process');
6
+
7
+ const config = require('./src/config.js');
6
8
  const { startServer } = require('./src/server.js');
7
9
 
8
10
  const DATA_DIR = path.join(os.homedir(), '.blockmine');
9
11
  if (!fs.existsSync(DATA_DIR)) {
10
12
  fs.mkdirSync(DATA_DIR, { recursive: true });
11
13
  }
12
- process.env.DATABASE_URL = `file:${path.join(DATA_DIR, 'blockmine.db')}`;
13
-
14
- if (os.platform() === 'android') {
15
- process.env.PRISMA_CLI_BINARY_TARGETS = 'linux-arm64-openssl-1.1.x';
16
- }
17
14
 
15
+ process.env.DATABASE_URL = `file:${path.join(DATA_DIR, 'blockmine.db')}`;
18
16
 
19
17
  function runCommand(command) {
20
18
  try {
21
19
  console.log(`> ${command}`);
22
- execSync(command, { stdio: 'inherit', cwd: __dirname, env: process.env });
20
+ execSync(command, { stdio: 'inherit', cwd: __dirname });
23
21
  } catch (e) {
24
22
  console.error(`Команда "${command}" не удалась:`, e);
25
23
  process.exit(1);
@@ -18,6 +18,7 @@
18
18
  "@prisma/client": "^5.14.0",
19
19
  "adm-zip": "^0.5.16",
20
20
  "archiver": "^7.0.1",
21
+ "bcryptjs": "^3.0.2",
21
22
  "cron-parser": "^5.3.0",
22
23
  "express": "^4.19.2",
23
24
  "fs-extra": "^11.3.0",
@@ -34,4 +35,4 @@
34
35
  "nodemon": "^3.1.2",
35
36
  "prisma": "^5.14.0"
36
37
  }
37
- }
38
+ }
@@ -0,0 +1,26 @@
1
+ -- CreateTable
2
+ CREATE TABLE "PanelUser" (
3
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
4
+ "uuid" TEXT NOT NULL,
5
+ "username" TEXT NOT NULL,
6
+ "passwordHash" TEXT NOT NULL,
7
+ "roleId" INTEGER NOT NULL,
8
+ "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
9
+ CONSTRAINT "PanelUser_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "PanelRole" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
10
+ );
11
+
12
+ -- CreateTable
13
+ CREATE TABLE "PanelRole" (
14
+ "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
15
+ "name" TEXT NOT NULL,
16
+ "permissions" TEXT NOT NULL DEFAULT '[]'
17
+ );
18
+
19
+ -- CreateIndex
20
+ CREATE UNIQUE INDEX "PanelUser_uuid_key" ON "PanelUser"("uuid");
21
+
22
+ -- CreateIndex
23
+ CREATE UNIQUE INDEX "PanelUser_username_key" ON "PanelUser"("username");
24
+
25
+ -- CreateIndex
26
+ CREATE UNIQUE INDEX "PanelRole_name_key" ON "PanelRole"("name");
@@ -1,6 +1,5 @@
1
1
  generator client {
2
2
  provider = "prisma-client-js"
3
- binaryTargets = ["native", "linux-arm64-openssl-1.1.x"]
4
3
  }
5
4
 
6
5
  datasource db {
@@ -149,4 +148,25 @@ model ScheduledTask {
149
148
  lastRun DateTime?
150
149
  createdAt DateTime @default(now())
151
150
  updatedAt DateTime @updatedAt
151
+ }
152
+
153
+ model PanelUser {
154
+ id Int @id @default(autoincrement())
155
+ uuid String @unique @default(uuid())
156
+ username String @unique
157
+ passwordHash String
158
+ role PanelRole @relation(fields: [roleId], references: [id])
159
+ roleId Int
160
+ createdAt DateTime @default(now())
161
+ }
162
+
163
+ // Роли пользователей (Admin, Moderator, Viewer)
164
+ model PanelRole {
165
+ id Int @id @default(autoincrement())
166
+ name String @unique // Например, "Admin", "Moderator", "Viewer"
167
+
168
+ // Храним права как джсон строку. SQLite не поддерживает массивы.
169
+ // Пример: '["bot:create", "bot:delete", "user:manage"]'
170
+ permissions String @default("[]")
171
+ users PanelUser[]
152
172
  }
@@ -0,0 +1,59 @@
1
+
2
+ const jwt = require('jsonwebtoken');
3
+ const config = require('../../config');
4
+
5
+ const JWT_SECRET = config.security.jwtSecret;
6
+
7
+ /**
8
+ * Middleware для проверки JWT-токена.
9
+ * Извлекает токен из заголовка Authorization, проверяет его подлинность
10
+ * и добавляет расшифрованные данные (payload) в req.user.
11
+ */
12
+ function authenticate(req, res, next) {
13
+ const authHeader = req.header('Authorization');
14
+
15
+ if (!authHeader) {
16
+ return res.status(401).json({ error: 'Нет токена, доступ запрещен' });
17
+ }
18
+
19
+ const tokenParts = authHeader.split(' ');
20
+ if (tokenParts.length !== 2 || tokenParts[0] !== 'Bearer') {
21
+ return res.status(401).json({ error: 'Неверный формат токена' });
22
+ }
23
+
24
+ const token = tokenParts[1];
25
+
26
+ try {
27
+ const decoded = jwt.verify(token, JWT_SECRET);
28
+ req.user = decoded;
29
+ next();
30
+ } catch (err) {
31
+ res.status(401).json({ error: 'Невалидный токен' });
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Middleware-фабрика для проверки прав доступа.
37
+ * @param {string} requiredPermission - Право, необходимое для доступа к роуту (например, 'bot:delete').
38
+ * @returns {function} - Express middleware.
39
+ */
40
+ function authorize(requiredPermission) {
41
+ return (req, res, next) => {
42
+ if (!req.user || !req.user.permissions) {
43
+ return res.status(403).json({ error: 'Ошибка прав доступа: пользователь не аутентифицирован.' });
44
+ }
45
+
46
+ const userPermissions = req.user.permissions;
47
+
48
+ if (userPermissions.includes('*') || userPermissions.includes(requiredPermission)) {
49
+ next();
50
+ } else {
51
+ res.status(403).json({ error: 'Доступ запрещен: недостаточно прав.' });
52
+ }
53
+ };
54
+ }
55
+
56
+ module.exports = {
57
+ authenticate,
58
+ authorize,
59
+ };
@@ -0,0 +1,445 @@
1
+
2
+ const express = require('express');
3
+ const bcrypt = require('bcryptjs');
4
+ const jwt = require('jsonwebtoken');
5
+ const { PrismaClient } = require('@prisma/client');
6
+ const config = require('../../config');
7
+ const { authenticate, authorize } = require('../middleware/auth');
8
+
9
+ const router = express.Router();
10
+ const prisma = new PrismaClient();
11
+
12
+ const JWT_SECRET = config.security.jwtSecret;
13
+ const JWT_EXPIRES_IN = '7d';
14
+
15
+ /**
16
+ * @route GET /api/auth/status
17
+ * @desc Проверяет, была ли произведена первоначальная настройка (создан админ)
18
+ * @access Public
19
+ */
20
+ router.get('/status', async (req, res) => {
21
+ try {
22
+ const userCount = await prisma.panelUser.count();
23
+ res.json({ needsSetup: userCount === 0 });
24
+ } catch (error) {
25
+ console.error('[Auth Status Error]', error);
26
+ res.status(500).json({ error: 'Не удалось проверить статус сервера' });
27
+ }
28
+ });
29
+
30
+ /**
31
+ * @route POST /api/auth/setup
32
+ * @desc Создает первого пользователя с ролью администратора
33
+ * @access Public (только если нет других пользователей)
34
+ */
35
+ router.post('/setup', async (req, res) => {
36
+ try {
37
+ const userCount = await prisma.panelUser.count();
38
+ if (userCount > 0) {
39
+ return res.status(403).json({ error: 'Настройка уже произведена.' });
40
+ }
41
+
42
+ const { username, password } = req.body;
43
+ if (!username || !password || password.length < 4) {
44
+ return res.status(400).json({ error: 'Имя пользователя и пароль (минимум 4 символа) обязательны.' });
45
+ }
46
+
47
+ const hashedPassword = await bcrypt.hash(password, 12);
48
+
49
+ let newUser;
50
+
51
+ await prisma.$transaction(async (tx) => {
52
+ const adminRole = await tx.panelRole.upsert({
53
+ where: { name: 'Admin' },
54
+ update: {},
55
+ create: {
56
+ name: 'Admin',
57
+ permissions: JSON.stringify(['*'])
58
+ },
59
+ });
60
+
61
+ newUser = await tx.panelUser.create({
62
+ data: {
63
+ username,
64
+ passwordHash: hashedPassword,
65
+ roleId: adminRole.id,
66
+ },
67
+ include: { role: true }
68
+ });
69
+ });
70
+
71
+
72
+ const permissions = JSON.parse(newUser.role.permissions || '[]');
73
+
74
+ const payload = {
75
+ userId: newUser.id,
76
+ username: newUser.username,
77
+ permissions: permissions,
78
+ };
79
+
80
+ const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
81
+
82
+ res.status(201).json({
83
+ token,
84
+ user: {
85
+ id: newUser.id,
86
+ username: newUser.username,
87
+ permissions: permissions,
88
+ }
89
+ });
90
+
91
+ } catch (error) {
92
+ if (error.code === 'P2002') {
93
+ return res.status(409).json({ error: 'Пользователь с таким именем уже существует.' });
94
+ }
95
+ console.error('[Setup Error]', error);
96
+ res.status(500).json({ error: 'Не удалось создать администратора' });
97
+ }
98
+ });
99
+
100
+ /**
101
+ * @route POST /api/auth/login
102
+ * @desc Аутентифицирует пользователя и возвращает токен
103
+ * @access Public
104
+ */
105
+ router.post('/login', async (req, res) => {
106
+ try {
107
+ const { username, password } = req.body;
108
+ if (!username || !password) {
109
+ return res.status(400).json({ error: 'Имя пользователя и пароль обязательны' });
110
+ }
111
+
112
+ const user = await prisma.panelUser.findUnique({
113
+ where: { username },
114
+ include: { role: true },
115
+ });
116
+
117
+ if (!user) {
118
+ return res.status(401).json({ error: 'Неверные учетные данные' });
119
+ }
120
+
121
+ const isMatch = await bcrypt.compare(password, user.passwordHash);
122
+ if (!isMatch) {
123
+ return res.status(401).json({ error: 'Неверные учетные данные' });
124
+ }
125
+
126
+ const permissions = JSON.parse(user.role.permissions || '[]');
127
+
128
+ const payload = {
129
+ userId: user.id,
130
+ username: user.username,
131
+ permissions: permissions,
132
+ };
133
+
134
+ const token = jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
135
+
136
+ res.json({
137
+ token,
138
+ user: {
139
+ id: user.id,
140
+ username: user.username,
141
+ permissions: permissions,
142
+ }
143
+ });
144
+
145
+ } catch (error) {
146
+ console.error('[Login Error]', error);
147
+ res.status(500).json({ error: 'Ошибка входа в систему' });
148
+ }
149
+ });
150
+
151
+
152
+ /**
153
+ * @route GET /api/auth/me
154
+ * @desc Получить данные текущего пользователя по токену
155
+ * @access Private
156
+ */
157
+ router.get('/me', authenticate, async (req, res) => {
158
+ try {
159
+ const user = await prisma.panelUser.findUnique({
160
+ where: { id: req.user.userId },
161
+ select: { id: true, username: true, role: { select: { permissions: true } } }
162
+ });
163
+
164
+ if (!user) {
165
+ return res.status(404).json({ error: "Пользователь не найден." });
166
+ }
167
+
168
+ res.json({
169
+ id: user.id,
170
+ username: user.username,
171
+ permissions: JSON.parse(user.role.permissions || '[]')
172
+ });
173
+ } catch (error) {
174
+ res.status(500).json({ error: "Ошибка сервера" });
175
+ }
176
+ });
177
+
178
+
179
+ /**
180
+ * @route GET /api/auth/users
181
+ * @desc Получить всех пользователей и их роли
182
+ * @access Private (Admin only)
183
+ */
184
+ router.get('/users', authenticate, authorize('panel:user:list'), async (req, res) => {
185
+ try {
186
+ const users = await prisma.panelUser.findMany({
187
+ include: { role: true },
188
+ orderBy: { username: 'asc' }
189
+ });
190
+ res.json(users);
191
+ } catch (error) {
192
+ res.status(500).json({ error: 'Не удалось получить пользователей' });
193
+ }
194
+ });
195
+
196
+ /**
197
+ * @route POST /api/auth/users
198
+ * @desc Создать нового пользователя
199
+ * @access Private (Admin only)
200
+ */
201
+ router.post('/users', authenticate, authorize('panel:user:create'), async (req, res) => {
202
+ const { username, password, roleId } = req.body;
203
+ if (!username || !password || !roleId) {
204
+ return res.status(400).json({ error: 'Имя, пароль и роль обязательны' });
205
+ }
206
+ if (password.length < 4) {
207
+ return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
208
+ }
209
+
210
+ try {
211
+ const hashedPassword = await bcrypt.hash(password, 12);
212
+ const newUser = await prisma.panelUser.create({
213
+ data: {
214
+ username,
215
+ passwordHash: hashedPassword,
216
+ roleId: parseInt(roleId, 10)
217
+ },
218
+ include: { role: true }
219
+ });
220
+ const { passwordHash, ...userToReturn } = newUser;
221
+ res.status(201).json(userToReturn);
222
+ } catch (error) {
223
+ if (error.code === 'P2002') {
224
+ return res.status(409).json({ error: 'Пользователь с таким именем уже существует' });
225
+ }
226
+ res.status(500).json({ error: 'Не удалось создать пользователя' });
227
+ }
228
+ });
229
+
230
+
231
+ /**
232
+ * @route GET /api/auth/roles
233
+ * @desc Получить все роли
234
+ * @access Private (Admin only)
235
+ */
236
+ router.get('/roles', authenticate, authorize('panel:role:list'), async (req, res) => {
237
+ try {
238
+ const roles = await prisma.panelRole.findMany();
239
+ const rolesWithParsedPermissions = roles.map(role => ({
240
+ ...role,
241
+ permissions: JSON.parse(role.permissions || '[]')
242
+ }));
243
+ res.json(rolesWithParsedPermissions);
244
+ } catch (error) {
245
+ res.status(500).json({ error: 'Не удалось получить роли' });
246
+ }
247
+ });
248
+
249
+ const ALL_PERMISSIONS = [
250
+ { id: '*', label: 'Все права (Администратор)' },
251
+ { id: 'bot:list', label: 'Просмотр ботов' },
252
+ { id: 'bot:create', label: 'Создание ботов' },
253
+ { id: 'bot:update', label: 'Редактирование ботов' },
254
+ { id: 'bot:delete', label: 'Удаление ботов' },
255
+ { id: 'bot:start_stop', label: 'Запуск/остановка ботов' },
256
+ { id: 'bot:interact', label: 'Взаимодействие с ботом (консоль)' },
257
+ { id: 'bot:export', label: 'Экспорт ботов' },
258
+ { id: 'bot:import', label: 'Импорт ботов' },
259
+ { id: 'management:view', label: 'Просмотр вкладки "Управление" у бота' },
260
+ { id: 'management:edit', label: 'Редактирование на вкладке "Управление" у бота' },
261
+ { id: 'plugin:list', label: 'Просмотр плагинов' },
262
+ { id: 'plugin:install', label: 'Установка плагинов' },
263
+ { id: 'plugin:delete', label: 'Удаление плагинов' },
264
+ { id: 'plugin:update', label: 'Обновление плагинов' },
265
+ { id: 'plugin:settings:view', label: 'Просмотр настроек плагинов' },
266
+ { id: 'plugin:settings:edit', label: 'Редактирование настроек плагинов' },
267
+ { id: 'plugin:browse', label: 'Просмотр каталога плагинов' },
268
+ { id: 'server:list', label: 'Просмотр серверов' },
269
+ { id: 'server:create', label: 'Создание серверов' },
270
+ { id: 'server:delete', label: 'Удаление серверов' },
271
+ { id: 'task:list', label: 'Просмотр задач' },
272
+ { id: 'task:create', label: 'Создание задач' },
273
+ { id: 'task:edit', label: 'Редактирование задач' },
274
+ { id: 'task:delete', label: 'Удаление задач' },
275
+ { id: 'panel:user:list', label: 'Просмотр пользователей панели' },
276
+ { id: 'panel:user:create', label: 'Создание пользователей панели' },
277
+ { id: 'panel:user:edit', label: 'Редактирование пользователей панели' },
278
+ { id: 'panel:user:delete', label: 'Удаление пользователей панели' },
279
+ { id: 'panel:role:list', label: 'Просмотр ролей панели' },
280
+ { id: 'panel:role:create', label: 'Создание ролей панели' },
281
+ { id: 'panel:role:edit', label: 'Редактирование ролей панели' },
282
+ { id: 'panel:role:delete', label: 'Удаление ролей панели' },
283
+ { id: 'panel:settings:view', label: 'Просмотр глобальных настроек' },
284
+ { id: 'panel:settings:edit', label: 'Редактирование глобальных настроек' },
285
+ ];
286
+
287
+ /**
288
+ * @route GET /api/auth/permissions
289
+ * @desc Получить список всех возможных прав в системе
290
+ * @access Private
291
+ */
292
+ router.get('/permissions', authenticate, (req, res) => {
293
+ res.json(ALL_PERMISSIONS);
294
+ });
295
+
296
+
297
+ /**
298
+ * @route PUT /api/auth/users/:id
299
+ * @desc Обновить пользователя (роль, пароль)
300
+ * @access Private (Admin only)
301
+ */
302
+ router.put('/users/:id', authenticate, authorize('panel:user:edit'), async (req, res) => {
303
+ const userId = parseInt(req.params.id, 10);
304
+ const { password, roleId } = req.body;
305
+
306
+ try {
307
+ const updateData = {};
308
+ if (password) {
309
+ if (password.length < 4) return res.status(400).json({ error: 'Пароль должен быть не менее 4 символов' });
310
+ updateData.passwordHash = await bcrypt.hash(password, 12);
311
+ }
312
+ if (roleId) {
313
+ updateData.roleId = parseInt(roleId, 10);
314
+ }
315
+
316
+ if (Object.keys(updateData).length === 0) {
317
+ return res.status(400).json({ error: 'Нет данных для обновления' });
318
+ }
319
+
320
+ const updatedUser = await prisma.panelUser.update({
321
+ where: { id: userId },
322
+ data: updateData,
323
+ include: { role: true }
324
+ });
325
+
326
+ const { passwordHash, ...userToReturn } = updatedUser;
327
+ res.json(userToReturn);
328
+
329
+ } catch (error) {
330
+ res.status(500).json({ error: 'Не удалось обновить пользователя' });
331
+ }
332
+ });
333
+
334
+ /**
335
+ * @route DELETE /api/auth/users/:id
336
+ * @desc Удалить пользователя
337
+ * @access Private (Admin only)
338
+ */
339
+ router.delete('/users/:id', authenticate, authorize('panel:user:delete'), async (req, res) => {
340
+ const userId = parseInt(req.params.id, 10);
341
+
342
+ if (req.user.userId === userId) {
343
+ return res.status(403).json({ error: 'Вы не можете удалить свою собственную учетную запись.' });
344
+ }
345
+
346
+ try {
347
+ await prisma.panelUser.delete({ where: { id: userId } });
348
+ res.status(204).send();
349
+ } catch (error) {
350
+ if (error.code === 'P2025') {
351
+ return res.status(404).json({ error: 'Пользователь не найден' });
352
+ }
353
+ res.status(500).json({ error: 'Не удалось удалить пользователя' });
354
+ }
355
+ });
356
+
357
+ /**
358
+ * @route POST /api/auth/roles
359
+ * @desc Создать новую роль
360
+ * @access Private (Admin only)
361
+ */
362
+ router.post('/roles', authenticate, authorize('panel:role:create'), async (req, res) => {
363
+ const { name, permissions } = req.body;
364
+ if (!name || !Array.isArray(permissions)) {
365
+ return res.status(400).json({ error: 'Имя и массив прав обязательны' });
366
+ }
367
+ try {
368
+ const newRole = await prisma.panelRole.create({
369
+ data: {
370
+ name,
371
+ permissions: JSON.stringify(permissions)
372
+ }
373
+ });
374
+ res.status(201).json({
375
+ ...newRole,
376
+ permissions: JSON.parse(newRole.permissions)
377
+ });
378
+ } catch (error) {
379
+ if (error.code === 'P2002') {
380
+ return res.status(409).json({ error: 'Роль с таким именем уже существует' });
381
+ }
382
+ res.status(500).json({ error: 'Не удалось создать роль' });
383
+ }
384
+ });
385
+
386
+ /**
387
+ * @route PUT /api/auth/roles/:id
388
+ * @desc Обновить роль (имя и права)
389
+ * @access Private (Admin only)
390
+ */
391
+ router.put('/roles/:id', authenticate, authorize('panel:role:edit'), async (req, res) => {
392
+ const roleId = parseInt(req.params.id, 10);
393
+ const { name, permissions } = req.body;
394
+ if (!name || !Array.isArray(permissions)) {
395
+ return res.status(400).json({ error: 'Имя и массив прав обязательны' });
396
+ }
397
+ try {
398
+ const role = await prisma.panelRole.findUnique({ where: { id: roleId } });
399
+ if (role && role.name === 'Admin') {
400
+ return res.status(403).json({ error: 'Редактирование роли "Admin" запрещено.' });
401
+ }
402
+
403
+ const updatedRole = await prisma.panelRole.update({
404
+ where: { id: roleId },
405
+ data: {
406
+ name,
407
+ permissions: JSON.stringify(permissions)
408
+ }
409
+ });
410
+ res.json({
411
+ ...updatedRole,
412
+ permissions: JSON.parse(updatedRole.permissions)
413
+ });
414
+ } catch (error) {
415
+ if (error.code === 'P2002') {
416
+ return res.status(409).json({ error: 'Роль с таким именем уже существует' });
417
+ }
418
+ res.status(500).json({ error: 'Не удалось обновить роль' });
419
+ }
420
+ });
421
+
422
+ /**
423
+ * @route DELETE /api/auth/roles/:id
424
+ * @desc Удалить роль
425
+ * @access Private (Admin only)
426
+ */
427
+ router.delete('/roles/:id', authenticate, authorize('panel:role:delete'), async (req, res) => {
428
+ const roleId = parseInt(req.params.id, 10);
429
+ try {
430
+ const role = await prisma.panelRole.findUnique({ where: { id: roleId }, include: { users: true } });
431
+ if (!role) return res.status(404).json({ error: 'Роль не найдена' });
432
+ if (role.name === 'Admin') {
433
+ return res.status(403).json({ error: 'Удаление роли "Admin" запрещено.' });
434
+ }
435
+ if (role.users.length > 0) {
436
+ return res.status(400).json({ error: 'Нельзя удалить роль, которая назначена пользователям.' });
437
+ }
438
+ await prisma.panelRole.delete({ where: { id: roleId } });
439
+ res.status(204).send();
440
+ } catch (error) {
441
+ res.status(500).json({ error: 'Не удалось удалить роль' });
442
+ }
443
+ });
444
+
445
+ module.exports = router;