Объекты передачи данных (DTO) являются основой проверки данных в приложениях NestJS. DTO обеспечивают различные уровни гибкой проверки данных в NestJS.
В этой публикации вы подробно рассмотрите объекты передачи данных, обсудите механизмы валидации, модели аутентификации и все остальное, что вам нужно знать об объектах передачи данных.
- Предварительные условия
- Что такое объект передачи данных?
- Механизмы валидации
- DTO в простой модели аутентификации пользователя
- Настройка среды разработки
- Структура DTO
- Реализация валидации данных
- Post & Put запросы
- Хеширование паролей
- Создание нового пользователя
- Тестирование конечных точек
- Обновление пользователя
- Вход пользователей в систему
- Заключение
Предварительные условия
Этот учебник не предназначен для новичков. Если вы только начинаете работать с 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.
Счастливого кодинга!