faloker/purify

View on GitHub
api/src/users/users.service.ts

Summary

Maintainability
A
2 hrs
Test Coverage
D
64%
/* eslint-disable @typescript-eslint/camelcase */
import { Model } from 'mongoose';
import { pbkdf2Sync, randomBytes } from 'crypto';
import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { User, Role } from './interfaces/user.interface';
import {
  CreateUserDto,
  ChangePasswordDto,
  EditUserDto,
  UserSelfChangeDto,
  CreateTokenDto,
  DeleteTokenDto,
  UserChangePasswordDto,
} from './dto/user.dto';
import { Token, TokenType } from './interfaces/token.interface';
import { ConfigService } from '@nestjs/config';
import { nanoid } from 'nanoid';

@Injectable()
export class UsersService {
  constructor(
    @InjectModel('User') private readonly userModel: Model<User>,
    @InjectModel('Token')
    private readonly tokenModel: Model<Token>,
    private readonly configService: ConfigService
  ) {}

  async getAll() {
    return this.userModel
      .find({}, { password: 0, token: 0, salt: 0, refreshToken: 0, recentProjects: 0, __v: 0 })
      .lean()
      .populate('memberships', 'displayName');
  }

  async getUser(userId: string) {
    return this.userModel
      .findOne(
        {
          _id: userId,
        },
        '_id name email image'
      )
      .lean();
  }

  async getUserRecentProjects(userId: string) {
    return this.userModel
      .findOne(
        {
          _id: userId,
        },
        'recentProjects'
      )
      .lean();
  }

  async delete(userId: string) {
    await this.userModel.deleteOne({ _id: userId });
  }

  async createUser(newUser: CreateUserDto) {
    return new this.userModel({
      ...newUser,
      salt: randomBytes(16).toString('hex'),
      image: `https://avatars.dicebear.com/api/avataaars/${nanoid()}.svg`,
    }).save();
  }

  async editUser(userId: string, user: EditUserDto) {
    return this.userModel.updateOne(
      { _id: userId },
      {
        name: user.name,
        email: user.email,
        role: user.role as Role,
        memberships: user.memberships,
        ssoBypass: user.ssoBypass,
      }
    );
  }

  async findOne(condition: any): Promise<User> {
    return this.userModel.findOne(condition);
  }

  genSecret(value: string, salt: string): string {
    return pbkdf2Sync(value, salt, 9999, 512, 'sha512').toString('hex');
  }

  isSecretValid(value: string, secret: string, salt: string): boolean {
    const hash = pbkdf2Sync(value, salt, 9999, 512, 'sha512').toString('hex');
    return secret === hash;
  }

  async getAPIAccessTokens(user: User) {
    return this.tokenModel
      .find(
        {
          user: user._id,
          type: TokenType.API_ACCESS_TOKEN,
        },
        { value: 0, user: 0, type: 0 }
      )
      .lean();
  }

  async createAPIAccessToken(user: User, createTokenDto: CreateTokenDto) {
    const doc = await this.tokenModel
      .findOne({
        user: user._id,
        name: createTokenDto.name,
        type: TokenType.API_ACCESS_TOKEN,
      })
      .lean();

    if (doc) {
      throw new BadRequestException('Token with this name already exists');
    }

    const token = await new this.tokenModel({
      user: user._id,
      name: createTokenDto.name,
      type: TokenType.API_ACCESS_TOKEN,
    }).save();

    return {
      value: token.value,
      _id: token._id,
      name: token.name,
    };
  }

  async trackTokenUsage(ip: string, ua: string, apikey: string) {
    const token = await this.tokenModel.findOne({
      value: apikey,
      type: TokenType.API_ACCESS_TOKEN,
    });

    if (token) {
      token.lastActivity.date = new Date();
      token.lastActivity.fromIP = ip;
      token.lastActivity.userAgent = ua;
      await token.save();
    }
  }

  async deleteAPIAccessToken(user: User, deleteTokenDto: DeleteTokenDto) {
    await this.tokenModel.deleteOne({
      user: user._id,
      _id: deleteTokenDto._id,
      type: TokenType.API_ACCESS_TOKEN,
    });
  }

  async validateAPIAccessToken(token: string) {
    return this.tokenModel
      .findOne({
        value: token,
        type: TokenType.API_ACCESS_TOKEN,
      })
      .lean();
  }

  async saveRefreshToken(userId: string, token: string) {
    return this.tokenModel.findOneAndUpdate(
      { user: userId, type: TokenType.REFRESH_TOKEN },
      {
        user: userId,
        value: token,
        type: TokenType.REFRESH_TOKEN,
      },
      {
        upsert: true,
      }
    );
  }

  async validateRefreshToken(userId: string, refreshToken: string) {
    const token = this.tokenModel
      .findOne({
        user: userId,
        value: refreshToken,
        type: TokenType.REFRESH_TOKEN,
      })
      .lean();
    return token ? true : false;
  }

  async removeRefreshToken(user: User) {
    await this.tokenModel.deleteMany({
      user: user._id,
      type: TokenType.REFRESH_TOKEN,
    });
  }

  async createInviteLink(userId: string) {
    const { value } = await new this.tokenModel({
      user: userId,
      type: TokenType.INVITE_TOKEN,
    }).save();
    return `https://${this.configService.get<string>(
      'DOMAIN'
    )}/#/welcome/${value}`;
  }

  async changePassword(payload: ChangePasswordDto) {
    const token = await this.tokenModel
      .findOne({
        value: payload.token,
        type: TokenType.INVITE_TOKEN,
      })
      .lean();
    if (token) {
      const user = await this.userModel.findOne({ _id: token.user as string });
      const secret = this.genSecret(payload.password, user.salt);
      user.password = secret;
      await user.save();
      await this.tokenModel.deleteOne({ _id: token._id });
    } else {
      throw new NotFoundException('Token not found');
    }
  }

  async selfChangePassword(user: User, payload: UserChangePasswordDto) {
    if (this.isSecretValid(payload.oldPassword, user.password, user.salt)) {
      const secret = this.genSecret(payload.newPassword, user.salt);
      user.password = secret;
      await user.save();
    } else {
      throw new NotFoundException('Bad passwords');
    }
  }

  async changeUser(userId: string, userSelfChange: UserSelfChangeDto) {
    const user = await this.userModel.findOne({ _id: userId });
    if (
      userSelfChange.trackMe &&
      !user.recentProjects.includes(userSelfChange.trackMe)
    ) {
      if (user.recentProjects.length < 4) {
        user.recentProjects.push(userSelfChange.trackMe);
      } else {
        user.recentProjects.shift();
        user.recentProjects.push(userSelfChange.trackMe);
      }
      await user.save();
    }

    if (userSelfChange.name) {
      user.name = userSelfChange.name;
      await user.save();
    }

    if (userSelfChange.image) {
      user.image = userSelfChange.image;
      await user.save();
    }

    if (userSelfChange.email) {
      user.email = userSelfChange.email;
      await user.save();
    }
  }
}