Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Performer Controller Code Samples :

Code Block
import {
  Controller,
  Injectable,
  UseGuards,
  Body,
  Post,
  HttpCode,
  HttpStatus,
  UsePipes,
  ValidationPipe,
  Put,
  Get,
  Param,
  Query,
  Request,
  UseInterceptors,
  HttpException
} from '@nestjs/common';
import {
  DataResponse,
  PageableData,
  getConfig,
  ForbiddenException
} from 'src/kernel';
import { AuthService, Roles } from 'src/modules/auth';
import { RoleGuard, LoadUser } from 'src/modules/auth/guards';
import { CurrentUser } from 'src/modules/auth/decorators';
import {
  FileUploadInterceptor, FileUploaded, FileDto
} from 'src/modules/file';
import { REF_TYPE } from 'src/modules/file/constants';
import { FileService } from 'src/modules/file/services';
import { UserDto } from 'src/modules/user/dtos';
import { CountryService } from 'src/modules/utils/services';
import { PERFORMER_STATUSES } from '../constants';
import {
  PerformerDto, IPerformerResponse
} from '../dtos';
import {
  SelfUpdatePayload, PerformerSearchPayload, BankingSettingPayload, PaymentGatewaySettingPayload
} from '../payloads';
import { PerformerService, PerformerSearchService } from '../services';

@Injectable()
@Controller('performers')
export class PerformerController {
  constructor(
    private readonly performerService: PerformerService,
    private readonly performerSearchService: PerformerSearchService,
    private readonly authService: AuthService,
    private readonly countryService: CountryService,
    private readonly fileService: FileService
  ) {}

  @Get('/me')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  async me(@Request() req: any): Promise<DataResponse<IPerformerResponse>> {
    const user = await this.performerService.getDetails(req.user._id, req.jwToken);
    return DataResponse.ok(new PerformerDto(user).toResponse(true, false));
  }

  @Get('/search')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true }))
  async usearch(
    @Query() req: PerformerSearchPayload
  ): Promise<DataResponse<PageableData<IPerformerResponse>>> {
    const data = await this.performerSearchService.search(req);
    return DataResponse.ok({
      total: data.total,
      data: data.data.map((p) => p.toPublicDetailsResponse())
    });
  }

  @Get('/top')
  @HttpCode(HttpStatus.OK)
  @UsePipes(new ValidationPipe({ transform: true }))
  async topPerformers(
    @Query() req: PerformerSearchPayload
  ): Promise<DataResponse<PageableData<IPerformerResponse>>> {
    const query = { ...req };
    // only query activated performer, sort by online time
    query.status = PERFORMER_STATUSES.ACTIVE;

    const data = await this.performerSearchService.topPerformers(query);
    return DataResponse.ok({
      total: data.total,
      data: data.data.map((p) => p.toSearchResponse())
    });
  }

  @Put('/:id')
  @Roles('performer')
  @UseGuards(RoleGuard)
  async updateUser(
    @Body() payload: SelfUpdatePayload,
    @Param('id') performerId: string,
    @Request() req: any
  ): Promise<DataResponse<IPerformerResponse>> {
    await this.performerService.selfUpdate(performerId, payload);
    const performer = await this.performerService.getDetails(performerId, req.jwToken);

    if (payload.password) {
      await Promise.all([
        performer.email && this.authService.create({
          source: 'performer',
          sourceId: performer._id,
          type: 'email',
          key: performer.email,
          value: payload.password
        }),
        performer.username && this.authService.create({
          source: 'performer',
          sourceId: performer._id,
          type: 'username',
          key: performer.username,
          value: payload.password
        })
      ]);
    }
    return DataResponse.ok(new PerformerDto(performer).toResponse(true, false));
  }

  @Get('/:username')
  @UseGuards(LoadUser)
  @HttpCode(HttpStatus.OK)
  async getDetails(
    @Param('username') performerUsername: string,
    @Request() req: any,
    @CurrentUser() user: UserDto
  ): Promise<DataResponse<Partial<PerformerDto>>> {
    let ipClient = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

    if (ipClient.substr(0, 7) === '::ffff:') {
      ipClient = ipClient.substr(7);
    }
    // const ipClient = '115.75.211.252';
    const whiteListIps = ['127.0.0.1', '0.0.0.1'];
    let countryCode = null;
    if (whiteListIps.indexOf(ipClient) === -1) {
      const userCountry = await this.countryService.findCountryByIP(ipClient) as any;
      if (userCountry?.status === 'success' && userCountry?.countryCode) {
        countryCode = userCountry.countryCode;
      }
    }
    const performer = await this.performerService.findByUsername(
      performerUsername,
      countryCode,
      user
    );

    if (!performer || performer.status !== PERFORMER_STATUSES.ACTIVE) {
      throw new HttpException('This account is suspended', 422);
    }

    return DataResponse.ok(performer.toPublicDetailsResponse());
  }

  @Post('/documents/upload')
  @Roles('performer')
  @UseGuards(RoleGuard)
  @HttpCode(HttpStatus.OK)
  @UseInterceptors(
    FileUploadInterceptor('performer-document', 'file', {
      destination: getConfig('file').documentDir
    })
  )
  async uploadPerformerDocument(
    @CurrentUser() currentUser: UserDto,
    @FileUploaded() file: FileDto,
    @Request() req: any
  ): Promise<any> {
    await this.fileService.addRef(file._id, {
      itemId: currentUser._id,
      itemType: REF_TYPE.PERFORMER
    });
    return DataResponse.ok({
      ...file,
      url: `${file.getUrl()}?documentId=${file._id}&token=${req.jwToken}`
    });
  }

  @Put('/:id/payment-gateway-settings')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  async updatePaymentGatewaySetting(
    @Body() payload: PaymentGatewaySettingPayload
  ) {
    const data = await this.performerService.updatePaymentGateway(payload);
    return DataResponse.ok(data);
  }

  @Post('/avatar/upload')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  @UseInterceptors(
    FileUploadInterceptor('avatar', 'avatar', {
      destination: getConfig('file').avatarDir,
      generateThumbnail: true,
      replaceByThumbnail: true,
      thumbnailSize: getConfig('image').avatar
    })
  )
  async uploadPerformerAvatar(
    @FileUploaded() file: FileDto,
    @CurrentUser() performer: PerformerDto
  ): Promise<any> {
    // TODO - define url for perfomer id if have?
    await this.performerService.updateAvatar(performer, file);
    return DataResponse.ok({
      ...file,
      url: file.getUrl()
    });
  }

  @Post('/cover/upload')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  @UseInterceptors(
    FileUploadInterceptor('cover', 'cover', {
      destination: getConfig('file').coverDir,
      generateThumbnail: true,
      thumbnailSize: getConfig('image').coverThumbnail
    })
  )
  async uploadPerformerCover(
    @FileUploaded() file: FileDto,
    @CurrentUser() performer: PerformerDto
  ): Promise<any> {
    // TODO - define url for perfomer id if have?
    await this.performerService.updateCover(performer, file);
    return DataResponse.ok({
      ...file,
      url: file.getUrl()
    });
  }

  @Post('/welcome-video/upload')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  @UseInterceptors(
    FileUploadInterceptor('performer-welcome-video', 'welcome-video', {
      destination: getConfig('file').videoDir
    })
  )
  async uploadPerformerVideo(
    @FileUploaded() file: FileDto,
    @CurrentUser() performer: PerformerDto
  ): Promise<any> {
    // TODO - define url for perfomer id if have?
    await this.performerService.updateWelcomeVideo(performer, file);
    return DataResponse.ok({
      ...file,
      url: file.getUrl()
    });
  }

  @Put('/:id/banking-settings')
  @HttpCode(HttpStatus.OK)
  @Roles('performer')
  @UseGuards(RoleGuard)
  async updateBankingSetting(
    @Param('id') performerId: string,
    @Body() payload: BankingSettingPayload,
    @CurrentUser() user: UserDto
  ) {
    const data = await this.performerService.updateBankingSetting(
      performerId,
      payload,
      user
    );
    return DataResponse.ok(data);
  }

  @Get('/documents/auth/check')
  @HttpCode(HttpStatus.OK)
  async checkAuth(
    @Request() req: any
  ) {
    if (!req.query.token) throw new ForbiddenException();
    const user = await this.authService.getSourceFromJWT(req.query.token);
    if (!user) {
      throw new ForbiddenException();
    }
    const valid = await this.performerService.checkAuthDocument(req, user);
    return DataResponse.ok(valid);
  }
}

Performer Service Code Samples:

Code Block
import {
  Injectable, Inject, NotAcceptableException, forwardRef, HttpException
} from '@nestjs/common';
import { Model } from 'mongoose';
import {
  EntityNotFoundException, ForbiddenException, QueueEventService, QueueEvent, StringHelper
} from 'src/kernel';
import { ObjectId } from 'mongodb';
import { FileService } from 'src/modules/file/services';
import { SettingService } from 'src/modules/settings';
import { SETTING_KEYS } from 'src/modules/settings/constants';
import { SubscriptionService } from 'src/modules/subscription/services/subscription.service';
import { FileDto } from 'src/modules/file';
import { UserDto } from 'src/modules/user/dtos';
import { AuthService } from 'src/modules/auth/services';
import { EVENT } from 'src/kernel/constants';
import { REF_TYPE } from 'src/modules/file/constants';
import { EmailHasBeenTakenException } from 'src/modules/user/exceptions';
import { MailerService } from 'src/modules/mailer';
import { UserService } from 'src/modules/user/services';
import { isObjectId } from 'src/kernel/helpers/string.helper';
import { PerformerBlockService } from 'src/modules/block/services';
import { PERFORMER_UPDATE_STATUS_CHANNEL, DELETE_PERFORMER_CHANNEL } from '../constants';
import { PerformerDto } from '../dtos';
import {
  UsernameExistedException, EmailExistedException
} from '../exceptions';
import {
  PerformerModel, PaymentGatewaySettingModel, CommissionSettingModel, BankingModel
} from '../models';
import {
  PerformerCreatePayload,
  PerformerUpdatePayload,
  PerformerRegisterPayload,
  SelfUpdatePayload,
  PaymentGatewaySettingPayload,
  CommissionSettingPayload,
  BankingSettingPayload
} from '../payloads';
import {
  PERFORMER_BANKING_SETTING_MODEL_PROVIDER,
  PERFORMER_COMMISSION_SETTING_MODEL_PROVIDER,
  PERFORMER_MODEL_PROVIDER,
  PERFORMER_PAYMENT_GATEWAY_SETTING_MODEL_PROVIDER
} from '../providers';

@Injectable()
export class PerformerService {
  constructor(

    @Inject(forwardRef(() => PerformerBlockService))
    private readonly performerBlockService: PerformerBlockService,
    @Inject(forwardRef(() => AuthService))
    private readonly authService: AuthService,
    @Inject(forwardRef(() => UserService))
    private readonly userService: UserService,
    @Inject(forwardRef(() => FileService))
    private readonly fileService: FileService,
    @Inject(forwardRef(() => SubscriptionService))
    private readonly subscriptionService: SubscriptionService,
    @Inject(PERFORMER_MODEL_PROVIDER)
    private readonly performerModel: Model<PerformerModel>,
    private readonly queueEventService: QueueEventService,
    private readonly mailService: MailerService,
    @Inject(PERFORMER_PAYMENT_GATEWAY_SETTING_MODEL_PROVIDER)
    private readonly paymentGatewaySettingModel: Model<PaymentGatewaySettingModel>,
    @Inject(PERFORMER_BANKING_SETTING_MODEL_PROVIDER)
    private readonly bankingSettingModel: Model<BankingModel>,
    @Inject(PERFORMER_COMMISSION_SETTING_MODEL_PROVIDER)
    private readonly commissionSettingModel: Model<CommissionSettingModel>
  ) { }

  public async findById(
    id: string | ObjectId
  ): Promise<PerformerModel> {
    const model = await this.performerModel.findById(id);
    if (!model) return null;
    return model;
  }

  public async findOne(query) {
    const data = await this.performerModel.findOne(query).lean();
    return data;
  }

  public async getBankingSettings(performerId: ObjectId) {
    return this.bankingSettingModel.findOne({
      performerId
    });
  }

  public async checkExistedEmailorUsername(payload) {
    const data = payload.username ? await this.performerModel.countDocuments({ username: payload.username.trim().toLowerCase() })
      : await this.performerModel.countDocuments({ email: payload.email.toLowerCase() });
    return data;
  }

  public async findByUsername(
    username: string,
    countryCode?: string,
    currentUser?: UserDto
  ): Promise<PerformerDto> {
    const query = isObjectId(username) ? { _id: username } : { username: username.trim() };
    const model = await this.performerModel.findOne(query);
    if (!model) throw new EntityNotFoundException();
    let isBlocked = false;
    let isSubscribed = false;
    if (countryCode && `${currentUser?._id}` !== `${model._id}`) {
      isBlocked = await this.performerBlockService.checkBlockedCountryByIp(model._id, countryCode);
      if (isBlocked) {
        throw new HttpException('Your country has been blocked by this model', 403);
      }
    }
    if (currentUser && currentUser._id) {
      isBlocked = `${currentUser?._id}` !== `${model._id}` && await this.performerBlockService.checkBlockedByPerformer(
        model._id,
        currentUser._id
      );
      if (isBlocked) throw new HttpException('You have been blocked by this model', 403);
      const checkSubscribe = await this.subscriptionService.checkSubscribed(model._id, currentUser._id);
      isSubscribed = !!checkSubscribe;
    }
    const dto = new PerformerDto(model);
    dto.isSubscribed = isSubscribed;
    if (model.avatarId) {
      const avatar = await this.fileService.findById(model.avatarId);
      dto.avatarPath = avatar ? avatar.path : null;
    }
    if (model.welcomeVideoId) {
      const welcomeVideo = await this.fileService.findById(
        model.welcomeVideoId
      );
      dto.welcomeVideoPath = welcomeVideo ? welcomeVideo.getUrl() : null;
    }
    await this.viewProfile(model._id);
    return dto;
  }

  public async findByEmail(email: string): Promise<PerformerDto> {
    if (!email) {
      return null;
    }
    const model = await this.performerModel.findOne({
      email: email.toLowerCase()
    });
    if (!model) return null;
    return new PerformerDto(model);
  }

  public async delete(id: string) {
    if (!StringHelper.isObjectId(id)) throw new ForbiddenException();
    const performer = await this.performerModel.findById(id);
    if (!performer) throw new EntityNotFoundException();
    await this.performerModel.deleteOne({ _id: id });
    await this.queueEventService.publish(new QueueEvent({
      channel: DELETE_PERFORMER_CHANNEL,
      eventName: EVENT.DELETED,
      data: new PerformerDto(performer).toResponse()
    }));
    return { deleted: true };
  }

  public async create(
    payload: PerformerCreatePayload,
    user?: UserDto
  ): Promise<PerformerDto> {
    const data = {
      ...payload,
      updatedAt: new Date(),
      createdAt: new Date()
    } as any;
    const countPerformerUsername = await this.performerModel.countDocuments({
      username: payload.username.trim().toLowerCase()
    });
    const countUserUsername = await this.userService.checkExistedEmailorUsername({ username: payload.username });
    if (countPerformerUsername || countUserUsername) {
      throw new UsernameExistedException();
    }

    const countPerformerEmail = await this.performerModel.countDocuments({
      email: payload.email.toLowerCase()
    });
    const countUserEmail = await this.userService.checkExistedEmailorUsername({ email: payload.email });
    if (countPerformerEmail || countUserEmail) {
      throw new EmailExistedException();
    }

    if (payload.avatarId) {
      const avatar = await this.fileService.findById(payload.avatarId);
      if (!avatar) {
        throw new EntityNotFoundException('Avatar not found!');
      }
      // TODO - check for other storaged
      data.avatarPath = avatar.path;
    }

    if (payload.coverId) {
      const cover = await this.fileService.findById(payload.coverId);
      if (!cover) {
        throw new EntityNotFoundException('Cover not found!');
      }
      // TODO - check for other storaged
      data.coverPath = cover.path;
    }

    // TODO - check for category Id, studio
    if (user) {
      data.createdBy = user._id;
    }
    data.username = data.username.trim().toLowerCase();
    data.email = data.email.toLowerCase();
    if (data.dateOfBirth) {
      data.dateOfBirth = new Date(data.dateOfBirth);
    }
    if (!data.name) {
      data.name = data.firstName && data.lastName ? [data.firstName, data.lastName].join(' ') : 'No_display_name';
    }
    const performer = await this.performerModel.create(data);

    await Promise.all([
      payload.idVerificationId
      && this.fileService.addRef(payload.idVerificationId, {
        itemId: performer._id as any,
        itemType: REF_TYPE.PERFORMER
      }),
      payload.documentVerificationId
      && this.fileService.addRef(payload.documentVerificationId, {
        itemId: performer._id as any,
        itemType: REF_TYPE.PERFORMER
      }),
      payload.avatarId
        && this.fileService.addRef(payload.avatarId, {
          itemId: performer._id as any,
          itemType: REF_TYPE.PERFORMER
        })
    ]);

    // TODO - fire event?
    return new PerformerDto(performer);
  }

  public async register(
    payload: PerformerRegisterPayload
  ): Promise<PerformerDto> {
    const data = {
      ...payload,
      updatedAt: new Date(),
      createdAt: new Date()
    } as any;
    const countPerformerUsername = await this.performerModel.countDocuments({
      username: payload.username.trim().toLowerCase()
    });
    const countUserUsername = await this.userService.checkExistedEmailorUsername({ username: payload.username });
    if (countPerformerUsername || countUserUsername) {
      throw new UsernameExistedException();
    }

    const countPerformerEmail = await this.performerModel.countDocuments({
      email: payload.email.toLowerCase()
    });
    const countUserEmail = await this.userService.checkExistedEmailorUsername({ email: payload.email });
    if (countPerformerEmail || countUserEmail) {
      throw new EmailExistedException();
    }

    if (payload.avatarId) {
      const avatar = await this.fileService.findById(payload.avatarId);
      if (!avatar) {
        throw new EntityNotFoundException('Avatar not found!');
      }
      // TODO - check for other storaged
      data.avatarPath = avatar.path;
    }
    data.username = data.username.trim().toLowerCase();
    data.email = data.email.toLowerCase();
    if (!data.name) {
      data.name = data.firstName && data.lastName ? [data.firstName, data.lastName].join(' ') : 'No_display_name';
    }
    if (data.dateOfBirth) {
      data.dateOfBirth = new Date(data.dateOfBirth);
    }
    const performer = await this.performerModel.create(data);

    await Promise.all([
      payload.idVerificationId
      && this.fileService.addRef(payload.idVerificationId, {
        itemId: performer._id as any,
        itemType: REF_TYPE.PERFORMER
      }),
      payload.documentVerificationId
      && this.fileService.addRef(payload.documentVerificationId, {
        itemId: performer._id as any,
        itemType: REF_TYPE.PERFORMER
      }),
      payload.avatarId && this.fileService.addRef(payload.avatarId, {
        itemId: performer._id as any,
        itemType: REF_TYPE.PERFORMER
      })
    ]);
    const adminEmail = await SettingService.getValueByKey(SETTING_KEYS.ADMIN_EMAIL);
    adminEmail && await this.mailService.send({
      subject: 'New model sign up',
      to: adminEmail,
      data: performer,
      template: 'new-performer-notify-admin'
    });

    // TODO - fire event?
    return new PerformerDto(performer);
  }

  public async adminUpdate(
    id: string | ObjectId,
    payload: PerformerUpdatePayload
  ): Promise<any> {
    const performer = await this.performerModel.findById(id);
    if (!performer) {
      throw new EntityNotFoundException();
    }

    const data = { ...payload } as any;
    if (!data.name) {
      data.name = [data.firstName || '', data.lastName || ''].join(' ');
    }

    if (data.email && data.email.toLowerCase() !== performer.email) {
      const emailCheck = await this.performerModel.countDocuments({
        email: data.email.toLowerCase(),
        _id: { $ne: performer._id }
      });
      const countUserEmail = await this.userService.checkExistedEmailorUsername({ email: data.email });
      if (emailCheck || countUserEmail) {
        throw new EmailExistedException();
      }
      data.email = data.email.toLowerCase();
    }

    if (data.username && data.username.trim() !== performer.username) {
      const usernameCheck = await this.performerModel.countDocuments({
        username: data.username.trim().toLowerCase(),
        _id: { $ne: performer._id }
      });
      const countUserUsername = await this.userService.checkExistedEmailorUsername({ username: data.username });
      if (usernameCheck || countUserUsername) {
        throw new UsernameExistedException();
      }
      data.username = data.username.trim().toLowerCase();
    }

    if (
      (payload.avatarId && !performer.avatarId)
      || (performer.avatarId
        && payload.avatarId
        && payload.avatarId !== performer.avatarId.toString())
    ) {
      const avatar = await this.fileService.findById(payload.avatarId);
      if (!avatar) {
        throw new EntityNotFoundException('Avatar not found!');
      }
      // TODO - check for other storaged
      data.avatarPath = avatar.path;
    }

    if (
      (payload.coverId && !performer.coverId)
      || (performer.coverId
        && payload.coverId
        && payload.coverId !== performer.coverId.toString())
    ) {
      const cover = await this.fileService.findById(payload.coverId);
      if (!cover) {
        throw new EntityNotFoundException('Cover not found!');
      }
      // TODO - check for other storaged
      data.coverPath = cover.path;
    }
    if (data.dateOfBirth) {
      data.dateOfBirth = new Date(data.dateOfBirth);
    }
    await this.performerModel.updateOne({ _id: id }, data);
    const newPerformer = await this.performerModel.findById(performer._id);
    const oldStatus = performer.status;
    // fire event that updated performer status
    if (data.status !== performer.status) {
      await this.queueEventService.publish(
        new QueueEvent({
          channel: PERFORMER_UPDATE_STATUS_CHANNEL,
          eventName: EVENT.UPDATED,
          data: {
            ...new PerformerDto(newPerformer),
            oldStatus
          }
        })
      );
    }
    // update auth key if email has changed
    if (data.email && data.email.toLowerCase() !== performer.email) {
      await this.authService.sendVerificationEmail({ email: newPerformer.email, _id: newPerformer._id });
      await this.authService.updateKey({
        source: 'performer',
        sourceId: newPerformer._id,
        type: 'email'
      });
    }
    // update auth key if username has changed
    if ((data.username && data.username.trim() !== performer.username)) {
      await this.authService.updateKey({
        source: 'performer',
        sourceId: newPerformer._id,
        type: 'username'
      });
    }
    return true;
  }

  public async selfUpdate(
    id: string | ObjectId,
    payload: SelfUpdatePayload
  ): Promise<boolean> {
    const performer = await this.performerModel.findById(id);
    if (!performer) {
      throw new EntityNotFoundException();
    }
    const data = { ...payload } as any;
    if (!data.name) {
      data.name = [data.firstName || '', data.lastName || ''].join(' ');
    }
    if (data.email && data.email.toLowerCase() !== performer.email) {
      const emailCheck = await this.performerModel.countDocuments({
        email: data.email.toLowerCase(),
        _id: { $ne: performer._id }
      });
      const countUserEmail = await this.userService.checkExistedEmailorUsername({ email: data.email });
      if (emailCheck || countUserEmail) {
        throw new EmailHasBeenTakenException();
      }
      data.email = data.email.toLowerCase();
    }

    if (data.username && data.username.trim() !== performer.username) {
      const usernameCheck = await this.performerModel.countDocuments({
        username: data.username.trim().toLowerCase(),
        _id: { $ne: performer._id }
      });
      const countUserUsername = await this.userService.checkExistedEmailorUsername({ username: data.username });
      if (usernameCheck || countUserUsername) {
        throw new UsernameExistedException();
      }
      data.username = data.username.trim().toLowerCase();
    }
    if (data.dateOfBirth) {
      data.dateOfBirth = new Date(data.dateOfBirth);
    }
    await this.performerModel.updateOne({ _id: id }, data);
    const newPerformer = await this.performerModel.findById(id);
    // update auth key if email has changed
    if (data.email && data.email.toLowerCase() !== performer.email) {
      await this.authService.sendVerificationEmail({ email: newPerformer.email, _id: newPerformer._id });
      await this.authService.updateKey({
        source: 'performer',
        sourceId: newPerformer._id,
        type: 'email'
      });
    }
    // update auth key if username has changed
    if (data.username && data.username.trim() !== performer.username) {
      await this.authService.updateKey({
        source: 'performer',
        sourceId: newPerformer._id,
        type: 'username'
      });
    }
    return true;
  }

  public async updateAvatar(user: PerformerDto, file: FileDto) {
    await this.performerModel.updateOne(
      { _id: user._id },
      {
        avatarId: file._id,
        avatarPath: file.path
      }
    );
    await this.fileService.addRef(file._id, {
      itemId: user._id,
      itemType: REF_TYPE.PERFORMER
    });

    // resend user info?
    // TODO - check others config for other storage
    return file;
  }

  public async updateCover(user: PerformerDto, file: FileDto) {
    await this.performerModel.updateOne(
      { _id: user._id },
      {
        coverId: file._id,
        coverPath: file.path
      }
    );
    await this.fileService.addRef(file._id, {
      itemId: user._id,
      itemType: REF_TYPE.PERFORMER
    });

    return file;
  }

  public async updateWelcomeVideo(user: PerformerDto, file: FileDto) {
    await this.performerModel.updateOne(
      { _id: user._id },
      {
        welcomeVideoId: file._id,
        welcomeVideoPath: file.path
      }
    );
    await this.fileService.addRef(file._id, {
      itemId: user._id,
      itemType: REF_TYPE.PERFORMER
    });
    await this.fileService.queueProcessVideo(file._id);
    if (user.welcomeVideoId) {
      await this.fileService.remove(user.welcomeVideoId);
    }
    return file;
  }

  public async checkSubscribed(performerId: string | ObjectId, user: UserDto) {
    const count = performerId && user ? await this.subscriptionService.checkSubscribed(
      performerId,
      user._id
    ) : 0;
    return { subscribed: count > 0 };
  }

  public async updatePaymentGateway(payload: PaymentGatewaySettingPayload) {
    let item = await this.paymentGatewaySettingModel.findOne({
      key: payload.key,
      performerId: payload.performerId
    });
    if (!item) {
      // eslint-disable-next-line new-cap
      item = new this.paymentGatewaySettingModel();
    }
    item.key = payload.key;
    item.performerId = payload.performerId as any;
    item.status = 'active';
    item.value = payload.value;
    return item.save();
  }

  public async getPaymentSetting(
    performerId: string | ObjectId,
    service = 'ccbill'
  ) {
    return this.paymentGatewaySettingModel.findOne({
      key: service,
      performerId
    });
  }

  public async updateCommissionSetting(
    performerId: string,
    payload: CommissionSettingPayload
  ) {
    let item = await this.commissionSettingModel.findOne({
      performerId
    });
    if (!item) {
      // eslint-disable-next-line new-cap
      item = new this.commissionSettingModel();
    }
    item.performerId = performerId as any;
    item.monthlySubscriptionCommission = payload.monthlySubscriptionCommission;
    item.yearlySubscriptionCommission = payload.yearlySubscriptionCommission;
    item.videoSaleCommission = payload.videoSaleCommission;
    item.productSaleCommission = payload.productSaleCommission;
    return item.save();
  }

  public async updateBankingSetting(
    performerId: string,
    payload: BankingSettingPayload,
    currentUser: UserDto
  ) {
    if (
      (currentUser.roles
        && currentUser.roles.indexOf('admin') === -1
        && currentUser._id.toString() !== performerId)
      || (!currentUser.roles
        && currentUser
        && currentUser._id.toString() !== performerId)
    ) {
      throw new NotAcceptableException('Permission denied');
    }
    let item = await this.bankingSettingModel.findOne({
      performerId
    });
    if (!item) {
      // eslint-disable-next-line new-cap
      item = new this.bankingSettingModel(payload);
    }
    item.performerId = performerId as any;
    item.firstName = payload.firstName;
    item.lastName = payload.lastName;
    item.SSN = payload.SSN;
    item.bankName = payload.bankName;
    item.bankAccount = payload.bankAccount;
    item.bankRouting = payload.bankRouting;
    item.bankSwiftCode = payload.bankSwiftCode;
    item.address = payload.address;
    item.city = payload.city;
    item.state = payload.state;
    item.country = payload.country;
    return item.save();
  }

  public async updateVerificationStatus(
    userId: string | ObjectId
  ): Promise<any> {
    const user = await this.performerModel.findById(userId);
    if (!user) return true;
    return this.performerModel.updateOne(
      {
        _id: userId
      },
      { verifiedEmail: true }
    );
  }

  public async getCommissions(performerId: string | ObjectId) {
    return this.commissionSettingModel.findOne({ performerId });
  }

  public async checkAuthDocument(req: any, user: UserDto) {
    const { query } = req;
    if (!query.documentId) {
      throw new ForbiddenException();
    }
    if (user.roles && user.roles.indexOf('admin') > -1) {
      return true;
    }
    // check type video
    const file = await this.fileService.findById(query.documentId);
    if (!file || !file.refItems || (file.refItems[0] && file.refItems[0].itemType !== REF_TYPE.PERFORMER)) return false;
    if (file.refItems && file.refItems[0].itemId && user._id.toString() === file.refItems[0].itemId.toString()) {
      return true;
    }
    throw new ForbiddenException();
  }
}

Performer Details Code Samples:

Code Block
import { PureComponent } from 'react';
import {
  Layout, Tabs, Button, message, Modal, Tooltip
} from 'antd';
import { connect } from 'react-redux';
import {
  getVideos, moreVideo, getVods, moreVod, resetVideoState
} from '@redux/video/actions';
import { getGalleries, moreGalleries, resetGalleryState } from '@redux/gallery/actions';
import { listProducts, moreProduct, resetProductState } from '@redux/product/actions';
import { performerService, paymentService, utilsService } from 'src/services';
import Head from 'next/head';
import {
  CheckCircleOutlined, LikeOutlined, ArrowLeftOutlined, EyeOutlined, MessageOutlined,
  ShoppingOutlined, VideoCameraOutlined, PictureOutlined, UsergroupAddOutlined
} from '@ant-design/icons';
import { ScrollListVideo, VideoPlayer } from '@components/video';
import { ScrollListProduct } from '@components/product/scroll-list-item';
import { ConfirmSubscriptionPerformerForm, PerformerInfo } from '@components/performer';
import { ScrollListGallery } from '@components/gallery';
import { shortenLargeNumber } from '@lib/index';
import {
  IPerformer, IUser, IUIConfig, ISettings, ICountry
} from 'src/interfaces';
import Error from 'next/error';
import Router from 'next/router';
import '@components/performer/performer.less';

interface IProps {
  ui: IUIConfig;
  countries: ICountry[];
  error: any;
  settings: ISettings;
  currentUser: IUser;
  performer: IPerformer;
  query: any;
  listProducts: Function;
  getVideos: Function;
  moreVideo: Function;
  getVods: Function;
  moreProduct: Function;
  moreVod: Function;
  videoState: any;
  saleVideoState: any;
  productState: any;
  getGalleries: Function;
  moreGalleries: Function;
  galleryState: any;
  resetVideoState: Function;
  resetProductState: Function;
  resetGalleryState: Function;
}
const { TabPane } = Tabs;

class PerformerProfile extends PureComponent<IProps> {
  static authenticate = true;

  static noredirect = true;

  subscriptionType = 'monthly';

  state = {
    tab: 'video',
    itemPerPage: 24,
    videoPage: 0,
    vodPage: 0,
    productPage: 0,
    galleryPage: 0,
    showWelcomVideo: false,
    openSubscriptionModal: false,
    submiting: false
  };

  static async getInitialProps({ ctx }) {
    try {
      const { query } = ctx;
      const [performer, countries] = await Promise.all([
        performerService.findOne(query.username, {
          Authorization: ctx.token || ''
        }),
        utilsService.countriesList()
      ]);
      return {
        performer: performer.data,
        countries: countries.data
      };
    } catch (e) {
      return { error: await e };
    }
  }

  async componentDidMount() {
    const {
      performer
    } = this.props;
    if (performer) {
      const notShownWelcomeVideos = localStorage.getItem('notShownWelcomeVideos');
      const showWelcomVideo = !notShownWelcomeVideos || (notShownWelcomeVideos && !notShownWelcomeVideos.includes(performer._id));
      this.setState({ showWelcomVideo });
      this.loadItems();
    }
  }

  componentWillUnmount() {
    const { resetGalleryState: resetGal, resetProductState: resetProd, resetVideoState: resetVid } = this.props;
    resetGal();
    resetProd();
    resetVid();
  }

  loadMoreItem = async () => {
    const {
      moreVideo: moreVideoHandler,
      moreProduct: moreProductHandler,
      moreGalleries: moreGalleryHandler,
      moreVod: moreVodHandler,
      videoState: videosVal,
      productState: productsVal,
      saleVideoState: saleVideosVal,
      galleryState: galleryVal,
      performer
    } = this.props;

    const {
      videoPage, itemPerPage, vodPage, productPage, galleryPage, tab
    } = this.state;
    if (tab === 'video') {
      if (videosVal.items.length >= videosVal.total) return;
      this.setState({
        videoPage: videoPage + 1
      }, () => moreVideoHandler({
        limit: itemPerPage,
        offset: (videoPage + 1) * itemPerPage,
        performerId: performer._id,
        isSaleVideo: false
      }));
    }
    if (tab === 'saleVideo') {
      if (saleVideosVal.items.length >= saleVideosVal.total) return;
      this.setState({
        vodPage: vodPage + 1
      }, () => moreVodHandler({
        limit: itemPerPage,
        offset: (vodPage + 1) * itemPerPage,
        performerId: performer._id,
        isSaleVideo: true
      }));
    }
    if (tab === 'gallery') {
      if (galleryVal.items.length >= galleryVal.total) return;
      this.setState({
        galleryPage: galleryPage + 1
      }, () => moreGalleryHandler({
        limit: itemPerPage,
        offset: (galleryPage + 1) * itemPerPage,
        performerId: performer._id
      }));
    }
    if (tab === 'store') {
      if (productsVal.items.length >= productsVal.total) return;
      this.setState({
        productPage: productPage + 1
      }, () => moreProductHandler({
        limit: itemPerPage,
        offset: (productPage + 1) * itemPerPage,
        performerId: performer._id
      }));
    }
  };

  loadItems = () => {
    const { itemPerPage, tab } = this.state;
    const {
      performer,
      getGalleries: getGalleriesHandler,
      listProducts: listProductsHandler,
      getVideos: getVideosHandler,
      getVods: getVodsHandler
    } = this.props;
    switch (tab) {
      case 'video':
        this.setState({ videoPage: 0 }, () => {
          getVideosHandler({
            limit: itemPerPage,
            offset: 0,
            performerId: performer._id,
            isSaleVideo: false
          });
        });
        break;
      case 'saleVideo':
        this.setState({ vodPage: 0 }, () => {
          getVodsHandler({
            limit: itemPerPage,
            offset: 0,
            performerId: performer._id,
            isSaleVideo: true
          });
        });
        break;
      case 'gallery':
        this.setState({ galleryPage: 0 }, () => {
          getGalleriesHandler({
            limit: itemPerPage,
            offset: 0,
            performerId: performer._id
          });
        });
        break;
      case 'store':
        this.setState({ productPage: 0 }, () => {
          listProductsHandler({
            performerId: performer._id,
            limit: itemPerPage,
            offset: 0
          });
        });
        break;
      default: break;
    }
  }

  handleViewWelcomeVideo() {
    const { performer } = this.props;
    const notShownWelcomeVideos = localStorage.getItem('notShownWelcomeVideos');
    if (!notShownWelcomeVideos?.includes(performer._id)) {
      const Ids = JSON.parse(notShownWelcomeVideos || '[]');
      const values = Array.isArray(Ids) ? Ids.concat([performer._id]) : [performer._id];
      localStorage.setItem('notShownWelcomeVideos', JSON.stringify(values));
    }
    this.setState({ showWelcomVideo: false });
  }

  handleClickMessage() {
    const { currentUser, performer } = this.props;
    if (!currentUser._id) {
      message.error('Please login!');
      return;
    }
    if (!performer.isSubscribed) {
      message.error('Please subscribe to the model to start chatting.');
      return;
    }
    Router.push({
      pathname: '/messages',
      query: {
        toSource: 'performer',
        toId: performer?._id
      }
    });
  }

  async subscribe(paymentGateway = 'ccbill') {
    const { performer } = this.props;
    try {
      await this.setState({ submiting: true });
      const resp = await (
        await paymentService.subscribe({ type: this.subscriptionType, performerId: performer._id, paymentGateway })
      ).data;
      message.info('Redirecting to payment gateway, do not reload page at this time', 30);
      if (['ccbill', 'verotel'].includes(paymentGateway)) window.location.href = resp.paymentUrl;
    } catch (e) {
      const err = await e;
      message.error(err?.message || 'error occured, please try again later');
      this.setState({ submiting: false });
    }
  }

  render() {
    const {
      error,
      performer,
      ui,
      countries,
      settings,
      currentUser,
      videoState: videoProps,
      productState: productProps,
      saleVideoState: saleVideoProps,
      galleryState: galleryProps
    } = this.props;
    if (error) {
      return <Error statusCode={error?.statusCode} title={error?.message} />;
    }
    const { items: videos = [], total: totalVideos, requesting: loadingVid } = videoProps;
    const { items: saleVideos = [], total: totalVods, requesting: loadingVod } = saleVideoProps;
    const { items: products, total: totalProducts, requesting: loadingProduct } = productProps;
    const { items: galleries, total: totalGalleries, requesting: loadingGallery } = galleryProps;
    const {
      showWelcomVideo, openSubscriptionModal, submiting
    } = this.state;
    return (
      <Layout>
        <Head>
          <title>
            {`${ui?.siteName} | ${performer?.name || performer?.username || ''}`}
          </title>
          <meta
            name="keywords"
            content={`${performer?.username}, ${performer?.name}`}
          />
          <meta name="description" content={performer?.bio} />
          {/* OG tags */}
          <meta
            property="og:title"
            content={`${ui?.siteName} | ${performer?.name || performer?.username || ''}`}
            key="title"
          />
          <meta property="og:image" content={performer?.avatar || '/no-avatar.png'} />
          <meta
            property="og:keywords"
            content={`${performer?.username}, ${performer?.name}`}
          />
          <meta
            property="og:description"
            content={performer?.bio}
          />
          {/* Twitter tags */}
          <meta
            name="twitter:title"
            content={`${ui?.siteName} | ${performer?.name || performer?.username || ''}`}
          />
          <meta name="twitter:image" content={performer?.avatar || '/no-avatar.png'} />
          <meta
            name="twitter:description"
            content={performer?.bio}
          />
        </Head>
        <div
          className="top-profile"
          style={{
            backgroundImage:
              performer?.cover
                ? `url('${performer?.cover}')`
                : "url('/banner-image.jpg')"
          }}
        >
          <div className="bg-2nd">
            <div className="main-container">
              <div className="top-banner">
                <a aria-hidden className="arrow-back" onClick={() => Router.back()}>
                  <ArrowLeftOutlined />
                </a>
                <div className="stats-row">
                  <Tooltip title={performer?.name}>
                    <div className="t-user-name">
                      {performer?.name || 'N/A'}
                      &nbsp;
                      {performer?.verifiedAccount && <CheckCircleOutlined />}
                    </div>
                  </Tooltip>
                  <div className="tab-stat">
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.views || 0)}
                        {' '}
                        <EyeOutlined />
                      </span>
                    </div>
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.totalVideos || 0)}
                        {' '}
                        <VideoCameraOutlined />
                      </span>
                    </div>
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.totalPhotos || 0)}
                        {' '}
                        <PictureOutlined />
                      </span>
                    </div>
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.totalProducts || 0)}
                        {' '}
                        <ShoppingOutlined />
                      </span>
                    </div>
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.likes || 0)}
                        {' '}
                        <LikeOutlined />
                      </span>
                    </div>
                    <div className="tab-item">
                      <span>
                        {shortenLargeNumber(performer?.stats?.subscribers || 0)}
                        {' '}
                        <UsergroupAddOutlined />
                      </span>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div className="main-profile">
          <div className="main-container">
            <div className="fl-col">
              <img
                alt="Avatar"
                src={performer?.avatar || '/no-avatar.png'}
              />
              <div className="m-user-name">
                <Tooltip title={performer?.name}>
                  <h4>
                    {performer?.name || 'N/A'}
                    &nbsp;
                    {performer?.verifiedAccount && <CheckCircleOutlined />}
                  </h4>
                </Tooltip>
                <h5>
                  @
                  {performer?.username || 'n/a'}
                </h5>
              </div>
            </div>
            <div className="btn-grp">
              {currentUser && !currentUser.isPerformer && (
              <Button className="primary" onClick={() => this.handleClickMessage()}>
                <MessageOutlined />
                {' '}
                Message
              </Button>
              )}
            </div>
            <div className={currentUser.isPerformer ? 'mar-0 pro-desc' : 'pro-desc'}>
              <div className="show-more">
                <PerformerInfo countries={countries} performer={performer} />
              </div>
            </div>
            {!performer?.isSubscribed && (
              <div className="subscription-bl">
                <h5>
                  Monthly Subscription
                </h5>
                <button
                  type="button"
                  className="sub-btn"
                  disabled={(submiting && this.subscriptionType === 'monthly') || currentUser?.isPerformer}
                  onClick={() => {
                    this.subscriptionType = 'monthly';
                    this.setState({ openSubscriptionModal: true });
                  }}
                >
                  SUBSCRIBE FOR $
                  {' '}
                  {performer?.monthlyPrice.toFixed(2)}
                </button>
              </div>
            )}
            {!performer?.isSubscribed && (
              <div className="subscription-bl">
                <h5>
                  Yearly Subscription
                </h5>
                <button
                  type="button"
                  className="sub-btn"
                  disabled={(submiting && this.subscriptionType === 'yearly') || currentUser?.isPerformer}
                  onClick={() => {
                    this.subscriptionType = 'yearly';
                    this.setState({ openSubscriptionModal: true });
                  }}
                >
                  SUBSCRIBE FOR $
                  {' '}
                  {performer?.yearlyPrice.toFixed(2)}
                </button>
              </div>
            )}
          </div>
        </div>
        <div className="main-container">
          <div className="model-content">
            <Tabs defaultActiveKey="Video" className="model-tabs" size="large" onTabClick={(tab) => this.setState({ tab }, () => this.loadItems())}>
              <TabPane
                tab={<VideoCameraOutlined />}
                key="video"
              >
                <div className="heading-tab">
                  <h4>
                    {totalVideos}
                    {' '}
                    VIDEO
                  </h4>
                </div>
                <ScrollListVideo
                  items={videos}
                  loading={loadingVid}
                  canLoadmore={videos && videos.length < totalVideos}
                  loadMore={this.loadMoreItem.bind(this)}
                />
              </TabPane>
              <TabPane
                tab={<img src="/video-crown.png" alt="$" />}
                key="saleVideo"
              >
                <div className="heading-tab">
                  <h4>
                    {totalVods}
                    {' '}
                    VIDEO ON DEMAND
                  </h4>
                </div>
                <ScrollListVideo
                  items={saleVideos}
                  loading={loadingVod}
                  canLoadmore={saleVideos && saleVideos.length < totalVods}
                  loadMore={this.loadMoreItem.bind(this)}
                />
              </TabPane>
              <TabPane
                tab={<PictureOutlined />}
                key="gallery"
              >
                <div className="heading-tab">
                  <h4>
                    {totalGalleries}
                    {' '}
                    GALLERY
                  </h4>
                </div>
                <ScrollListGallery
                  items={galleries}
                  loading={loadingGallery}
                  canLoadmore={galleries && galleries.length < totalGalleries}
                  loadMore={this.loadMoreItem.bind(this)}
                />
              </TabPane>
              <TabPane
                tab={<ShoppingOutlined />}
                key="store"
              >
                <div className="heading-tab">
                  <h4>
                    {totalProducts}
                    {' '}
                    PRODUCT
                  </h4>
                </div>
                <ScrollListProduct
                  items={products}
                  loading={loadingProduct}
                  canLoadmore={products && products.length < totalProducts}
                  loadMore={this.loadMoreItem.bind(this)}
                />
              </TabPane>
            </Tabs>
          </div>
        </div>
        {performer
          && performer?.welcomeVideoPath
          && performer?.activateWelcomeVideo && (
            <Modal
              key="welcome-video"
              destroyOnClose
              width={767}
              visible={showWelcomVideo}
              title={null}
              onCancel={() => this.setState({ showWelcomVideo: false })}
              footer={[
                <Button
                  key="close"
                  className="secondary"
                  onClick={() => this.setState({ showWelcomVideo: false })}
                >
                  Close
                </Button>,
                <Button
                  style={{ marginLeft: 0 }}
                  key="close-show"
                  className="primary"
                  onClick={this.handleViewWelcomeVideo.bind(this)}
                >
                  Don&apos;t show me again
                </Button>
              ]}
            >
              <VideoPlayer {...{
                key: performer?._id,
                autoplay: true,
                controls: true,
                playsinline: true,
                fluid: true,
                sources: [
                  {
                    src: performer?.welcomeVideoPath,
                    type: 'video/mp4'
                  }
                ]
              }}
              />
            </Modal>
        )}
        <Modal
          key="subscribe_performer"
          centered
          width={600}
          title={null}
          visible={openSubscriptionModal}
          confirmLoading={submiting}
          footer={null}
          onCancel={() => this.setState({ openSubscriptionModal: false })}
        >
          <ConfirmSubscriptionPerformerForm
            settings={settings}
            type={this.subscriptionType || 'monthly'}
            performer={performer}
            submiting={submiting}
            onFinish={this.subscribe.bind(this)}
          />
        </Modal>
      </Layout>
    );
  }
}

const mapStates = (state: any) => ({
  ui: { ...state.ui },
  settings: { ...state.settings },
  videoState: { ...state.video.videos },
  saleVideoState: { ...state.video.saleVideos },
  productState: { ...state.product.products },
  galleryState: { ...state.gallery.galleries },
  currentUser: { ...state.user.current }
});

const mapDispatch = {
  getVideos,
  moreVideo,
  getVods,
  listProducts,
  moreProduct,
  getGalleries,
  moreGalleries,
  moreVod,
  resetProductState,
  resetVideoState,
  resetGalleryState
};
export default connect(mapStates, mapDispatch)(PerformerProfile);