084 Сервис конфигурации
Добавляем в приложение возможность читать файлы .env
npm i dotenv
Создаём файл конфигурации окружения .env
корень проекта > .env
SALT=10
Далее нужно создать интерфейс для получения значения из конфигурации окружения
config > config.service.interface.ts
export interface IConfigService {
get: (key: string) => string;
}
И тут реализуем сам сервис по получению данных из файла конфига окружения
config > config.service.ts
import { IConfigService } from './config.service.interface';
import { config, DotenvConfigOutput, DotenvParseOutput } from 'dotenv';
import { inject, injectable } from 'inversify';
import { TYPES } from '../types';
import { ILogger } from '../logger/logger.interface';
@injectable()
export class ConfigService implements IConfigService {
// это наш конфиг, который попадёт в класс
private config: DotenvParseOutput;
constructor(@inject(TYPES.ILogger) private logger: ILogger) {
// тут мы запрашиваем отпаршенный конфиг .env
const result: DotenvConfigOutput = config();
// если ошибка, то выведем логгер
if (result.error) {
this.logger.error('[ConfigService] Не удалось прочитать файл конфигурации');
} else {
// если всё хорошо, то будем присваивать отпаршенный конфиг в наш конфиг
// делаем жёсткий прокаст, так как присваивается result, а не отдельный от него parsed
this.config = result.parsed as DotenvParseOutput;
this.logger.log('[ConfigService] Конфигурация загружена');
}
}
get(key: string): string {
return this.config[key];
}
}
Добавляем новый символ тайп по нашему сервису
types.ts
export const TYPES = {
Application: Symbol.for('Application'),
ILogger: Symbol.for('ILogger'),
UserController: Symbol.for('UserController'),
UserService: Symbol.for('UserService'),
ExeptionFilter: Symbol.for('ExeptionFilter'),
ConfigService: Symbol.for('ConfigService'),
};
Инжектим сервис в приложение
app.ts
constructor(
@inject(TYPES.ILogger) private logger: ILogger,
@inject(TYPES.UserController) private userController: UserController,
@inject(TYPES.ExeptionFilter) private exeptionFilter: IExeptionFilter,
// добавляем ConfigService
@inject(TYPES.ConfigService) private configService: IConfigService,
) {
this.app = express();
this.port = 8000;
}
И инжектим в сервис пользователя, так как в нём мы генерируем новый пароль (нам нужен тут параметр SALT
)
users.service.ts
constructor(
// добавляем ConfigService
@inject(TYPES.ConfigService) private configService: IConfigService,
) {}
И тут же биндим наш новый сервис в контейнер модулей приложения
main.ts
export const appBindings = new ContainerModule((bind: interfaces.Bind) => {
bind<ILogger>(TYPES.ILogger).to(LoggerService);
bind<IExeptionFilter>(TYPES.ExeptionFilter).to(ExeptionFilter);
bind<IUserController>(TYPES.UserController).to(UserController);
bind<IUserService>(TYPES.UserService).to(UserService);
bind<App>(TYPES.Application).to(App);
//
bind<IConfigService>(TYPES.ConfigService).to(ConfigService);
});
И тут мы видим, что у нас вызывается два разных инстанса одного класса, что может быть довольно опасно
Чтобы исправить проблему, нужно перевести конкретно данный класс в синглтон, что позволит нам инстанциировать ровно один раз одну копию класса (то есть, она всегда буде одинакова). Чтобы сделать это, нужно при биндинге указать дополнительным методом inSingletonScope()
, что нам нужно иметь именно один конкретный инстанс класса
export const appBindings = new ContainerModule((bind: interfaces.Bind) => {
bind<ILogger>(TYPES.ILogger).to(LoggerService).inSingletonScope();
bind<IExeptionFilter>(TYPES.ExeptionFilter).to(ExeptionFilter);
bind<IUserController>(TYPES.UserController).to(UserController);
bind<IUserService>(TYPES.UserService).to(UserService);
bind<IConfigService>(TYPES.ConfigService).to(ConfigService).inSingletonScope();
bind<App>(TYPES.Application).to(App);
});
Тут меняем метод генерации пароля на получение соли (salt
) извне
user.entity.ts
public async setPassword(pass: string, salt: number): Promise<void> {
this._password = await hash(pass, salt);
}
А тут получаем из ConfigService
через метод get
определённое значение нашего .env
файла, а конкретно параметра SALT
. Далее новому пользователю устанавливаем пароль
users.service.ts
constructor(
// добавляем ConfigService
@inject(TYPES.ConfigService) private configService: IConfigService,
) {}
async createUser({ email, name, password }: UserRegisterDto): Promise<User | null> {
const newUser = new User(email, name);
// получаем соль для генерации пароля
const salt = this.configService.get('SALT');
await newUser.setPassword(password, Number(salt));
console.log(salt);
// проверка что он есть?
// если есть - возвращаем null
// если нет - создаём
return null;
}
И теперь после отправки запроса на сервер, можно увидеть полученные данные из .env
085 Работа с prisma
Для написания запросов и построения моделей баз данных будет использоваться Prism ORM. Она позволяет прямо из кода приложения описать нашу базу данных и взаимодействовать с ней.
Конкретно в курсе будет использоваться SQLite
+ Prism ORM
Первым делом, нужно установить призму
// устанавливаем призму
npm i -D prisma
// устанавливаем клиент призмы
npm i @prisma/client
// инициализируем призму
npx prisma init
Далее настроим плагин в VSCode для призмы
.vscode > settings.json
{
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
}
После последней команды терминала у нас появляется папка prisma
, в которой находится файл схемы призмы. Тут мы формируем саму модель работы призмы.
Первым делом делаем коннект в db
к нужной базе данных и к файлу, в которой будет храниться база.
Далее нужно создать модель пользователя в UserModel
, где укажем нужные поля под его данные
schema.prisma
// клиент генератора запросов
generator client {
provider = "prisma-client-js"
}
// исходники для данных
datasource db {
// тут нужно указать, к какой БД подключаемся
provider = "sqlite"
// это ссылка до файла базы данных
// создастся автоматически при начале работы с призмой
url = "file:./dev.db"
}
// тут уже будет находиться само статическое описание модели пользователя
model UserModel {
id Int @id @default(autoincrement())
email String
password String
name String
}
Дальше произведём первую миграцию, при которой у нас создастся файл БД
npx prisma migrate dev
Каждой миграции даётся своё имя
И мы имеем примерно такую структуру после миграции:
Уберём базы и окружение из отслеживания гита (так как там может храниться важная информация)
.gitignore
/node_modules
/dist
/.clinic
/.env
/prisma/dev.db
/prisma/dev.db-journal
Так же отдельно вынесем команду, которая будет генерировать типы нашей сформированной базы данных
package.json
"scripts": {
"generate": "prisma generate"
},
После срабатывания данной команды генерации типов, описанные в схеме модели конвертируются в типы, которые мы можем использовать в проекте
Сейчас нам нужно привязать сервис призмы к нашему приложению через DI
types.ts
export const TYPES = {
Application: Symbol.for('Application'),
ILogger: Symbol.for('ILogger'),
UserController: Symbol.for('UserController'),
UserService: Symbol.for('UserService'),
ExeptionFilter: Symbol.for('ExeptionFilter'),
ConfigService: Symbol.for('ConfigService'),
//
PrismaService: Symbol.for('PrismaService'),
};
main.ts
export const appBindings = new ContainerModule((bind: interfaces.Bind) => {
bind<ILogger>(TYPES.ILogger).to(LoggerService).inSingletonScope();
bind<IExeptionFilter>(TYPES.ExeptionFilter).to(ExeptionFilter);
bind<IUserController>(TYPES.UserController).to(UserController);
bind<IUserService>(TYPES.UserService).to(UserService);
// биндим призму, используя синглтон
bind<PrismaService>(TYPES.PrismaService).to(PrismaService).inSingletonScope();
bind<IConfigService>(TYPES.ConfigService).to(ConfigService).inSingletonScope();
bind<App>(TYPES.Application).to(App);
});
app.ts
constructor(
@inject(TYPES.ILogger) private logger: ILogger,
@inject(TYPES.UserController) private userController: UserController,
@inject(TYPES.ExeptionFilter) private exeptionFilter: IExeptionFilter,
@inject(TYPES.ConfigService) private configService: IConfigService,
// инжектим инстанс призмы к приложению
@inject(TYPES.PrismaService) private prismaService: PrismaService,
) {
this.app = express();
this.port = 8000;
}
public async init(): Promise<void> {
this.useMiddleware();
this.useRoutes();
this.useExeptionFilters();
// подключаемся асинхронно к сервису призмы
await this.prismaService.connect();
this.server = this.app.listen(this.port);
this.logger.log(`Сервер запущен на http://localhost:${this.port}`);
}
И тут уже представлена сама реализация функции нашего сервиса по работе с призмой
database > prisma.service.ts
import { PrismaClient, UserModel } from '@prisma/client';
import { inject, injectable } from 'inversify';
import { TYPES } from '../types';
import { ILogger } from '../logger/logger.interface';
@injectable()
export class PrismaService {
// клиент призмы
client: PrismaClient;
// инстанциируем сам клиент призмы
constructor(@inject(TYPES.ILogger) private logger: ILogger) {
this.client = new PrismaClient();
}
// метод подключения к базе данных
async connect(): Promise<void> {
try {
await this.client.$connect();
this.logger.log('[PrismaService] совершено успешное подключение к базе данных');
} catch (e: unknown) {
if (e instanceof Error) {
this.logger.log(`[PrismaService] не удалось подключиться к базе данных ${e.message}`);
}
}
}
// метод отключения от базы данных
async disconnect(): Promise<void> {
await this.client.$disconnect();
this.logger.log('[PrismaService] совершено отключение от базе данных');
}
}
Лог о подключении к базе данных
086 Репозиторий users
Далее нам нужно реализовать репозиторий, который будет общаться с базой данных, записывать новых и получать старых пользователей системы.
Первым делом, нам нужно реализовать интерфейс, который будет представлять наш репозиторий.
Метод create
на вход получает определённую entity
и на выходе через определённый промежуток получает модель из базы.
Метод find
будет искать пользователя по его почте.
users.repository.interface.ts
import { UserModel } from '@prisma/client';
import { User } from './user.entity';
export interface IUsersRepository {
create: (user: User) => Promise<UserModel>;
find: (email: string) => Promise<UserModel | null>;
}
И далее тут реализуем из интерфейса саму работу с БД через репозиторий
users.repository.ts
import { IUsersRepository } from './users.repository.interface';
import { User } from './user.entity';
import { UserModel } from '@prisma/client';
import { inject, injectable } from 'inversify';
import { TYPES } from '../types';
import { PrismaService } from '../database/prisma.service';
@injectable()
export class UsersRepository implements IUsersRepository {
// тут мы добавляем сервис призмы
constructor(@inject(TYPES.PrismaService) private prismaService: PrismaService) {}
// это метод создания нового пользователя
// так как мы сюда передаём пользователя, то его сразу можно и деструктуризировать
async create({ email, password, name }: User): Promise<UserModel> {
return this.prismaService.client.userModel.create({
data: {
email,
name,
password,
},
});
}
// тут будет осуществляться поиск пользователя
async find(email: string): Promise<UserModel | null> {
return this.prismaService.client.userModel.findFirst({
where: {
email,
},
});
}
}
Так же нужно сказать, что мы можем выполнять над моделью пользователя все те действия, которые нам могут потребоваться: обновить, удалить, добавить, посчитать и так далее
Ну и сейчас нужно забиндить наш UsersRepository
в DI, чтобы иметь доступ к инстансу класса при инициализации в конструкторе
export const TYPES = {
// code ...
UsersRepository: Symbol.for('UsersRepository'),
};
export const appBindings = new ContainerModule((bind: interfaces.Bind) => {
// code ...
bind<IUsersRepository>(TYPES.UsersRepository).to(UsersRepository).inSingletonScope();
});
Далее же нам требуется изменить логику работы UserService
, чтобы он возвращал не пользователя, а модель пользователя из базы данных
export interface IUserService {
createUser: (dto: UserRegisterDto) => Promise<UserModel | null>;
validateUser: (dto: UserLoginDto) => Promise<boolean>;
}
Тут уже при отправке запроса на сервер, мы сначала проверяем в existedUser
, что у нас нет данного пользователя в базе, и если его нет, то в конечном return
вызываем метод create()
для создания нового пользователя
users.service.ts
@injectable()
export class UserService implements IUserService {
constructor(
@inject(TYPES.ConfigService) private configService: IConfigService,
// инжектим инстанс репозитория пользователя
@inject(TYPES.UsersRepository) private usersRepository: IUsersRepository,
) {}
async createUser({ email, name, password }: UserRegisterDto): Promise<UserModel | null> {
const newUser = new User(email, name);
const salt = this.configService.get('SALT');
await newUser.setPassword(password, Number(salt));
// ищем пользователя по почте
const existedUser = await this.usersRepository.find(email);
// если есть - возвращаем null
if (existedUser) {
return null;
}
// если нет - создаём
return this.usersRepository.create(newUser);
}
async validateUser(dto: UserLoginDto): Promise<boolean> {
return true;
}
}
Так же добавим вывод id
пользователя в запросе
users.controller.ts
async register(
{ body }: Request<{}, {}, UserRegisterDto>,
res: Response,
next: NextFunction,
): Promise<void> {
const result = await this.userService.createUser(body);
if (!result) {
return next(new HTTPError(422, 'Такой пользователь уже существует'));
}
this.ok(res, { email: result.email, id: result.id });
}
Этим запросом мы создали пользователя и записали в БД
При повторной отправке запроса у нас выпадает ошибка
И так же у нас присутствует инкрементированность id
для новых пользователей
087 Упражнение - Логин пользователя
Первым делом нужно накинуть декораторы внутри класса-интерфейса, которые будут проверять получаемые значения
users > dto > user-login.dto.ts
import { IsEmail, IsString } from 'class-validator';
export class UserLoginDto {
@IsEmail({}, { message: 'Неверно задан email' })
email: string;
@IsString()
password: string;
}
В отдельную сущность пользователя сначала добавляем возможность передать пароль. Далее добавляем метод для сравнения паролей compare()
из библиотеки bcryptjs
user.entity.ts
import { compare, hash } from 'bcryptjs';
export class User {
private _password: string;
constructor(
private readonly _email: string,
private readonly _name: string,
// так же сюда передаём хеш пароля
passwordHash?: string,
) {
// если хеш пароля передали, то
if (passwordHash) {
// присвоим пароль пользователя
this._password = passwordHash;
}
}
/// CODE ...
// метод для проверки пароля
public async comparePassword(pass: string): Promise<boolean> {
// берём функцию для проверки переданного пароля с паролем пользователя
return compare(pass, this._password);
}
}
В сервисах пользователя реализуем валидацию данных, где мы сначала находим пользователя в базе по почте, а затем уже только проверяем пароль на соответствие
users.service.ts
async validateUser({ email, password }: UserLoginDto): Promise<boolean> {
// проверяем при логине пользователя
const existedUser = await this.usersRepository.find(email);
// если пользователь не существует
if (!existedUser) {
return false;
}
// создаём инстанс пользователя, если он есть в базе
const newUser = new User(existedUser.email, existedUser.name, existedUser.password);
return newUser.comparePassword(password);
}
Уже в контроллере пользователя добавляем middlewares
для обработки данных логина.
Далее реализуем асинхронный метод логина пользователя, который будет возвращать ответ о статусе логина.
users.controller.ts
export class UserController extends BaseController implements IUserController {
constructor(
@inject(TYPES.ILogger) private loggerService: ILogger,
@inject(TYPES.UserService) private userService: UserService,
) {
super(loggerService);
this.bindRoutes([
{
path: '/register',
method: 'post',
func: this.register,
middlewares: [new ValidateMiddleware(UserRegisterDto)],
},
{
path: '/login',
method: 'post',
func: this.login,
// так же в миддлвейры добавляем сюда проверку логина
middlewares: [new ValidateMiddleware(UserLoginDto)],
},
]);
}
// реализаця метода логина пользователя
async login(
req: Request<{}, {}, UserLoginDto>,
res: Response,
next: NextFunction,
): Promise<void> {
// получаем ответ от валидатора на основе переданного в него пользователя из тела запроса
const result = await this.userService.validateUser(req.body);
// если результата нет, то выведем ошибку
if (!result) {
return next(new HTTPError(401, 'ошибка авторизации', 'login'));
}
// если всё нормально, то отправляем ответ на фронт
this.ok(res, {});
}
/// CODE ...
}
Если передать несуществующего пользователя, то получим ошибку
Если мы передадим существующего пользователя, то мы получим удовлетворительный ответ от сервера