Понимание объектов передачи данных (DTO) и проверки данных в TypeScript (NestJS)

Объекты передачи данных (DTO) являются основой проверки данных в приложениях NestJS. DTO обеспечивают различные уровни гибкой проверки данных в NestJS.

В этой публикации вы подробно рассмотрите объекты передачи данных, обсудите механизмы валидации, модели аутентификации и все остальное, что вам нужно знать об объектах передачи данных.

Предварительные условия

Этот учебник не предназначен для новичков. Если вы только начинаете работать с NestJS, прочитайте эту статью.

Для полного понимания этой статьи вам необходимо выполнить следующие требования.

  • Предварительный опыт создания приложений NestJS с MongoDB.
  • Знание PostMan (рекомендуется) или любого другого инструмента тестирования API.
  • Базовые знания bcrypt или криптографического модуля NodeJS.

Как только вы выполните эти требования, мы сможем приступить к работе.

Что такое объект передачи данных?

Объект передачи данных, обычно называемый DTO, — это объект, используемый для проверки данных и определения структуры данных, отправляемых в ваши приложения Nest. DTO похожи на интерфейсы, но отличаются от них следующим образом:

  • Интерфейсы используются для проверки типов и определения структуры.
  • DTO используется для проверки типов, определения структуры и валидации данных.
  • Интерфейсы исчезают во время компиляции, поскольку они являются родными для TypeScript и не существуют в JavaScript.
  • DTO определяются с помощью классов, которые поддерживаются в родном JavaScript. Следовательно, они остаются после компиляции.

DTO могут выполнять только проверку типов и определение структур. Чтобы выполнить проверку данных с помощью DTO, необходимо использовать NestJS ValidationPipe.

Механизмы валидации

Трубы используются для проверки данных в NestJS. Данные, проходящие через трубу, оцениваются, и если они проходят тест на валидность, то возвращаются неизмененными, в противном случае выдается ошибка.

NestJS имеет 8 встроенных труб, но вы сосредоточитесь на ValidationPipe, который использует пакет class-validator, потому что он абстрагирует много многословного кода и упрощает проверку данных с помощью декораторов.

DTO в простой модели аутентификации пользователя

Модель аутентификации пользователя является идеальным примером для демистификации DTO. Это связано с тем, что она требует многоуровневой проверки данных. Давайте создадим такую модель и изучим DTO и ValidationPipe для проверки всех форм данных, поступающих в наше приложение.

Настройка среды разработки

Чтобы настроить среду разработки, выполните следующие действия:

  • Сгенерируйте новый проект с помощью Nest CLI,
  • Создайте Module, Service и Controller с помощью CLI,
  • Установите Mongoose и подключите ваше приложение к базе данных,
  • Создайте папку схемы и определите Schema.

Типичная схема авторизации пользователя должна выглядеть следующим образом:

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop({ required: true })
  fullName: string;

  @Prop({ required: true })
  email: string;

  @Prop({ required: true })
  password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Вход в полноэкранный режим Выход из полноэкранного режима

Имея fullName, email, и password как необходимые свойства.

  • Создайте папку внутри вашего модуля и назовите ее dto. Здесь будут храниться и экспортироваться ваши классы dto.
  • Создайте файл в папке dto и назовите его user.dto.ts.

Структура DTO

DTO — это класс, поэтому он имеет тот же синтаксис, что и класс.

export class newUserDto {
  fullName: string;
  email: string;
  password: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Выше приведена структура базового DTO.

Эта структура полезна только для проверки типов, она не имеет свойств проверки данных.

Реализация валидации данных

Чтобы добавить функции валидации, выполните следующие действия;

  • Внутри вашего main.ts,
  • Импортируйте {ValidationPipe} из '@nestjs/common',
  • Внутри функции bootstrap и непосредственно под константой app вызовите метод useGlobalPipes для app и передайте в качестве аргумента new ValidationPipe().
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import * as dotenv from 'dotenv';
dotenv.config();

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(process.env.PORT);
}
bootstrap();
Вход в полноэкранный режим Выйдите из полноэкранного режима

Это включит автоматическую проверку и обеспечит защиту всех конечных точек от получения неверных данных. Validation pipe принимает в качестве аргумента объект options, подробнее о нем можно узнать в официальной документации.

  • Установите пакет class-validator и class-transformer, выполнив:
npm i --save class-validator class-transformer
Войдите в полноэкранный режим Выйти из полноэкранного режима

Пакет class-transformer преобразует объекты JSON в экземпляр вашего класса DTO и наоборот.

Пакет class-validator имеет множество запросов валидации, которые вы можете найти в их документации, но вы сосредоточитесь на нескольких из них, связанных с валидацией пользовательских данных. В вашем файле user.dto.ts импортируйте следующие запросы из class-validator:

Они будут использоваться в качестве декораторов для валидации соответствующих полей. Реализуйте это в своем DTO,

import { IsNotEmpty, IsEmail, Length, IsString } from 'class-validator';

export class newUserDto {
  @IsNotEmpty({ message: 'Please Enter Full Name' })
  @IsString({ message: 'Please Enter Valid Name' })
  fullName: string;

  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  @Length(6, 50, {
    message: 'Password length Must be between 6 and 50 charcters',
  })
  password: string;
}

Вход в полноэкранный режим Выход из полноэкранного режима

Поле пароля имеет только проверку Length, но в производственных условиях вам может понадобиться добавить еще несколько полей для обеспечения большей безопасности пароля.

Эти новые поля проверки могут включать проверку того, содержит ли пароль заглавные, строчные буквы и цифры.

Это можно сделать несколькими способами, включая:

  • Добавление большего количества декораторов проверки, как это было сделано в поле fullName.
  • Использование валидатора @Matches и передача в качестве аргумента регулярного выражения, которое проверяет ваши требования.

Но самым чистым способом сделать это будет создание пользовательского валидатора в соответствии с вашими требованиями. Вы можете узнать больше о пользовательских декораторах здесь.

Post & Put запросы

Проверьте валидацию данных, сделав несколько запросов POST и PUT к вашему приложению. Но перед этим следует отметить, что хранить пароли в открытом виде — плохая практика. Поэтому сначала хэшируйте их, прежде чем хранить в базе данных;

Хеширование паролей

Вы можете хэшировать пароли, используя пакет bcrypt или криптомодуль NodeJS. В этой публикации рассматривается bcrypt. Чтобы установить bcrypt, выполните следующие действия.

npm i bcrypt
npm i -D @types/bcrypt
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Создайте папку utils для хранения файлов, содержащих служебные функции, например, функцию, которую вы будете использовать для хэширования паролей.

Чтобы обеспечить защиту паролей пользователей, необходимо хэшировать их перед сохранением в базе данных. Давайте реализуем это

  • Создайте файл в папке utils, он будет содержать ваши служебные функции.
  • Импортируйте * как bcrypt из 'bcrypt'.
  • Создайте функцию, которая принимает необработанный пароль и хэширует его с помощью bcrypt, а затем возвращает хэшированный пароль. Экспортируйте функцию.
import * as bcrypt from 'bcrypt';

export async function hashPassword(textPassword: string) {
  const salt = await bcrypt.genSalt();
  const hash = await bcrypt.hash(textPassword, salt);
  return hash;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Существуют различные способы использования пакета bcrypt, которые вы можете найти здесь.

Теперь, когда у вас есть функция для хэширования наших паролей, выполните свой первый запрос POST.

Создание нового пользователя

Чтобы создать нового пользователя с действительными учетными данными, вам нужно импортировать DTO, созданную ранее, в ваш сервис.

Логика работы сервиса

  • Создайте функцию async newUser, которая принимает параметр user, который должен быть данными нового пользователя. Установите его тип на DTO, созданный ранее.
  • Импортируйте функцию hashPassword.
  • Внутри функции async создайте константу password и присвойте ей возвращаемое значение ожидания функции hashPassword с user.password в качестве аргумента.
  • Верните новый экземпляр собственной версии моей модели userModel, в качестве аргумента передайте объект, содержащий деструктурированный user и password, и вызовите для него метод save().
import { Injectable } from '@nestjs/common';
import { Model } from 'mongoose';
import { InjectModel } from '@nestjs/mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { newUserDto } from './dto/user.dto';
import { hashPassword } from './utilis/bcrypt.utils';

@Injectable()
export class AuthService {
  constructor(
    @InjectModel(User.name)
    private userModel: Model<UserDocument>,
  ) {}

  async newUser(user: newUserDto): Promise<User> {
    const password = await hashPassword(user.password);
    return await new this.userModel({ ...user, password }).save();
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

DTO и валидаторы, которые мы настроили ранее, будут постоянно проверять достоверность данных перед сохранением пользовательских данных в базе данных.

Реализуйте логику контроллера так, чтобы вы могли протестировать DTO с некоторыми фиктивными данными.

Логика контроллера

import {
  Controller,
  Body,
  Post, 
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { newUserDto } from './dto/user.dto';

@Controller('users')
export class AuthController {
  constructor(private readonly service: AuthService) {}

  @Post('signup')
  async createUser(
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return await this.service.newUser(user);
  }
Вход в полноэкранный режим Выйти из полноэкранного режима

Как видно из приведенного выше блока кода, параметр user имеет тип newUserDto. Это гарантирует, что все данные, поступающие в приложение, соответствуют DTO, иначе будет выдана ошибка.

Тестирование конечных точек

Протестируйте проверку с помощью фиктивных данных, делая запросы к http://localhost:3000/users/signup с помощью PostMan или предпочитаемого вами инструмента тестирования.

{
    "fullName":"Jon Snow",
    "email":"snow@housestark.com",
    "password":"password"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Приведенные выше данные JSON проверяют все поля валидации. Следовательно, вы получите ответ 201 с данными, со свойством _id и хэшированным паролем. Скопируйте свойство _id, оно понадобится вам при тестировании проверки данных в запросах PUT.

{
    "fullName":"Arya Stark",
    "email":"aryathefaceless@housestark",
    "password":"password"
}
Вход в полноэкранный режим Выход из полноэкранного режима

Приведенные выше данные JSON имеют недопустимое свойство email. Следовательно, вы получите ответ 400 с сообщением, описывающим ошибку. Сохраните его в базе данных, обновив свойство email до aryathefaceless@housestark.com.

{
    "fullName":"Tyrion Lannister",
    "email":"tyrion@houselannister.com",
    "password":"pass",
     "house":"Lannister"
}
Вход в полноэкранный режим Выйдите из полноэкранного режима

Приведенные выше данные JSON имеют две проблемы: слишком короткий пароль (менее 6 символов) и лишнее поле house.

Увеличьте длину пароля и отправьте запрос снова. Вы получите запрос 200 с обработанными данными, но поле house не будет сохранено в базе данных, поскольку оно было отфильтровано при прохождении через трубу валидации.

Обновление пользователя

Реализуйте маршрут PUT для обновления данных пользователя, когда это необходимо.

Логика работы сервиса

async updateUser(id: string, userData: newUserDto): Promise<User> {
    return await this.userModel.findByIdAndUpdate(id, userData);
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Логика контроллера

@Put(':id')
  async updateuser(
    @Param('id')
    id: string,
    @Body()
    user: newUserDto,
  ): Promise<newUserDto> {
    return this.service.updateUser(id, user);
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Аналогично запросу POST, для user задается тип newUserDto. Таким образом, все данные тщательно проверяются перед сохранением в базе данных.

Протестируйте эту конечную точку, обновив одного из пользователей, хранящихся в вашей базе данных. Вспомните, что вы скопировали _id, принадлежащий Джону Сноу.

Поэтому сделайте запрос PUT на http://localhost:3000/id со следующими данными;

{
    "fullName":"Jon Snow",
    "email":"snow@housetargaryen.com",
    "password":"password"
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенные выше данные проверяют все поля валидации, поэтому они вернут ответ 200. Если любое из полей данных содержит недопустимые данные, то в ответ будет выдан ответ 400 и запрос PUT завершится неудачей.

Вход пользователей в систему

Модель аутентификации пользователей была бы неполной, если бы вы не могли регистрировать пользователей. Чтобы реализовать логику для этого;

Во-первых, вам понадобится служебная функция bcrypt, которая будет сравнивать хэшированный и обычный пароли. Например,

export async function validatePassword(textPassword: string, hash: string) {
  const validUser = await bcrypt.compare(textPassword, hash);
  return validUser;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем нужно создать новый DTO для данных входа в систему. DTO похож на newUserDto, но без свойства fullName.

import { IsEmail } from 'class-validator';

export class loginUserDto {
  @IsEmail({ message: 'Please Enter a Valid Email' })
  email: string;

  password: string;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что к свойству password не был добавлен валидатор. Это связано с тем, что оно может представлять угрозу безопасности в будущем, так как раскрывает диапазон длины пароля для злоумышленников.

Логика работы сервиса

async loginUser(loginData: loginUserDto) {
    const email = loginData.email;
    const user = await this.userModel.findOne({ email });
    if (user) {
      const valid = await validatePassword(loginData.password, user.password);
      if (valid) {
        return user;
      }
    }
    return new UnauthorizedException('Invalid Credentials');
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Ваша логика обслуживания должна уметь,

Проверять, существует ли пользователь;

  • Если пользователь существует, проверять пароли.
  • Если пользователь не существует, выбросить исключение

Проверять пароли;

  • Если пароли совпадают, вернуть пользователя
  • Если пароли не совпадают, выбросьте исключение.

Логика работы контроллера

@Post('login')
  async loginUser(
    @Body()
    loginData: loginUserDto,
  ) {
    return await this.service.loginUser(loginData);
  }
Вход в полноэкранный режим Выход из полноэкранного режима

Ваш контроллер должен сделать запрос POST на http://localhost:3000/users/login.

Заключение

Наконец-то вы подошли к концу этой статьи. Вот краткий обзор того, что вы изучили.

  • Что такое DTO,
  • Различия между DTO и интерфейсом,
  • Структура DTO,
  • Механизм валидации NestJS,
  • Создание простой модели аутентификации пользователя с помощью bcrypt.

Это довольно много, поздравляем вас с тем, что вы дошли так далеко.

Вы можете найти код на Github.

Счастливого кодинга!

Оцените статью
Procodings.ru
Добавить комментарий