Skip to content

Commit ef944c3

Browse files
committed
feat: Add upload plugin via AdminCP
1 parent a4c2843 commit ef944c3

File tree

19 files changed

+302
-85
lines changed

19 files changed

+302
-85
lines changed

apps/backend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"@nestjs/core": "^10.4.15",
1919
"@nestjs/platform-express": "^10.4.15",
2020
"@nestjs/schedule": "^4.1.2",
21+
"@nestjs/swagger": "^8.1.0",
2122
"@react-email/components": "^0.0.31",
2223
"class-transformer": "^0.5.1",
2324
"class-validator": "^0.14.1",

apps/frontend/src/plugins/admin/langs/en.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,11 @@
428428
"title": "Upload Plugin",
429429
"desc": "Upload a new plugin to your website.",
430430
"title_new_version": "Upload New Version Plugin",
431-
"info": "After uploading the plugin, you need to restart the server to apply the changes.",
431+
"info": "After the process will be complete, you need to restart the server to apply the changes.",
432432
"errors": {
433433
"PLUGIN_ALREADY_EXISTS": "Plugin with this code already exists. If you want to update the plugin, use the upload new version form from the plugin dropdown menu.",
434-
"PLUGIN_VERSION_IS_LOWER": "The version of the uploaded plugin is lower than the current version."
434+
"PLUGIN_VERSION_IS_LOWER": "The version of the uploaded plugin is lower than the current version.",
435+
"CONFLICT_PLUGIN_CODE": "Files with the same name already exist your project. Please remove them and try again."
435436
},
436437
"success": {
437438
"upload": "Plugin has been uploaded.",
@@ -442,7 +443,7 @@
442443
"delete": {
443444
"submit": "Yes, delete plugin",
444445
"desc": "This action will delete <name></name> plugin created by <author></author> and all data associated with it.",
445-
"info": "After the process is complete, you need to restart the server to apply the changes."
446+
"info": "After the process will be complete, you need to restart the server to apply the changes."
446447
}
447448
},
448449
"styles": {

packages/backend/scripts/update-plugins.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export const updatePlugins = async ({
3838
return;
3939
}
4040

41-
// const config: ConfigPlugin = JSON.parse(
4241
const config = JSON.parse(
4342
await readFile(join(pluginPath, 'config.json'), 'utf8'),
4443
);

packages/backend/src/core/admin/plugins/helpers/validate-files.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class ValidateFilesPluginsAdminHelpersService {
1818
pluginPath.frontend.plugin,
1919
// Shared
2020
pluginPath.shared,
21+
// Backend
22+
pluginPath.root,
2123
];
2224

2325
// Check if the folders exist

packages/backend/src/core/admin/plugins/plugins.controller.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Response } from 'express';
22

33
import { OnlyForDevelopment } from '@/guards/dev.guard';
44
import { Controllers } from '@/helpers/controller.decorator';
5+
import { FilesValidationPipe } from '@/helpers/files/files.pipe';
6+
import { UploadFilesMethod } from '@/helpers/upload-files.decorator';
57
import {
68
Body,
79
Delete,
@@ -32,8 +34,6 @@ import { ExportPluginsAdminService } from './services/export.service';
3234
import { ItemPluginsAdminService } from './services/item.service';
3335
import { ShowPluginsAdminService } from './services/show.service';
3436
import { UploadPluginsAdminService } from './services/upload.service';
35-
import { UploadFilesMethod } from '@/helpers/upload-files.decorator';
36-
import { FilesValidationPipe } from '@/helpers/files/files.pipe';
3737

3838
@Controllers({ plugin_name: 'Core', plugin_code: 'plugins', isAdmin: true })
3939
export class PluginsAdminController {
@@ -47,25 +47,6 @@ export class PluginsAdminController {
4747
private readonly uploadService: UploadPluginsAdminService,
4848
) {}
4949

50-
@ApiCreatedResponse({ description: 'Plugin uploaded' })
51-
@Post('upload')
52-
@UseGuards(OnlyForDevelopment)
53-
@UploadFilesMethod({ fields: ['file'] })
54-
async uploadPlugin(
55-
@UploadedFiles(
56-
new FilesValidationPipe({
57-
file: {
58-
maxSize: 1024 * 1024 * 10, // 10 MB
59-
acceptMimeType: ['application/gzip', 'application/x-compressed'],
60-
maxCount: 1,
61-
},
62-
}),
63-
)
64-
files: Pick<UploadPluginsAdminBody, 'file'>,
65-
): Promise<void> {
66-
await this.uploadService.upload({ files });
67-
}
68-
6950
@ApiCreatedResponse({ description: 'Plugin created', type: ShowPluginAdmin })
7051
@Post()
7152
@UseGuards(OnlyForDevelopment)
@@ -118,4 +99,24 @@ export class PluginsAdminController {
11899
): Promise<ShowPluginsAdminObj> {
119100
return await this.showService.show(query);
120101
}
102+
103+
@ApiCreatedResponse({ description: 'Plugin uploaded' })
104+
@Post('upload')
105+
@UploadFilesMethod({ fields: ['file'] })
106+
@UseGuards(OnlyForDevelopment)
107+
async uploadPlugin(
108+
@UploadedFiles(
109+
new FilesValidationPipe({
110+
file: {
111+
maxSize: 1024 * 1024 * 10, // 10 MB
112+
acceptMimeType: ['application/gzip', 'application/x-compressed'],
113+
maxCount: 1,
114+
},
115+
}),
116+
)
117+
files: Pick<UploadPluginsAdminBody, 'file'>,
118+
@Body() body: UploadPluginsAdminBody,
119+
): Promise<void> {
120+
await this.uploadService.upload({ files, body });
121+
}
121122
}

packages/backend/src/core/admin/plugins/services/delete.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { eq } from 'drizzle-orm';
1313
import { existsSync } from 'fs';
1414
import { rm } from 'fs/promises';
15+
import { join } from 'path';
1516

1617
import { ChangeFilesPluginsAdminHelpersService } from '../helpers/change-files.service';
1718

@@ -71,6 +72,11 @@ export class DeletePluginsAdminService {
7172
// Shared
7273
await this.deleteFolderWhenExists(pluginPaths.shared);
7374

75+
// Uploads
76+
await this.deleteFolderWhenExists(
77+
join(ABSOLUTE_PATHS.uploads.public, plugin.code),
78+
);
79+
7480
await Promise.all([
7581
this.databaseService.db
7682
.delete(core_plugins)

packages/backend/src/core/admin/plugins/services/export.service.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class ExportPluginsAdminService {
116116
await cp(backendSource, backendPath, { recursive: true });
117117

118118
// Copy frontend files
119+
const pathFiles = ABSOLUTE_PATHS.plugin({ code });
119120
const frontendPaths = [
120121
'admin_pages_auth',
121122
'admin_pages',
@@ -127,7 +128,7 @@ export class ExportPluginsAdminService {
127128
] as const;
128129
await Promise.all(
129130
frontendPaths.map(async path => {
130-
const source = ABSOLUTE_PATHS.plugin({ code }).frontend[path];
131+
const source = pathFiles.frontend[path];
131132
if (!existsSync(source)) {
132133
if (path === 'plugin') {
133134
return res

packages/backend/src/core/admin/plugins/services/upload.service.ts

Lines changed: 165 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,104 @@
11
import { ABSOLUTE_PATHS } from '@/app.module';
2+
import { core_plugins } from '@/database/schema/plugins';
3+
import { ConfigHelperService } from '@/helpers/config.service';
24
import { InternalDatabaseService } from '@/utils/database/internal_database.service';
3-
import { BadRequestException, Injectable } from '@nestjs/common';
4-
import { mkdir, readdir, readFile, rm } from 'fs/promises';
5+
import {
6+
BadRequestException,
7+
ConflictException,
8+
Injectable,
9+
} from '@nestjs/common';
10+
import { existsSync } from 'fs';
11+
import { cp, mkdir, readFile, rm } from 'fs/promises';
512
import { join } from 'path';
13+
import { Readable } from 'stream';
14+
import * as tar from 'tar';
615
import { ConfigPlugin } from 'vitnode-shared/admin/plugin.dto';
716
import { UploadPluginsAdminBody } from 'vitnode-shared/admin/plugins.dto';
8-
import * as tar from 'tar';
9-
import { Readable } from 'stream';
17+
18+
import { ChangeFilesPluginsAdminHelpersService } from '../helpers/change-files.service';
19+
import { ValidateFilesPluginsAdminHelpersService } from '../helpers/validate-files.service';
1020

1121
@Injectable()
1222
export class UploadPluginsAdminService {
13-
constructor(private readonly databaseService: InternalDatabaseService) {}
23+
constructor(
24+
private readonly databaseService: InternalDatabaseService,
25+
private readonly validateFilesHelper: ValidateFilesPluginsAdminHelpersService,
26+
private readonly changeFilesHelper: ChangeFilesPluginsAdminHelpersService,
27+
private readonly configHelper: ConfigHelperService,
28+
) {}
29+
30+
private async copyFrontendFiles({
31+
code,
32+
tempFolder,
33+
}: {
34+
code: string;
35+
tempFolder: string;
36+
}) {
37+
const pathToFrontend = ABSOLUTE_PATHS.plugin({ code });
38+
const frontendPaths = [
39+
'admin_pages_auth',
40+
'admin_pages',
41+
'pages',
42+
'pages_main',
43+
'pages_main_layout',
44+
'pages_root',
45+
'plugin',
46+
] as const;
47+
await Promise.all(
48+
frontendPaths.map(async path => {
49+
const pathToCopy = join(tempFolder, 'frontend', path);
50+
if (!existsSync(pathToCopy)) return;
51+
const pathToPaste = pathToFrontend.frontend[path];
52+
53+
await cp(pathToCopy, pathToPaste, { recursive: true });
54+
}),
55+
);
56+
57+
// Copy languages
58+
const languages =
59+
await this.databaseService.db.query.core_languages.findMany({
60+
columns: {
61+
code: true,
62+
},
63+
});
64+
await Promise.all(
65+
languages.map(async language => {
66+
const langPath = join(
67+
pathToFrontend.frontend.languages,
68+
`${language.code}.json`,
69+
);
70+
71+
if (existsSync(langPath)) return;
72+
73+
const sourceLang = join(pathToFrontend.frontend.languages, 'en.json');
74+
await cp(sourceLang, langPath);
75+
}),
76+
);
77+
}
78+
79+
private async copyFrontendOrBackendFiles({
80+
code,
81+
tempPath,
82+
type,
83+
}: {
84+
code: string;
85+
tempPath: string;
86+
type: 'backend' | 'shared';
87+
}) {
88+
const path =
89+
type === 'shared'
90+
? ABSOLUTE_PATHS.plugin({ code }).shared
91+
: ABSOLUTE_PATHS.plugin({ code }).root;
92+
93+
// If exists, remove the folder
94+
if (existsSync(path)) {
95+
await rm(path, { recursive: true });
96+
}
97+
await mkdir(path);
98+
99+
// Copy from temp folder to plugin folder
100+
await cp(join(tempPath, type), path, { recursive: true });
101+
}
14102

15103
private async getPluginConfigAndSaveFilesIntoTempFile({
16104
file,
@@ -49,18 +137,17 @@ export class UploadPluginsAdminService {
49137
return config;
50138
}
51139

52-
private isTgzFile(file: Express.Multer.File) {
53-
if (!file.originalname.endsWith('.tgz')) {
54-
throw new BadRequestException('Invalid file type');
55-
}
56-
}
57-
58140
async upload({
59141
files: { file },
142+
body: { code },
60143
}: {
144+
body: Omit<UploadPluginsAdminBody, 'file'>;
61145
files: Pick<UploadPluginsAdminBody, 'file'>;
62146
}) {
63-
this.isTgzFile(file);
147+
if (!file.originalname.endsWith('.tgz')) {
148+
throw new BadRequestException('Invalid file type');
149+
}
150+
64151
const tempPath = join(
65152
ABSOLUTE_PATHS.uploads.temp,
66153
'plugins',
@@ -71,6 +158,71 @@ export class UploadPluginsAdminService {
71158
tempPath,
72159
});
73160

74-
return 'test';
161+
// Validation
162+
if (code) {
163+
const checkPlugin =
164+
await this.databaseService.db.query.core_plugins.findFirst({
165+
where: (table, { eq }) => eq(table.code, code),
166+
});
167+
168+
if (
169+
(checkPlugin && !code) ||
170+
code === 'core' ||
171+
code === 'admin' ||
172+
code === 'members'
173+
) {
174+
await rm(tempPath, { recursive: true });
175+
throw new ConflictException('PLUGIN_ALREADY_EXISTS');
176+
}
177+
178+
if (code && code !== configPlugin.code) {
179+
await rm(tempPath, { recursive: true });
180+
throw new BadRequestException('PLUGIN_CODE_NOT_MATCH');
181+
}
182+
183+
if (
184+
checkPlugin &&
185+
code &&
186+
configPlugin.version_code < checkPlugin.version_code
187+
) {
188+
await rm(tempPath, { recursive: true });
189+
throw new BadRequestException('PLUGIN_VERSION_IS_LOWER');
190+
}
191+
} else {
192+
try {
193+
this.validateFilesHelper.validateFiles({ code: configPlugin.code });
194+
} catch (e) {
195+
const error = e as Error;
196+
await rm(tempPath, { recursive: true });
197+
throw new ConflictException(error.message);
198+
}
199+
}
200+
201+
await this.configHelper.updateConfig({
202+
restart_server: true,
203+
});
204+
205+
// Copy files
206+
await Promise.all([
207+
this.copyFrontendOrBackendFiles({
208+
code: configPlugin.code,
209+
tempPath,
210+
type: 'shared',
211+
}),
212+
this.copyFrontendFiles({
213+
code: configPlugin.code,
214+
tempFolder: tempPath,
215+
}),
216+
this.changeFilesHelper.changeFiles({
217+
code: configPlugin.code,
218+
action: 'add',
219+
}),
220+
this.copyFrontendOrBackendFiles({
221+
code: configPlugin.code,
222+
tempPath,
223+
type: 'backend',
224+
}),
225+
this.databaseService.db.insert(core_plugins).values(configPlugin),
226+
]);
75227
}
76228
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import { CONFIG } from '@/helpers/config-with-env';
21
import { CreateActionPluginAdmin } from './create/create';
32
import { UploadActionPluginAdmin } from './upload/upload';
43

54
export const ActionsPluginsAdmin = () => {
65
return (
76
<>
87
<CreateActionPluginAdmin />
9-
{CONFIG.node_development && <UploadActionPluginAdmin />}
8+
<UploadActionPluginAdmin />
109
</>
1110
);
1211
};

0 commit comments

Comments
 (0)