Skip to content

Commit 20f075e

Browse files
committed
feat: update config profile and internal squad commands to include optional name field
- Enhanced UpdateConfigProfileCommand and UpdateInternalSquadCommand to accept an optional `name` field with validation. - Updated service methods to handle the new `name` parameter and added error handling for required fields.
1 parent 30fef5d commit 20f075e

File tree

8 files changed

+162
-80
lines changed

8 files changed

+162
-80
lines changed

libs/contract/commands/config-profiles/update-config-profile.command.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ export namespace UpdateConfigProfileCommand {
1616

1717
export const RequestSchema = z.object({
1818
uuid: z.string().uuid('UUID must be a valid UUID'),
19-
config: z.object({}).passthrough(),
19+
name: z
20+
.string()
21+
.min(2, 'Name must be at least 2 characters')
22+
.max(20, 'Name must be less than 20 characters')
23+
.regex(
24+
/^[A-Za-z0-9_-]+$/,
25+
'Name can only contain letters, numbers, underscores and dashes',
26+
)
27+
.optional(),
28+
config: z.object({}).passthrough().optional(),
2029
});
2130

2231
export type Request = z.infer<typeof RequestSchema>;

libs/contract/commands/internal-squads/update-internal-squad.command.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ export namespace UpdateInternalSquadCommand {
1616

1717
export const RequestSchema = z.object({
1818
uuid: z.string().uuid(),
19-
inbounds: z.array(z.string().uuid()),
19+
name: z
20+
.string()
21+
.min(2, 'Name must be at least 2 characters')
22+
.max(20, 'Name must be less than 20 characters')
23+
.regex(
24+
/^[A-Za-z0-9_-]+$/,
25+
'Name can only contain letters, numbers, underscores and dashes',
26+
)
27+
.optional(),
28+
inbounds: z.array(z.string().uuid()).optional(),
2029
});
2130

2231
export type Request = z.infer<typeof RequestSchema>;

libs/contract/constants/errors/errors.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -754,4 +754,14 @@ export const ERRORS = {
754754
message: 'Get all host tags error',
755755
httpCode: 500,
756756
},
757+
NAME_OR_CONFIG_REQUIRED: {
758+
code: 'A152',
759+
message: 'Name or config is required',
760+
httpCode: 400,
761+
},
762+
NAME_OR_INBOUNDS_REQUIRED: {
763+
code: 'A153',
764+
message: 'Name or inbounds is required',
765+
httpCode: 400,
766+
},
757767
} as const;

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.1.8",
3+
"version": "2.1.10",
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/config-profiles/config-profile.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export class ConfigProfileController {
194194
): Promise<UpdateConfigProfileResponseDto> {
195195
const result = await this.configProfileService.updateConfigProfile(
196196
updateConfigProfileDto.uuid,
197+
updateConfigProfileDto.name,
197198
updateConfigProfileDto.config,
198199
);
199200

src/modules/config-profiles/config-profile.service.ts

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ export class ConfigProfileService {
188188
@Transactional()
189189
public async updateConfigProfile(
190190
uuid: string,
191-
config: object,
191+
name?: string,
192+
config?: object,
192193
): Promise<ICommandResponse<GetConfigProfileByUuidResponseModel>> {
193194
try {
194195
const existingConfigProfile =
@@ -201,39 +202,57 @@ export class ConfigProfileService {
201202
};
202203
}
203204

204-
const existingInbounds = existingConfigProfile.inbounds;
205+
if (!name && !config) {
206+
return {
207+
isOk: false,
208+
...ERRORS.NAME_OR_CONFIG_REQUIRED,
209+
};
210+
}
205211

206-
const validatedConfig = new XRayConfig(config);
207-
const sortedConfig = validatedConfig.getSortedConfig();
208-
const inbounds = validatedConfig.getAllInbounds();
212+
const configProfileEntity = new ConfigProfileEntity({
213+
uuid,
214+
});
209215

210-
const inboundsEntities = inbounds.map(
211-
(inbound) =>
212-
new ConfigProfileInboundEntity({
213-
profileUuid: existingConfigProfile.uuid,
214-
tag: inbound.tag,
215-
type: inbound.type,
216-
network: inbound.network,
217-
security: inbound.security,
218-
port: inbound.port,
219-
rawInbound: inbound.rawInbound as unknown as object,
220-
}),
221-
);
216+
if (name) {
217+
configProfileEntity.name = name;
218+
}
222219

223-
await this.syncInbounds(existingInbounds, inboundsEntities);
220+
if (config) {
221+
const existingInbounds = existingConfigProfile.inbounds;
222+
223+
const validatedConfig = new XRayConfig(config);
224+
const sortedConfig = validatedConfig.getSortedConfig();
225+
const inbounds = validatedConfig.getAllInbounds();
226+
227+
const inboundsEntities = inbounds.map(
228+
(inbound) =>
229+
new ConfigProfileInboundEntity({
230+
profileUuid: existingConfigProfile.uuid,
231+
tag: inbound.tag,
232+
type: inbound.type,
233+
network: inbound.network,
234+
security: inbound.security,
235+
port: inbound.port,
236+
rawInbound: inbound.rawInbound as unknown as object,
237+
}),
238+
);
224239

225-
await this.configProfileRepository.update({
226-
uuid: existingConfigProfile.uuid,
227-
config: sortedConfig as object,
228-
});
240+
await this.syncInbounds(existingInbounds, inboundsEntities);
241+
242+
configProfileEntity.config = sortedConfig as object;
243+
}
229244

230-
// No need for now
231-
// await this.commandBus.execute(new SyncActiveProfileCommand());
245+
await this.configProfileRepository.update(configProfileEntity);
232246

233-
await this.startAllNodesByProfileQueueService.startAllNodesByProfile({
234-
profileUuid: existingConfigProfile.uuid,
235-
emitter: 'updateConfigProfile',
236-
});
247+
if (config) {
248+
// No need for now
249+
// await this.commandBus.execute(new SyncActiveProfileCommand());
250+
251+
await this.startAllNodesByProfileQueueService.startAllNodesByProfile({
252+
profileUuid: existingConfigProfile.uuid,
253+
emitter: 'updateConfigProfile',
254+
});
255+
}
237256

238257
return this.getConfigProfileByUUID(existingConfigProfile.uuid);
239258
} catch (error) {

src/modules/internal-squads/internal-squad.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export class InternalSquadController {
128128
): Promise<UpdateInternalSquadResponseDto> {
129129
const result = await this.internalSquadService.updateInternalSquad(
130130
updateInternalSquadDto.uuid,
131+
updateInternalSquadDto.name,
131132
updateInternalSquadDto.inbounds,
132133
);
133134

src/modules/internal-squads/internal-squad.service.ts

Lines changed: 82 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ export class InternalSquadService {
117117
@Transactional()
118118
public async updateInternalSquad(
119119
uuid: string,
120-
inbounds: string[],
120+
name?: string,
121+
inbounds?: string[],
121122
): Promise<ICommandResponse<GetInternalSquadByUuidResponseModel>> {
122123
try {
123124
const internalSquad = await this.internalSquadRepository.findByUUID(uuid);
@@ -129,69 +130,101 @@ export class InternalSquadService {
129130
};
130131
}
131132

132-
const currentInbounds = await this.internalSquadRepository.getInboundsBySquadUuid(
133-
internalSquad.uuid,
134-
);
135-
136-
const currentProfilesMap = new Map<string, Set<string>>();
137-
for (const inbound of currentInbounds) {
138-
if (!currentProfilesMap.has(inbound.configProfileUuid)) {
139-
currentProfilesMap.set(inbound.configProfileUuid, new Set());
140-
}
141-
currentProfilesMap.get(inbound.configProfileUuid)!.add(inbound.inboundUuid);
133+
if (!name && !inbounds) {
134+
return {
135+
isOk: false,
136+
...ERRORS.NAME_OR_INBOUNDS_REQUIRED,
137+
};
142138
}
143139

144-
/* Clean & Add inbounds */
145-
await this.internalSquadRepository.cleanInbounds(internalSquad.uuid);
146-
147-
if (inbounds.length > 0) {
148-
await this.internalSquadRepository.createInbounds(inbounds, internalSquad.uuid);
140+
if (name) {
141+
await this.internalSquadRepository.update({
142+
uuid,
143+
name,
144+
});
149145
}
150-
/* Clean & Add inbounds */
151146

152-
const newInbounds = await this.internalSquadRepository.getInboundsBySquadUuid(
153-
internalSquad.uuid,
154-
);
147+
if (inbounds !== undefined) {
148+
const currentInbounds = await this.internalSquadRepository.getInboundsBySquadUuid(
149+
internalSquad.uuid,
150+
);
151+
152+
const currentProfilesMap = new Map<string, Set<string>>();
153+
for (const inbound of currentInbounds) {
154+
if (!currentProfilesMap.has(inbound.configProfileUuid)) {
155+
currentProfilesMap.set(inbound.configProfileUuid, new Set());
156+
}
157+
currentProfilesMap.get(inbound.configProfileUuid)!.add(inbound.inboundUuid);
158+
}
159+
160+
/* Clean & Add inbounds */
161+
await this.internalSquadRepository.cleanInbounds(internalSquad.uuid);
155162

156-
const newProfilesMap = new Map<string, Set<string>>();
157-
for (const inbound of newInbounds) {
158-
if (!newProfilesMap.has(inbound.configProfileUuid)) {
159-
newProfilesMap.set(inbound.configProfileUuid, new Set());
163+
if (inbounds.length > 0) {
164+
await this.internalSquadRepository.createInbounds(inbounds, internalSquad.uuid);
165+
}
166+
/* Clean & Add inbounds */
167+
168+
const newInbounds = await this.internalSquadRepository.getInboundsBySquadUuid(
169+
internalSquad.uuid,
170+
);
171+
172+
const newProfilesMap = new Map<string, Set<string>>();
173+
for (const inbound of newInbounds) {
174+
if (!newProfilesMap.has(inbound.configProfileUuid)) {
175+
newProfilesMap.set(inbound.configProfileUuid, new Set());
176+
}
177+
newProfilesMap.get(inbound.configProfileUuid)!.add(inbound.inboundUuid);
160178
}
161-
newProfilesMap.get(inbound.configProfileUuid)!.add(inbound.inboundUuid);
162-
}
163179

164-
const allProfileUuids = new Set([
165-
...currentProfilesMap.keys(),
166-
...newProfilesMap.keys(),
167-
]);
180+
const allProfileUuids = new Set([
181+
...currentProfilesMap.keys(),
182+
...newProfilesMap.keys(),
183+
]);
168184

169-
const affectedConfigProfiles: string[] = [];
185+
const affectedConfigProfiles: string[] = [];
170186

171-
for (const profileUuid of allProfileUuids) {
172-
const currentSet = currentProfilesMap.get(profileUuid) || new Set();
173-
const newSet = newProfilesMap.get(profileUuid) || new Set();
187+
for (const profileUuid of allProfileUuids) {
188+
const currentSet = currentProfilesMap.get(profileUuid) || new Set();
189+
const newSet = newProfilesMap.get(profileUuid) || new Set();
174190

175-
if (currentSet.symmetricDifference(newSet).size > 0) {
176-
affectedConfigProfiles.push(profileUuid);
191+
if (currentSet.symmetricDifference(newSet).size > 0) {
192+
affectedConfigProfiles.push(profileUuid);
193+
}
177194
}
178-
}
179-
180-
this.logger.log(
181-
`Internal squad changed, restart nodes for profiles: ${affectedConfigProfiles.join(
182-
', ',
183-
)}`,
184-
);
185195

186-
affectedConfigProfiles.forEach(async (profileUuid) => {
187-
await this.startAllNodesByProfileQueueService.startAllNodesByProfile({
188-
profileUuid,
189-
emitter: 'updateInternalSquad',
190-
});
191-
});
196+
if (affectedConfigProfiles.length > 0) {
197+
this.logger.log(
198+
`Internal squad changed, restart nodes for profiles: ${affectedConfigProfiles.join(
199+
', ',
200+
)}`,
201+
);
202+
203+
await Promise.all(
204+
affectedConfigProfiles.map((profileUuid) =>
205+
this.startAllNodesByProfileQueueService.startAllNodesByProfile({
206+
profileUuid,
207+
emitter: 'updateInternalSquad',
208+
}),
209+
),
210+
);
211+
}
212+
}
192213

193214
return await this.getInternalSquadByUuid(internalSquad.uuid);
194215
} catch (error) {
216+
if (
217+
error instanceof PrismaClientKnownRequestError &&
218+
error.code === 'P2002' &&
219+
error.meta?.modelName === 'InternalSquads' &&
220+
Array.isArray(error.meta.target)
221+
) {
222+
const fields = error.meta.target as string[];
223+
if (fields.includes('name')) {
224+
return { isOk: false, ...ERRORS.INTERNAL_SQUAD_NAME_ALREADY_EXISTS };
225+
}
226+
}
227+
195228
this.logger.error(error);
196229
return {
197230
isOk: false,

0 commit comments

Comments
 (0)