
Приветствую вас в серии моих блогов, посвященных аутентификации пользователей в среде NestJS. Эта серия представляет собой набор из нескольких руководств, дополняющих официальную документацию.
Если вы программируете корпоративное приложение, вы, скорее всего, захотите включить полную поддержку пользователей, т. е. подтверждение учетной записи электронной почты, возможность напоминания пароля и т. д.
Этот пост является первой частью серии, и ниже вы можете найти список всех других статей по этой теме.
- Часть 1: Регистрация пользователя
- Часть 2: Подтверждение регистрации пользователя по электронной почте
- Часть 3. Аутентификация пользователя с помощью JWT и файлов cookie
- Часть 4. Реализация обновления JWT в файлах cookie
- Часть 5: Выход пользователя
- Часть 6: Забыл/сброс пароля
Примечание. Мы начнем программировать на подготовленном Nest.js официальном стартере машинописного текста, который вы можете найти по этой ссылке. Также помните, что весь исходный код из этой статьи доступен в моем профиле GitHub.
Стек технологий
Приложение использует следующий стек технологий:
- Node.js и NestJS в качестве платформы выполнения сервера,
- реляционная база данных PostgreSQL,
- Стандартный протокол REST API,
- Один из самых популярных ORM: TypeORM.
Архитектура программного обеспечения
Мы будем использовать мою собственную программную архитектуру, потому что, на мой взгляд, она лучше той, что представлена в официальном шаблоне фреймворка.
.
├── node_modules
├── src
│ ├── app
│ │ ├── constants
│ │ │ ├── app.constant.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── authentication
│ │ ├── controllers
│ │ │ ├── authentication.controller.ts
│ │ │ └── index.ts
│ │ ├── dtos
│ │ │ ├── authentication.dto.ts
│ │ │ ├── create-authentication.dto.ts
│ │ │ ├── index.ts
│ │ │ └── registration.dto.ts
│ │ ├── entities
│ │ │ ├── authentication.entity.ts
│ │ │ └── index.ts
│ │ ├── exceptions
│ │ │ ├── index.ts
│ │ │ └── user-already-exist.exception.ts
│ │ ├── providers
│ │ │ ├── authentication.provider.ts
│ │ │ └── index.ts
│ │ ├── repositories
│ │ │ ├── authentication.repository.ts
│ │ │ └── index.ts
│ │ ├── services
│ │ │ ├── authentication.service.ts
│ │ │ └── index.ts
│ │ ├── subscribers
│ │ │ ├── authentication.subscriber.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── common
│ │ ├── dtos
│ │ │ ├── abstract.dto.ts
│ │ │ └── index.ts
│ │ └── entities
│ │ ├── abstract.entity.ts
│ │ └── index.ts
│ ├── database
│ │ ├── constraints
│ │ │ ├── errors.constraint.ts
│ │ │ └── index.ts
│ │ ├── strategies
│ │ │ ├── index.ts
│ │ │ └── snake-naming.strategy.ts
│ │ └── index.ts
│ ├── user
│ │ ├── dtos
│ │ │ ├── create-user.dto.ts
│ │ │ ├── index.ts
│ │ │ └── user.dto.ts
│ │ ├── entities
│ │ │ ├── index.ts
│ │ │ └── user.entity.ts
│ │ ├── repositories
│ │ │ ├── index.ts
│ │ │ └── user.repository.ts
│ │ ├── services
│ │ │ ├── index.ts
│ │ │ └── user.service.ts
│ │ └── index.ts
│ ├── util
│ │ ├── index.ts
│ │ └── setup-swagger.util.ts
│ └── main.ts
├── .env
├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── nest-cli.json
├── package.json
├── README.md
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
Разработка программного обеспечения
1. Установите необходимые зависимости npm
Нам понадобятся некоторые внешние пакеты, которые мы будем использовать для программирования авторизации.
Примечание. По умолчанию я использую менеджер пакетов yarn вместо npm, поэтому вы можете удалить свой файл package-lock.json, если вы выбрали стартер машинописного текста. эм>
yarn add pg @hapi/joi @nestjs/config @nestjs/typeorm typeorm class-transformer class-validator bcryptyarn add @types/bcrypt @types/hapi__joi --dev
2. Определите переменные среды
Ваше приложение будет использовать переменные среды, которые останутся полностью закрытыми и не могут быть сохранены в репозитории. Мы также установили библиотеку hapi, которая всегда будет проверять правильность формата данных перед компиляцией кода.
Мы также установили библиотеку @hapi/joi, которая всегда будет проверять правильность формата данных перед компиляцией кода.
2.1. Создайте файл .env
Создайте файл .env в корневом каталоге:
# Application Settings
PORT=9000
NODE_ENV=development
# Database Settings
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=pietrzakadrian
POSTGRES_PASSWORD=
POSTGRES_DB=nestjs-authentication-full
2.2. Определить переменную среды
Это сделает наш код более читабельным. В каталоге src/app/constants я создаю новый файл app.constant.ts:
export enum NODE_ENV {
DEVELOPMENT = "development",
PRODUCTION = "production",
}
2.3. Проверка переменных среды
Теперь мы можем определить валидатор в файле index.ts модуля app. Реализация выглядит так:
import * as Joi from "@hapi/joi";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { NODE_ENV } from "./constants";
@Module({
imports: [
ConfigModule.forRoot({
validationSchema: Joi.object({
PORT: Joi.number().required(),
NODE_ENV: Joi.string()
.required()
.valid(NODE_ENV.DEVELOPMENT, NODE_ENV.PRODUCTION),
POSTGRES_HOST: Joi.string().required(),
POSTGRES_PORT: Joi.number().required(),
POSTGRES_USER: Joi.string().required(),
POSTGRES_PASSWORD: Joi.string().required().allow(""),
POSTGRES_DB: Joi.string().required(),
}),
}),
],
})
export class AppModule {}
Примечание. Пароль моей базы данных представляет собой пустую строку, поэтому я разрешаю оставить ее пустой.
Мы также можем использовать их в другом месте приложения, например, в файле main.ts:
import { ConfigService } from "@nestjs/config";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app";
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const PORT = +configService.get<number>("PORT");
await app.listen(PORT);
}
void bootstrap();
Были реализованы переменные среды. Великолепно! 👊
3. Создайте модуль базы данных
Пришло время запрограммировать модуль базы данных. Этот модуль, как следует из названия, отвечает за взаимодействие с postgres.
Мы включим стратегию змеиного именования и добавим ограничение для уникального значения.
3.1. Код ошибки для уникальных значений
В каталоге src/database/constraints создайте файл errors.constraint.ts:
export enum PostgresErrorCode {
UniqueViolation = "23505",
}
Это нужно, чтобы поймать ошибку в контроллере, если указанный адрес электронной почты повторяется.
3.2. Стратегия именования змей
Определенное в машинописной модели поле, такое как firstName, будет храниться как first_name в базе данных. Это улучшит читаемость кода и сохранит его в формате titleCase.
Создайте snake-naming.strategy.ts в src/database/strategies:
import { DefaultNamingStrategy, NamingStrategyInterface } from "typeorm";
import { snakeCase } from "typeorm/util/StringUtils";
export class SnakeNamingStrategy
extends DefaultNamingStrategy
implements NamingStrategyInterface
{
tableName(className: string, customName: string): string {
return customName ? customName : snakeCase(className);
}
columnName(
propertyName: string,
customName: string,
embeddedPrefixes: string[]
): string {
return (
snakeCase(embeddedPrefixes.join("_")) +
(customName ? customName : snakeCase(propertyName))
);
}
relationName(propertyName: string): string {
return snakeCase(propertyName);
}
joinColumnName(relationName: string, referencedColumnName: string): string {
return snakeCase(relationName + "_" + referencedColumnName);
}
joinTableName(
firstTableName: string,
secondTableName: string,
firstPropertyName: string,
_secondPropertyName: string
): string {
return snakeCase(
firstTableName +
"_" +
firstPropertyName.replace(/\./gi, "_") +
"_" +
secondTableName
);
}
joinTableColumnName(
tableName: string,
propertyName: string,
columnName?: string
): string {
return snakeCase(
tableName + "_" + (columnName ? columnName : propertyName)
);
}
classTableInheritanceParentColumnName(
parentTableName: string,
parentTableIdPropertyName: string
): string {
return snakeCase(`${parentTableName}_${parentTableIdPropertyName}`);
}
}
3.3. Конфигурация TypeORM
Наконец, создайте базовый класс, импортировав необходимые зависимости:
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { NODE_ENV } from "src/app/constants";
import { SnakeNamingStrategy } from "./strategies";
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: "postgres",
host: configService.get("POSTGRES_HOST"),
port: configService.get("POSTGRES_PORT"),
username: configService.get("POSTGRES_USER"),
password: configService.get("POSTGRES_PASSWORD"),
database: configService.get("POSTGRES_DB"),
entities: [__dirname + "/../**/*.entity{.ts,.js}"],
namingStrategy: new SnakeNamingStrategy(),
synchronize: configService.get("NODE_ENV") === NODE_ENV.DEVELOPMENT,
logging: configService.get("NODE_ENV") === NODE_ENV.DEVELOPMENT,
extra: { charset: "utf8mb4_unicode_ci" },
}),
}),
],
})
export class DatabaseModule {}
Примечание. Определите кодировку utf8mb4, если вы хотите иметь возможность хранить эмодзи в базе данных.
Чтобы усложнить код, импортируйте этот модуль в основной модуль приложения.
+ import { DatabaseModule } from 'src/database';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
+ DatabaseModule,
],
})
export class AppModule {}
Правильно, теперь база данных используется в нашем приложении. Все ближе к завершению! 💨
4. Общие файлы
Наши модели данных будут содержать общие части, такие как столбец id, uuid, created_at и т. д. Это идеальная возможность создать абстрактный класс, из которого другие модели унаследуют.
4.1. Создать абстрактную сущность
Создайте новый файл в каталоге src/common/entities с именем abstract.entity.ts.
import { Exclude } from "class-transformer";
import {
Column,
CreateDateColumn,
Generated,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
export abstract class AbstractEntity {
@PrimaryGeneratedColumn()
@Exclude()
public id: number;
@Column()
@Generated("uuid")
public uuid: string;
@CreateDateColumn()
@Exclude()
public createdAt: Date;
@UpdateDateColumn()
@Exclude()
public updatedAt: Date;
}
Мы используем декоратор @Exclude(), чтобы не возвращать конфиденциальные данные в ответе контроллера. Чтобы это работало, нам нужно внести некоторые изменения в файл приложения main.ts:
+ import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const reflector = app.get(Reflector);
const configService = app.get(ConfigService);
const PORT = +configService.get<number>('PORT');
+ app.useGlobalPipes(new ValidationPipe({ transform: true }));
+ app.useGlobalInterceptors(new ClassSerializerInterceptor(reflector));
await app.listen(PORT);
}
void bootstrap();
Сейчас все нормально. Валидация будет протестирована в конце этой статьи.
5. Создайте пользовательский модуль
Мы будем регистрировать нового пользователя, поэтому было бы неплохо создать модуль, отвечающий за обработку пользовательских данных. Конфиденциальные данные, такие как электронная почта, пароль, токены, будут храниться в другом объекте и будут связаны отношением один к одному.
Примечание. Это сделает базу данных более организованной, но при создании нового пользователя нам придется использовать транзакцию.
5.1 Создание модели
Сначала определите модель данных. Как я упоминал ранее, информация для авторизации, такая как адрес электронной почты или пароль, будет храниться в другой таблице. Поэтому мы будем делать отношения один к одному.
Итак, в каталоге src/users/entities мы создаем файл с именем user.entity.ts:
import { AuthenticationEntity } from "src/authentication/entities";
import { AbstractEntity } from "src/common/entities";
import { Column, Entity, JoinColumn, OneToOne } from "typeorm";
@Entity({ name: "users" })
export class UserEntity extends AbstractEntity {
@Column()
public firstName: string;
@OneToOne(
() => AuthenticationEntity,
(authentication: AuthenticationEntity) => authentication.user,
{ eager: true, nullable: false, onDelete: "CASCADE" }
)
@JoinColumn()
public authentication: AuthenticationEntity;
}
❗️ Примечание. На данный момент Объект аутентификации еще не определен. Мы сделаем это, когда перейдем к разделу аутентификации.
Это также то, для чего создан репозиторий, поэтому давайте сделаем его похожим на то, что мы только что сделали. В каталоге repositories создадим файл user.repository.ts:
import { Repository } from "typeorm";
import { EntityRepository } from "typeorm/decorator/EntityRepository";
import { UserEntity } from "../entities";
@EntityRepository(UserEntity)
export class UserRepository extends Repository<UserEntity> {}
Давайте также подготовим класс объекта DTtransform O, который будет содержать проверки. Можно поместить эти декораторы просто в файл user.entity.ts, но это будет не стильно. Лучше создать файл user.dto.ts в каталоге src/user/dtos.
import { IsNotEmpty, IsString } from "class-validator";
import { CreateAuthenticationDto } from "src/authentication/dtos";
export class CreateUserDto extends CreateAuthenticationDto {
@IsString()
@IsNotEmpty()
readonly firstName: string;
}
Примечание. Параметр CreateAuthenticationDto еще не определен, но мы сделаем это чуть позже.
С этого момента при создании новой сущности будет требоваться firstName и без него код выполняться не будет.
5.2 Создание службы
Службы отвечают за выполнение логики. Контроллер использует сервисные зависимости.
Как я уже упоминал ранее, мы будем использовать транзакции, поэтому давайте уже подготовим функцию, предоставляющую все необходимые параметры.
Создайте файл user.service.ts в каталоге src/user/services:
import { Injectable } from "@nestjs/common";
import { CreateUserDto } from "src/user/dtos";
import { AuthenticationEntity } from "src/authentication/entities";
import { QueryRunner } from "typeorm";
import { UserEntity } from "../entities";
import { UserRepository } from "../repositories";
@Injectable()
export class UserService {
constructor(private readonly _userRepository: UserRepository) {}
async createUser(
createUserDto: CreateUserDto,
authentication: AuthenticationEntity,
queryRunner: QueryRunner
): Promise<UserEntity> {
const user = this._userRepository.create({
...createUserDto,
authentication,
});
return queryRunner.manager.save(user);
}
}
Теперь мы можем собрать все зависимости вместе. В классе основного модуля создайте экземпляры сервисов:
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserRepository } from "./repositories";
import { UserService } from "./services";
@Module({
imports: [TypeOrmModule.forFeature([UserRepository])],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
Также не забудьте добавить модуль в базовый класс:
+ import { UserModule } from 'src/user';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
DatabaseModule,
+ UserModule,
],
})
export class AppModule {}
Теперь пользовательский модуль полностью запрограммирован. ✅
6. Создайте модуль аутентификации
Поговорим о модуле аутентификации. В этом каталоге вы будете хранить всю связанную с ним логику.
6.1. Модель аутентификации
Как я писал ранее, здесь будет модель данных, в которой будет храниться конфиденциальная информация, такая как адрес электронной почты или пароль.
Создайте файл authentication.entity.ts в каталоге src/authentication/entities:
import { Exclude } from "class-transformer";
import { AbstractEntity } from "src/common/entities";
import { UserEntity } from "src/user/entities";
import { Column, Entity, OneToOne } from "typeorm";
@Entity({ name: "authentications" })
export class AuthenticationEntity extends AbstractEntity {
@Column({ unique: true })
public emailAddress: string;
@Column()
@Exclude()
public password: string;
@OneToOne(() => UserEntity, (user: UserEntity) => user.authentication)
@Exclude()
public user: UserEntity;
}
Примечание. Теперь вы можете связать эти два объекта отношением "один к одному".
Для модели вы традиционно создаете репозиторий:
import { Repository } from "typeorm";
import { EntityRepository } from "typeorm/decorator/EntityRepository";
import { AuthenticationEntity } from "../entities";
@EntityRepository(AuthenticationEntity)
export class AuthenticationRepository extends Repository<AuthenticationEntity> {}
И давайте, наконец, создадим недостающий класс dto:
import { IsEmail, IsNotEmpty, IsString, MinLength } from "class-validator";
export class CreateAuthenticationDto {
@IsEmail()
@IsNotEmpty()
readonly emailAddress: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
readonly password: string;
}
6.2. Поставщик аутентификации
Вам нужен класс, отвечающий за выполнение хэш-сервиса.
Хорошей практикой является создание файла authentication.provider.ts в каталоге src/authentication/providers. :
import * as bcrypt from "bcrypt";
export class AuthenticationProvider {
static async generateHash(password: string): Promise<string> {
return bcrypt.hash(password, 10);
}
}
6.3. Подписчик
Теперь займемся подписчиком. Вы уже должны знать, что конфиденциальные данные, такие как пароль, не должны быть простой строкой символов. В случае утечки данных из базы данных все ваши учетные записи будут украдены.
TypeORM имеет возможность выполнять операцию перед вставкой, и это идеальное место для преобразования пароля из строки в хэш.
Создайте authentication.subscriber.ts в каталоге src/authentication/subscribers:
import {
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
UpdateEvent,
} from "typeorm";
import { AuthenticationEntity } from "../entities";
import { AuthenticationProvider } from "../providers";
@EventSubscriber()
export class AuthenticationSubscriber
implements EntitySubscriberInterface<AuthenticationEntity>
{
listenTo() {
return AuthenticationEntity;
}
async beforeInsert({
entity,
}: InsertEvent<AuthenticationEntity>): Promise<void> {
if (entity.password) {
entity.password = await AuthenticationProvider.generateHash(
entity.password
);
}
if (entity.emailAddress) {
entity.emailAddress = entity.emailAddress.toLowerCase();
}
}
async beforeUpdate({
entity,
databaseEntity,
}: UpdateEvent<AuthenticationEntity>): Promise<void> {
if (entity.password) {
const password = await AuthenticationProvider.generateHash(
entity.password
);
if (password !== databaseEntity?.password) {
entity.password = password;
}
}
}
}
Примечание. Это хорошая возможность сохранить адрес электронной почты в нижнем регистре.
Не забудьте запустить подписчика в массиве подписчиков в модуле база данных:
+ import { AuthenticationSubscriber } from 'src/authentication/subscribers';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
...
+ subscribers: [AuthenticationSubscriber],
}),
}),
],
})
export class DatabaseModule {}
6.4. Услуга
Здесь мы напишем логику, которую будет выполнять контроллер.
Как я уже говорил, мы будем использовать транзакции, потому что мы вставляем данные в две таблицы (пользователи и аутентификации).
Теперь посмотрите на код, который вы поместили в файл authentication.service.ts:
import { Injectable, InternalServerErrorException } from "@nestjs/common";
import { PostgresErrorCode } from "src/database/constraints";
import { UserEntity } from "src/user/entities";
import { UserService } from "src/user/services";
import { Connection, QueryRunner } from "typeorm";
import { CreateAuthenticationDto } from "../dtos";
import { RegistrationDto } from "../dtos/registration.dto";
import { AuthenticationEntity } from "../entities";
import { UserAlreadyExistException } from "../exceptions";
import { AuthenticationRepository } from "../repositories";
@Injectable()
export class AuthenticationService {
constructor(
private readonly _authenticationRepository: AuthenticationRepository,
private readonly _userService: UserService,
private readonly _connection: Connection
) {}
async registration(registrationDto: RegistrationDto): Promise<UserEntity> {
let user: UserEntity;
const queryRunner = this._connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const authentication = await this._createAuthentication(
registrationDto,
queryRunner
);
user = await this._userService.createUser(
registrationDto,
authentication,
queryRunner
);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
if (error?.code === PostgresErrorCode.UniqueViolation) {
throw new UserAlreadyExistException();
}
throw new InternalServerErrorException();
} finally {
await queryRunner.release();
}
return user;
}
private async _createAuthentication(
createAuthenticationDto: CreateAuthenticationDto,
queryRunner: QueryRunner
): Promise<AuthenticationEntity> {
const authentication = this._authenticationRepository.create(
createAuthenticationDto
);
return queryRunner.manager.save(authentication);
}
}
Помните, что адрес электронной почты должен быть уникальным. Хорошо иметь возможность перехватывать эту ошибку, поэтому мы создаем user-already-exist.exception.ts в каталоге exceptions:
import { BadRequestException } from "@nestjs/common";
export class UserAlreadyExistException extends BadRequestException {
constructor(error?: string) {
super("User with that email already exists", error);
}
}
Кроме того, мы используем RegistrationD, чтобы код выглядел более стильно. Это в основном то же самое, что и CreateUserDto, но по-другому:
import { CreateUserDto } from "src/user/dtos";
export class RegistrationDto extends CreateUserDto {}
Сервис готов, теперь его можно выставить контроллеру для использования. ✅
6.5. Контроллер
Логика регистрации находится по адресу /Authentication/registration.
Примечание. В идеале он должен размещаться в папке /Users с помощью метода POST. Но это сложно сделать, потому что тогда получится циклическая зависимость. Пользовательский модуль будет использовать зависимость аутентификации и наоборот. Можно решить так, но это антипаттерн. Это не стоит делать.
Создайте файл authentication.controller.ts в каталоге controlers:
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common";
import { UserEntity } from "src/user/entities";
import { RegistrationDto } from "../dtos";
import { AuthenticationService } from "../services";
@Controller("Authentication")
export class AuthenticationController {
constructor(private readonly _authenticationService: AuthenticationService) {}
@Post("registration")
@HttpCode(HttpStatus.OK)
async registration(
@Body() registrationDto: RegistrationDto
): Promise<UserEntity> {
return this._authenticationService.registration(registrationDto);
}
}
6.6. Внедрить все зависимости в модуль
Теперь все сливается в единое целое с помощью модуля.
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { UserModule } from "src/user";
import { AuthenticationController } from "./controllers";
import { AuthenticationRepository } from "./repositories";
import { AuthenticationService } from "./services";
@Module({
imports: [UserModule, TypeOrmModule.forFeature([AuthenticationRepository])],
providers: [AuthenticationService],
controllers: [AuthenticationController],
})
export class AuthenticationModule {}
Вы также должны объявить этот модуль в основном модуле приложения:
+ import { AuthenticationModule } from 'src/authentication';
@Module({
imports: [
ConfigModule.forRoot({
...
}),
DatabaseModule,
+ AuthenticationModule,
UserModule,
],
})
export class AppModule {}
Да, сэр, все готово. Молодец, как ты сюда попал. 👍 Посмотрим, работает ли вообще этот шедевр. 😆
7. Тесты
Пришло время протестировать наше приложение. Отправим POST запрос на контроллер с адресом /Authentication/registration с прикрепленным телом:
{
"firstName": "Adrian",
"emailAddress": "[email protected]",
"password": "123456"
}
Контроллер выполнит следующую операцию SQL:
START TRANSACTION
INSERT INTO "authentications"("uuid", "created_at", "updated_at", "email_address", "password") VALUES (DEFAULT, DEFAULT, DEFAULT, $1, $2) RETURNING "id", "uuid", "created_at", "updated_at" -- PARAMETERS: ["[email protected]","$2b$10$x0oV4oPS7ehhCSp537ygruWKxKpSX4MXlluqvxzSibRFCh2kMSS7i"]
INSERT INTO "users"("uuid", "created_at", "updated_at", "first_name", "authentication_id") VALUES (DEFAULT, DEFAULT, DEFAULT, $1, $2) RETURNING "id", "uuid", "created_at", "updated_at" -- PARAMETERS: ["Adrian",1]
COMMIT
Примечание. Вы можете видеть, что пароль был закодирован подписчиком перед вставкой записи в базу данных.
А в ответ получаем созданный объект, без конфиденциальных данных типа id или пароля:
{
"uuid": "0cc8f6cd-44f4-4d73-9bef-9f3b872180c4",
"firstName": "Adrian",
"authentication": {
"uuid": "b3c257e6-85b4-49b8-b0a0-877e3c936a4e",
"emailAddress": "[email protected]"
}
}
Давайте все же проверим, что валидация работает. Теперь я отправлю запрос без имени:
{
"statusCode": 400,
"message": ["firstName should not be empty", "firstName must be a string"],
"error": "Bad Request"
}
Я все еще попытаюсь добавить тот же адрес электронной почты:
{
"statusCode": 400,
"message": "User with that email already exists",
"error": "Bad Request"
}
Идеальный! Желаемый эффект был получен. 🎉
8. Бонус: документация OpenAPI
Техническая документация необходима при производстве профессионального программного обеспечения. Это также очень полезно для разработчика. Платформа NestJS предоставляет инструмент, который позволяет очень легко создавать документацию OpenAPI. Теперь мы добавим это в проект, это позволит нам позже сохранить исходный код прозрачным.
8.1. Добавьте необходимые зависимости
Установите необходимые зависимости в свой проект:
yarn add @nestjs/swagger swagger-ui-express
Создайте файл setup-swagger.util.ts в каталоге utils:
import type { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
export function setupSwagger(app: INestApplication): void {
const options = new DocumentBuilder()
.setTitle("NestJS-Authentication-Full")
.setContact(
"Adrian Pietrzak",
"https://pietrzakadrian.com",
"[email protected]"
)
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup("documentation", app, document);
}
Теперь вы можете скомпилировать документацию вместе с приложением.
+ import { setupSwagger } from './util';
async function bootstrap() {
...
+ if (configService.get<string>('NODE_ENV') === NODE_ENV.DEVELOPMENT) {
+ setupSwagger(app);
+ }
await app.listen(PORT);
}
void bootstrap();
Примечание. Не забудьте сделать его доступным только в режиме разработки.
8.3. Классы украшения
Теперь вам нужно добавить несколько декораторов в классы dto и контроллера. Следите за изменениями, которые я вношу в файлы:
import { ApiProperty } from "@nestjs/swagger"; export class AbstractDto { @ApiProperty({ format: "uuid" }) readonly uuid: string; }+ import { ApiProperty } from '@nestjs/swagger'; export class CreateUserDto extends CreateAuthenticationDto { @IsString() @IsNotEmpty() + @ApiProperty() readonly firstName: string; }+ import { ApiProperty } from '@nestjs/swagger'; - export class UserDto { + export class UserDto extends AbstractDto { + @ApiProperty() readonly firstName: string; + @ApiProperty({ type: () => AuthenticationDto }) readonly authentication: AuthenticationDto; }+ import { ApiProperty } from '@nestjs/swagger'; export class CreateAuthenticationDto { @IsEmail() @IsNotEmpty() + @ApiProperty() readonly emailAddress: string; @IsString() @IsNotEmpty() @MinLength(6) + @ApiProperty() readonly password: string; }import { ApiProperty } from '@nestjs/swagger'; import { AbstractDto } from 'src/common/dtos'; - export class AuthenticationDto { + export class AuthenticationDto extends AbstractDto { + @ApiProperty() readonly emailAddress: string; }+ import { ApiBadRequestResponse, ApiInternalServerErrorResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger'; + import { UserDto } from 'src/user/dtos'; @Controller('Authentication') + @ApiTags('Authentication') export class AuthenticationController { constructor(private readonly _authenticationService: AuthenticationService) {} @Post('registration') @HttpCode(HttpStatus.OK) + @ApiOkResponse({ type: UserDto, description: 'Successfully created user' }) + @ApiBadRequestResponse({ description: 'User with that email already exists.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error' }) async registration( @Body() registrationDto: RegistrationDto, ): Promise<UserEntity> { return this._authenticationService.registration(registrationDto); } }
8.4. Предварительный просмотр
Когда вы перейдете к /documentation, вы получите правильно сгенерированный документ Swagger:

Очень быстрое изменение и упрощает процесс документирования программного приложения. 📄
Краткое содержание
В этой статье представлена полная реализация регистрации новых пользователей на основе передовых методов программирования языка TypeScript и платформы NestJS. Переменные среды были определены и проверены. Была объяснена работа подписчика в TypeORM и применены транзакции в postgres.
Вы также можете найти эту статью в моем блоге разработчиков программного обеспечения, где я делюсь своими решениями проблем, с которыми я столкнулся в своей карьере инженера-программиста.
Если у вас есть дополнительные вопросы, вы можете написать мне в LinkedIn или Twitter.
Первоначально опубликовано на https://pietrzakadrian.com.