blockmine 1.5.5 → 1.5.7
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/src/api/routes/auth.js +10 -3
- package/backend/src/api/routes/bots.js +2 -0
- package/backend/src/api/routes/pluginIde.js +463 -0
- package/backend/src/api/routes/plugins.js +4 -6
- package/backend/src/core/PluginManager.js +7 -3
- package/backend/src/server.js +44 -5
- package/frontend/dist/assets/index-BRG5IJlS.js +8203 -0
- package/frontend/dist/assets/index-CxAe5KlR.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/package.json +2 -0
- package/package.json +4 -1
- package/frontend/dist/assets/index-BSQ59n1K.js +0 -8203
- package/frontend/dist/assets/index-DC4RjP6E.css +0 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
const express = require('express');
|
|
3
2
|
const bcrypt = require('bcryptjs');
|
|
4
3
|
const jwt = require('jsonwebtoken');
|
|
@@ -49,12 +48,16 @@ router.post('/setup', async (req, res) => {
|
|
|
49
48
|
let newUser;
|
|
50
49
|
|
|
51
50
|
await prisma.$transaction(async (tx) => {
|
|
51
|
+
const adminPermissions = ALL_PERMISSIONS
|
|
52
|
+
.map(p => p.id)
|
|
53
|
+
.filter(id => id !== '*' && id !== 'plugin:develop');
|
|
54
|
+
|
|
52
55
|
const adminRole = await tx.panelRole.upsert({
|
|
53
56
|
where: { name: 'Admin' },
|
|
54
57
|
update: {},
|
|
55
58
|
create: {
|
|
56
59
|
name: 'Admin',
|
|
57
|
-
permissions: JSON.stringify(
|
|
60
|
+
permissions: JSON.stringify(adminPermissions)
|
|
58
61
|
},
|
|
59
62
|
});
|
|
60
63
|
|
|
@@ -265,6 +268,7 @@ const ALL_PERMISSIONS = [
|
|
|
265
268
|
{ id: 'plugin:settings:view', label: 'Просмотр настроек плагинов' },
|
|
266
269
|
{ id: 'plugin:settings:edit', label: 'Редактирование настроек плагинов' },
|
|
267
270
|
{ id: 'plugin:browse', label: 'Просмотр каталога плагинов' },
|
|
271
|
+
{ id: 'plugin:develop', label: 'Разработка и редактирование плагинов (IDE)' },
|
|
268
272
|
{ id: 'server:list', label: 'Просмотр серверов' },
|
|
269
273
|
{ id: 'server:create', label: 'Создание серверов' },
|
|
270
274
|
{ id: 'server:delete', label: 'Удаление серверов' },
|
|
@@ -442,4 +446,7 @@ router.delete('/roles/:id', authenticate, authorize('panel:role:delete'), async
|
|
|
442
446
|
}
|
|
443
447
|
});
|
|
444
448
|
|
|
445
|
-
module.exports =
|
|
449
|
+
module.exports = {
|
|
450
|
+
router,
|
|
451
|
+
ALL_PERMISSIONS,
|
|
452
|
+
};
|
|
@@ -10,6 +10,7 @@ const { authenticate, authorize } = require('../middleware/auth');
|
|
|
10
10
|
const { encrypt } = require('../../core/utils/crypto');
|
|
11
11
|
const { randomUUID } = require('crypto');
|
|
12
12
|
const eventGraphsRouter = require('./eventGraphs');
|
|
13
|
+
const pluginIdeRouter = require('./pluginIde');
|
|
13
14
|
|
|
14
15
|
const multer = require('multer');
|
|
15
16
|
const archiver = require('archiver');
|
|
@@ -22,6 +23,7 @@ const router = express.Router();
|
|
|
22
23
|
|
|
23
24
|
router.use(authenticate);
|
|
24
25
|
router.use('/:botId/event-graphs', eventGraphsRouter);
|
|
26
|
+
router.use('/:botId/plugins/ide', pluginIdeRouter);
|
|
25
27
|
|
|
26
28
|
async function setupDefaultPermissionsForBot(botId, prismaClient = prisma) {
|
|
27
29
|
const initialData = {
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { authenticate, authorize } = require('../middleware/auth');
|
|
3
|
+
const fse = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { PrismaClient } = require('@prisma/client');
|
|
7
|
+
const slugify = require('slugify');
|
|
8
|
+
|
|
9
|
+
const prisma = new PrismaClient();
|
|
10
|
+
const router = express.Router({ mergeParams: true });
|
|
11
|
+
|
|
12
|
+
const DATA_DIR = path.join(os.homedir(), '.blockmine');
|
|
13
|
+
const PLUGINS_BASE_DIR = path.join(DATA_DIR, 'storage', 'plugins');
|
|
14
|
+
|
|
15
|
+
// All routes in this file require plugin development permission
|
|
16
|
+
router.use(authenticate, authorize('plugin:develop'));
|
|
17
|
+
|
|
18
|
+
// Middleware to resolve plugin path and ensure it's safe
|
|
19
|
+
const resolvePluginPath = async (req, res, next) => {
|
|
20
|
+
try {
|
|
21
|
+
const { botId, pluginName } = req.params;
|
|
22
|
+
if (!pluginName) {
|
|
23
|
+
return res.status(400).json({ error: 'Имя плагина обязательно в пути.' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
|
|
27
|
+
const pluginPath = path.resolve(botPluginsDir, pluginName);
|
|
28
|
+
|
|
29
|
+
// Security check: ensure the resolved path is still within the bot's plugins directory
|
|
30
|
+
if (!pluginPath.startsWith(botPluginsDir)) {
|
|
31
|
+
return res.status(403).json({ error: 'Доступ запрещен: попытка доступа за пределы директории плагина.' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!await fse.pathExists(pluginPath)) {
|
|
35
|
+
return res.status(404).json({ error: 'Директория плагина не найдена.' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Attach the safe path to the request object
|
|
39
|
+
req.pluginPath = pluginPath;
|
|
40
|
+
next();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('[Plugin IDE Middleware Error]', error);
|
|
43
|
+
res.status(500).json({ error: 'Не удалось определить путь к плагину.' });
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
router.post('/create', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { botId } = req.params;
|
|
50
|
+
const {
|
|
51
|
+
name,
|
|
52
|
+
version = '1.0.0',
|
|
53
|
+
description = '',
|
|
54
|
+
author = '',
|
|
55
|
+
template = 'empty' // 'empty' or 'command'
|
|
56
|
+
} = req.body;
|
|
57
|
+
|
|
58
|
+
if (!name) {
|
|
59
|
+
return res.status(400).json({ error: 'Имя плагина обязательно.' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pluginNameSlug = slugify(name, { lower: true, strict: true });
|
|
63
|
+
const botPluginsDir = path.join(PLUGINS_BASE_DIR, `bot_${botId}`);
|
|
64
|
+
const pluginPath = path.join(botPluginsDir, pluginNameSlug);
|
|
65
|
+
|
|
66
|
+
if (await fse.pathExists(pluginPath)) {
|
|
67
|
+
return res.status(409).json({ error: `Плагин с именем "${pluginNameSlug}" уже существует физически.` });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const existingPlugin = await prisma.installedPlugin.findFirst({
|
|
71
|
+
where: { botId: parseInt(botId), name: pluginNameSlug }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (existingPlugin) {
|
|
75
|
+
return res.status(409).json({ error: `Плагин с именем "${pluginNameSlug}" уже зарегистрирован для этого бота.` });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await fse.mkdirp(pluginPath);
|
|
79
|
+
|
|
80
|
+
const packageJson = {
|
|
81
|
+
name: pluginNameSlug,
|
|
82
|
+
version,
|
|
83
|
+
description,
|
|
84
|
+
author,
|
|
85
|
+
botpanel: {
|
|
86
|
+
main: 'index.js',
|
|
87
|
+
settings: {
|
|
88
|
+
"helloMessage": {
|
|
89
|
+
"type": "string",
|
|
90
|
+
"label": "Сообщение приветствия",
|
|
91
|
+
"description": "Используйте {targetUser} и {user.username} для подстановок.",
|
|
92
|
+
"default": "Привет, {targetUser}, от {user.username}!"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
await fse.writeFile(path.join(pluginPath, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
98
|
+
|
|
99
|
+
if (template === 'command') {
|
|
100
|
+
await fse.mkdirp(path.join(pluginPath, 'commands'));
|
|
101
|
+
|
|
102
|
+
const commandContent = `module.exports = (bot) => {
|
|
103
|
+
const Command = bot.api.Command;
|
|
104
|
+
|
|
105
|
+
class HelloCommand extends Command {
|
|
106
|
+
constructor(settings) {
|
|
107
|
+
super({
|
|
108
|
+
name: 'hello',
|
|
109
|
+
description: 'Тестовая команда с расширенными параметрами',
|
|
110
|
+
aliases: ['hi', 'привет'],
|
|
111
|
+
permissions: '${pluginNameSlug}.use',
|
|
112
|
+
owner: 'plugin:${pluginNameSlug}',
|
|
113
|
+
cooldown: 10,
|
|
114
|
+
allowedChatTypes: ['chat'],
|
|
115
|
+
args: [
|
|
116
|
+
{
|
|
117
|
+
name: 'targetUser',
|
|
118
|
+
type: 'string',
|
|
119
|
+
description: 'Имя игрока, которого нужно поприветствовать.',
|
|
120
|
+
required: true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'repeat',
|
|
124
|
+
type: 'number',
|
|
125
|
+
description: 'Количество повторений приветствия.',
|
|
126
|
+
required: false,
|
|
127
|
+
default: 1
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.settings = settings;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async handler(bot, typeChat, user, args) {
|
|
136
|
+
const { targetUser, repeat } = args;
|
|
137
|
+
|
|
138
|
+
// Используем сообщение из настроек, или значение по-умолчанию, если оно не найдено
|
|
139
|
+
const messageTemplate = this.settings?.helloMessage || 'Привет, {targetUser}, от {user.username}!';
|
|
140
|
+
|
|
141
|
+
const message = messageTemplate
|
|
142
|
+
.replace('{targetUser}', targetUser)
|
|
143
|
+
.replace('{user.username}', user.username);
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < repeat; i++) {
|
|
146
|
+
await bot.api.sendMessage(typeChat, message, user.username);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return HelloCommand;
|
|
152
|
+
};`;
|
|
153
|
+
await fse.writeFile(path.join(pluginPath, 'commands', 'hello.js'), commandContent);
|
|
154
|
+
|
|
155
|
+
const indexJsContent = `const createHelloCommand = require('./commands/hello.js');
|
|
156
|
+
|
|
157
|
+
const PERMISSION_NAME = '${pluginNameSlug}';
|
|
158
|
+
const PLUGIN_OWNER_ID = 'plugin:${pluginNameSlug}';
|
|
159
|
+
|
|
160
|
+
async function onLoad(bot, options) {
|
|
161
|
+
const log = bot.sendLog;
|
|
162
|
+
const settings = options.settings;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const HelloCommand = createHelloCommand(bot);
|
|
166
|
+
|
|
167
|
+
// Регистрация прав.
|
|
168
|
+
// Если право, указанное в команде, не будет найдено,
|
|
169
|
+
// система создаст его автоматически.
|
|
170
|
+
// Эта регистрация нужна для того, чтобы задать правам описание.
|
|
171
|
+
await bot.api.registerPermissions([
|
|
172
|
+
{
|
|
173
|
+
name: PERMISSION_NAME + '.use',
|
|
174
|
+
description: 'Доступ к команде hello из плагина ${pluginNameSlug}',
|
|
175
|
+
owner: PLUGIN_OWNER_ID
|
|
176
|
+
}
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
await bot.api.registerCommand(new HelloCommand(settings));
|
|
180
|
+
|
|
181
|
+
log(\`[\${PLUGIN_OWNER_ID}] Команда 'hello' успешно загружена.\`);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
log(\`[\${PLUGIN_OWNER_ID}] Ошибка при загрузке: \${error.message}\`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function onUnload({ botId, prisma }) {
|
|
188
|
+
try {
|
|
189
|
+
await prisma.command.deleteMany({ where: { botId, owner: PLUGIN_OWNER_ID } });
|
|
190
|
+
await prisma.permission.deleteMany({ where: { botId, owner: PLUGIN_OWNER_ID } });
|
|
191
|
+
console.log(\`[\${PLUGIN_OWNER_ID}] Ресурсы для бота ID \${botId} успешно удалены.\`);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error(\`[\${PLUGIN_OWNER_ID}] Ошибка при очистке ресурсов:\`, error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
onLoad,
|
|
199
|
+
onUnload,
|
|
200
|
+
};`;
|
|
201
|
+
await fse.writeFile(path.join(pluginPath, 'index.js'), indexJsContent);
|
|
202
|
+
} else {
|
|
203
|
+
const indexJsContent = `module.exports = {
|
|
204
|
+
onLoad: () => {
|
|
205
|
+
console.log(\`Плагин "${pluginNameSlug}" загружен.\`);
|
|
206
|
+
},
|
|
207
|
+
onUnload: () => {
|
|
208
|
+
console.log(\`Плагин "${pluginNameSlug}" выгружен.\`);
|
|
209
|
+
}
|
|
210
|
+
};`;
|
|
211
|
+
await fse.writeFile(path.join(pluginPath, 'index.js'), indexJsContent);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const newPlugin = await prisma.installedPlugin.create({
|
|
215
|
+
data: {
|
|
216
|
+
botId: parseInt(botId),
|
|
217
|
+
name: pluginNameSlug,
|
|
218
|
+
version,
|
|
219
|
+
description,
|
|
220
|
+
path: pluginPath,
|
|
221
|
+
sourceType: 'LOCAL_IDE',
|
|
222
|
+
sourceUri: pluginPath,
|
|
223
|
+
manifest: JSON.stringify(packageJson.botpanel),
|
|
224
|
+
isEnabled: true
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
res.status(201).json(newPlugin);
|
|
229
|
+
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('[Plugin IDE Error] /create:', error);
|
|
232
|
+
res.status(500).json({ error: 'Не удалось создать плагин.' });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const readDirectoryRecursive = async (basePath, currentPath = '') => {
|
|
237
|
+
const absolutePath = path.join(basePath, currentPath);
|
|
238
|
+
const dirents = await fse.readdir(absolutePath, { withFileTypes: true });
|
|
239
|
+
const files = await Promise.all(dirents.map(async (dirent) => {
|
|
240
|
+
const direntPath = path.join(currentPath, dirent.name);
|
|
241
|
+
if (dirent.isDirectory()) {
|
|
242
|
+
return {
|
|
243
|
+
name: dirent.name,
|
|
244
|
+
type: 'folder',
|
|
245
|
+
path: direntPath.replace(/\\\\/g, '/'),
|
|
246
|
+
children: await readDirectoryRecursive(basePath, direntPath)
|
|
247
|
+
};
|
|
248
|
+
} else {
|
|
249
|
+
return {
|
|
250
|
+
name: dirent.name,
|
|
251
|
+
type: 'file',
|
|
252
|
+
path: direntPath.replace(/\\\\/g, '/'),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}));
|
|
256
|
+
return files.sort((a, b) => {
|
|
257
|
+
if (a.type === b.type) return a.name.localeCompare(b.name);
|
|
258
|
+
return a.type === 'folder' ? -1 : 1;
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
router.get('/:pluginName/structure', resolvePluginPath, async (req, res) => {
|
|
264
|
+
try {
|
|
265
|
+
const structure = await readDirectoryRecursive(req.pluginPath);
|
|
266
|
+
res.json(structure);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(`[Plugin IDE Error] /structure for ${req.params.pluginName}:`, error);
|
|
269
|
+
res.status(500).json({ error: 'Failed to read plugin structure.' });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
router.get('/:pluginName/file', resolvePluginPath, async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const filePath = path.join(req.pluginPath, req.query.path);
|
|
276
|
+
if (!filePath.startsWith(req.pluginPath)) {
|
|
277
|
+
return res.status(403).json({ error: 'Доступ запрещен.' });
|
|
278
|
+
}
|
|
279
|
+
const content = await fse.readFile(filePath, 'utf-8');
|
|
280
|
+
res.send(content);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
res.status(500).json({ error: 'Не удалось прочитать файл.' });
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
router.post('/:pluginName/file', resolvePluginPath, async (req, res) => {
|
|
287
|
+
try {
|
|
288
|
+
const { path: relativePath, content } = req.body;
|
|
289
|
+
if (!relativePath || content === undefined) {
|
|
290
|
+
return res.status(400).json({ error: 'File path and content are required.' });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const safePath = path.resolve(req.pluginPath, relativePath);
|
|
294
|
+
|
|
295
|
+
if (!safePath.startsWith(req.pluginPath)) {
|
|
296
|
+
return res.status(403).json({ error: 'Доступ запрещен.' });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await fse.writeFile(safePath, content, 'utf-8');
|
|
300
|
+
res.status(200).json({ message: 'Файл успешно сохранен.' });
|
|
301
|
+
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error(`[Plugin IDE Error] /file POST for ${req.params.pluginName}:`, error);
|
|
304
|
+
res.status(500).json({ error: 'Не удалось сохранить файл.' });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
router.post('/:pluginName/fs', resolvePluginPath, async (req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
const { operation, path: relativePath, newPath, content } = req.body;
|
|
311
|
+
|
|
312
|
+
if (!operation || !relativePath) {
|
|
313
|
+
return res.status(400).json({ error: 'Operation and path are required.' });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const safePath = path.resolve(req.pluginPath, relativePath);
|
|
317
|
+
if (!safePath.startsWith(req.pluginPath)) {
|
|
318
|
+
return res.status(403).json({ error: 'Access denied: Path is outside of plugin directory.' });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
switch (operation) {
|
|
322
|
+
case 'createFile':
|
|
323
|
+
if (await fse.pathExists(safePath)) return res.status(409).json({ error: 'File already exists.' });
|
|
324
|
+
await fse.writeFile(safePath, content || '', 'utf-8');
|
|
325
|
+
res.status(201).json({ message: 'File created successfully.' });
|
|
326
|
+
break;
|
|
327
|
+
|
|
328
|
+
case 'createFolder':
|
|
329
|
+
if (await fse.pathExists(safePath)) return res.status(409).json({ error: 'Folder already exists.' });
|
|
330
|
+
await fse.mkdirp(safePath);
|
|
331
|
+
res.status(201).json({ message: 'Folder created successfully.' });
|
|
332
|
+
break;
|
|
333
|
+
|
|
334
|
+
case 'delete':
|
|
335
|
+
if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'File or folder not found.' });
|
|
336
|
+
await fse.remove(safePath);
|
|
337
|
+
res.status(200).json({ message: 'File or folder deleted successfully.' });
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
case 'rename':
|
|
341
|
+
if (!newPath) return res.status(400).json({ error: 'New path is required for rename operation.' });
|
|
342
|
+
const safeNewPath = path.resolve(req.pluginPath, newPath);
|
|
343
|
+
if (!safeNewPath.startsWith(req.pluginPath)) return res.status(403).json({ error: 'Access denied: New path is outside of plugin directory.' });
|
|
344
|
+
if (!await fse.pathExists(safePath)) return res.status(404).json({ error: 'Source file or folder not found.' });
|
|
345
|
+
if (await fse.pathExists(safeNewPath)) return res.status(409).json({ error: 'Destination path already exists.' });
|
|
346
|
+
|
|
347
|
+
await fse.move(safePath, safeNewPath);
|
|
348
|
+
res.status(200).json({ message: 'Renamed successfully.' });
|
|
349
|
+
break;
|
|
350
|
+
|
|
351
|
+
default:
|
|
352
|
+
res.status(400).json({ error: 'Invalid operation specified.' });
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error(`[Plugin IDE Error] /fs POST for ${req.params.pluginName}:`, error);
|
|
356
|
+
res.status(500).json({ error: 'File system operation failed.' });
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
router.get('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
|
|
361
|
+
try {
|
|
362
|
+
const packageJsonPath = path.join(req.pluginPath, 'package.json');
|
|
363
|
+
const packageJson = await fse.readJson(packageJsonPath);
|
|
364
|
+
res.json(packageJson);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
res.status(500).json({ error: 'Не удалось прочитать package.json.' });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
router.post('/:pluginName/manifest', resolvePluginPath, async (req, res) => {
|
|
371
|
+
try {
|
|
372
|
+
const manifestPath = path.join(req.pluginPath, 'package.json');
|
|
373
|
+
if (!await fse.pathExists(manifestPath)) {
|
|
374
|
+
return res.status(404).json({ error: 'package.json not found.' });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const currentManifest = await fse.readJson(manifestPath);
|
|
378
|
+
const { name, version, description, author } = req.body;
|
|
379
|
+
|
|
380
|
+
const newManifest = {
|
|
381
|
+
...currentManifest,
|
|
382
|
+
name: name || currentManifest.name,
|
|
383
|
+
version: version || currentManifest.version,
|
|
384
|
+
description: description,
|
|
385
|
+
author: author,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
await fse.writeJson(manifestPath, newManifest, { spaces: 2 });
|
|
389
|
+
|
|
390
|
+
// Also update the DB record
|
|
391
|
+
await prisma.installedPlugin.updateMany({
|
|
392
|
+
where: {
|
|
393
|
+
botId: parseInt(req.params.botId),
|
|
394
|
+
path: req.pluginPath,
|
|
395
|
+
},
|
|
396
|
+
data: {
|
|
397
|
+
name: newManifest.name,
|
|
398
|
+
version: newManifest.version,
|
|
399
|
+
description: newManifest.description,
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
res.status(200).json({ message: 'package.json успешно обновлен.' });
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(`[Plugin IDE Error] /manifest POST for ${req.params.pluginName}:`, error);
|
|
406
|
+
res.status(500).json({ error: 'Не удалось обновить package.json.' });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
router.post('/:pluginName/fork', resolvePluginPath, async (req, res) => {
|
|
411
|
+
try {
|
|
412
|
+
const { botId, pluginName } = req.params;
|
|
413
|
+
const currentPlugin = await prisma.installedPlugin.findFirst({
|
|
414
|
+
where: { botId: parseInt(botId), name: pluginName }
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (!currentPlugin || currentPlugin.sourceType !== 'GITHUB') {
|
|
418
|
+
return res.status(400).json({ error: 'Копировать можно только плагины, установленные из GitHub.' });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const originalPath = req.pluginPath;
|
|
422
|
+
let newName = `${pluginName}-copy`;
|
|
423
|
+
let newPath = path.join(path.dirname(originalPath), newName);
|
|
424
|
+
let counter = 1;
|
|
425
|
+
|
|
426
|
+
while (await fse.pathExists(newPath) || await prisma.installedPlugin.findFirst({ where: { botId: parseInt(botId), name: newName } })) {
|
|
427
|
+
newName = `${pluginName}-copy-${counter}`;
|
|
428
|
+
newPath = path.join(path.dirname(originalPath), newName);
|
|
429
|
+
counter++;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
await fse.copy(originalPath, newPath);
|
|
433
|
+
|
|
434
|
+
const packageJsonPath = path.join(newPath, 'package.json');
|
|
435
|
+
const packageJson = await fse.readJson(packageJsonPath);
|
|
436
|
+
|
|
437
|
+
packageJson.name = newName;
|
|
438
|
+
packageJson.description = `(Forked from ${pluginName}) ${packageJson.description || ''}`;
|
|
439
|
+
await fse.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
440
|
+
|
|
441
|
+
const forkedPlugin = await prisma.installedPlugin.create({
|
|
442
|
+
data: {
|
|
443
|
+
botId: parseInt(botId),
|
|
444
|
+
name: newName,
|
|
445
|
+
version: packageJson.version,
|
|
446
|
+
description: packageJson.description,
|
|
447
|
+
path: newPath,
|
|
448
|
+
sourceType: 'LOCAL_IDE',
|
|
449
|
+
sourceUri: newPath,
|
|
450
|
+
manifest: JSON.stringify(packageJson.botpanel || {}),
|
|
451
|
+
isEnabled: false // Forked plugins are disabled by default
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
res.status(201).json(forkedPlugin);
|
|
456
|
+
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.error(`[Plugin IDE Error] /fork POST for ${req.params.pluginName}:`, error);
|
|
459
|
+
res.status(500).json({ error: 'Не удалось скопировать плагин.' });
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
module.exports = router;
|
|
@@ -7,12 +7,10 @@ const { pluginManager } = require('../../core/services');
|
|
|
7
7
|
const prisma = new PrismaClient();
|
|
8
8
|
const OFFICIAL_CATALOG_URL = "https://raw.githubusercontent.com/blockmineJS/official-plugins-list/main/index.json";
|
|
9
9
|
|
|
10
|
-
router.use(authenticate);
|
|
11
|
-
|
|
12
10
|
const getCacheBustedUrl = (url) => `${url}?t=${new Date().getTime()}`;
|
|
13
11
|
|
|
14
12
|
|
|
15
|
-
router.get('/catalog',
|
|
13
|
+
router.get('/catalog', async (req, res) => {
|
|
16
14
|
try {
|
|
17
15
|
const response = await fetch(getCacheBustedUrl(OFFICIAL_CATALOG_URL));
|
|
18
16
|
|
|
@@ -29,7 +27,7 @@ router.get('/catalog', authorize('plugin:browse'), async (req, res) => {
|
|
|
29
27
|
}
|
|
30
28
|
});
|
|
31
29
|
|
|
32
|
-
router.post('/check-updates/:botId', authorize('plugin:update'), async (req, res) => {
|
|
30
|
+
router.post('/check-updates/:botId', authenticate, authorize('plugin:update'), async (req, res) => {
|
|
33
31
|
try {
|
|
34
32
|
const botId = parseInt(req.params.botId);
|
|
35
33
|
|
|
@@ -45,7 +43,7 @@ router.post('/check-updates/:botId', authorize('plugin:update'), async (req, res
|
|
|
45
43
|
}
|
|
46
44
|
});
|
|
47
45
|
|
|
48
|
-
router.post('/update/:pluginId', authorize('plugin:update'), async (req, res) => {
|
|
46
|
+
router.post('/update/:pluginId', authenticate, authorize('plugin:update'), async (req, res) => {
|
|
49
47
|
try {
|
|
50
48
|
const pluginId = parseInt(req.params.pluginId);
|
|
51
49
|
const updatedPlugin = await pluginManager.updatePlugin(pluginId);
|
|
@@ -55,7 +53,7 @@ router.post('/update/:pluginId', authorize('plugin:update'), async (req, res) =>
|
|
|
55
53
|
}
|
|
56
54
|
});
|
|
57
55
|
|
|
58
|
-
router.get('/catalog/:name',
|
|
56
|
+
router.get('/catalog/:name', async (req, res) => {
|
|
59
57
|
try {
|
|
60
58
|
const pluginName = req.params.name;
|
|
61
59
|
|
|
@@ -209,10 +209,14 @@ class PluginManager {
|
|
|
209
209
|
throw new Error('Ошибка при удалении данных плагина из БД. Файлы не были удалены.');
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
if (plugin.
|
|
212
|
+
if (plugin.path && plugin.path.startsWith(PLUGINS_BASE_DIR)) {
|
|
213
213
|
try {
|
|
214
|
-
await fse.
|
|
215
|
-
|
|
214
|
+
if (await fse.pathExists(plugin.path)) {
|
|
215
|
+
await fse.remove(plugin.path);
|
|
216
|
+
console.log(`[PluginManager] Папка плагина ${plugin.path} успешно удалена.`);
|
|
217
|
+
} else {
|
|
218
|
+
console.log(`[PluginManager] Папка плагина ${plugin.path} не найдена, удаление не требуется.`);
|
|
219
|
+
}
|
|
216
220
|
} catch (fileError) {
|
|
217
221
|
console.error(`Не удалось удалить папку плагина ${plugin.path}:`, fileError);
|
|
218
222
|
}
|
package/backend/src/server.js
CHANGED
|
@@ -3,25 +3,23 @@ const http = require('http');
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { PrismaClient } = require('@prisma/client');
|
|
6
7
|
|
|
7
|
-
const config = require('./config');
|
|
8
|
+
const config = require('./config');
|
|
8
9
|
const { initializeSocket } = require('./real-time/socketHandler');
|
|
9
10
|
const { botManager, pluginManager } = require('./core/services');
|
|
10
11
|
|
|
11
|
-
|
|
12
12
|
const botRoutes = require('./api/routes/bots');
|
|
13
13
|
const pluginRoutes = require('./api/routes/plugins');
|
|
14
14
|
const serverRoutes = require('./api/routes/servers');
|
|
15
15
|
const permissionsRoutes = require('./api/routes/permissions');
|
|
16
16
|
const taskRoutes = require('./api/routes/tasks');
|
|
17
|
-
const authRoutes = require('./api/routes/auth');
|
|
17
|
+
const { router: authRoutes, ALL_PERMISSIONS } = require('./api/routes/auth');
|
|
18
18
|
const searchRoutes = require('./api/routes/search');
|
|
19
19
|
const eventGraphsRouter = require('./api/routes/eventGraphs');
|
|
20
20
|
const TaskScheduler = require('./core/TaskScheduler');
|
|
21
21
|
const panelRoutes = require('./api/routes/panel');
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
23
|
const app = express();
|
|
26
24
|
const server = http.createServer(app);
|
|
27
25
|
|
|
@@ -76,7 +74,48 @@ app.get(/^(?!\/api).*/, (req, res) => {
|
|
|
76
74
|
}
|
|
77
75
|
});
|
|
78
76
|
|
|
77
|
+
async function runStartupMigrations() {
|
|
78
|
+
const prisma = new PrismaClient();
|
|
79
|
+
try {
|
|
80
|
+
// 1. Migrate legacy '*' permission for Admin role
|
|
81
|
+
const adminRole = await prisma.panelRole.findUnique({ where: { name: 'Admin' } });
|
|
82
|
+
if (adminRole) {
|
|
83
|
+
const permissions = JSON.parse(adminRole.permissions);
|
|
84
|
+
if (permissions.includes('*')) {
|
|
85
|
+
const newPermissions = ALL_PERMISSIONS
|
|
86
|
+
.map(p => p.id)
|
|
87
|
+
.filter(id => id !== '*' && id !== 'plugin:develop');
|
|
88
|
+
|
|
89
|
+
await prisma.panelRole.update({
|
|
90
|
+
where: { id: adminRole.id },
|
|
91
|
+
data: { permissions: JSON.stringify(newPermissions) }
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 2. Ensure root user (ID 1) has all permissions
|
|
97
|
+
const rootUser = await prisma.panelUser.findUnique({ where: { id: 1 }, include: { role: true } });
|
|
98
|
+
if (rootUser && rootUser.role) {
|
|
99
|
+
const allPermissions = ALL_PERMISSIONS.map(p => p.id).filter(id => id !== '*');
|
|
100
|
+
const currentPermissions = JSON.parse(rootUser.role.permissions);
|
|
101
|
+
|
|
102
|
+
if (JSON.stringify(allPermissions.sort()) !== JSON.stringify(currentPermissions.sort())) {
|
|
103
|
+
await prisma.panelRole.update({
|
|
104
|
+
where: { id: rootUser.role.id },
|
|
105
|
+
data: { permissions: JSON.stringify(allPermissions) }
|
|
106
|
+
});
|
|
107
|
+
console.log(`[Migration] Права для root-пользователя "${rootUser.username}" (ID: 1) были синхронизированы.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('[Migration] Ошибка во время миграции прав:', error);
|
|
112
|
+
} finally {
|
|
113
|
+
await prisma.$disconnect();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
async function startServer() {
|
|
118
|
+
await runStartupMigrations();
|
|
80
119
|
return new Promise((resolve) => {
|
|
81
120
|
server.listen(PORT, HOST, async () => {
|
|
82
121
|
console.log(`\nBackend сервер успешно запущен на http://${HOST}:${PORT}`);
|