Skip to content

Commit bc04bbc

Browse files
committed
feat: add bulk profile modification functionality for nodes
- Introduced new bulk actions route for profile modification in the nodes API. - Implemented profile modification command in the nodes service, allowing bulk updates of node inbounds based on the provided configuration profile. - Enhanced NodesController to handle profile modification requests and return appropriate responses.
1 parent 4b33d1a commit bc04bbc

File tree

11 files changed

+177
-2
lines changed

11 files changed

+177
-2
lines changed

libs/contract/api/controllers/nodes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const NODES_CONTROLLER = 'nodes' as const;
22

33
export const NODE_ACTIONS_ROUTE = 'actions' as const;
4+
const BULK_ACTIONS_ROUTE = 'bulk-actions' as const;
45

56
export const NODES_ROUTES = {
67
CREATE: '', // create
@@ -18,6 +19,9 @@ export const NODES_ROUTES = {
1819
RESTART_ALL: `${NODE_ACTIONS_ROUTE}/restart-all`,
1920
REORDER: `${NODE_ACTIONS_ROUTE}/reorder`,
2021
},
22+
BULK_ACTIONS: {
23+
PROFILE_MODIFICATION: `${BULK_ACTIONS_ROUTE}/profile-modification`,
24+
},
2125

2226
STATS: {
2327
USAGE_BY_RANGE: 'usage/range',

libs/contract/api/routes.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const REST_API = {
7272
REORDER: `${ROOT}/${CONTROLLERS.NODES_CONTROLLER}/${CONTROLLERS.NODES_ROUTES.ACTIONS.REORDER}`,
7373
},
7474

75+
BULK_ACTIONS: {
76+
PROFILE_MODIFICATION: `${ROOT}/${CONTROLLERS.NODES_CONTROLLER}/${CONTROLLERS.NODES_ROUTES.BULK_ACTIONS.PROFILE_MODIFICATION}`,
77+
},
78+
7579
STATS: {
7680
USAGE_BY_RANGE: `${ROOT}/${CONTROLLERS.NODES_CONTROLLER}/${CONTROLLERS.NODES_ROUTES.STATS.USAGE_BY_RANGE}`,
7781
USAGE_BY_RANGE_USER: (uuid: string) =>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './profile-modification.command';
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { z } from 'zod';
2+
3+
import { getEndpointDetails } from '../../../constants';
4+
import { NODES_ROUTES, REST_API } from '../../../api';
5+
6+
export namespace BulkNodesProfileModificationCommand {
7+
export const url = REST_API.NODES.BULK_ACTIONS.PROFILE_MODIFICATION;
8+
export const TSQ_url = url;
9+
10+
export const endpointDetails = getEndpointDetails(
11+
NODES_ROUTES.BULK_ACTIONS.PROFILE_MODIFICATION,
12+
'post',
13+
'Modify Inbounds & Profile for many nodes',
14+
);
15+
16+
export const RequestSchema = z.object({
17+
uuids: z.array(z.string().uuid()).min(1, 'Must be at least 1 Node UUID'),
18+
configProfile: z.object({
19+
activeConfigProfileUuid: z.string().uuid(),
20+
activeInbounds: z
21+
.array(z.string().uuid(), {
22+
invalid_type_error: 'Must be an array of UUIDs',
23+
})
24+
.min(1, 'Must be at least 1 inbound UUID'),
25+
}),
26+
});
27+
28+
export type Request = z.infer<typeof RequestSchema>;
29+
30+
export const ResponseSchema = z.object({
31+
response: z.object({
32+
eventSent: z.boolean(),
33+
}),
34+
});
35+
36+
export type Response = z.infer<typeof ResponseSchema>;
37+
}

libs/contract/commands/nodes/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './actions';
2+
export * from './bulk-actions';
23
export * from './create.command';
34
export * from './delete.command';
45
export * from './get-all.command';

libs/contract/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@remnawave/backend-contract",
3-
"version": "2.3.24",
3+
"version": "2.3.26",
44
"public": true,
55
"license": "AGPL-3.0-only",
66
"description": "A contract library for Remnawave Backend. It can be used in backend and frontend.",

src/modules/nodes/dtos/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export * from './enable-node.request.dto';
55
export * from './get-all-nodes.dto';
66
export * from './get-all-tags.dto';
77
export * from './get-one-node.dto';
8+
export * from './profile-modification.dto';
89
export * from './reorder.dto';
910
export * from './reset-traffic.request.dto';
1011
export * from './restart-all.dto';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { createZodDto } from 'nestjs-zod';
2+
3+
import { BulkNodesProfileModificationCommand } from '@contract/commands';
4+
5+
export class ProfileModificationResponseDto extends createZodDto(
6+
BulkNodesProfileModificationCommand.ResponseSchema,
7+
) {}
8+
9+
export class ProfileModificationRequestDto extends createZodDto(
10+
BulkNodesProfileModificationCommand.RequestSchema,
11+
) {}

src/modules/nodes/nodes.controller.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
GetAllNodesCommand,
2525
GetAllNodesTagsCommand,
2626
GetOneNodeCommand,
27+
BulkNodesProfileModificationCommand,
2728
ReorderNodeCommand,
2829
ResetNodeTrafficCommand,
2930
RestartAllNodesCommand,
@@ -43,6 +44,8 @@ import {
4344
GetAllNodesTagsResponseDto,
4445
GetOneNodeRequestParamDto,
4546
GetOneNodeResponseDto,
47+
ProfileModificationRequestDto,
48+
ProfileModificationResponseDto,
4649
ReorderNodeRequestDto,
4750
ReorderNodeResponseDto,
4851
ResetNodeTrafficRequestDto,
@@ -278,4 +281,24 @@ export class NodesController {
278281
response: data.map((node) => new GetAllNodesResponseModel(node)),
279282
};
280283
}
284+
285+
@ApiOkResponse({
286+
type: ProfileModificationResponseDto,
287+
description: 'Event sent successfully',
288+
})
289+
@Endpoint({
290+
command: BulkNodesProfileModificationCommand,
291+
httpCode: HttpStatus.OK,
292+
apiBody: ProfileModificationRequestDto,
293+
})
294+
async profileModification(
295+
@Body() body: ProfileModificationRequestDto,
296+
): Promise<ProfileModificationResponseDto> {
297+
const result = await this.nodesService.profileModification(body);
298+
299+
const data = errorHandler(result);
300+
return {
301+
response: data,
302+
};
303+
}
281304
}

src/modules/nodes/nodes.service.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ import { CreateNodeTrafficUsageHistoryCommand } from '@modules/nodes-traffic-usa
1717
import { NodesTrafficUsageHistoryEntity } from '@modules/nodes-traffic-usage-history/entities/nodes-traffic-usage-history.entity';
1818
import { GetConfigProfileByUuidQuery } from '@modules/config-profiles/queries/get-config-profile-by-uuid';
1919

20+
import { StartAllNodesByProfileQueueService } from '@queue/start-all-nodes-by-profile';
2021
import { StartNodeQueueService } from '@queue/start-node/start-node.service';
2122
import { StopNodeQueueService } from '@queue/stop-node/stop-node.service';
2223

24+
import {
25+
CreateNodeRequestDto,
26+
ProfileModificationRequestDto,
27+
ReorderNodeRequestDto,
28+
UpdateNodeRequestDto,
29+
} from './dtos';
2330
import {
2431
BaseEventResponseModel,
2532
DeleteNodeResponseModel,
2633
RestartNodeResponseModel,
2734
} from './models';
28-
import { CreateNodeRequestDto, ReorderNodeRequestDto, UpdateNodeRequestDto } from './dtos';
2935
import { NodesRepository } from './repositories/nodes.repository';
3036
import { NodesEntity } from './entities';
3137

@@ -37,6 +43,7 @@ export class NodesService {
3743
private readonly nodesRepository: NodesRepository,
3844
private readonly eventEmitter: EventEmitter2,
3945
private readonly startAllNodesQueue: StartAllNodesQueueService,
46+
private readonly startAllNodesByProfileQueueService: StartAllNodesByProfileQueueService,
4047
private readonly startNodeQueue: StartNodeQueueService,
4148
private readonly stopNodeQueue: StopNodeQueueService,
4249
private readonly queryBus: QueryBus,
@@ -541,4 +548,54 @@ export class NodesService {
541548
return { isOk: false, ...ERRORS.INTERNAL_SERVER_ERROR };
542549
}
543550
}
551+
552+
public async profileModification(
553+
body: ProfileModificationRequestDto,
554+
): Promise<ICommandResponse<BaseEventResponseModel>> {
555+
try {
556+
const { uuids, configProfile } = body;
557+
558+
const configProfileResponse = await this.queryBus.execute(
559+
new GetConfigProfileByUuidQuery(configProfile.activeConfigProfileUuid),
560+
);
561+
562+
if (!configProfileResponse.isOk || !configProfileResponse.response) {
563+
return {
564+
isOk: false,
565+
...ERRORS.CONFIG_PROFILE_NOT_FOUND,
566+
};
567+
}
568+
569+
const inbounds = configProfileResponse.response.inbounds;
570+
571+
const allActiveInboundsExistInProfile = configProfile.activeInbounds.every(
572+
(activeInboundUuid) =>
573+
inbounds.some((inbound) => inbound.uuid === activeInboundUuid),
574+
);
575+
576+
if (!allActiveInboundsExistInProfile) {
577+
return {
578+
isOk: false,
579+
...ERRORS.CONFIG_PROFILE_INBOUND_NOT_FOUND_IN_SPECIFIED_PROFILE,
580+
};
581+
}
582+
583+
await this.nodesRepository.removeInboundsFromNodes(uuids);
584+
585+
await this.nodesRepository.addInboundsToNodes(uuids, configProfile.activeInbounds);
586+
587+
await this.startAllNodesByProfileQueueService.startAllNodesByProfile({
588+
profileUuid: configProfile.activeConfigProfileUuid,
589+
emitter: 'bulkProfileModification',
590+
}); // no need to restart all nodes
591+
592+
return {
593+
isOk: true,
594+
response: new BaseEventResponseModel(true),
595+
};
596+
} catch (error) {
597+
this.logger.error(error);
598+
return { isOk: false, ...ERRORS.INTERNAL_SERVER_ERROR };
599+
}
600+
}
544601
}

0 commit comments

Comments
 (0)