From 72461c26199c2e6dc04ad594d399b3af75f65ee0 Mon Sep 17 00:00:00 2001 From: Echenko Date: Fri, 7 Aug 2026 23:29:59 +0300 Subject: [PATCH 1/8] ref: user id, offer id, import, add speck --- src/cli/commands/import.command.ts | 71 ++++++++++--------- src/cli/commands/version.command.ts | 11 ++- src/shared/const.ts | 1 - src/shared/helpers/common.ts | 7 ++ src/shared/helpers/password.helper.ts | 10 +++ .../offer-generator/tsv-offer-generator.ts | 3 - src/shared/libs/tsv-parser/tsv-parser.ts | 2 - .../modules/offer/default-offer.repository.ts | 46 +++++++++--- src/shared/modules/offer/offer.entity.ts | 34 +++++++-- src/shared/modules/offer/offer.interface.ts | 23 +++++- .../offer/offer.repository.interface.ts | 6 +- .../modules/user/default-user.repository.ts | 32 ++++++++- src/shared/modules/user/user.entity.ts | 29 ++++++-- src/shared/modules/user/user.interface.ts | 10 ++- .../modules/user/user.repository.interface.ts | 2 + src/shared/types/offers-item.type.ts | 1 - 16 files changed, 212 insertions(+), 76 deletions(-) create mode 100644 src/shared/helpers/password.helper.ts diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 8640fd4..f68ff4d 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -8,13 +8,15 @@ import { TYPES } from '../../shared/libs/container/index.js'; import { LoggerInterface } from '../../shared/libs/logger/index.js'; import { DatabaseClientInterface } from '../../shared/libs/database/index.js'; import { OffersItemType } from '../../shared/types/index.type.js'; +import { CityName, CreateOffer, OfferType } from '../../shared/modules/offer/index.js'; +import { hashPassword } from './../../shared/helpers/password.helper.js'; @injectable() export class ImportCommand implements Command { constructor( @inject(TYPES.Logger) private readonly logger: LoggerInterface, @inject(TYPES.DatabaseClient) private readonly databaseClient: DatabaseClientInterface, - ) {} + ) { } public getName(): string { return '--import'; @@ -77,7 +79,6 @@ export class ImportCommand implements Command { private async saveOffer(item: OffersItemType): Promise { const { - id, title, type, price, @@ -110,38 +111,36 @@ export class ImportCommand implements Command { maxAdults, } = item; - const user = await this.findOrCreateUser(userName, userAvatarUrl, userIsPro); - - await OfferModel.findOneAndUpdate( - { id }, - { - title, - type, - price, - previewImage, - cityName, - cityLatitude, - cityLongitude, - cityZoom, - offerLatitude, - offerLongitude, - offerZoom, - isFavorite, - isPremium, - rating, - description, - bedrooms, - offerGoods, - user: user._id, - images, - maxAdults, - }, - { - upsert: true, - returnDocument: 'after', - setDefaultsOnInsert: true, - }, - ).exec(); + const user = await this.findOrCreateUser( + userName, + userAvatarUrl, + userIsPro, + ); + + const offerData: CreateOffer = { + title, + type: type as OfferType, + price, + previewImage, + cityName: cityName as CityName, + cityLatitude, + cityLongitude, + cityZoom, + offerLatitude, + offerLongitude, + offerZoom, + isFavorite, + isPremium, + rating, + description, + bedrooms, + offerGoods, + user: user._id, + images, + maxAdults, + }; + + await OfferModel.create(offerData); } private async findOrCreateUser( @@ -154,10 +153,12 @@ export class ImportCommand implements Command { let user = await UserModel.findOne({ email }).exec(); if (!user) { + const hashedPassword = await hashPassword('default-password'); + user = await UserModel.create({ name, email, - password: 'default-password', + password: hashedPassword, avatarUrl: avatarUrl || 'https://example.com/default-avatar.jpg', type: isPro ? 'pro' : 'regular', }); diff --git a/src/cli/commands/version.command.ts b/src/cli/commands/version.command.ts index 42a3dd1..32b604e 100644 --- a/src/cli/commands/version.command.ts +++ b/src/cli/commands/version.command.ts @@ -24,7 +24,7 @@ export class VersionCommand implements Command { constructor( @inject(TYPES.Logger) private readonly logger: LoggerInterface, private readonly filePath: string = './package.json', - ) {} + ) { } private readVersion(): string { const absolutePath = resolve(this.filePath); @@ -48,11 +48,10 @@ export class VersionCommand implements Command { const version = this.readVersion(); this.logger.info(`Version: ${version}`); } catch (error: unknown) { - this.logger.error(error as Error, 'VersionCommand: Failed to read version from package.json'); - - if (error instanceof Error) { - this.logger.error(error, 'VersionCommand: Failed to read version from package.json'); - } + this.logger.error( + error instanceof Error ? error : new Error(String(error)), + 'VersionCommand: Failed to read version from package.json', + ); } } } diff --git a/src/shared/const.ts b/src/shared/const.ts index 69526cc..9e13817 100644 --- a/src/shared/const.ts +++ b/src/shared/const.ts @@ -37,7 +37,6 @@ const OFFER_CITIES_NAMES = ['Paris', 'Cologne', 'Brussels', 'Amsterdam', 'Hambur const OFFER_CITY_DEFAULT = OFFER_CITIES_NAMES[0]; const TSV_FIELDS_OFFER = [ - 'id', 'title', 'type', 'price', diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index bbb91ce..29dfa6c 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -1,3 +1,5 @@ +import { randomUUID } from 'node:crypto'; + function getRandomValue(min: number, max: number, numAfterDigit: number = 0): number { return Number((Math.random() * (max - min) + min).toFixed(numAfterDigit)); } @@ -40,10 +42,15 @@ function setNestedValue(obj: Record, path: string, value: unkno current[lastKey] = value; } +function generateId(): string { + return randomUUID(); +} + export { getRandomValue, getRandomItem, getRandomItems, getNestedValue, setNestedValue, + generateId, }; diff --git a/src/shared/helpers/password.helper.ts b/src/shared/helpers/password.helper.ts new file mode 100644 index 0000000..8158973 --- /dev/null +++ b/src/shared/helpers/password.helper.ts @@ -0,0 +1,10 @@ +import bcrypt from 'bcrypt'; + +const SALT_ROUNDS = 10; + +export const hashPassword = async (password: string): Promise => bcrypt.hash(password, SALT_ROUNDS); + +export const comparePassword = async ( + password: string, + hash: string, +): Promise => bcrypt.compare(password, hash); diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index 1175d12..988357e 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -1,6 +1,5 @@ import { OfferGenerator } from './offer-generator.interface.js'; import { MockServerDataType, OffersItemType } from './../../types/index.type.js'; -import { v4 } from 'uuid'; import { getRandomValue, getRandomItem, @@ -34,7 +33,6 @@ export class TSVOfferGenerator implements OfferGenerator { users, } = this.mockData; - const id = v4(); const title = getRandomItem(titles); const type = getRandomItem(types); const cityName = getRandomItem(cites); @@ -59,7 +57,6 @@ export class TSVOfferGenerator implements OfferGenerator { const maxAdults = getRandomValue(OFFER_ADULTS.min, OFFER_ADULTS.max); const offer: OffersItemType = { - id, title, type, price, diff --git a/src/shared/libs/tsv-parser/tsv-parser.ts b/src/shared/libs/tsv-parser/tsv-parser.ts index 4cbff0c..7ccd17d 100644 --- a/src/shared/libs/tsv-parser/tsv-parser.ts +++ b/src/shared/libs/tsv-parser/tsv-parser.ts @@ -16,7 +16,6 @@ export class TSVParser { } const [ - id, title, type, price, @@ -42,7 +41,6 @@ export class TSVParser { ] = columns; return { - id, title, type: (OFFER_TYPES as readonly string[]).includes(type) ? (type as OffersItemType['type']) diff --git a/src/shared/modules/offer/default-offer.repository.ts b/src/shared/modules/offer/default-offer.repository.ts index 186f2d0..8e7b448 100644 --- a/src/shared/modules/offer/default-offer.repository.ts +++ b/src/shared/modules/offer/default-offer.repository.ts @@ -3,35 +3,53 @@ import { TYPES } from '../../libs/container/container.types.js'; import { LoggerInterface } from '../../libs/logger/logger.interface.js'; import { OfferRepository } from './offer.repository.interface.js'; import { DocumentOffer, OfferModel } from './offer.entity.js'; -import { OfferInterface } from './offer.interface.js'; +import { CityName, CreateOffer } from './offer.interface.js'; @injectable() export class DefaultOfferRepository implements OfferRepository { constructor( @inject(TYPES.Logger) private readonly logger: LoggerInterface, - ) {} + ) { } + /** + * Ищет оффер по публичному строковому id. + */ public async findById(id: string): Promise { - this.logger.debug(`DefaultOfferRepository: Searching for offer by ID ${id}`); + this.logger.debug( + `DefaultOfferRepository: Searching for offer by ID ${id}`, + ); + return OfferModel.findOne({ id }).populate('user').exec(); } - public async create(dto: OfferInterface): Promise { + public async create(dto: CreateOffer): Promise { this.logger.info('DefaultOfferRepository: Creating new offer'); + const offer = new OfferModel(dto); + return offer.save(); } + /** + * Важно: + * userId здесь — это Mongo _id пользователя, а не его публичный id. + */ public async findByUserId(userId: string): Promise { - this.logger.debug(`DefaultOfferRepository: Searching offers by user ID ${userId}`); + this.logger.debug( + `DefaultOfferRepository: Searching offers by user ID ${userId}`, + ); + return OfferModel.find({ user: userId }) .populate('user') .sort({ createdAt: -1 }) .exec(); } - public async findByCity(city: string): Promise { - this.logger.debug(`DefaultOfferRepository: Searching offers in city ${city}`); + public async findByCity(city: CityName): Promise { + this.logger.debug( + `DefaultOfferRepository: Searching offers in city ${city}`, + ); + return OfferModel.find({ cityName: city }) .populate('user') .sort({ createdAt: -1 }) @@ -39,7 +57,10 @@ export class DefaultOfferRepository implements OfferRepository { } public async findAll(limit: number = 60): Promise { - this.logger.debug(`DefaultOfferRepository: Fetching all offers (limit: ${limit})`); + this.logger.debug( + `DefaultOfferRepository: Fetching all offers (limit: ${limit})`, + ); + return OfferModel.find() .populate('user') .sort({ createdAt: -1 }) @@ -47,9 +68,16 @@ export class DefaultOfferRepository implements OfferRepository { .exec(); } + /** + * Удаляет оффер по публичному строковому id. + */ public async deleteById(id: string): Promise { - this.logger.info(`DefaultOfferRepository: Deleting offer by ID ${id}`); + this.logger.info( + `DefaultOfferRepository: Deleting offer by ID ${id}`, + ); + const result = await OfferModel.deleteOne({ id }).exec(); + return result.deletedCount > 0; } } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index bbce82c..9fd37f9 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -1,7 +1,11 @@ -import { Document, model, Model, Schema } from 'mongoose'; +import { Document, model, Model, Schema, Types } from 'mongoose'; +import { generateId } from './../../helpers/index.js'; import { OfferInterface } from './offer.interface.js'; -export type DocumentOffer = OfferInterface & Document; +export type DocumentOffer = OfferInterface & + Document & { + _id: Types.ObjectId; + }; export const offerSchema = new Schema( { @@ -9,6 +13,7 @@ export const offerSchema = new Schema( type: String, required: true, unique: true, + default: () => generateId(), }, title: { type: String, @@ -34,7 +39,14 @@ export const offerSchema = new Schema( cityName: { type: String, required: true, - enum: ['Paris', 'Cologne', 'Brussels', 'Amsterdam', 'Hamburg', 'Dusseldorf'], + enum: [ + 'Paris', + 'Cologne', + 'Brussels', + 'Amsterdam', + 'Hamburg', + 'Dusseldorf', + ], }, cityLatitude: { type: Number, @@ -107,9 +119,23 @@ export const offerSchema = new Schema( }, }, { + id: false, timestamps: true, collection: 'offers', + toJSON: { + transform: (_doc, ret) => { + const result = ret as Record; + + delete result._id; + delete result.__v; + + return result; + }, + }, } ); -export const OfferModel: Model = model('Offer', offerSchema); +export const OfferModel: Model = model( + 'Offer', + offerSchema, +); diff --git a/src/shared/modules/offer/offer.interface.ts b/src/shared/modules/offer/offer.interface.ts index 84fd16c..7413831 100644 --- a/src/shared/modules/offer/offer.interface.ts +++ b/src/shared/modules/offer/offer.interface.ts @@ -1,12 +1,24 @@ import { Types } from 'mongoose'; +export type OfferType = 'apartment' | 'house' | 'room' | 'hotel'; + +export type CityName = + | 'Paris' + | 'Cologne' + | 'Brussels' + | 'Amsterdam' + | 'Hamburg' + | 'Dusseldorf'; + export interface OfferInterface { id: string; + createdAt: Date; + updatedAt: Date; title: string; - type: string; + type: OfferType; price: number; previewImage: string; - cityName: string; + cityName: CityName; cityLatitude: number; cityLongitude: number; cityZoom: number; @@ -23,3 +35,10 @@ export interface OfferInterface { images: string[]; maxAdults: number; } + +export type CreateOffer = Omit< + OfferInterface, + 'id' | 'createdAt' | 'updatedAt' +>; + +export type CreateOfferInput = Omit; diff --git a/src/shared/modules/offer/offer.repository.interface.ts b/src/shared/modules/offer/offer.repository.interface.ts index 50f5541..59ff576 100644 --- a/src/shared/modules/offer/offer.repository.interface.ts +++ b/src/shared/modules/offer/offer.repository.interface.ts @@ -1,11 +1,11 @@ import { Repository } from '../../libs/repository/repository.interface.js'; import { DocumentOffer } from './offer.entity.js'; -import { OfferInterface } from './offer.interface.js'; +import { CityName, CreateOffer } from './offer.interface.js'; export interface OfferRepository extends Repository { findByUserId(userId: string): Promise; - findByCity(city: string): Promise; + findByCity(city: CityName): Promise; findAll(limit?: number): Promise; deleteById(id: string): Promise; - create(dto: OfferInterface): Promise; + create(dto: CreateOffer): Promise; } diff --git a/src/shared/modules/user/default-user.repository.ts b/src/shared/modules/user/default-user.repository.ts index ae3d8ac..cbb4bb2 100644 --- a/src/shared/modules/user/default-user.repository.ts +++ b/src/shared/modules/user/default-user.repository.ts @@ -1,4 +1,5 @@ import { injectable, inject } from 'inversify'; +import { Types } from 'mongoose'; import { TYPES } from '../../libs/container/container.types.js'; import { LoggerInterface } from '../../libs/logger/logger.interface.js'; import { UserRepository } from './user.repository.interface.js'; @@ -11,24 +12,51 @@ export class DefaultUserRepository implements UserRepository { @inject(TYPES.Logger) private readonly logger: LoggerInterface, ) {} + /** + * Ищем пользователя по публичному строковому id. + */ public async findById(id: string): Promise { this.logger.debug(`DefaultUserRepository: Searching for user by ID ${id}`); - return UserModel.findById(id).exec(); + + return UserModel.findOne({ id }).exec(); + } + + /** + * Ищем пользователя по внутреннему Mongo _id. + */ + public async findByInternalId( + internalId: string, + ): Promise { + this.logger.debug( + `DefaultUserRepository: Searching for user by internal ID ${internalId}`, + ); + + if (!Types.ObjectId.isValid(internalId)) { + return null; + } + + return UserModel.findById(internalId).exec(); } public async findByEmail(email: string): Promise { this.logger.debug('DefaultUserRepository: Searching for user by email'); + return UserModel.findOne({ email }).exec(); } - public async findByEmailForAuth(email: string): Promise { + public async findByEmailForAuth( + email: string, + ): Promise { this.logger.debug('DefaultUserRepository: Auth lookup for user'); + return UserModel.findOne({ email }).select('+password').exec(); } public async create(dto: CreateUser): Promise { this.logger.info('DefaultUserRepository: Creating new user'); + const user = new UserModel(dto); + return user.save(); } } diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index d68f863..f20d670 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -1,12 +1,20 @@ import { Document, model, Model, Schema, Types } from 'mongoose'; +import { generateId } from './../../helpers/index.js'; import { UserInterface } from './user.interface.js'; -export type DocumentUser = UserInterface & Document & { - _id: Types.ObjectId; -}; +export type DocumentUser = UserInterface & + Document & { + _id: Types.ObjectId; + }; const userSchema: Schema = new Schema( { + id: { + type: String, + required: true, + unique: true, + default: () => generateId(), + }, name: { type: String, required: true, @@ -37,15 +45,24 @@ const userSchema: Schema = new Schema( }, }, { + id: false, timestamps: true, collection: 'users', toJSON: { transform: (_doc, ret) => { - delete (ret as { password?: string }).password; - return ret; + const result = ret as Record; + + delete result.password; + delete result._id; + delete result.__v; + + return result; }, }, } ); -export const UserModel: Model = model('User', userSchema); +export const UserModel: Model = model( + 'User', + userSchema, +); diff --git a/src/shared/modules/user/user.interface.ts b/src/shared/modules/user/user.interface.ts index 51a2e67..87ebbc6 100644 --- a/src/shared/modules/user/user.interface.ts +++ b/src/shared/modules/user/user.interface.ts @@ -1,11 +1,17 @@ +export type UserType = 'regular' | 'pro'; + export interface UserInterface { + id: string; createdAt: Date; updatedAt: Date; name: string; email: string; password: string; avatarUrl: string; - type: 'regular' | 'pro'; + type: UserType; } -export type CreateUser = Omit; +export type CreateUser = Omit< + UserInterface, + 'id' | 'createdAt' | 'updatedAt' +>; diff --git a/src/shared/modules/user/user.repository.interface.ts b/src/shared/modules/user/user.repository.interface.ts index e59f45a..1a8cb41 100644 --- a/src/shared/modules/user/user.repository.interface.ts +++ b/src/shared/modules/user/user.repository.interface.ts @@ -3,6 +3,8 @@ import { DocumentUser } from './user.entity.js'; import { CreateUser } from './user.interface.js'; export interface UserRepository extends Repository { + findById(id: string): Promise; + findByInternalId(internalId: string): Promise; findByEmail(email: string): Promise; findByEmailForAuth(email: string): Promise; create(dto: CreateUser): Promise; diff --git a/src/shared/types/offers-item.type.ts b/src/shared/types/offers-item.type.ts index 566e4ad..4fb585c 100644 --- a/src/shared/types/offers-item.type.ts +++ b/src/shared/types/offers-item.type.ts @@ -1,7 +1,6 @@ import { LocationType, CityType, UserType } from './index.type.js'; export type OffersItemType = { - id: string; title: string; type: string; price: number; From 44d40391042bd6745cddea1a668782496815cd21 Mon Sep 17 00:00:00 2001 From: Echenko Date: Sun, 9 Aug 2026 22:36:24 +0300 Subject: [PATCH 2/8] add: comment --- src/shared/libs/container/container.ts | 2 + src/shared/libs/container/container.types.ts | 1 + src/shared/modules/comment/comment.entity.ts | 59 +++++++++++++++++++ .../modules/comment/comment.interface.ts | 18 ++++++ .../comment/comment.repository.interface.ts | 10 ++++ .../comment/default-comment.repository.ts | 42 +++++++++++++ src/shared/modules/comment/index.ts | 4 ++ 7 files changed, 136 insertions(+) create mode 100644 src/shared/modules/comment/comment.entity.ts create mode 100644 src/shared/modules/comment/comment.interface.ts create mode 100644 src/shared/modules/comment/comment.repository.interface.ts create mode 100644 src/shared/modules/comment/default-comment.repository.ts create mode 100644 src/shared/modules/comment/index.ts diff --git a/src/shared/libs/container/container.ts b/src/shared/libs/container/container.ts index 47d387f..4666ec0 100644 --- a/src/shared/libs/container/container.ts +++ b/src/shared/libs/container/container.ts @@ -13,6 +13,7 @@ import { GenerateCommand } from './../../../cli/commands/index.js'; import { CliApplication } from '../../../cli/index.js'; +import { CommentRepository, DefaultCommentRepository } from '../../modules/comment/index.js'; const container = new Container(); @@ -29,6 +30,7 @@ container.bind(TYPES.DatabaseClient).to(MongoClient).in // Репозитории container.bind(TYPES.UserRepository).to(DefaultUserRepository); container.bind(TYPES.OfferRepository).to(DefaultOfferRepository); +container.bind(TYPES.CommentRepository).to(DefaultCommentRepository); // CLI container.bind(TYPES.CLIApplication).to(CliApplication); diff --git a/src/shared/libs/container/container.types.ts b/src/shared/libs/container/container.types.ts index 47636e0..2606b40 100644 --- a/src/shared/libs/container/container.types.ts +++ b/src/shared/libs/container/container.types.ts @@ -10,4 +10,5 @@ export const TYPES = { VersionCommand: Symbol.for('VersionCommand'), ImportCommand: Symbol.for('ImportCommand'), GenerateCommand: Symbol.for('GenerateCommand'), + CommentRepository: Symbol.for('CommentRepository'), }; diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts new file mode 100644 index 0000000..d5af9fc --- /dev/null +++ b/src/shared/modules/comment/comment.entity.ts @@ -0,0 +1,59 @@ +import { Document, model, Model, Schema, Types } from 'mongoose'; +import { generateId } from './../../helpers/index.js'; +import { CommentInterface } from './comment.interface.js'; + +export type DocumentComment = CommentInterface & + Document & { + _id: Types.ObjectId; + }; + +const commentSchema: Schema = new Schema( + { + id: { + type: String, + required: true, + unique: true, + default: () => generateId(), + }, + text: { + type: String, + required: true, + minlength: 5, + maxlength: 1024, + }, + rating: { + type: Number, + required: true, + min: 1, + max: 5, + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true, + }, + offer: { + type: Schema.Types.ObjectId, + ref: 'Offer', + required: true, + }, + }, + { + id: false, + timestamps: true, + collection: 'comments', + toJSON: { + transform: (_doc, ret) => { + const result = ret as Record; + delete result._id; + delete result.__v; + return result; + }, + }, + }, +); + +export const CommentModel: Model = model( + 'Comment', + commentSchema, +); diff --git a/src/shared/modules/comment/comment.interface.ts b/src/shared/modules/comment/comment.interface.ts new file mode 100644 index 0000000..5e2bce4 --- /dev/null +++ b/src/shared/modules/comment/comment.interface.ts @@ -0,0 +1,18 @@ +import { Types } from 'mongoose'; + +export interface CommentInterface { + id: string; + createdAt: Date; + updatedAt: Date; + text: string; + rating: number; + user: Types.ObjectId; + offer: Types.ObjectId; +} + +export type CreateComment = Omit< + CommentInterface, + 'id' | 'createdAt' | 'updatedAt' +>; + +export type CreateCommentInput = Omit; diff --git a/src/shared/modules/comment/comment.repository.interface.ts b/src/shared/modules/comment/comment.repository.interface.ts new file mode 100644 index 0000000..33c5b7c --- /dev/null +++ b/src/shared/modules/comment/comment.repository.interface.ts @@ -0,0 +1,10 @@ +import { Repository } from '../../libs/repository/repository.interface.js'; +import { DocumentComment } from './comment.entity.js'; +import { CreateComment } from './comment.interface.js'; + +export interface CommentRepository extends Repository { + findByOfferId(offerId: string): Promise; + findById(id: string): Promise; + deleteById(id: string): Promise; + create(dto: CreateComment): Promise; +} diff --git a/src/shared/modules/comment/default-comment.repository.ts b/src/shared/modules/comment/default-comment.repository.ts new file mode 100644 index 0000000..9743048 --- /dev/null +++ b/src/shared/modules/comment/default-comment.repository.ts @@ -0,0 +1,42 @@ +import { injectable, inject } from 'inversify'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { CommentRepository } from './comment.repository.interface.js'; +import { DocumentComment, CommentModel } from './comment.entity.js'; +import { CreateComment } from './comment.interface.js'; + +@injectable() +export class DefaultCommentRepository implements CommentRepository { + constructor( + @inject(TYPES.Logger) private readonly logger: LoggerInterface, + ) {} + + public async create(dto: CreateComment): Promise { + this.logger.info('DefaultCommentRepository: Creating new comment'); + const comment = new CommentModel(dto); + return comment.save(); + } + + public async findByOfferId(offerId: string): Promise { + this.logger.debug( + `DefaultCommentRepository: Searching comments by offer ID ${offerId}`, + ); + return CommentModel.find({ offer: offerId }) + .populate('user') + .sort({ createdAt: -1 }) + .exec(); + } + + public async findById(id: string): Promise { + this.logger.debug( + `DefaultCommentRepository: Searching comment by ID ${id}`, + ); + return CommentModel.findOne({ id }).populate('user').exec(); + } + + public async deleteById(id: string): Promise { + this.logger.info(`DefaultCommentRepository: Deleting comment by ID ${id}`); + const result = await CommentModel.deleteOne({ id }).exec(); + return result.deletedCount > 0; + } +} diff --git a/src/shared/modules/comment/index.ts b/src/shared/modules/comment/index.ts new file mode 100644 index 0000000..3e5d89c --- /dev/null +++ b/src/shared/modules/comment/index.ts @@ -0,0 +1,4 @@ +export * from './comment.entity.js'; +export * from './comment.interface.js'; +export * from './comment.repository.interface.js'; +export * from './default-comment.repository.js'; From 0d943501d9746e431630da802b4e8811f02ed73f Mon Sep 17 00:00:00 2001 From: Echenko Date: Sun, 9 Aug 2026 22:50:49 +0300 Subject: [PATCH 3/8] ref: offer --- .../modules/offer/default-offer.repository.ts | 25 +++++++++++-------- src/shared/modules/offer/offer.entity.ts | 4 +++ src/shared/modules/offer/offer.interface.ts | 4 +-- .../offer/offer.repository.interface.ts | 1 + 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/shared/modules/offer/default-offer.repository.ts b/src/shared/modules/offer/default-offer.repository.ts index 8e7b448..66ed408 100644 --- a/src/shared/modules/offer/default-offer.repository.ts +++ b/src/shared/modules/offer/default-offer.repository.ts @@ -11,9 +11,6 @@ export class DefaultOfferRepository implements OfferRepository { @inject(TYPES.Logger) private readonly logger: LoggerInterface, ) { } - /** - * Ищет оффер по публичному строковому id. - */ public async findById(id: string): Promise { this.logger.debug( `DefaultOfferRepository: Searching for offer by ID ${id}`, @@ -30,10 +27,6 @@ export class DefaultOfferRepository implements OfferRepository { return offer.save(); } - /** - * Важно: - * userId здесь — это Mongo _id пользователя, а не его публичный id. - */ public async findByUserId(userId: string): Promise { this.logger.debug( `DefaultOfferRepository: Searching offers by user ID ${userId}`, @@ -68,9 +61,6 @@ export class DefaultOfferRepository implements OfferRepository { .exec(); } - /** - * Удаляет оффер по публичному строковому id. - */ public async deleteById(id: string): Promise { this.logger.info( `DefaultOfferRepository: Deleting offer by ID ${id}`, @@ -80,4 +70,19 @@ export class DefaultOfferRepository implements OfferRepository { return result.deletedCount > 0; } + + public async updateStats( + id: string, + rating: number, + commentsCount: number, + ): Promise { + this.logger.debug( + `DefaultOfferRepository: Updating stats for offer ${id} (rating: ${rating}, comments: ${commentsCount})`, + ); + return OfferModel.findOneAndUpdate( + { id }, + { rating, commentsCount }, + { new: true }, + ).exec(); + } } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 9fd37f9..c1a10a2 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -117,6 +117,10 @@ export const offerSchema = new Schema( min: 1, max: 10, }, + commentsCount: { + type: Number, + default: 0, + }, }, { id: false, diff --git a/src/shared/modules/offer/offer.interface.ts b/src/shared/modules/offer/offer.interface.ts index 7413831..406c951 100644 --- a/src/shared/modules/offer/offer.interface.ts +++ b/src/shared/modules/offer/offer.interface.ts @@ -1,7 +1,6 @@ import { Types } from 'mongoose'; export type OfferType = 'apartment' | 'house' | 'room' | 'hotel'; - export type CityName = | 'Paris' | 'Cologne' @@ -34,11 +33,12 @@ export interface OfferInterface { user: Types.ObjectId; images: string[]; maxAdults: number; + commentsCount: number; } export type CreateOffer = Omit< OfferInterface, - 'id' | 'createdAt' | 'updatedAt' + 'id' | 'createdAt' | 'updatedAt' | 'commentsCount' >; export type CreateOfferInput = Omit; diff --git a/src/shared/modules/offer/offer.repository.interface.ts b/src/shared/modules/offer/offer.repository.interface.ts index 59ff576..1036833 100644 --- a/src/shared/modules/offer/offer.repository.interface.ts +++ b/src/shared/modules/offer/offer.repository.interface.ts @@ -8,4 +8,5 @@ export interface OfferRepository extends Repository { findAll(limit?: number): Promise; deleteById(id: string): Promise; create(dto: CreateOffer): Promise; + updateStats(id: string, rating: number, commentsCount: number): Promise; } From b4c29f435eaf5478185c865f88dd1ae6034afe12 Mon Sep 17 00:00:00 2001 From: Echenko Date: Mon, 10 Aug 2026 07:38:04 +0300 Subject: [PATCH 4/8] add: comment service --- src/shared/libs/container/container.ts | 5 +- src/shared/libs/container/container.types.ts | 1 + src/shared/modules/comment/comment.service.ts | 104 ++++++++++++++++++ src/shared/modules/comment/index.ts | 1 + .../modules/offer/default-offer.repository.ts | 14 ++- .../offer/offer.repository.interface.ts | 11 +- 6 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/shared/modules/comment/comment.service.ts diff --git a/src/shared/libs/container/container.ts b/src/shared/libs/container/container.ts index 4666ec0..9d6f88b 100644 --- a/src/shared/libs/container/container.ts +++ b/src/shared/libs/container/container.ts @@ -13,7 +13,7 @@ import { GenerateCommand } from './../../../cli/commands/index.js'; import { CliApplication } from '../../../cli/index.js'; -import { CommentRepository, DefaultCommentRepository } from '../../modules/comment/index.js'; +import { CommentRepository, CommentService, DefaultCommentRepository, DefaultCommentService } from '../../modules/comment/index.js'; const container = new Container(); @@ -39,4 +39,7 @@ container.bind(TYPES.VersionCommand).to(VersionCommand); container.bind(TYPES.ImportCommand).to(ImportCommand); container.bind(TYPES.GenerateCommand).to(GenerateCommand); +// Сервисы +container.bind(TYPES.CommentService).to(DefaultCommentService).inSingletonScope(); + export { container }; diff --git a/src/shared/libs/container/container.types.ts b/src/shared/libs/container/container.types.ts index 2606b40..3c36f4a 100644 --- a/src/shared/libs/container/container.types.ts +++ b/src/shared/libs/container/container.types.ts @@ -11,4 +11,5 @@ export const TYPES = { ImportCommand: Symbol.for('ImportCommand'), GenerateCommand: Symbol.for('GenerateCommand'), CommentRepository: Symbol.for('CommentRepository'), + CommentService: Symbol.for('CommentService'), }; diff --git a/src/shared/modules/comment/comment.service.ts b/src/shared/modules/comment/comment.service.ts new file mode 100644 index 0000000..ef154bb --- /dev/null +++ b/src/shared/modules/comment/comment.service.ts @@ -0,0 +1,104 @@ +import { injectable, inject } from 'inversify'; +import { Types } from 'mongoose'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { CommentRepository } from './comment.repository.interface.js'; +import { OfferRepository } from '../offer/offer.repository.interface.js'; +import { DocumentComment } from './comment.entity.js'; +import { CreateCommentInput } from './comment.interface.js'; + +export interface CommentService { + create( + publicOfferId: string, + dto: CreateCommentInput, + userId: string, + ): Promise; + findByOfferId(publicOfferId: string): Promise; + findById(id: string): Promise; + deleteById(id: string): Promise; +} + +@injectable() +export class DefaultCommentService implements CommentService { + constructor( + @inject(TYPES.Logger) private readonly logger: LoggerInterface, + @inject(TYPES.CommentRepository) private readonly commentRepository: CommentRepository, + @inject(TYPES.OfferRepository) private readonly offerRepository: OfferRepository, + ) {} + + public async create( + publicOfferId: string, + dto: CreateCommentInput, + userId: string, + ): Promise { + this.logger.info(`DefaultCommentService: Creating comment for offer ${publicOfferId}`); + + const offer = await this.offerRepository.findById(publicOfferId); + if (!offer) { + throw new Error(`Offer with id ${publicOfferId} not found`); + } + + const comment = await this.commentRepository.create({ + text: dto.text, + rating: dto.rating, + offer: offer._id, + user: new Types.ObjectId(userId), + }); + + await this.recalculateOfferStats(offer._id.toString()); + + return comment; + } + + public async findByOfferId(publicOfferId: string): Promise { + this.logger.debug(`DefaultCommentService: Finding comments for offer ${publicOfferId}`); + + const offer = await this.offerRepository.findById(publicOfferId); + if (!offer) { + throw new Error(`Offer with id ${publicOfferId} not found`); + } + + return this.commentRepository.findByOfferId(offer._id.toString()); + } + + public async findById(id: string): Promise { + this.logger.debug(`DefaultCommentService: Finding comment by id ${id}`); + return this.commentRepository.findById(id); + } + + public async deleteById(id: string): Promise { + this.logger.info(`DefaultCommentService: Deleting comment ${id}`); + + const comment = await this.commentRepository.findById(id); + if (!comment) { + return false; + } + + const offerInternalId = comment.offer.toString(); + + const result = await this.commentRepository.deleteById(id); + + if (result) { + await this.recalculateOfferStats(offerInternalId); + } + + return result; + } + + private async recalculateOfferStats(offerInternalId: string): Promise { + + const comments = await this.commentRepository.findByOfferId(offerInternalId); + + const rating = comments.length > 0 + ? comments.reduce((sum, c) => sum + c.rating, 0) / comments.length + : 0; + + const commentsCount = comments.length; + + this.logger.debug( + `DefaultCommentService: Recalculated offer ${offerInternalId}: rating=${rating.toFixed(2)}, comments=${commentsCount}`, + ); + + await this.offerRepository.updateStats(offerInternalId, rating, commentsCount); + } +} diff --git a/src/shared/modules/comment/index.ts b/src/shared/modules/comment/index.ts index 3e5d89c..4005a41 100644 --- a/src/shared/modules/comment/index.ts +++ b/src/shared/modules/comment/index.ts @@ -2,3 +2,4 @@ export * from './comment.entity.js'; export * from './comment.interface.js'; export * from './comment.repository.interface.js'; export * from './default-comment.repository.js'; +export * from './comment.service.js'; diff --git a/src/shared/modules/offer/default-offer.repository.ts b/src/shared/modules/offer/default-offer.repository.ts index 66ed408..970e546 100644 --- a/src/shared/modules/offer/default-offer.repository.ts +++ b/src/shared/modules/offer/default-offer.repository.ts @@ -1,4 +1,5 @@ import { injectable, inject } from 'inversify'; +import { Types } from 'mongoose'; import { TYPES } from '../../libs/container/container.types.js'; import { LoggerInterface } from '../../libs/logger/logger.interface.js'; import { OfferRepository } from './offer.repository.interface.js'; @@ -72,15 +73,20 @@ export class DefaultOfferRepository implements OfferRepository { } public async updateStats( - id: string, + internalId: string, rating: number, commentsCount: number, ): Promise { this.logger.debug( - `DefaultOfferRepository: Updating stats for offer ${id} (rating: ${rating}, comments: ${commentsCount})`, + `DefaultOfferRepository: Updating stats for offer ${internalId} (rating: ${rating}, comments: ${commentsCount})`, ); - return OfferModel.findOneAndUpdate( - { id }, + + if (!Types.ObjectId.isValid(internalId)) { + return null; + } + + return OfferModel.findByIdAndUpdate( + internalId, { rating, commentsCount }, { new: true }, ).exec(); diff --git a/src/shared/modules/offer/offer.repository.interface.ts b/src/shared/modules/offer/offer.repository.interface.ts index 1036833..72fd675 100644 --- a/src/shared/modules/offer/offer.repository.interface.ts +++ b/src/shared/modules/offer/offer.repository.interface.ts @@ -8,5 +8,14 @@ export interface OfferRepository extends Repository { findAll(limit?: number): Promise; deleteById(id: string): Promise; create(dto: CreateOffer): Promise; - updateStats(id: string, rating: number, commentsCount: number): Promise; + updateStats( + id: string, + rating: number, + commentsCount: number + ): Promise; + updateStats( + internalId: string, + rating: number, + commentsCount: number, + ): Promise; } From 1b9b19abb6afb77eb403291c87182b1b9bcc9259 Mon Sep 17 00:00:00 2001 From: Echenko Date: Mon, 10 Aug 2026 08:41:19 +0300 Subject: [PATCH 5/8] add: auth --- package-lock.json | 248 +++++++++++++++++- package.json | 4 +- src/shared/libs/container/container.ts | 6 + src/shared/libs/container/container.types.ts | 3 + src/shared/modules/auth/auth.service.ts | 42 +++ .../modules/offer/default-offer.repository.ts | 2 - .../offer/offer.repository.interface.ts | 5 - src/shared/modules/offer/offer.service.ts | 71 +++++ src/shared/modules/user/user.service.ts | 50 ++++ 9 files changed, 410 insertions(+), 21 deletions(-) create mode 100644 src/shared/modules/auth/auth.service.ts create mode 100644 src/shared/modules/offer/offer.service.ts create mode 100644 src/shared/modules/user/user.service.ts diff --git a/package-lock.json b/package-lock.json index 2ba9f4d..8e0e549 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,14 @@ "name": "six-cities", "version": "5.0.0", "dependencies": { - "bcrypt": "^6.0.0", + "bcrypt": "6.0.0", "chalk": "5.6.2", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", "dotenv": "17.4.2", "got": "15.1.0", "inversify": "^8.2.1", + "jsonwebtoken": "9.0.3", "mongoose": "9.8.0", "pino": "10.3.1", "reflect-metadata": "^0.2.2", @@ -25,6 +26,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", @@ -1184,6 +1186,22 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, "node_modules/@types/node": { "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", @@ -1630,6 +1648,11 @@ "node": ">=20.19.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -2123,6 +2146,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3760,6 +3791,27 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -3773,6 +3825,25 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz", @@ -3834,12 +3905,47 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3882,7 +3988,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -4108,8 +4213,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4941,6 +5045,25 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -4992,7 +5115,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -5838,8 +5960,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { "version": "18.0.0", @@ -6687,6 +6808,22 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "requires": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, "@types/node": { "version": "18.19.130", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", @@ -6983,6 +7120,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-7.3.1.tgz", "integrity": "sha512-h/C0qe6857pQhcSJHLfsR1uYGj98Ge3wKAD3Ed9KqH3wcVh+BM4Jq4xISD7vs9OPuT07n+q3QQVjslJ286j6ag==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -7316,6 +7458,14 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -8506,6 +8656,23 @@ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, + "jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "requires": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + } + }, "jsx-ast-utils": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz", @@ -8516,6 +8683,25 @@ "object.assign": "^4.1.3" } }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "requires": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, "kareem": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz", @@ -8565,12 +8751,47 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -8598,7 +8819,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -8733,8 +8953,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "natural-compare": { "version": "1.4.0", @@ -9310,6 +9529,11 @@ "tslib": "^2.1.0" } }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "safe-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", @@ -9345,7 +9569,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "requires": { "lru-cache": "^6.0.0" } @@ -9939,8 +10162,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { "version": "18.0.0", diff --git a/package.json b/package.json index 79629df..4540e0f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", "@typescript-eslint/eslint-plugin": "6.7.0", "@typescript-eslint/parser": "6.7.0", @@ -48,13 +49,14 @@ "npm": ">=8" }, "dependencies": { - "bcrypt": "^6.0.0", + "bcrypt": "6.0.0", "chalk": "5.6.2", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", "dotenv": "17.4.2", "got": "15.1.0", "inversify": "^8.2.1", + "jsonwebtoken": "9.0.3", "mongoose": "9.8.0", "pino": "10.3.1", "reflect-metadata": "^0.2.2", diff --git a/src/shared/libs/container/container.ts b/src/shared/libs/container/container.ts index 9d6f88b..15553b7 100644 --- a/src/shared/libs/container/container.ts +++ b/src/shared/libs/container/container.ts @@ -14,6 +14,9 @@ import { } from './../../../cli/commands/index.js'; import { CliApplication } from '../../../cli/index.js'; import { CommentRepository, CommentService, DefaultCommentRepository, DefaultCommentService } from '../../modules/comment/index.js'; +import { DefaultUserService, UserService } from '../../modules/user/user.service.js'; +import { DefaultOfferService, OfferService } from '../../modules/offer/offer.service.js'; +import { AuthService, DefaultAuthService } from '../../modules/auth/auth.service.js'; const container = new Container(); @@ -41,5 +44,8 @@ container.bind(TYPES.GenerateCommand).to(GenerateCommand); // Сервисы container.bind(TYPES.CommentService).to(DefaultCommentService).inSingletonScope(); +container.bind(TYPES.UserService).to(DefaultUserService).inSingletonScope(); +container.bind(TYPES.OfferService).to(DefaultOfferService).inSingletonScope(); +container.bind(TYPES.AuthService).to(DefaultAuthService).inSingletonScope(); export { container }; diff --git a/src/shared/libs/container/container.types.ts b/src/shared/libs/container/container.types.ts index 3c36f4a..c0a534e 100644 --- a/src/shared/libs/container/container.types.ts +++ b/src/shared/libs/container/container.types.ts @@ -12,4 +12,7 @@ export const TYPES = { GenerateCommand: Symbol.for('GenerateCommand'), CommentRepository: Symbol.for('CommentRepository'), CommentService: Symbol.for('CommentService'), + UserService: Symbol.for('UserService'), + OfferService: Symbol.for('OfferService'), + AuthService: Symbol.for('AuthService'), }; diff --git a/src/shared/modules/auth/auth.service.ts b/src/shared/modules/auth/auth.service.ts new file mode 100644 index 0000000..f9eae31 --- /dev/null +++ b/src/shared/modules/auth/auth.service.ts @@ -0,0 +1,42 @@ +import { injectable, inject } from 'inversify'; +import { compare } from 'bcrypt'; +import jwt from 'jsonwebtoken'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { UserRepository } from '../user/user.repository.interface.js'; +import { DocumentUser } from '../user/user.entity.js'; + +export interface AuthService { + login(email: string, password: string): Promise<{ token: string; user: DocumentUser }>; +} + +@injectable() +export class DefaultAuthService implements AuthService { + constructor( + @inject(TYPES.Logger) private readonly logger: LoggerInterface, + @inject(TYPES.UserRepository) private readonly userRepository: UserRepository, + ) {} + + public async login(email: string, password: string): Promise<{ token: string; user: DocumentUser }> { + this.logger.info(`DefaultAuthService: Login attempt for ${email}`); + + const user = await this.userRepository.findByEmailForAuth(email); + if (!user) { + throw new Error('Invalid email or password'); + } + + const isValid = await compare(password, user.password); + if (!isValid) { + throw new Error('Invalid email or password'); + } + + const secret = process.env.JWT_SECRET || 'default-secret-key-change-in-production'; + const token = jwt.sign( + { userId: user._id, email: user.email }, + secret, + { expiresIn: '7d' } + ); + + return { token, user }; + } +} diff --git a/src/shared/modules/offer/default-offer.repository.ts b/src/shared/modules/offer/default-offer.repository.ts index 970e546..c73bf8b 100644 --- a/src/shared/modules/offer/default-offer.repository.ts +++ b/src/shared/modules/offer/default-offer.repository.ts @@ -80,11 +80,9 @@ export class DefaultOfferRepository implements OfferRepository { this.logger.debug( `DefaultOfferRepository: Updating stats for offer ${internalId} (rating: ${rating}, comments: ${commentsCount})`, ); - if (!Types.ObjectId.isValid(internalId)) { return null; } - return OfferModel.findByIdAndUpdate( internalId, { rating, commentsCount }, diff --git a/src/shared/modules/offer/offer.repository.interface.ts b/src/shared/modules/offer/offer.repository.interface.ts index 72fd675..2ee1ce8 100644 --- a/src/shared/modules/offer/offer.repository.interface.ts +++ b/src/shared/modules/offer/offer.repository.interface.ts @@ -8,11 +8,6 @@ export interface OfferRepository extends Repository { findAll(limit?: number): Promise; deleteById(id: string): Promise; create(dto: CreateOffer): Promise; - updateStats( - id: string, - rating: number, - commentsCount: number - ): Promise; updateStats( internalId: string, rating: number, diff --git a/src/shared/modules/offer/offer.service.ts b/src/shared/modules/offer/offer.service.ts new file mode 100644 index 0000000..c9d2797 --- /dev/null +++ b/src/shared/modules/offer/offer.service.ts @@ -0,0 +1,71 @@ +import { injectable, inject } from 'inversify'; +import { Types } from 'mongoose'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { OfferRepository } from './offer.repository.interface.js'; +import { UserRepository } from '../user/user.repository.interface.js'; +import { DocumentOffer } from './offer.entity.js'; +import { CityName, CreateOfferInput } from './offer.interface.js'; + +export interface OfferService { + create(publicUserId: string, dto: CreateOfferInput): Promise; + findById(id: string): Promise; + findAll(limit?: number): Promise; + findByCity(city: CityName): Promise; + findByUserId(publicUserId: string): Promise; + deleteById(id: string): Promise; +} + +@injectable() +export class DefaultOfferService implements OfferService { + constructor( + @inject(TYPES.Logger) private readonly logger: LoggerInterface, + @inject(TYPES.OfferRepository) private readonly offerRepository: OfferRepository, + @inject(TYPES.UserRepository) private readonly userRepository: UserRepository, + ) {} + + public async create(publicUserId: string, dto: CreateOfferInput): Promise { + this.logger.info(`DefaultOfferService: Creating offer for user ${publicUserId}`); + + const user = await this.userRepository.findById(publicUserId); + if (!user) { + throw new Error(`User with id ${publicUserId} not found`); + } + + return this.offerRepository.create({ + ...dto, + user: user._id, + }); + } + + public async findById(id: string): Promise { + this.logger.debug(`DefaultOfferService: Finding offer by id ${id}`); + return this.offerRepository.findById(id); + } + + public async findAll(limit?: number): Promise { + this.logger.debug(`DefaultOfferService: Finding all offers (limit: ${limit})`); + return this.offerRepository.findAll(limit); + } + + public async findByCity(city: CityName): Promise { + this.logger.debug(`DefaultOfferService: Finding offers in city ${city}`); + return this.offerRepository.findByCity(city); + } + + public async findByUserId(publicUserId: string): Promise { + this.logger.debug(`DefaultOfferService: Finding offers for user ${publicUserId}`); + + const user = await this.userRepository.findById(publicUserId); + if (!user) { + throw new Error(`User with id ${publicUserId} not found`); + } + + return this.offerRepository.findByUserId(user._id.toString()); + } + + public async deleteById(id: string): Promise { + this.logger.info(`DefaultOfferService: Deleting offer ${id}`); + return this.offerRepository.deleteById(id); + } +} diff --git a/src/shared/modules/user/user.service.ts b/src/shared/modules/user/user.service.ts new file mode 100644 index 0000000..8048df8 --- /dev/null +++ b/src/shared/modules/user/user.service.ts @@ -0,0 +1,50 @@ +import { injectable, inject } from 'inversify'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { UserRepository } from './user.repository.interface.js'; +import { DocumentUser } from './user.entity.js'; +import { CreateUser } from './user.interface.js'; +import { hash } from 'bcrypt'; + +export interface UserService { + create(dto: CreateUser): Promise; + findById(id: string): Promise; + findByInternalId(internalId: string): Promise; + findByEmail(email: string): Promise; +} + +@injectable() +export class DefaultUserService implements UserService { + constructor( + @inject(TYPES.Logger) private readonly logger: LoggerInterface, + @inject(TYPES.UserRepository) private readonly userRepository: UserRepository, + ) { } + + public async create(dto: CreateUser): Promise { + this.logger.info('DefaultUserService: Creating new user'); + + // Проверяем, не существует ли уже пользователь с таким email + const existingUser = await this.userRepository.findByEmail(dto.email); + if (existingUser) { + throw new Error(`User with email ${dto.email} already exists`); + } + + const hashedPassword = await hash(dto.password, 10); + const userDto = { ...dto, password: hashedPassword }; + + return this.userRepository.create(userDto); + } + + public async findById(id: string): Promise { + this.logger.debug(`DefaultUserService: Finding user by id ${id}`); + return this.userRepository.findById(id); + } + + public async findByInternalId(internalId: string): Promise { + return this.userRepository.findByInternalId(internalId); + } + + public async findByEmail(email: string): Promise { + return this.userRepository.findByEmail(email); + } +} From 4387ea188ac79dabdd769e60731dbcd4a9937ae1 Mon Sep 17 00:00:00 2001 From: Echenko Date: Mon, 10 Aug 2026 21:46:08 +0300 Subject: [PATCH 6/8] ref: user, comment, offer --- src/shared/libs/container/container.types.ts | 10 ++++++++-- src/shared/modules/comment/comment.entity.ts | 5 +---- src/shared/modules/comment/comment.interface.ts | 5 +---- .../modules/comment/default-comment.repository.ts | 8 ++++++-- src/shared/modules/offer/index.ts | 1 + src/shared/modules/offer/offer.entity.ts | 8 ++------ src/shared/modules/offer/offer.interface.ts | 5 +---- src/shared/modules/offer/offer.service.ts | 1 - src/shared/modules/user/default-user.repository.ts | 6 ------ src/shared/modules/user/index.ts | 1 + src/shared/modules/user/user.entity.ts | 5 +---- src/shared/modules/user/user.interface.ts | 5 +---- 12 files changed, 23 insertions(+), 37 deletions(-) diff --git a/src/shared/libs/container/container.types.ts b/src/shared/libs/container/container.types.ts index c0a534e..27944d4 100644 --- a/src/shared/libs/container/container.types.ts +++ b/src/shared/libs/container/container.types.ts @@ -1,16 +1,22 @@ export const TYPES = { + // Общие зависимости) Logger: Symbol.for('Logger'), Config: Symbol.for('Config'), + // REST Application: Symbol.for('Application'), - CLIApplication: Symbol.for('CLIApplication'), + // База данных DatabaseClient: Symbol.for('DatabaseClient'), + // Репозитории UserRepository: Symbol.for('UserRepository'), OfferRepository: Symbol.for('OfferRepository'), + CommentRepository: Symbol.for('CommentRepository'), + // CLI + CLIApplication: Symbol.for('CLIApplication'), HelpCommand: Symbol.for('HelpCommand'), VersionCommand: Symbol.for('VersionCommand'), ImportCommand: Symbol.for('ImportCommand'), GenerateCommand: Symbol.for('GenerateCommand'), - CommentRepository: Symbol.for('CommentRepository'), + // Сервисы CommentService: Symbol.for('CommentService'), UserService: Symbol.for('UserService'), OfferService: Symbol.for('OfferService'), diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts index d5af9fc..dc8d8d3 100644 --- a/src/shared/modules/comment/comment.entity.ts +++ b/src/shared/modules/comment/comment.entity.ts @@ -53,7 +53,4 @@ const commentSchema: Schema = new Schema( }, ); -export const CommentModel: Model = model( - 'Comment', - commentSchema, -); +export const CommentModel: Model = model('Comment', commentSchema,); diff --git a/src/shared/modules/comment/comment.interface.ts b/src/shared/modules/comment/comment.interface.ts index 5e2bce4..cfc0e70 100644 --- a/src/shared/modules/comment/comment.interface.ts +++ b/src/shared/modules/comment/comment.interface.ts @@ -10,9 +10,6 @@ export interface CommentInterface { offer: Types.ObjectId; } -export type CreateComment = Omit< - CommentInterface, - 'id' | 'createdAt' | 'updatedAt' ->; +export type CreateComment = Omit; export type CreateCommentInput = Omit; diff --git a/src/shared/modules/comment/default-comment.repository.ts b/src/shared/modules/comment/default-comment.repository.ts index 9743048..fc87db8 100644 --- a/src/shared/modules/comment/default-comment.repository.ts +++ b/src/shared/modules/comment/default-comment.repository.ts @@ -9,7 +9,7 @@ import { CreateComment } from './comment.interface.js'; export class DefaultCommentRepository implements CommentRepository { constructor( @inject(TYPES.Logger) private readonly logger: LoggerInterface, - ) {} + ) { } public async create(dto: CreateComment): Promise { this.logger.info('DefaultCommentRepository: Creating new comment'); @@ -23,6 +23,7 @@ export class DefaultCommentRepository implements CommentRepository { ); return CommentModel.find({ offer: offerId }) .populate('user') + .populate('offer') .sort({ createdAt: -1 }) .exec(); } @@ -31,7 +32,10 @@ export class DefaultCommentRepository implements CommentRepository { this.logger.debug( `DefaultCommentRepository: Searching comment by ID ${id}`, ); - return CommentModel.findOne({ id }).populate('user').exec(); + return CommentModel.findOne({ id }) + .populate('user') + .populate('offer') + .exec(); } public async deleteById(id: string): Promise { diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index a0fd32a..a10eeed 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -2,3 +2,4 @@ export * from './offer.entity.js'; export * from './offer.interface.js'; export * from './offer.repository.interface.js'; export * from './default-offer.repository.js'; +export * from './offer.service.js'; diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index c1a10a2..761cfed 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -82,8 +82,7 @@ export const offerSchema = new Schema( }, rating: { type: Number, - required: true, - min: 1, + min: 0, max: 5, }, description: { @@ -139,7 +138,4 @@ export const offerSchema = new Schema( } ); -export const OfferModel: Model = model( - 'Offer', - offerSchema, -); +export const OfferModel: Model = model('Offer', offerSchema,); diff --git a/src/shared/modules/offer/offer.interface.ts b/src/shared/modules/offer/offer.interface.ts index 406c951..4b1cbbe 100644 --- a/src/shared/modules/offer/offer.interface.ts +++ b/src/shared/modules/offer/offer.interface.ts @@ -36,9 +36,6 @@ export interface OfferInterface { commentsCount: number; } -export type CreateOffer = Omit< - OfferInterface, - 'id' | 'createdAt' | 'updatedAt' | 'commentsCount' ->; +export type CreateOffer = Omit; export type CreateOfferInput = Omit; diff --git a/src/shared/modules/offer/offer.service.ts b/src/shared/modules/offer/offer.service.ts index c9d2797..2587607 100644 --- a/src/shared/modules/offer/offer.service.ts +++ b/src/shared/modules/offer/offer.service.ts @@ -1,5 +1,4 @@ import { injectable, inject } from 'inversify'; -import { Types } from 'mongoose'; import { TYPES } from '../../libs/container/container.types.js'; import { LoggerInterface } from '../../libs/logger/logger.interface.js'; import { OfferRepository } from './offer.repository.interface.js'; diff --git a/src/shared/modules/user/default-user.repository.ts b/src/shared/modules/user/default-user.repository.ts index cbb4bb2..d00480e 100644 --- a/src/shared/modules/user/default-user.repository.ts +++ b/src/shared/modules/user/default-user.repository.ts @@ -12,18 +12,12 @@ export class DefaultUserRepository implements UserRepository { @inject(TYPES.Logger) private readonly logger: LoggerInterface, ) {} - /** - * Ищем пользователя по публичному строковому id. - */ public async findById(id: string): Promise { this.logger.debug(`DefaultUserRepository: Searching for user by ID ${id}`); return UserModel.findOne({ id }).exec(); } - /** - * Ищем пользователя по внутреннему Mongo _id. - */ public async findByInternalId( internalId: string, ): Promise { diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 9adb86a..5bbf675 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -2,3 +2,4 @@ export * from './user.entity.js'; export * from './user.interface.js'; export * from './user.repository.interface.js'; export * from './default-user.repository.js'; +export * from './user.service.js'; diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index f20d670..99d43f2 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -62,7 +62,4 @@ const userSchema: Schema = new Schema( } ); -export const UserModel: Model = model( - 'User', - userSchema, -); +export const UserModel: Model = model('User', userSchema,); diff --git a/src/shared/modules/user/user.interface.ts b/src/shared/modules/user/user.interface.ts index 87ebbc6..1725032 100644 --- a/src/shared/modules/user/user.interface.ts +++ b/src/shared/modules/user/user.interface.ts @@ -11,7 +11,4 @@ export interface UserInterface { type: UserType; } -export type CreateUser = Omit< - UserInterface, - 'id' | 'createdAt' | 'updatedAt' ->; +export type CreateUser = Omit; From fff086b4859c256bbeb4c1f3dbf987ba2c1d2229 Mon Sep 17 00:00:00 2001 From: Echenko Date: Tue, 11 Aug 2026 21:58:37 +0300 Subject: [PATCH 7/8] install: express --- package-lock.json | 1494 +++++++++++++++++++++++++++++++++++++++++---- package.json | 5 + 2 files changed, 1396 insertions(+), 103 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e0e549..ee3e7a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,14 @@ "dependencies": { "bcrypt": "6.0.0", "chalk": "5.6.2", + "class-transformer": "0.5.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", "dotenv": "17.4.2", + "express": "5.2.1", + "express-async-handler": "1.2.0", "got": "15.1.0", + "http-status-codes": "2.3.0", "inversify": "^8.2.1", "jsonwebtoken": "9.0.3", "mongoose": "9.8.0", @@ -26,6 +30,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/express": "5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", "@typescript-eslint/eslint-plugin": "6.7.0", @@ -1157,6 +1162,25 @@ "@types/node": "*" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -1175,11 +1199,40 @@ "@types/convict": "*" } }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", @@ -1217,12 +1270,43 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "node_modules/@types/semver": { "version": "7.5.2", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -1425,6 +1509,18 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1618,6 +1714,41 @@ "node": ">= 18" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1676,6 +1807,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -1714,6 +1853,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1775,6 +1941,11 @@ "node": ">=8" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, "node_modules/clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -1955,6 +2126,26 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convict": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", @@ -1978,6 +2169,22 @@ "node": ">=6" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -2014,12 +2221,11 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2066,6 +2272,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2140,6 +2354,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2154,12 +2381,25 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -2226,6 +2466,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -2316,6 +2583,11 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2707,6 +2979,61 @@ "url": "https://github.com/bgub/eta?sponsor=1" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-async-handler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", + "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" + }, "node_modules/fast-copy": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz", @@ -2798,6 +3125,26 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2893,6 +3240,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2914,10 +3277,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -2968,19 +3333,40 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", @@ -3121,12 +3507,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3232,10 +3617,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -3258,6 +3642,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/header-range-parser": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.5.tgz", @@ -3284,6 +3679,30 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" + }, "node_modules/http-status-emojis": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz", @@ -3302,6 +3721,21 @@ "node": ">=10.19.0" } }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3367,8 +3801,7 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { "version": "1.0.5", @@ -3394,6 +3827,14 @@ "@inversifyjs/core": "14.0.0" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -3571,6 +4012,11 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4001,11 +4447,42 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4052,6 +4529,29 @@ "node": ">=16" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -4180,11 +4680,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -4211,9 +4706,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -4221,6 +4716,14 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-addon-api": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", @@ -4281,10 +4784,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true, + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4385,11 +4890,21 @@ "node": ">=14.0.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -4480,6 +4995,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4538,6 +5061,15 @@ "node": "14 || >=16.14" } }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4673,6 +5205,18 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -4691,6 +5235,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4727,6 +5286,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -5013,6 +5598,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5095,6 +5695,11 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -5125,6 +5730,54 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5159,14 +5812,68 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5283,6 +5990,14 @@ "node": ">= 10.x" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/steno": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", @@ -5542,6 +6257,14 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -5673,6 +6396,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -5732,6 +6484,14 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5777,6 +6537,14 @@ "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -5945,8 +6713,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/y18n": { "version": "5.0.8", @@ -6779,6 +7546,25 @@ "@types/node": "*" } }, + "@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/convict": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/@types/convict/-/convict-6.1.6.tgz", @@ -6797,11 +7583,40 @@ "@types/convict": "*" } }, + "@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "@types/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==" }, + "@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, "@types/json-schema": { "version": "7.0.13", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", @@ -6839,12 +7654,43 @@ "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", "dev": true }, + "@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, "@types/semver": { "version": "7.5.2", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", "dev": true }, + "@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "requires": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "@types/webidl-conversions": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", @@ -6958,6 +7804,15 @@ "eslint-visitor-keys": "^3.4.1" } }, + "accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "requires": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + } + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -7096,6 +7951,29 @@ "node-gyp-build": "^4.8.4" } }, + "body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "requires": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "dependencies": { + "content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -7136,6 +8014,11 @@ "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==" }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, "cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -7165,6 +8048,24 @@ "get-intrinsic": "^1.0.2" } }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7196,6 +8097,11 @@ "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true }, + "class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, "clean-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", @@ -7324,6 +8230,16 @@ } } }, + "content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==" + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, "convict": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.5.tgz", @@ -7341,6 +8257,16 @@ "validator": "^13.6.0" } }, + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -7371,12 +8297,11 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decompress-response": { @@ -7403,6 +8328,11 @@ "object-keys": "^1.1.1" } }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -7452,6 +8382,16 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7466,12 +8406,22 @@ "safe-buffer": "^5.0.1" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + }, "end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -7532,6 +8482,24 @@ "which-typed-array": "^1.1.9" } }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "requires": { + "es-errors": "^1.3.0" + } + }, "es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -7603,6 +8571,11 @@ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7879,6 +8852,51 @@ "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", "dev": true }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "requires": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + } + }, + "express-async-handler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/express-async-handler/-/express-async-handler-1.2.0.tgz", + "integrity": "sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w==" + }, "fast-copy": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz", @@ -7960,6 +8978,19 @@ "to-regex-range": "^5.0.1" } }, + "finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "requires": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + } + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -8030,6 +9061,16 @@ "signal-exit": "^4.0.1" } }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8044,10 +9085,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { "version": "1.1.5", @@ -8080,14 +9120,29 @@ "dev": true }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", - "dev": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" } }, "get-stream": { @@ -8184,13 +9239,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "got": { "version": "15.1.0", @@ -8264,10 +9315,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.0", @@ -8278,6 +9328,14 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "requires": { + "function-bind": "^1.1.2" + } + }, "header-range-parser": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.5.tgz", @@ -8301,6 +9359,23 @@ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" }, + "http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "requires": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + } + }, + "http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" + }, "http-status-emojis": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz", @@ -8316,6 +9391,14 @@ "resolve-alpn": "^1.2.0" } }, + "iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -8363,8 +9446,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { "version": "1.0.5", @@ -8387,6 +9469,11 @@ "@inversifyjs/core": "14.0.0" } }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, "is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -8504,6 +9591,11 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8829,11 +9921,26 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, + "media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==" + }, "memory-pager": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" }, + "merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==" + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8862,6 +9969,19 @@ "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", "dev": true }, + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==" + }, + "mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "requires": { + "mime-db": "^1.54.0" + } + }, "mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -8925,13 +10045,6 @@ "mquery": "6.0.0", "ms": "2.1.3", "sift": "17.1.3" - }, - "dependencies": { - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - } } }, "mpath": { @@ -8951,9 +10064,9 @@ "dev": true }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "natural-compare": { "version": "1.4.0", @@ -8961,6 +10074,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==" + }, "node-addon-api": { "version": "8.9.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", @@ -9003,10 +10121,9 @@ "dev": true }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "dev": true + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "object-keys": { "version": "1.1.1", @@ -9074,11 +10191,18 @@ "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "requires": { "wrappy": "1" } @@ -9142,6 +10266,11 @@ "lines-and-columns": "^1.1.6" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9184,6 +10313,11 @@ } } }, + "path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==" + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9284,6 +10418,15 @@ "react-is": "^16.13.1" } }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, "pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -9299,6 +10442,15 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, + "qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "requires": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9315,6 +10467,22 @@ "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==" }, + "range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==" + }, + "raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "requires": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -9511,6 +10679,18 @@ "glob": "^10.2.5" } }, + "router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "requires": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + } + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9559,6 +10739,11 @@ "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -9573,6 +10758,40 @@ "lru-cache": "^6.0.0" } }, + "send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "requires": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + } + }, + "serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "requires": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9595,14 +10814,47 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "sift": { @@ -9695,6 +10947,11 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, + "statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==" + }, "steno": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", @@ -9880,6 +11137,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, "totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -9958,6 +11220,23 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "requires": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "dependencies": { + "content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==" + } + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -9998,6 +11277,11 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -10033,6 +11317,11 @@ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==" }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, "webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -10150,8 +11439,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "y18n": { "version": "5.0.8", diff --git a/package.json b/package.json index 4540e0f..b6f6ade 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/express": "5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", "@typescript-eslint/eslint-plugin": "6.7.0", @@ -51,10 +52,14 @@ "dependencies": { "bcrypt": "6.0.0", "chalk": "5.6.2", + "class-transformer": "0.5.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", "dotenv": "17.4.2", + "express": "5.2.1", + "express-async-handler": "1.2.0", "got": "15.1.0", + "http-status-codes": "2.3.0", "inversify": "^8.2.1", "jsonwebtoken": "9.0.3", "mongoose": "9.8.0", From e71946aaf8167a5d4b1bba6a127e545a4b033d9f Mon Sep 17 00:00:00 2001 From: Echenko Date: Thu, 13 Aug 2026 07:29:17 +0300 Subject: [PATCH 8/8] add: auth controls & speck --- .gitignore | 1 - package-lock.json | 49 +- package.json | 2 + specification/specification.yml | 929 ++++++++++++++++++ src/rest/rest.application.ts | 74 +- src/shared/libs/container/container.ts | 4 + src/shared/libs/container/container.types.ts | 2 + src/shared/libs/controller/base.controller.ts | 59 ++ src/shared/libs/controller/index.ts | 1 + src/shared/modules/auth/auth.controller.ts | 48 + 10 files changed, 1153 insertions(+), 16 deletions(-) create mode 100644 specification/specification.yml create mode 100644 src/shared/libs/controller/base.controller.ts create mode 100644 src/shared/libs/controller/index.ts create mode 100644 src/shared/modules/auth/auth.controller.ts diff --git a/.gitignore b/.gitignore index 054c943..0acfabd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,5 @@ coverage *.log logs/* *.env -*.yml !logs/.gitkeep diff --git a/package-lock.json b/package-lock.json index ee3e7a6..e16e99c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "class-transformer": "0.5.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", + "cors": "2.8.6", "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", @@ -30,6 +31,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/cors": "2.8.19", "@types/express": "5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", @@ -1199,6 +1201,15 @@ "@types/convict": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -2185,6 +2196,22 @@ "node": ">=6.6.0" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4778,7 +4805,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7583,6 +7609,15 @@ "@types/convict": "*" } }, + "@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -8267,6 +8302,15 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==" }, + "cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -10117,8 +10161,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-inspect": { "version": "1.13.4", diff --git a/package.json b/package.json index b6f6ade..c7bbc60 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@types/bcrypt": "^6.0.0", "@types/convict": "^6.1.6", "@types/convict-format-with-validator": "6.0.6", + "@types/cors": "2.8.19", "@types/express": "5.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^18.19.130", @@ -55,6 +56,7 @@ "class-transformer": "0.5.1", "convict": "6.2.5", "convict-format-with-validator": "6.2.0", + "cors": "2.8.6", "dotenv": "17.4.2", "express": "5.2.1", "express-async-handler": "1.2.0", diff --git a/specification/specification.yml b/specification/specification.yml new file mode 100644 index 0000000..b6b975d --- /dev/null +++ b/specification/specification.yml @@ -0,0 +1,929 @@ +openapi: 3.0.3 +info: + title: Real Estate Rental API + description: | + REST API для сервиса аренды недвижимости. + Спецификация построена на основе сущностей: + - User; + - Offer; + - Comment. + Основные ресурсы: + - пользователи; + - предложения; + - комментарии; + - аутентификация. + В качестве публичных идентификаторов ресурсов используются строковые id, + сгенерированные на сервере. Внутренний MongoDB _id наружу не отдаётся. + version: 1.0.0 +servers: + - url: http://localhost:3000 + description: Local development server +tags: + - name: Users + description: Операции с пользователями + - name: Offers + description: Операции с предложениями + - name: Comments + description: Операции с комментариями + - name: Auth + description: Аутентификация +paths: + /users: + post: + tags: + - Users + summary: Создание пользователя + description: | + Создаёт нового пользователя. + Используется как регистрация. + Поля id, createdAt и updatedAt генерируются сервером автоматически. + operationId: createUser + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUser' + responses: + '201': + description: Пользователь успешно создан + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + description: Ошибка валидации запроса + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '409': + description: Пользователь с таким email уже существует + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /users/{userId}: + get: + tags: + - Users + summary: Получение пользователя по ID + description: Возвращает данные пользователя по публичному идентификатору. + operationId: getUserById + parameters: + - name: userId + in: path + required: true + description: Публичный идентификатор пользователя + schema: + type: string + format: uuid + responses: + '200': + description: Пользователь успешно найден + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: Пользователь не найден + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /users/{userId}/offers: + get: + tags: + - Offers + summary: Получение предложений пользователя + description: Возвращает список предложений, созданных конкретным пользователем. + operationId: getUserOffers + parameters: + - name: userId + in: path + required: true + description: Публичный идентификатор пользователя + schema: + type: string + format: uuid + - name: limit + in: query + required: false + description: Максимальное количество возвращаемых предложений + schema: + type: integer + minimum: 1 + maximum: 100 + default: 60 + responses: + '200': + description: Список предложений пользователя + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Offer' + '404': + description: Пользователь не найден + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /offers: + get: + tags: + - Offers + summary: Получение списка предложений + description: | + Возвращает список предложений. + Поддерживает фильтрацию по городу и ограничение количества результатов. + operationId: getOffers + parameters: + - name: city + in: query + required: false + description: Название города + schema: + $ref: '#/components/schemas/CityName' + - name: limit + in: query + required: false + description: Максимальное количество возвращаемых предложений + schema: + type: integer + minimum: 1 + maximum: 100 + default: 60 + responses: + '200': + description: Список предложений + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Offer' + post: + tags: + - Offers + summary: Создание предложения + description: | + Создаёт новое предложение. + Требует авторизации пользователя. + Поля id, createdAt и updatedAt генерируются сервером автоматически. + Поле user не передаётся клиентом и определяется из токена авторизации. + operationId: createOffer + security: + - bearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateOfferInput' + responses: + '201': + description: Предложение успешно создано + content: + application/json: + schema: + $ref: '#/components/schemas/Offer' + '400': + description: Ошибка валидации запроса + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /offers/{offerId}: + get: + tags: + - Offers + summary: Получение предложения по ID + description: Возвращает подробную информацию о предложении. + operationId: getOfferById + parameters: + - name: offerId + in: path + required: true + description: Публичный идентификатор предложения + schema: + type: string + format: uuid + responses: + '200': + description: Предложение успешно найдено + content: + application/json: + schema: + $ref: '#/components/schemas/Offer' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + delete: + tags: + - Offers + summary: Удаление предложения + description: | + Удаляет предложение по публичному идентификатору. + Требует авторизации пользователя. + operationId: deleteOffer + security: + - bearerAuth: [] + parameters: + - name: offerId + in: path + required: true + description: Публичный идентификатор предложения + schema: + type: string + format: uuid + responses: + '204': + description: Предложение успешно удалено + '401': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /offers/{offerId}/comments: + get: + tags: + - Comments + summary: Получение комментариев предложения + description: Возвращает список комментариев, оставленных к предложению. + operationId: getOfferComments + parameters: + - name: offerId + in: path + required: true + description: Публичный идентификатор предложения + schema: + type: string + format: uuid + - name: limit + in: query + required: false + description: Максимальное количество возвращаемых комментариев + schema: + type: integer + minimum: 1 + maximum: 50 + default: 50 + responses: + '200': + description: Список комментариев предложения + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Comment' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + post: + tags: + - Comments + summary: Создание комментария + description: | + Создаёт новый комментарий к предложению. + Требует авторизации пользователя. + Поля id, createdAt и updatedAt генерируются сервером автоматически. + Поле user не передаётся клиентом и определяется из токена авторизации. + operationId: createComment + security: + - bearerAuth: [] + parameters: + - name: offerId + in: path + required: true + description: Публичный идентификатор предложения + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateCommentInput' + responses: + '201': + description: Комментарий успешно создан + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '400': + description: Ошибка валидации запроса + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Предложение не найдено + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /offers/{offerId}/comments/{commentId}: + delete: + tags: + - Comments + summary: Удаление комментария + description: | + Удаляет комментарий по публичному идентификатору. + Требует авторизации пользователя. + Удалить может только автор комментария. + operationId: deleteComment + security: + - bearerAuth: [] + parameters: + - name: offerId + in: path + required: true + description: Публичный идентификатор предложения + schema: + type: string + format: uuid + - name: commentId + in: path + required: true + description: Публичный идентификатор комментария + schema: + type: string + format: uuid + responses: + '204': + description: Комментарий успешно удалён + '401': + description: Пользователь не авторизован + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Пользователь не является автором комментария + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: Комментарий не найден + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /auth/login: + post: + tags: + - Auth + summary: Вход в систему + description: | + Аутентифицирует пользователя по email и паролю. + В случае успеха возвращает токен и данные пользователя. + operationId: login + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LoginRequest' + responses: + '200': + description: Успешная аутентификация + content: + application/json: + schema: + $ref: '#/components/schemas/AuthResponse' + '400': + description: Ошибка валидации запроса + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Неверный email или пароль + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + UserType: + type: string + enum: + - regular + - pro + OfferType: + type: string + enum: + - apartment + - house + - room + - hotel + CityName: + type: string + enum: + - Paris + - Cologne + - Brussels + - Amsterdam + - Hamburg + - Dusseldorf + User: + type: object + required: + - id + - name + - email + - avatarUrl + - type + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + readOnly: true + description: Публичный идентификатор пользователя + example: 3f7b9f2e-8d4c-4b1a-9c6e-2f8a7d6b5c4d + name: + type: string + description: Имя пользователя + minLength: 1 + maxLength: 50 + example: Ivan + email: + type: string + format: email + description: Email пользователя + example: ivan@example.com + avatarUrl: + type: string + description: Ссылка на аватар пользователя + example: /avatars/ivan.png + type: + $ref: '#/components/schemas/UserType' + createdAt: + type: string + format: date-time + readOnly: true + description: Дата и время создания пользователя + updatedAt: + type: string + format: date-time + readOnly: true + description: Дата и время обновления пользователя + CreateUser: + type: object + required: + - name + - email + - password + properties: + name: + type: string + description: Имя пользователя + minLength: 1 + maxLength: 50 + example: Ivan + email: + type: string + format: email + description: Email пользователя + example: ivan@example.com + password: + type: string + format: password + description: Пароль пользователя + example: password123 + avatarUrl: + type: string + description: Ссылка на аватар пользователя + example: /avatars/ivan.png + type: + $ref: '#/components/schemas/UserType' + LoginRequest: + type: object + required: + - email + - password + properties: + email: + type: string + format: email + description: Email пользователя + example: ivan@example.com + password: + type: string + format: password + description: Пароль пользователя + example: password123 + AuthResponse: + type: object + required: + - token + - user + properties: + token: + type: string + description: JWT-токен доступа + example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 + user: + $ref: '#/components/schemas/User' + Offer: + type: object + required: + - id + - title + - type + - price + - previewImage + - cityName + - cityLatitude + - cityLongitude + - cityZoom + - offerLatitude + - offerLongitude + - offerZoom + - isFavorite + - isPremium + - rating + - description + - bedrooms + - offerGoods + - user + - images + - maxAdults + - commentsCount + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + readOnly: true + description: Публичный идентификатор предложения + example: 7c9e6679-7425-40de-944b-e07fc1f90ae7 + title: + type: string + description: Название предложения + minLength: 10 + maxLength: 100 + example: Beautiful apartment in Paris + type: + $ref: '#/components/schemas/OfferType' + price: + type: number + description: Стоимость аренды + minimum: 100 + maximum: 100000 + example: 1200 + previewImage: + type: string + description: Ссылка на превью изображение + example: /images/offer-preview.jpg + cityName: + $ref: '#/components/schemas/CityName' + cityLatitude: + type: number + format: double + description: Широта города + example: 48.8566 + cityLongitude: + type: number + format: double + description: Долгота города + example: 2.3522 + cityZoom: + type: number + description: Zoom карты города + example: 12 + offerLatitude: + type: number + format: double + description: Широта предложения + example: 48.8566 + offerLongitude: + type: number + format: double + description: Долгота предложения + example: 2.3522 + offerZoom: + type: number + description: Zoom карты предложения + example: 16 + isFavorite: + type: boolean + description: Флаг избранного + example: false + isPremium: + type: boolean + description: Флаг премиум-предложения + example: true + rating: + type: number + format: float + description: Рейтинг предложения + minimum: 1 + maximum: 5 + example: 4.5 + description: + type: string + description: Описание предложения + minLength: 20 + maxLength: 1024 + example: A cozy apartment located in the center of Paris. + bedrooms: + type: integer + description: Количество спален + minimum: 1 + maximum: 8 + example: 2 + offerGoods: + type: array + description: Список удобств + items: + type: string + example: + - Wi-Fi + - Kitchen + - Washing machine + user: + $ref: '#/components/schemas/User' + images: + type: array + description: Список изображений предложения + items: + type: string + example: + - /images/offer-1.jpg + - /images/offer-2.jpg + - /images/offer-3.jpg + maxAdults: + type: integer + description: Максимальное количество гостей + minimum: 1 + maximum: 10 + example: 4 + commentsCount: + type: integer + description: Количество комментариев к предложению + minimum: 0 + example: 5 + createdAt: + type: string + format: date-time + readOnly: true + description: Дата и время создания предложения + updatedAt: + type: string + format: date-time + readOnly: true + description: Дата и время обновления предложения + CreateOfferInput: + type: object + description: | + Данные для создания предложения от клиента. + Поле user не передаётся клиентом и определяется на сервере + из авторизованного пользователя. + Поля id, createdAt, updatedAt и commentsCount генерируются сервером автоматически. + required: + - title + - type + - price + - previewImage + - cityName + - cityLatitude + - cityLongitude + - cityZoom + - offerLatitude + - offerLongitude + - offerZoom + - rating + - description + - bedrooms + - offerGoods + - images + - maxAdults + properties: + title: + type: string + description: Название предложения + minLength: 10 + maxLength: 100 + example: Beautiful apartment in Paris + type: + $ref: '#/components/schemas/OfferType' + price: + type: number + description: Стоимость аренды + minimum: 100 + maximum: 100000 + example: 1200 + previewImage: + type: string + description: Ссылка на превью изображение + example: /images/offer-preview.jpg + cityName: + $ref: '#/components/schemas/CityName' + cityLatitude: + type: number + format: double + description: Широта города + example: 48.8566 + cityLongitude: + type: number + format: double + description: Долгота города + example: 2.3522 + cityZoom: + type: number + description: Zoom карты города + example: 12 + offerLatitude: + type: number + format: double + description: Широта предложения + example: 48.8566 + offerLongitude: + type: number + format: double + description: Долгота предложения + example: 2.3522 + offerZoom: + type: number + description: Zoom карты предложения + example: 16 + isFavorite: + type: boolean + description: Флаг избранного + default: false + example: false + isPremium: + type: boolean + description: Флаг премиум-предложения + default: false + example: false + rating: + type: number + format: float + description: Рейтинг предложения + minimum: 1 + maximum: 5 + example: 4.5 + description: + type: string + description: Описание предложения + minLength: 20 + maxLength: 1024 + example: A cozy apartment located in the center of Paris. + bedrooms: + type: integer + description: Количество спален + minimum: 1 + maximum: 8 + example: 2 + offerGoods: + type: array + description: Список удобств + items: + type: string + example: + - Wi-Fi + - Kitchen + - Washing machine + images: + type: array + description: Список изображений предложения + items: + type: string + example: + - /images/offer-1.jpg + - /images/offer-2.jpg + - /images/offer-3.jpg + maxAdults: + type: integer + description: Максимальное количество гостей + minimum: 1 + maximum: 10 + example: 4 + Comment: + type: object + description: Комментарий к предложению. + required: + - id + - text + - rating + - user + - offer + - createdAt + - updatedAt + properties: + id: + type: string + format: uuid + readOnly: true + description: Публичный идентификатор комментария + example: 9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d + text: + type: string + description: Текст комментария + minLength: 5 + maxLength: 1024 + example: Great place, highly recommend! + rating: + type: number + format: float + description: Оценка предложения от автора комментария + minimum: 1 + maximum: 5 + example: 4.0 + user: + $ref: '#/components/schemas/User' + offer: + $ref: '#/components/schemas/Offer' + createdAt: + type: string + format: date-time + readOnly: true + description: Дата и время создания комментария + updatedAt: + type: string + format: date-time + readOnly: true + description: Дата и время обновления комментария + CreateCommentInput: + type: object + description: | + Данные для создания комментария от клиента. + Поле user не передаётся клиентом и определяется на сервере + из авторизованного пользователя. + Поля id, createdAt и updatedAt генерируются сервером автоматически. + required: + - text + - rating + properties: + text: + type: string + description: Текст комментария + minLength: 5 + maxLength: 1024 + example: Great place, highly recommend! + rating: + type: number + format: float + description: Оценка предложения + minimum: 1 + maximum: 5 + example: 4.0 + Error: + type: object + required: + - statusCode + - message + properties: + statusCode: + type: integer + description: HTTP-статус ошибки + example: 404 + error: + type: string + description: Текстовое название ошибки + example: Not Found + message: + type: string + description: Сообщение об ошибке + example: Entity not found diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index d7de1a9..c1df8df 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -1,26 +1,29 @@ +import express, { Express } from 'express'; +import cors from 'cors'; +import { injectable, inject } from 'inversify'; import { LoggerInterface } from '../shared/libs/logger/logger.interface.js'; import { RestConfig } from './../shared/libs/config/index.js'; import { DatabaseClientInterface } from './../shared/libs/database/index.js'; -// import { UserRepository } from '../shared/modules/user/user.repository.interface.js'; -// import { OfferRepository } from '../shared/modules/offer/offer.repository.interface.js'; -import { injectable, inject } from 'inversify'; import { TYPES } from '../shared/libs/container/container.types.js'; -// import { Types } from 'mongoose'; -// import * as bcrypt from 'bcrypt'; +import { AuthController } from './../shared/modules/auth/auth.controller.js'; @injectable() export class RestApplication { + private readonly app: Express; + constructor( @inject(TYPES.Logger) private readonly logger: LoggerInterface, @inject(TYPES.Config) private readonly config: RestConfig, @inject(TYPES.DatabaseClient) private readonly databaseClient: DatabaseClientInterface, - // @inject(TYPES.UserRepository) private readonly userRepository: UserRepository, - // @inject(TYPES.OfferRepository) private readonly offerRepository: OfferRepository - ) { } + @inject(TYPES.AuthController) private readonly authController: AuthController, + ) { + // Создаём Express-приложение сразу в конструкторе + this.app = express(); + } public async init(): Promise { - const port = this.config.get('port'); - this.logger.info(`RestApplication: Rest application started on port ${port}`); + const port = this.config.get('port') || 3000; + this.logger.info(`RestApplication: Initializing REST application on port ${port}`); // 1. Подключение к БД try { @@ -31,8 +34,56 @@ export class RestApplication { throw error; } - // 2. Настройка graceful shutdown + // 2. Настройка middleware + this.initMiddleware(); + + // 3. Подключение роутов + this.initRoutes(); + + // 4. Настройка graceful shutdown this.setupGracefulShutdown(); + + // 5. Запуск сервера + this.app.listen(port, () => { + this.logger.info(`RestApplication: Server started on http://localhost:${port}`); + }); + } + + /** + * Инициализация глобальных middleware + */ + private initMiddleware(): void { + // CORS — чтобы фронтенд с другого порта мог обращаться к API + this.app.use(cors()); + + // Парсинг JSON-тела запросов (обязательно для POST/PUT) + this.app.use(express.json()); + + // Парсинг URL-encoded данных (для HTML-форм) + this.app.use(express.urlencoded({ extended: true })); + + // Простое логирование всех входящих запросов + this.app.use((req, _res, next) => { + this.logger.info(`→ ${req.method} ${req.url}`); + next(); + }); + + this.logger.info('RestApplication: Middleware initialized.'); + } + + /** + * Подключение роутов контроллеров + */ + private initRoutes(): void { + // Все роуты authController будут доступны по префиксу /auth + // Итоговый путь: POST /auth/login + this.app.use('/auth', this.authController.getRouter()); + + // Здесь позже подключим другие контроллеры: + // this.app.use('/users', this.userController.getRouter()); + // this.app.use('/offers', this.offerController.getRouter()); + + this.logger.info('RestApplication: Routes initialized.'); } private setupGracefulShutdown(): void { @@ -41,7 +92,6 @@ export class RestApplication { try { await this.databaseClient.disconnect(); this.logger.info('RestApplication: Graceful shutdown completed.'); - process.exitCode = 0; } catch (error) { this.logger.error(error as Error, 'RestApplication: Error during shutdown'); diff --git a/src/shared/libs/container/container.ts b/src/shared/libs/container/container.ts index 15553b7..a9ced03 100644 --- a/src/shared/libs/container/container.ts +++ b/src/shared/libs/container/container.ts @@ -17,6 +17,7 @@ import { CommentRepository, CommentService, DefaultCommentRepository, DefaultCom import { DefaultUserService, UserService } from '../../modules/user/user.service.js'; import { DefaultOfferService, OfferService } from '../../modules/offer/offer.service.js'; import { AuthService, DefaultAuthService } from '../../modules/auth/auth.service.js'; +import { AuthController } from '../../modules/auth/auth.controller.js'; const container = new Container(); @@ -48,4 +49,7 @@ container.bind(TYPES.UserService).to(DefaultUserService).inSingleto container.bind(TYPES.OfferService).to(DefaultOfferService).inSingletonScope(); container.bind(TYPES.AuthService).to(DefaultAuthService).inSingletonScope(); +// Контроллеры +container.bind(TYPES.AuthController).to(AuthController).inSingletonScope(); + export { container }; diff --git a/src/shared/libs/container/container.types.ts b/src/shared/libs/container/container.types.ts index 27944d4..68d5d94 100644 --- a/src/shared/libs/container/container.types.ts +++ b/src/shared/libs/container/container.types.ts @@ -21,4 +21,6 @@ export const TYPES = { UserService: Symbol.for('UserService'), OfferService: Symbol.for('OfferService'), AuthService: Symbol.for('AuthService'), + // Контроллеры + AuthController: Symbol.for('AuthController'), }; diff --git a/src/shared/libs/controller/base.controller.ts b/src/shared/libs/controller/base.controller.ts new file mode 100644 index 0000000..f1243b9 --- /dev/null +++ b/src/shared/libs/controller/base.controller.ts @@ -0,0 +1,59 @@ +import { Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'inversify'; +import { TYPES } from '../container/container.types.js'; +import { LoggerInterface } from '../logger/logger.interface.js'; + +@injectable() +export abstract class BaseController { + private readonly DEFAULT_CONTENT_TYPE = 'application/json'; + + constructor( + @inject(TYPES.Logger) protected readonly logger: LoggerInterface, + ) {} + + public send(res: Response, statusCode: number, data: T): void { + res + .status(statusCode) + .type(this.DEFAULT_CONTENT_TYPE) + .json(data); + } + + public ok(res: Response, data: T): void { + this.send(res, StatusCodes.OK, data); + } + + public created(res: Response, data: T): void { + this.send(res, StatusCodes.CREATED, data); + } + + public noContent(res: Response): void { + res + .status(StatusCodes.NO_CONTENT) + .send(); + } + + public badRequest(res: Response, message: string): void { + this.send(res, StatusCodes.BAD_REQUEST, { message }); + } + + public unauthorized(res: Response, message: string): void { + this.send(res, StatusCodes.UNAUTHORIZED, { message }); + } + + public forbidden(res: Response, message: string): void { + this.send(res, StatusCodes.FORBIDDEN, { message }); + } + + public notFound(res: Response, message: string): void { + this.send(res, StatusCodes.NOT_FOUND, { message }); + } + + public conflict(res: Response, message: string): void { + this.send(res, StatusCodes.CONFLICT, { message }); + } + + public internalServerError(res: Response, message: string): void { + this.send(res, StatusCodes.INTERNAL_SERVER_ERROR, { message }); + } +} diff --git a/src/shared/libs/controller/index.ts b/src/shared/libs/controller/index.ts new file mode 100644 index 0000000..a0a60c9 --- /dev/null +++ b/src/shared/libs/controller/index.ts @@ -0,0 +1 @@ +export * from './base.controller.js'; diff --git a/src/shared/modules/auth/auth.controller.ts b/src/shared/modules/auth/auth.controller.ts new file mode 100644 index 0000000..c5088ca --- /dev/null +++ b/src/shared/modules/auth/auth.controller.ts @@ -0,0 +1,48 @@ +import { Router, Request, Response } from 'express'; +import { inject, injectable } from 'inversify'; +import asyncHandler from 'express-async-handler'; +import { BaseController } from '../../libs/controller/index.js'; +import { TYPES } from '../../libs/container/container.types.js'; +import { LoggerInterface } from '../../libs/logger/logger.interface.js'; +import { AuthService } from './auth.service.js'; + +@injectable() +export class AuthController extends BaseController { + private readonly router: Router; + + constructor( + @inject(TYPES.Logger) protected override readonly logger: LoggerInterface, + @inject(TYPES.AuthService) private readonly authService: AuthService, + ) { + super(logger); + this.router = Router(); + this.initRoutes(); + } + + private initRoutes(): void { + this.router.post( + '/login', + asyncHandler(async (req: Request, res: Response) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + this.badRequest(res, 'Email and password are required'); + return; + } + + const { token, user } = await this.authService.login(email, password); + + this.ok(res, { token, user }); + } catch (error) { + this.logger.warn(`AuthController: Login failed for ${req.body.email}`); + this.unauthorized(res, 'Invalid email or password'); + } + }), + ); + } + + public getRouter(): Router { + return this.router; + } +}