diff --git a/src/common/utils/templates/replace-templates-values.ts b/src/common/utils/templates/replace-templates-values.ts index 9d2ed941..d35e135e 100644 --- a/src/common/utils/templates/replace-templates-values.ts +++ b/src/common/utils/templates/replace-templates-values.ts @@ -29,7 +29,7 @@ export class TemplateEngine { static formatWithUser( template: string, user: UserEntity, - subPublicDomain: string, + subscriptionLink: string, forHeader: boolean = false, ): string { return this.replace(template, { @@ -43,7 +43,7 @@ export class TemplateEngine { USERNAME: user.username, EMAIL: user.email || '', TELEGRAM_ID: user.telegramId?.toString() || '', - SUBSCRIPTION_URL: `https://${subPublicDomain}/${user.shortUuid}`, + SUBSCRIPTION_URL: subscriptionLink, TAG: user.tag || '', EXPIRE_UNIX: dayjs(user.expireAt).unix(), }); diff --git a/src/modules/hosts/index.ts b/src/modules/hosts/index.ts index a6c094b7..3a98b7d4 100644 --- a/src/modules/hosts/index.ts +++ b/src/modules/hosts/index.ts @@ -1,5 +1,5 @@ -export * from './entities/hosts.entity'; export * from './controllers/hosts.controller'; +export * from './entities/hosts.entity'; export * from './hosts.module'; export * from './hosts.service'; export * from './repositories/hosts.repository'; diff --git a/src/modules/subscription-template/generators/format-hosts.service.ts b/src/modules/subscription-template/generators/format-hosts.service.ts index 7a5e1c03..5f2873d2 100644 --- a/src/modules/subscription-template/generators/format-hosts.service.ts +++ b/src/modules/subscription-template/generators/format-hosts.service.ts @@ -9,6 +9,7 @@ import { QueryBus } from '@nestjs/cqrs'; import { GrpcObject, HttpUpgradeObject, + RawObject, StreamSettingsObject, TcpObject, WebSocketObject, @@ -19,7 +20,6 @@ import { resolveInboundAndMlDsa65PublicKey, resolveInboundAndPublicKey, } from '@common/helpers/xray-config'; -import { RawObject } from '@common/helpers/xray-config/interfaces/transport.config'; import { TemplateEngine } from '@common/utils/templates/replace-templates-values'; import { ICommandResponse } from '@common/types/command-response.type'; import { InboundObject } from '@common/helpers/xray-config/interfaces'; @@ -28,6 +28,7 @@ import { SECURITY_LAYERS, USERS_STATUS } from '@libs/contracts/constants'; import { SubscriptionSettingsEntity } from '@modules/subscription-settings/entities/subscription-settings.entity'; import { GetSubscriptionSettingsQuery } from '@modules/subscription-settings/queries/get-subscription-settings'; +import { resolveSubscriptionUrl } from '@modules/subscription/utils/resolve-subscription-url'; import { HostWithRawInbound } from '@modules/hosts/entities/host-with-inbound-tag.entity'; import { ExternalSquadEntity } from '@modules/external-squads/entities'; import { UserEntity } from '@modules/users/entities'; @@ -64,12 +65,20 @@ export class FormatHostsService { let specialRemarks: string[] = []; - if (user.status !== USERS_STATUS.ACTIVE) { - const settings = await this.getSubscriptionSettings(); - if (!settings) { - return formattedHosts; - } + const settings = await this.getSubscriptionSettings(); + + if (!settings) { + return formattedHosts; + } + + const subscriptionUrl = resolveSubscriptionUrl( + user.shortUuid, + user.username, + settings?.addUsernameToBaseSubscription, + this.subPublicDomain, + ); + if (user.status !== USERS_STATUS.ACTIVE) { if (settings.isShowCustomRemarks) { switch (user.status) { case USERS_STATUS.EXPIRED: @@ -84,7 +93,7 @@ export class FormatHostsService { } const templatedRemarks = specialRemarks.map((remark) => - TemplateEngine.formatWithUser(remark, user, this.subPublicDomain), + TemplateEngine.formatWithUser(remark, user, subscriptionUrl), ); formattedHosts.push(...this.createFallbackHosts(templatedRemarks)); @@ -146,11 +155,7 @@ export class FormatHostsService { } } - const remark = TemplateEngine.formatWithUser( - inputHost.remark, - user, - this.subPublicDomain, - ); + const remark = TemplateEngine.formatWithUser(inputHost.remark, user, subscriptionUrl); const currentCount = knownRemarks.get(remark) || 0; knownRemarks.set(remark, currentCount + 1); diff --git a/src/modules/subscription/queries/get-subscription-url/get-subscription-url.handler.ts b/src/modules/subscription/queries/get-subscription-url/get-subscription-url.handler.ts new file mode 100644 index 00000000..cbacfc9d --- /dev/null +++ b/src/modules/subscription/queries/get-subscription-url/get-subscription-url.handler.ts @@ -0,0 +1,18 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; + +import { ICommandResponse } from '@common/types/command-response.type'; + +import { SubscriptionService } from '@modules/subscription/subscription.service'; + +import { GetSubscriptionUrlQuery } from './get-subscription-url.query'; + +@QueryHandler(GetSubscriptionUrlQuery) +export class GetSubscriptionUrlHandler + implements IQueryHandler> +{ + constructor(private readonly subscriptionService: SubscriptionService) {} + + async execute(query: GetSubscriptionUrlQuery): Promise> { + return this.subscriptionService.getSubscriptionUrl(query.userShortUuid, query.username); + } +} diff --git a/src/modules/subscription/queries/get-subscription-url/get-subscription-url.query.ts b/src/modules/subscription/queries/get-subscription-url/get-subscription-url.query.ts new file mode 100644 index 00000000..99ebf403 --- /dev/null +++ b/src/modules/subscription/queries/get-subscription-url/get-subscription-url.query.ts @@ -0,0 +1,6 @@ +export class GetSubscriptionUrlQuery { + constructor( + public readonly userShortUuid: string, + public readonly username: string, + ) {} +} diff --git a/src/modules/subscription/queries/get-subscription-url/index.ts b/src/modules/subscription/queries/get-subscription-url/index.ts new file mode 100644 index 00000000..bce1a609 --- /dev/null +++ b/src/modules/subscription/queries/get-subscription-url/index.ts @@ -0,0 +1,2 @@ +export * from './get-subscription-url.handler'; +export * from './get-subscription-url.query'; diff --git a/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.handler.ts b/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.handler.ts new file mode 100644 index 00000000..577810e8 --- /dev/null +++ b/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.handler.ts @@ -0,0 +1,20 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; + +import { ICommandResponse } from '@common/types/command-response.type'; + +import { SubscriptionService } from '@modules/subscription/subscription.service'; + +import { GetUsersSubscriptionUrlQuery } from './get-users-subscription-url.query'; + +@QueryHandler(GetUsersSubscriptionUrlQuery) +export class GetUsersSubscriptionUrlHandler + implements IQueryHandler>> +{ + constructor(private readonly subscriptionService: SubscriptionService) {} + + async execute( + query: GetUsersSubscriptionUrlQuery, + ): Promise>> { + return this.subscriptionService.getUsersSubscriptionUrl(query.users); + } +} diff --git a/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.query.ts b/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.query.ts new file mode 100644 index 00000000..22144a4b --- /dev/null +++ b/src/modules/subscription/queries/get-users-subscription-url/get-users-subscription-url.query.ts @@ -0,0 +1,5 @@ +import { UserEntity } from '@modules/users/entities'; + +export class GetUsersSubscriptionUrlQuery { + constructor(public readonly users: Pick[]) {} +} diff --git a/src/modules/subscription/queries/get-users-subscription-url/index.ts b/src/modules/subscription/queries/get-users-subscription-url/index.ts new file mode 100644 index 00000000..4d7f7606 --- /dev/null +++ b/src/modules/subscription/queries/get-users-subscription-url/index.ts @@ -0,0 +1,2 @@ +export * from './get-users-subscription-url.handler'; +export * from './get-users-subscription-url.query'; diff --git a/src/modules/subscription/queries/index.ts b/src/modules/subscription/queries/index.ts new file mode 100644 index 00000000..ea5cf0ad --- /dev/null +++ b/src/modules/subscription/queries/index.ts @@ -0,0 +1,4 @@ +import { GetUsersSubscriptionUrlHandler } from './get-users-subscription-url'; +import { GetSubscriptionUrlHandler } from './get-subscription-url'; + +export const QUERIES = [GetSubscriptionUrlHandler, GetUsersSubscriptionUrlHandler]; diff --git a/src/modules/subscription/subscription.module.ts b/src/modules/subscription/subscription.module.ts index faccbff4..47aba595 100644 --- a/src/modules/subscription/subscription.module.ts +++ b/src/modules/subscription/subscription.module.ts @@ -8,6 +8,7 @@ import { SUBSCRIPTION_CONTROLLER, SUBSCRIPTION_ROUTES } from '@libs/contracts/ap import { SubscriptionResponseRulesModule } from '@modules/subscription-response-rules/subscription-response-rules.module'; import { ResponseRulesMiddleware } from '@modules/subscription-response-rules/middleware/response-rules.middleware'; import { SubscriptionTemplateModule } from '@modules/subscription-template/subscription-template.module'; +import { QUERIES } from '@modules/subscription/queries'; import { SubscriptionController, SubscriptionsController } from './controllers'; import { SubscriptionService } from './subscription.service'; @@ -15,7 +16,7 @@ import { SubscriptionService } from './subscription.service'; @Module({ imports: [CqrsModule, SubscriptionTemplateModule, SubscriptionResponseRulesModule], controllers: [SubscriptionController, SubscriptionsController], - providers: [SubscriptionService], + providers: [SubscriptionService, ...QUERIES], exports: [], }) export class SubscriptionModule implements NestModule { diff --git a/src/modules/subscription/subscription.service.ts b/src/modules/subscription/subscription.service.ts index afa0c526..a817c472 100644 --- a/src/modules/subscription/subscription.service.ts +++ b/src/modules/subscription/subscription.service.ts @@ -37,6 +37,7 @@ import { CountUsersDevicesQuery } from '@modules/hwid-user-devices/queries/count import { IFormattedHost, IRawHost } from '@modules/subscription-template/generators/interfaces'; import { GetUsersWithPaginationQuery } from '@modules/users/queries/get-users-with-pagination'; import { ExternalSquadEntity } from '@modules/external-squads/entities/external-squad.entity'; +import { resolveSubscriptionUrl } from '@modules/subscription/utils/resolve-subscription-url'; import { CheckHwidExistsQuery } from '@modules/hwid-user-devices/queries/check-hwid-exists'; import { GetUserByUniqueFieldQuery } from '@modules/users/queries/get-user-by-unique-field'; import { GetTemplateNameQuery } from '@modules/external-squads/queries/get-template-name'; @@ -283,6 +284,13 @@ export class SubscriptionService { }; } + const subscriptionUrl = resolveSubscriptionUrl( + user.response.shortUuid, + user.response.username, + settingEntity.addUsernameToBaseSubscription, + this.subPublicDomain, + ); + let isHwidLimited: boolean | undefined; const headers = await this.getUserProfileHeadersInfo( @@ -343,7 +351,7 @@ export class SubscriptionService { return { isOk: true, response: new RawSubscriptionWithHostsResponse({ - user: new GetUserResponseModel(user.response, this.subPublicDomain), + user: new GetUserResponseModel(user.response, subscriptionUrl), convertedUserInfo: { daysLeft: dayjs(user.response.expireAt).diff(dayjs(), 'day'), trafficUsed: prettyBytesUtil(user.response.usedTrafficBytes), @@ -507,10 +515,11 @@ export class SubscriptionService { ssConfLinks: Record, settingEntity: SubscriptionSettingsEntity, ): Promise { - const subscriptionUrl = this.resolveSubscriptionUrl( + const subscriptionUrl = resolveSubscriptionUrl( user.shortUuid, user.username, settingEntity.addUsernameToBaseSubscription, + this.subPublicDomain, ); return new SubscriptionRawResponse({ @@ -667,6 +676,58 @@ export class SubscriptionService { } } + public async getSubscriptionUrl( + userShortUuid: string, + username: string, + ): Promise> { + const settingEntity = await this.getCachedSubscriptionSettings(); + + if (!settingEntity) { + return { + isOk: false, + ...ERRORS.INTERNAL_SERVER_ERROR, + }; + } + + return { + isOk: true, + response: resolveSubscriptionUrl( + userShortUuid, + username, + settingEntity.addUsernameToBaseSubscription, + this.subPublicDomain, + ), + }; + } + + public async getUsersSubscriptionUrl( + users: Pick[], + ): Promise>> { + const settingEntity = await this.getCachedSubscriptionSettings(); + + if (!settingEntity) { + return { + isOk: false, + ...ERRORS.INTERNAL_SERVER_ERROR, + }; + } + + const linksEntries = users.map((user) => [ + user.shortUuid, + resolveSubscriptionUrl( + user.shortUuid, + user.username, + settingEntity.addUsernameToBaseSubscription, + this.subPublicDomain, + ), + ]); + + return { + isOk: true, + response: Object.fromEntries(linksEntries), + }; + } + private async generateSsConfLinks( subscriptionShortUuid: string, formattedHosts: IFormattedHost[], @@ -693,11 +754,18 @@ export class SubscriptionService { isHapp: boolean, settings: SubscriptionSettingsEntity, ): Promise { + const subscriptionLink = resolveSubscriptionUrl( + user.shortUuid, + user.username, + settings.addUsernameToBaseSubscription, + this.subPublicDomain, + ); + const headers: ISubscriptionHeaders = { 'content-disposition': `attachment; filename=${user.username}`, 'support-url': settings.supportLink, 'profile-title': `base64:${Buffer.from( - TemplateEngine.formatWithUser(settings.profileTitle, user, this.subPublicDomain), + TemplateEngine.formatWithUser(settings.profileTitle, user, subscriptionLink), ).toString('base64')}`, 'profile-update-interval': settings.profileUpdateInterval.toString(), 'subscription-userinfo': Object.entries(getSubscriptionUserInfo(user)) @@ -707,7 +775,7 @@ export class SubscriptionService { if (settings.happAnnounce) { headers.announce = `base64:${Buffer.from( - TemplateEngine.formatWithUser(settings.happAnnounce, user, this.subPublicDomain), + TemplateEngine.formatWithUser(settings.happAnnounce, user, subscriptionLink), ).toString('base64')}`; } @@ -716,11 +784,7 @@ export class SubscriptionService { } if (settings.isProfileWebpageUrlEnabled) { - headers['profile-web-page-url'] = this.resolveSubscriptionUrl( - user.shortUuid, - user.username, - settings.addUsernameToBaseSubscription, - ); + headers['profile-web-page-url'] = subscriptionLink; } const refillDate = getSubscriptionRefillDate(user.trafficLimitStrategy); @@ -730,12 +794,7 @@ export class SubscriptionService { if (settings.customResponseHeaders) { for (const [key, value] of Object.entries(settings.customResponseHeaders)) { - headers[key] = TemplateEngine.formatWithUser( - value, - user, - this.subPublicDomain, - true, - ); + headers[key] = TemplateEngine.formatWithUser(value, user, subscriptionLink, true); } } @@ -941,18 +1000,6 @@ export class SubscriptionService { } } - private resolveSubscriptionUrl( - shortUuid: string, - username: string, - addUsernameToBaseSubscription: boolean, - ): string { - if (addUsernameToBaseSubscription) { - return `https://${this.subPublicDomain}/${shortUuid}#${username}`; - } - - return `https://${this.subPublicDomain}/${shortUuid}`; - } - private async updateAndReportSubscriptionRequest( userUuid: string, userAgent: string, diff --git a/src/modules/subscription/utils/resolve-subscription-url.ts b/src/modules/subscription/utils/resolve-subscription-url.ts new file mode 100644 index 00000000..4e1ba136 --- /dev/null +++ b/src/modules/subscription/utils/resolve-subscription-url.ts @@ -0,0 +1,12 @@ +export const resolveSubscriptionUrl = ( + shortUuid: string, + username: string, + addUsernameToBaseSubscription: boolean, + subPublicDomain: string, +): string => { + if (addUsernameToBaseSubscription) { + return `https://${subPublicDomain}/${shortUuid}#${username}`; + } + + return `https://${subPublicDomain}/${shortUuid}`; +}; diff --git a/src/modules/users/controllers/users.controller.ts b/src/modules/users/controllers/users.controller.ts index 8b8db51c..e7076516 100644 --- a/src/modules/users/controllers/users.controller.ts +++ b/src/modules/users/controllers/users.controller.ts @@ -114,7 +114,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new CreateUserResponseModel(data, this.subPublicDomain), + response: new CreateUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -132,7 +132,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetUserResponseModel(data, this.subPublicDomain), + response: new GetUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -193,7 +193,8 @@ export class UsersController { response: new GetAllUsersResponseModel({ total: data.total, users: data.users.map( - (item) => new GetFullUserResponseModel(item, this.subPublicDomain), + (item) => + new GetFullUserResponseModel(item, data.subscriptionUrls[item.shortUuid]), ), }), }; @@ -297,7 +298,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetFullUserResponseModel(data, this.subPublicDomain), + response: new GetFullUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -320,7 +321,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetFullUserResponseModel(data, this.subPublicDomain), + response: new GetFullUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -350,7 +351,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetFullUserResponseModel(data, this.subPublicDomain), + response: new GetFullUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -380,7 +381,9 @@ export class UsersController { const data = errorHandler(result); return { - response: data.map((item) => new GetFullUserResponseModel(item, this.subPublicDomain)), + response: data.users.map( + (item) => new GetFullUserResponseModel(item, data.subscriptionUrls[item.shortUuid]), + ), }; } @@ -410,7 +413,9 @@ export class UsersController { const data = errorHandler(result); return { - response: data.map((item) => new GetFullUserResponseModel(item, this.subPublicDomain)), + response: data.users.map( + (item) => new GetFullUserResponseModel(item, data.subscriptionUrls[item.shortUuid]), + ), }; } @@ -441,7 +446,9 @@ export class UsersController { const data = errorHandler(result); return { - response: data.map((item) => new GetFullUserResponseModel(item, this.subPublicDomain)), + response: data.users.map( + (item) => new GetFullUserResponseModel(item, data.subscriptionUrls[item.shortUuid]), + ), }; } @@ -474,7 +481,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetUserResponseModel(data, this.subPublicDomain), + response: new GetUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -495,7 +502,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetUserResponseModel(data, this.subPublicDomain), + response: new GetUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -516,7 +523,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetUserResponseModel(data, this.subPublicDomain), + response: new GetUserResponseModel(data.user, data.subscriptionUrl), }; } @@ -539,7 +546,7 @@ export class UsersController { const data = errorHandler(result); return { - response: new GetUserResponseModel(data, this.subPublicDomain), + response: new GetUserResponseModel(data.user, data.subscriptionUrl), }; } } diff --git a/src/modules/users/models/create-user.response.model.ts b/src/modules/users/models/create-user.response.model.ts index ab3cb9cc..7a7aaa68 100644 --- a/src/modules/users/models/create-user.response.model.ts +++ b/src/modules/users/models/create-user.response.model.ts @@ -44,7 +44,7 @@ export class CreateUserResponseModel { cryptoLink: string; }; - constructor(entity: UserEntity, subPublicDomain: string) { + constructor(entity: UserEntity, subscriptionUrl: string) { this.uuid = entity.uuid; this.username = entity.username; this.shortUuid = entity.shortUuid; @@ -75,7 +75,7 @@ export class CreateUserResponseModel { this.firstConnectedAt = entity.firstConnectedAt; this.lastTriggeredThreshold = entity.lastTriggeredThreshold; - this.subscriptionUrl = `https://${subPublicDomain}/${entity.shortUuid}`; + this.subscriptionUrl = subscriptionUrl; this.activeInternalSquads = entity.activeInternalSquads; this.lastConnectedNode = entity.lastConnectedNode; diff --git a/src/modules/users/models/get-full-user.response.model.ts b/src/modules/users/models/get-full-user.response.model.ts index d26edb5f..be0056de 100644 --- a/src/modules/users/models/get-full-user.response.model.ts +++ b/src/modules/users/models/get-full-user.response.model.ts @@ -48,7 +48,7 @@ export class GetFullUserResponseModel { cryptoLink: string; }; - constructor(entity: UserEntity, subPublicDomain: string) { + constructor(entity: UserEntity, subscriptionUrl: string) { this.uuid = entity.uuid; this.username = entity.username; this.shortUuid = entity.shortUuid; @@ -80,7 +80,7 @@ export class GetFullUserResponseModel { this.firstConnectedAt = entity.firstConnectedAt; this.lastTriggeredThreshold = entity.lastTriggeredThreshold; - this.subscriptionUrl = `https://${subPublicDomain}/${entity.shortUuid}`; + this.subscriptionUrl = subscriptionUrl; this.lastConnectedNode = entity.lastConnectedNode; this.activeInternalSquads = entity.activeInternalSquads; diff --git a/src/modules/users/models/get-user.response.model.ts b/src/modules/users/models/get-user.response.model.ts index 53e6c445..3107ce2b 100644 --- a/src/modules/users/models/get-user.response.model.ts +++ b/src/modules/users/models/get-user.response.model.ts @@ -49,7 +49,7 @@ export class GetUserResponseModel { cryptoLink: string; }; - constructor(entity: UserEntity, subPublicDomain: string) { + constructor(entity: UserEntity, subscriptionUrl: string) { this.uuid = entity.uuid; this.username = entity.username; this.shortUuid = entity.shortUuid; @@ -81,7 +81,7 @@ export class GetUserResponseModel { this.firstConnectedAt = entity.firstConnectedAt; this.lastTriggeredThreshold = entity.lastTriggeredThreshold; - this.subscriptionUrl = `https://${subPublicDomain}/${entity.shortUuid}`; + this.subscriptionUrl = subscriptionUrl; this.activeInternalSquads = entity.activeInternalSquads; this.lastConnectedNode = entity.lastConnectedNode; diff --git a/src/modules/users/users.module.ts b/src/modules/users/users.module.ts index 62787bee..9cd62303 100644 --- a/src/modules/users/users.module.ts +++ b/src/modules/users/users.module.ts @@ -1,7 +1,7 @@ import { CqrsModule } from '@nestjs/cqrs'; import { Module } from '@nestjs/common'; -import { UsersController, UsersBulkActionsController, UsersStatsController } from './controllers'; +import { UsersBulkActionsController, UsersController, UsersStatsController } from './controllers'; import { UsersRepository } from './repositories/users.repository'; import { UserConverter } from './users.converter'; import { UsersService } from './users.service'; diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 660405cc..453605e6 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -21,6 +21,8 @@ import { UserEvent } from '@integration-modules/notifications/interfaces'; import { GetUserSubscriptionRequestHistoryQuery } from '@modules/user-subscription-request-history/queries/get-user-subscription-request-history'; import { CreateUserTrafficHistoryCommand } from '@modules/user-traffic-history/commands/create-user-traffic-history'; import { GetUserUsageByRangeQuery } from '@modules/nodes-user-usage-history/queries/get-user-usage-by-range'; +import { GetUsersSubscriptionUrlQuery } from '@modules/subscription/queries/get-users-subscription-url'; +import { GetSubscriptionUrlQuery } from '@modules/subscription/queries/get-subscription-url'; import { RemoveUserFromNodeEvent } from '@modules/nodes/events/remove-user-from-node'; import { AddUserToNodeEvent } from '@modules/nodes/events/add-user-to-node'; import { UserTrafficHistoryEntity } from '@modules/user-traffic-history'; @@ -73,12 +75,23 @@ export class UsersService { this.shortUuidLength = this.configService.getOrThrow('SHORT_UUID_LENGTH'); } - public async createUser(dto: CreateUserRequestDto): Promise> { + public async createUser( + dto: CreateUserRequestDto, + ): Promise> { try { const user = await this.createUserTransactional(dto); if (!user.isOk || !user.response) { - return user; + return { ...user, response: undefined }; + } + + const subscriptionUrlResult = await this.getSubscriptionUrl( + user.response.shortUuid, + user.response.username, + ); + + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; } if (user.response.status === USERS_STATUS.ACTIVE) { @@ -92,7 +105,13 @@ export class UsersService { event: EVENTS.USER.CREATED, }), ); - return user; + return { + isOk: true, + response: { + user: user.response, + subscriptionUrl: subscriptionUrlResult.response, + }, + }; } catch (error) { this.logger.error(error); if ( @@ -117,7 +136,9 @@ export class UsersService { } } - public async updateUser(dto: UpdateUserRequestDto): Promise> { + public async updateUser( + dto: UpdateUserRequestDto, + ): Promise> { try { const user = await this.updateUserTransactional(dto); @@ -128,6 +149,15 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + user.response.user.shortUuid, + user.response.user.username, + ); + + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + if ( user.response.user.status === USERS_STATUS.ACTIVE && user.response.isNeedToBeAddedToNode && @@ -155,7 +185,10 @@ export class UsersService { return { isOk: true, - response: user.response.user, + response: { + user: user.response.user, + subscriptionUrl: subscriptionUrlResult.response, + }, }; } catch (error) { if (error instanceof Error && error.message === ERRORS.USER_NOT_FOUND.code) { @@ -408,16 +441,23 @@ export class UsersService { ICommandResponse<{ total: number; users: UserEntity[]; + subscriptionUrls: Record; }> > { try { const [users, total] = await this.userRepository.getAllUsersV2(dto); + const subscriptionUrlsResult = await this.getUsersSubscriptionUrl(users); + if (!subscriptionUrlsResult.isOk || !subscriptionUrlsResult.response) { + return { ...subscriptionUrlsResult, response: undefined }; + } + return { isOk: true, response: { users, total, + subscriptionUrls: subscriptionUrlsResult.response, }, }; } catch (error) { @@ -431,7 +471,7 @@ export class UsersService { public async getUserByUniqueFields( dto: IGetUserByUnique, - ): Promise> { + ): Promise> { try { const result = await this.userRepository.findUniqueByCriteria({ username: dto.username || undefined, @@ -446,9 +486,17 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + result.shortUuid, + result.username, + ); + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + return { isOk: true, - response: result, + response: { user: result, subscriptionUrl: subscriptionUrlResult.response }, }; } catch (error) { this.logger.error(error); @@ -461,7 +509,9 @@ export class UsersService { public async getUsersByNonUniqueFields( dto: IGetUsersByTelegramIdOrEmail, - ): Promise> { + ): Promise< + ICommandResponse<{ users: UserEntity[]; subscriptionUrls: Record }> + > { try { const result = await this.userRepository.findByNonUniqueCriteria({ email: dto.email || undefined, @@ -476,9 +526,14 @@ export class UsersService { }; } + const subscriptionUrlsResult = await this.getUsersSubscriptionUrl(result); + if (!subscriptionUrlsResult.isOk || !subscriptionUrlsResult.response) { + return { ...subscriptionUrlsResult, response: undefined }; + } + return { isOk: true, - response: result, + response: { users: result, subscriptionUrls: subscriptionUrlsResult.response }, }; } catch (error) { this.logger.error(error); @@ -492,7 +547,7 @@ export class UsersService { public async revokeUserSubscription( userUuid: string, shortUuid?: string, - ): Promise> { + ): Promise> { try { const user = await this.userRepository.getPartialUserByUniqueFields( { uuid: userUuid }, @@ -531,6 +586,14 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + updatedUser.shortUuid, + updatedUser.username, + ); + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + if (updatedUser.status === USERS_STATUS.ACTIVE) { this.eventBus.publish(new AddUserToNodeEvent(updatedUser.uuid, user.vlessUuid)); } @@ -545,7 +608,7 @@ export class UsersService { return { isOk: true, - response: updatedUser, + response: { user: updatedUser, subscriptionUrl: subscriptionUrlResult.response }, }; } catch (error) { this.logger.error(error); @@ -594,7 +657,9 @@ export class UsersService { } } - public async disableUser(userUuid: string): Promise> { + public async disableUser( + userUuid: string, + ): Promise> { try { const user = await this.userRepository.getPartialUserByUniqueFields( { uuid: userUuid }, @@ -626,6 +691,14 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + updatedUser.shortUuid, + updatedUser.username, + ); + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + this.eventBus.publish( new RemoveUserFromNodeEvent(updatedUser.username, updatedUser.vlessUuid), ); @@ -639,7 +712,7 @@ export class UsersService { return { isOk: true, - response: updatedUser, + response: { user: updatedUser, subscriptionUrl: subscriptionUrlResult.response }, }; } catch (error) { this.logger.error(error); @@ -650,7 +723,9 @@ export class UsersService { } } - public async enableUser(userUuid: string): Promise> { + public async enableUser( + userUuid: string, + ): Promise> { try { const user = await this.userRepository.getPartialUserByUniqueFields( { uuid: userUuid }, @@ -682,6 +757,14 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + updatedUser.shortUuid, + updatedUser.username, + ); + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + this.eventBus.publish(new AddUserToNodeEvent(user.uuid)); this.eventEmitter.emit( @@ -694,7 +777,7 @@ export class UsersService { return { isOk: true, - response: updatedUser, + response: { user: updatedUser, subscriptionUrl: subscriptionUrlResult.response }, }; } catch (error) { this.logger.error(error); @@ -706,7 +789,9 @@ export class UsersService { } @Transactional() - public async resetUserTraffic(userUuid: string): Promise> { + public async resetUserTraffic( + userUuid: string, + ): Promise> { try { const user = await this.userRepository.getPartialUserByUniqueFields( { uuid: userUuid }, @@ -755,6 +840,14 @@ export class UsersService { }; } + const subscriptionUrlResult = await this.getSubscriptionUrl( + newUser.shortUuid, + newUser.username, + ); + if (!subscriptionUrlResult.isOk || !subscriptionUrlResult.response) { + return { ...subscriptionUrlResult, response: undefined }; + } + if (user.status === USERS_STATUS.LIMITED) { this.eventEmitter.emit( EVENTS.USER.ENABLED, @@ -775,7 +868,7 @@ export class UsersService { return { isOk: true, - response: newUser, + response: { user: newUser, subscriptionUrl: subscriptionUrlResult.response }, }; } catch (error) { this.logger.error(error); @@ -1178,4 +1271,22 @@ export class UsersService { ICommandResponse >(new GetUserUsageByRangeQuery(userUuid, start, end)); } + + private getSubscriptionUrl = async ( + shortUuid: string, + username: string, + ): Promise> => { + return this.queryBus.execute>( + new GetSubscriptionUrlQuery(shortUuid, username), + ); + }; + + private getUsersSubscriptionUrl = async ( + users: Pick[], + ): Promise>> => { + return this.queryBus.execute< + GetUsersSubscriptionUrlQuery, + ICommandResponse> + >(new GetUsersSubscriptionUrlQuery(users)); + }; } diff --git a/src/queue/bulk-user-operations/index.ts b/src/queue/bulk-user-operations/index.ts index d5829ae4..e2e6547a 100644 --- a/src/queue/bulk-user-operations/index.ts +++ b/src/queue/bulk-user-operations/index.ts @@ -1,2 +1,2 @@ -export * from './enums'; export * from './bulk-user-operations.service'; +export * from './enums';