diff --git a/.env-example b/.env-example index 669ca99..5d2969c 100644 --- a/.env-example +++ b/.env-example @@ -10,3 +10,5 @@ DB_PASSWORD=password DB_NAME=six-cities-2 DB_PORT=27017 UPLOAD_DIRECTORY=upload + +JWT_SECRET=secret diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 0000000..778a665 --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,7 @@ +import { TokenPayload } from './src/shared/modules/auth/index.js'; + +declare module 'express-serve-static-core' { + export interface Request { + tokenPayload: TokenPayload; + } +} diff --git a/mocks/mock-data.tsv b/mocks/mock-data.tsv deleted file mode 100644 index 4819ba4..0000000 --- a/mocks/mock-data.tsv +++ /dev/null @@ -1,2 +0,0 @@ -Modern apartment near Eiffel Tower Spacious and bright apartment located in the heart of Paris, close to major attractions and public transport. Perfect for couples or small families looking for комфортный отдых. 2026-02-15 Paris https://example.com/images/paris-preview.jpg https://example.com/images/paris1.jpg;https://example.com/images/paris2.jpg;https://example.com/images/paris3.jpg;https://example.com/images/paris4.jpg;https://example.com/images/paris5.jpg;https://example.com/images/paris6.jpg true false 4.8 apartment 2 4 1200 Breakfast;Air conditioning;Washer;Towels;Fridge Angela Nights angela@example.com https://example.com/images/avatar.jpg base 48.85661;2.351499 -Cozy house with garden in Hamburg Charming family house with private garden situated in a quiet district of Hamburg. Ideal for a relaxing stay with friends or family, featuring modern amenities and comfortable rooms. 2026-02-15 Hamburg https://example.com/images/hamburg-preview.jpg https://example.com/images/hamburg1.jpg;https://example.com/images/hamburg2.jpg;https://example.com/images/hamburg3.jpg;https://example.com/images/hamburg4.jpg;https://example.com/images/hamburg5.jpg;https://example.com/images/hamburg6.jpg false true 4.5 house 4 6 2500 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Johnny Silverhand johnny@example.com https://example.com/images/avatar.jpg pro 53.550341;10.000654 diff --git a/mocks/test-data.tsv b/mocks/test-data.tsv new file mode 100644 index 0000000..69a1e07 --- /dev/null +++ b/mocks/test-data.tsv @@ -0,0 +1,10 @@ +Quiet room in residential district Modern interior, fast Wi-Fi, and easy access to public transport make this property ideal for short and long stays. 2026-04-30T12:32:34.314Z Hamburg preview-2.jpg offer-10.jpg;offer-11.jpg;offer-12.jpg false room 4 4 49223 Air conditioning;Laptop friendly workspace;Baby seat;Washer;Towels;Fridge Michael Brown michael.brown@example.com avatar-5.jpg base 49.837202;6.067401 +Comfortable house for family stay Enjoy peaceful surroundings and high-quality amenities in this carefully designed and comfortable accommodation. 2026-04-30T12:32:34.315Z Cologne preview-6.jpg offer-4.jpg;offer-5.jpg;offer-6.jpg;offer-7.jpg;offer-8.jpg;offer-9.jpg;offer-10.jpg;offer-11.jpg;offer-12.jpg true house 2 7 41824 Baby seat;Washer;Towels;Fridge Sophia Wilson emma.johnson@example.com avatar-2.png pro 53.294908;8.239346 +Cozy studio near metro station Bright and spacious living area with everything you need for a pleasant and relaxing stay. 2026-04-30T12:32:34.315Z Cologne preview-1.jpg offer-9.jpg;offer-10.jpg;offer-11.jpg;offer-12.jpg false hotel 8 7 60880 Air conditioning;Laptop friendly workspace;Baby seat Sophia Wilson michael.brown@example.com avatar-5.jpg pro 48.796494;4.630213 +Cozy studio near metro station Ideal choice for business trips or vacations, offering comfort, privacy, and excellent facilities. 2026-04-28T12:32:34.315Z Cologne preview-3.jpg offer-11.jpg;offer-12.jpg false hotel 3 3 4586 Air conditioning;Laptop friendly workspace;Baby seat;Washer Emma Johnson emma.johnson@example.com avatar-4.png pro 50.109832;2.546524 +Bright apartment with balcony Perfect location for exploring the city, with shops, restaurants, and attractions within walking distance. 2026-04-28T12:32:34.315Z Paris preview-2.jpg offer-9.jpg;offer-10.jpg;offer-11.jpg;offer-12.jpg true hotel 5 6 71384 Towels;Fridge Emma Johnson michael.brown@example.com avatar-2.png pro 50.759415;10.787393 +Quiet room in residential district Perfect location for exploring the city, with shops, restaurants, and attractions within walking distance. 2026-04-28T12:32:34.315Z Hamburg preview-3.jpg offer-10.jpg;offer-11.jpg;offer-12.jpg true apartment 7 1 88006 Baby seat;Washer;Towels;Fridge Emma Johnson daniel.miller@example.com avatar-5.jpg pro 50.366498;10.419582 +Luxury hotel room with river view Perfect location for exploring the city, with shops, restaurants, and attractions within walking distance. 2026-05-02T12:32:34.316Z Dusseldorf preview-4.jpg offer-3.jpg;offer-4.jpg;offer-5.jpg;offer-6.jpg;offer-7.jpg;offer-8.jpg;offer-9.jpg;offer-10.jpg;offer-11.jpg true house 1 2 51326 Air conditioning;Laptop friendly workspace;Baby seat Sophia Wilson emma.johnson@example.com avatar-4.png base 48.660732;2.247895 +Bright apartment with balcony Perfect location for exploring the city, with shops, restaurants, and attractions within walking distance. 2026-05-01T12:32:34.316Z Cologne preview-3.jpg offer-9.jpg;offer-10.jpg;offer-11.jpg;offer-12.jpg true hotel 3 6 11163 Washer;Towels;Fridge Emma Johnson olivia.davis@example.com avatar-2.png pro 50.531817;4.426451 +Charming room close to downtown A perfect combination of style, comfort, and convenience for any type of traveler. 2026-04-28T12:32:34.316Z Cologne preview-5.jpg offer-4.jpg;offer-5.jpg;offer-6.jpg;offer-7.jpg;offer-8.jpg false apartment 8 2 40212 Towels;Fridge John Smith daniel.miller@example.com avatar-5.jpg pro 51.225049;4.385628 +Bright apartment with balcony Enjoy peaceful surroundings and high-quality amenities in this carefully designed and comfortable accommodation. 2026-04-29T12:32:34.316Z Dusseldorf preview-4.jpg offer-3.jpg;offer-4.jpg;offer-5.jpg;offer-6.jpg;offer-7.jpg;offer-8.jpg;offer-9.jpg;offer-10.jpg;offer-11.jpg true house 1 9 36044 Towels;Fridge Sophia Wilson sophia.wilson@example.com avatar-3.jpg base 53.861462;4.117904 diff --git a/package-lock.json b/package-lock.json index 7dffb7b..f4bd236 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,8 +19,9 @@ "got": "14.6.6", "http-status-codes": "2.3.0", "inversify": "6.2.2", + "jose": "6.2.3", "mime-types": "3.0.2", - "mongoose": "9.2.2", + "mongoose": "7.8.9", "multer": "2.1.1", "nanoid": "5.1.9", "nodemon": "3.1.14", @@ -29,7 +30,7 @@ "tslib": "2.8.1" }, "devDependencies": { - "@typegoose/typegoose": "13.2.1", + "@typegoose/typegoose": "11.8.1", "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.6", "@types/express": "5.0.6", @@ -373,10 +374,11 @@ "license": "MIT" }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", - "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.10.tgz", + "integrity": "sha512-DDb3OAw8ezai9p2i1F7R5wMVmyrMIuT8ixjV56R4Hl4cazCo2tOMTDyPR5rrCUcHiMbWHzCXOdife5OD6H1C8w==", "license": "MIT", + "optional": true, "dependencies": { "sparse-bitfield": "^3.0.3" } @@ -740,25 +742,32 @@ "dev": true }, "node_modules/@typegoose/typegoose": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-13.2.1.tgz", - "integrity": "sha512-RGoaTYTy2P7RQ81Pr+LWS5bhmcGMsL5w3X6GPT16HK8NC7fC3WwUnPrsT92xRyRXByUIA3rTCIHq0uryck8Vpw==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-11.8.1.tgz", + "integrity": "sha512-Vozadw9OjE+Jz98m2YcK1WFMxPXoOfYOOuLGmrpFRfRLAlCXYiCpwMW5mwhJNSkO4FDjXuzrCDA6sgirJipRgw==", "dev": true, "license": "MIT", "dependencies": { "lodash": "^4.17.20", - "loglevel": "^1.9.2", - "reflect-metadata": "^0.2.2", - "semver": "^7.7.3", - "tslib": "^2.8.1" + "loglevel": "^1.9.1", + "reflect-metadata": "^0.1.14", + "semver": "^7.6.2", + "tslib": "^2.6.3" }, "engines": { - "node": ">=20.19.0" + "node": ">=14.17.0" }, "peerDependencies": { - "mongoose": "~9.2.2" + "mongoose": "~7.8.0" } }, + "node_modules/@typegoose/typegoose/node_modules/reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -865,7 +874,6 @@ "version": "25.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -931,11 +939,12 @@ "license": "MIT" }, "node_modules/@types/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", "license": "MIT", "dependencies": { + "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -1402,12 +1411,12 @@ } }, "node_modules/bson": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", - "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", "license": "Apache-2.0", "engines": { - "node": ">=20.19.0" + "node": ">=14.20.1" } }, "node_modules/buffer-from": { @@ -3362,6 +3371,15 @@ "reflect-metadata": "~0.2.2" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3688,6 +3706,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -3802,12 +3829,12 @@ } }, "node_modules/kareem": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", - "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==", "license": "Apache-2.0", "engines": { - "node": ">=18.0.0" + "node": ">=12.0.0" } }, "node_modules/keyv": { @@ -3960,7 +3987,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/merge-descriptors": { "version": "2.0.0", @@ -4103,26 +4131,27 @@ } }, "node_modules/mongodb": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", - "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", - "mongodb-connection-string-url": "^7.0.0" + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" }, "engines": { - "node": ">=20.19.0" + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" }, "peerDependencies": { - "@aws-sdk/credential-providers": "^3.806.0", - "@mongodb-js/zstd": "^7.0.0", - "gcp-metadata": "^7.0.1", - "kerberos": "^7.0.0", - "mongodb-client-encryption": ">=7.0.0 <7.1.0", - "snappy": "^7.3.2", - "socks": "^2.8.6" + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" }, "peerDependenciesMeta": { "@aws-sdk/credential-providers": { @@ -4131,9 +4160,6 @@ "@mongodb-js/zstd": { "optional": true }, - "gcp-metadata": { - "optional": true - }, "kerberos": { "optional": true }, @@ -4142,40 +4168,35 @@ }, "snappy": { "optional": true - }, - "socks": { - "optional": true } } }, "node_modules/mongodb-connection-string-url": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", - "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", "license": "Apache-2.0", "dependencies": { - "@types/whatwg-url": "^13.0.0", - "whatwg-url": "^14.1.0" - }, - "engines": { - "node": ">=20.19.0" + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" } }, "node_modules/mongoose": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.2.tgz", - "integrity": "sha512-e06XdPPlH/L9aEq4vcnqIz5AxFFdfhlqrmymYDO7fZwnqwVp/u8pAH/cCEvvpXg0VlV0Tt5qwu6RUk8lhu6ifg==", + "version": "7.8.9", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.9.tgz", + "integrity": "sha512-V3GBAJbmOAdzEP8murOvlg7q1szlbe4jTBRyW+JBHRduJBe7F9dk5eyqJDTuYrdBcOOWfLbr6AgXrDK7F0/o5A==", "license": "MIT", "dependencies": { - "kareem": "3.2.0", - "mongodb": "~7.0", + "bson": "^5.5.0", + "kareem": "2.5.1", + "mongodb": "5.9.2", "mpath": "0.9.0", - "mquery": "6.0.0", + "mquery": "5.0.0", "ms": "2.1.3", - "sift": "17.1.3" + "sift": "16.0.1" }, "engines": { - "node": ">=20.19.0" + "node": ">=14.20.1" }, "funding": { "type": "opencollective", @@ -4192,12 +4213,15 @@ } }, "node_modules/mquery": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", - "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", "license": "MIT", + "dependencies": { + "debug": "4.x" + }, "engines": { - "node": ">=20.19.0" + "node": ">=14.0.0" } }, "node_modules/mrmime": { @@ -5626,9 +5650,9 @@ } }, "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==", "license": "MIT" }, "node_modules/signal-exit": { @@ -5679,6 +5703,30 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, "node_modules/sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -5709,6 +5757,7 @@ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", "license": "MIT", + "optional": true, "dependencies": { "memory-pager": "^1.0.2" } @@ -6074,15 +6123,15 @@ } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "license": "MIT", "dependencies": { - "punycode": "^2.3.1" + "punycode": "^2.1.1" }, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/ts-api-utils": { @@ -6244,7 +6293,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -6315,16 +6363,16 @@ } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "license": "MIT", "dependencies": { - "tr46": "^5.1.0", + "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=18" + "node": ">=12" } }, "node_modules/which": { @@ -6755,9 +6803,10 @@ "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" }, "@mongodb-js/saslprep": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", - "integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.10.tgz", + "integrity": "sha512-DDb3OAw8ezai9p2i1F7R5wMVmyrMIuT8ixjV56R4Hl4cazCo2tOMTDyPR5rrCUcHiMbWHzCXOdife5OD6H1C8w==", + "optional": true, "requires": { "sparse-bitfield": "^3.0.3" } @@ -7006,16 +7055,24 @@ "dev": true }, "@typegoose/typegoose": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-13.2.1.tgz", - "integrity": "sha512-RGoaTYTy2P7RQ81Pr+LWS5bhmcGMsL5w3X6GPT16HK8NC7fC3WwUnPrsT92xRyRXByUIA3rTCIHq0uryck8Vpw==", + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/@typegoose/typegoose/-/typegoose-11.8.1.tgz", + "integrity": "sha512-Vozadw9OjE+Jz98m2YcK1WFMxPXoOfYOOuLGmrpFRfRLAlCXYiCpwMW5mwhJNSkO4FDjXuzrCDA6sgirJipRgw==", "dev": true, "requires": { "lodash": "^4.17.20", - "loglevel": "^1.9.2", - "reflect-metadata": "^0.2.2", - "semver": "^7.7.3", - "tslib": "^2.8.1" + "loglevel": "^1.9.1", + "reflect-metadata": "^0.1.14", + "semver": "^7.6.2", + "tslib": "^2.6.3" + }, + "dependencies": { + "reflect-metadata": { + "version": "0.1.14", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", + "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", + "dev": true + } } }, "@types/body-parser": { @@ -7114,7 +7171,6 @@ "version": "25.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", - "dev": true, "requires": { "undici-types": "~7.18.0" } @@ -7173,10 +7229,11 @@ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, "@types/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", "requires": { + "@types/node": "*", "@types/webidl-conversions": "*" } }, @@ -7472,9 +7529,9 @@ } }, "bson": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", - "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==" + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==" }, "buffer-from": { "version": "1.1.2", @@ -8832,6 +8889,11 @@ "@inversifyjs/core": "1.3.5" } }, + "ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9044,6 +9106,11 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==" + }, "joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -9127,9 +9194,9 @@ } }, "kareem": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz", - "integrity": "sha512-VS8MWZz/cT+SqBCpVfNN4zoVz5VskR3N4+sTmUXme55e9avQHntpwpNq0yjnosISXqwJ3AQVjlbI4Dyzv//JtA==" + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz", + "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==" }, "keyv": { "version": "5.6.0", @@ -9234,7 +9301,8 @@ "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==" + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true }, "merge-descriptors": { "version": "2.0.0", @@ -9315,35 +9383,37 @@ "dev": true }, "mongodb": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", - "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==", + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", "requires": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^7.0.0", - "mongodb-connection-string-url": "^7.0.0" + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" } }, "mongodb-connection-string-url": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", - "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", "requires": { - "@types/whatwg-url": "^13.0.0", - "whatwg-url": "^14.1.0" + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" } }, "mongoose": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.2.2.tgz", - "integrity": "sha512-e06XdPPlH/L9aEq4vcnqIz5AxFFdfhlqrmymYDO7fZwnqwVp/u8pAH/cCEvvpXg0VlV0Tt5qwu6RUk8lhu6ifg==", + "version": "7.8.9", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-7.8.9.tgz", + "integrity": "sha512-V3GBAJbmOAdzEP8murOvlg7q1szlbe4jTBRyW+JBHRduJBe7F9dk5eyqJDTuYrdBcOOWfLbr6AgXrDK7F0/o5A==", "requires": { - "kareem": "3.2.0", - "mongodb": "~7.0", + "bson": "^5.5.0", + "kareem": "2.5.1", + "mongodb": "5.9.2", "mpath": "0.9.0", - "mquery": "6.0.0", + "mquery": "5.0.0", "ms": "2.1.3", - "sift": "17.1.3" + "sift": "16.0.1" } }, "mpath": { @@ -9352,9 +9422,12 @@ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==" }, "mquery": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", - "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", + "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", + "requires": { + "debug": "4.x" + } }, "mrmime": { "version": "2.0.1", @@ -10280,9 +10353,9 @@ } }, "sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", + "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, "signal-exit": { "version": "4.1.0", @@ -10315,6 +10388,20 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==" + }, + "socks": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.8.tgz", + "integrity": "sha512-NlGELfPrgX2f1TAAcz0WawlLn+0r3FyhhCRpFFK2CemXenPYvzMWWZINv3eDNo9ucdwme7oCHRY0Jnbs4aIkog==", + "requires": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + } + }, "sonic-boom": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", @@ -10336,6 +10423,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "optional": true, "requires": { "memory-pager": "^1.0.2" } @@ -10590,11 +10678,11 @@ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==" }, "tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", "requires": { - "punycode": "^2.3.1" + "punycode": "^2.1.1" } }, "ts-api-utils": { @@ -10697,8 +10785,7 @@ "undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==" }, "unpipe": { "version": "1.0.0", @@ -10751,11 +10838,11 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", "requires": { - "tr46": "^5.1.0", + "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, diff --git a/package.json b/package.json index 0e89de1..d574872 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "mock:server": "json-server ./mocks/mock-server-data.json --port 3123" }, "devDependencies": { - "@typegoose/typegoose": "13.2.1", + "@typegoose/typegoose": "11.8.1", "@types/convict": "6.1.6", "@types/convict-format-with-validator": "6.0.6", "@types/express": "5.0.6", @@ -55,8 +55,9 @@ "got": "14.6.6", "http-status-codes": "2.3.0", "inversify": "6.2.2", + "jose": "6.2.3", "mime-types": "3.0.2", - "mongoose": "9.2.2", + "mongoose": "7.8.9", "multer": "2.1.1", "nanoid": "5.1.9", "nodemon": "3.1.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97dd1ad..9681e06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: class-transformer: specifier: 0.5.1 version: 0.5.1 + class-validator: + specifier: 0.15.1 + version: 0.15.1 convict: specifier: 6.2.5 version: 6.2.5 @@ -38,9 +41,21 @@ importers: inversify: specifier: 6.2.2 version: 6.2.2(reflect-metadata@0.2.2) + jose: + specifier: 6.2.3 + version: 6.2.3 + mime-types: + specifier: 3.0.2 + version: 3.0.2 mongoose: specifier: 9.2.2 version: 9.2.2 + multer: + specifier: 2.1.1 + version: 2.1.1 + nanoid: + specifier: 5.1.9 + version: 5.1.9 nodemon: specifier: 3.1.14 version: 3.1.14 @@ -69,6 +84,12 @@ importers: '@types/express-serve-static-core': specifier: 5.1.1 version: 5.1.1 + '@types/mime-types': + specifier: 3.0.1 + version: 3.0.1 + '@types/multer': + specifier: 2.1.0 + version: 2.1.0 '@types/node': specifier: 25.5.2 version: 25.5.2 @@ -329,6 +350,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mime-types@3.0.1': + resolution: {integrity: sha512-xRMsfuQbnRq1Ef+C+RKaENOxXX87Ygl38W1vDfPHRku02TgQr+Qd8iivLtAMcR0KF5/29xlnFihkTlbqFrGOVQ==} + + '@types/multer@2.1.0': + resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==} + '@types/node@25.5.2': resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} @@ -350,6 +377,9 @@ packages: '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -455,6 +485,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -538,10 +571,17 @@ packages: resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} engines: {node: '>=20.19.0'} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + byte-counter@0.1.0: resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==} engines: {node: '>=20'} @@ -597,6 +637,9 @@ packages: class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + class-validator@0.15.1: + resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -614,6 +657,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + content-disposition@1.1.0: resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} engines: {node: '>=18'} @@ -1261,6 +1308,9 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1321,6 +1371,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.12.42: + resolution: {integrity: sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1367,6 +1420,10 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -1390,10 +1447,18 @@ packages: resolution: {integrity: sha512-jkEDaSWZp4/Q3vprqdqukBqUEyNNqC1pwTjZ5cp9YkaR1wv5fvTCd8VFsecbw7i8DNBGjzhJ83MDoPZlcTaPQg==} engines: {node: '>=18.13 || >=19.20 || >=20'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -1479,6 +1544,15 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} + engines: {node: '>= 10.16.0'} + + nanoid@5.1.9: + resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1704,6 +1778,10 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -1789,6 +1867,9 @@ packages: resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -1927,6 +2008,10 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1954,6 +2039,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2064,6 +2152,10 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} @@ -2084,6 +2176,9 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -2106,6 +2201,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2417,6 +2515,12 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/mime-types@3.0.1': {} + + '@types/multer@2.1.0': + dependencies: + '@types/express': 5.0.6 + '@types/node@25.5.2': dependencies: undici-types: 7.18.2 @@ -2438,6 +2542,8 @@ snapshots: '@types/http-errors': 2.0.5 '@types/node': 25.5.2 + '@types/validator@13.15.10': {} + '@types/webidl-conversions@7.0.3': {} '@types/whatwg-url@13.0.0': @@ -2566,6 +2672,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.2 + append-field@1.0.0: {} + arg@4.1.3: {} argparse@2.0.1: {} @@ -2676,8 +2784,14 @@ snapshots: bson@7.2.0: {} + buffer-from@1.1.2: {} + builtin-modules@3.3.0: {} + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + byte-counter@0.1.0: {} bytes@3.1.2: {} @@ -2740,6 +2854,12 @@ snapshots: class-transformer@0.5.1: {} + class-validator@0.15.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.42 + validator: 13.15.35 + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -2754,6 +2874,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -3575,6 +3702,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jose@6.2.3: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -3635,6 +3764,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.42: {} + lines-and-columns@1.2.4: {} locate-path@5.0.0: @@ -3669,6 +3800,8 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} memory-pager@1.5.0: {} @@ -3684,8 +3817,14 @@ snapshots: milliparsec@5.1.1: {} + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -3748,6 +3887,15 @@ snapshots: ms@2.1.3: {} + multer@2.1.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + nanoid@5.1.9: {} + natural-compare@1.4.0: {} negotiator@1.0.0: {} @@ -3998,6 +4146,12 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.2 @@ -4094,6 +4248,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -4257,6 +4413,8 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + streamsearch@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4313,6 +4471,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4401,6 +4563,11 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.0.1: dependencies: content-type: 1.0.5 @@ -4440,6 +4607,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typedarray@0.0.6: {} + typescript@5.9.3: {} unbox-primitive@1.1.0: @@ -4459,6 +4628,8 @@ snapshots: dependencies: punycode: 2.3.1 + util-deprecate@1.0.2: {} + v8-compile-cache-lib@3.0.1: {} validate-npm-package-license@3.0.4: diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 085c793..719f6cd 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -24,6 +24,7 @@ import { ConsoleLogger, Logger } from '../../shared/libs/logger/index.js'; import { Offer } from '../../shared/types/index.js'; import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './command.constant.js'; import { CommentModel } from '../../shared/modules/comment/index.js'; +import { FavoriteModel } from '../../shared/modules/favorite/index.js'; export class ImportCommand implements Command { private userService: UserService; @@ -41,6 +42,7 @@ export class ImportCommand implements Command { this.logger, OfferModel, CommentModel, + FavoriteModel, ); this.userService = new DefaultUserService(this.logger, UserModel); this.databaseClient = new MongoDatabaseClient(this.logger); @@ -66,8 +68,6 @@ export class ImportCommand implements Command { previewImage: offer.previewImage, images: offer.images, isPremium: offer.isPremium, - isFavorite: offer.isFavorite, - rating: offer.rating, type: offer.type, rooms: offer.rooms, guests: offer.guests, diff --git a/src/main.rest.ts b/src/main.rest.ts index 353ddd2..6eaa4e7 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -9,6 +9,8 @@ import { Component } from './shared/types/index.js'; import { createUserContainer } from './shared/modules/user/index.js'; import { createOfferContainer } from './shared/modules/offer/index.js'; import { createCommentContainer } from './shared/modules/comment/index.js'; +import { createAuthContainer } from './shared/modules/auth/index.js'; +import { createFavoriteContainer } from './shared/modules/favorite/index.js'; async function bootstrap() { const appContainer = Container.merge( @@ -16,6 +18,8 @@ async function bootstrap() { createUserContainer(), createOfferContainer(), createCommentContainer(), + createAuthContainer(), + createFavoriteContainer(), ); const app = appContainer.get(Component.RestApplication); diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 54358cc..f7a760d 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -5,7 +5,11 @@ import { Config, RestSchema } from '../shared/libs/config/index.js'; import { Component } from '../shared/types/index.js'; import { DatabaseClient } from '../shared/libs/database-client/index.js'; import { getMongoURI } from '../shared/helpers/index.js'; -import { Controller, ExceptionFilter } from '../shared/libs/rest/index.js'; +import { + Controller, + ExceptionFilter, + ParseTokenMiddleware, +} from '../shared/libs/rest/index.js'; @injectable() export class RestApplication { @@ -24,6 +28,8 @@ export class RestApplication { private readonly offerController: Controller, @inject(Component.CommentController) private readonly commentController: Controller, + @inject(Component.AuthExceptionFilter) + private readonly authExceptionFilter: ExceptionFilter, ) { this.server = express(); } @@ -52,14 +58,24 @@ export class RestApplication { } private async _initMiddleware() { + const authenticateMiddleware = new ParseTokenMiddleware( + this.config.get('JWT_SECRET'), + ); + this.server.use(express.json()); this.server.use( '/upload', express.static(this.config.get('UPLOAD_DIRECTORY')), ); + this.server.use( + authenticateMiddleware.execute.bind(authenticateMiddleware), + ); } private async _initExceptionFilters() { + this.server.use( + this.authExceptionFilter.catch.bind(this.authExceptionFilter), + ); this.server.use( this.appExceptionFilter.catch.bind(this.appExceptionFilter), ); diff --git a/src/shared/helpers/offer.ts b/src/shared/helpers/offer.ts index 44b44a7..34410fe 100644 --- a/src/shared/helpers/offer.ts +++ b/src/shared/helpers/offer.ts @@ -16,8 +16,6 @@ export function createOffer(offerData: string): Offer { previewImage, images, isPremium, - isFavorite, - rating, type, rooms, guests, @@ -39,8 +37,6 @@ export function createOffer(offerData: string): Offer { previewImage, images: images.split(';'), isPremium: booleanFromString(isPremium), - isFavorite: booleanFromString(isFavorite), - rating: parseFloat(rating), type: parseHousing(type), rooms: parseInt(rooms, 10), guests: parseInt(guests, 10), diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 000617c..151c9f4 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -26,6 +26,7 @@ export type RestSchema = { DB_PORT: string; DB_NAME: string; UPLOAD_DIRECTORY: string; + JWT_SECRET: string; }; export const configRestSchema = convict({ @@ -77,4 +78,10 @@ export const configRestSchema = convict({ env: 'UPLOAD_DIRECTORY', default: null, }, + JWT_SECRET: { + doc: 'Secret for sign JWT', + format: String, + env: 'JWT_SECRET', + default: null, + }, }); diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index c50afa4..43e96d1 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -11,7 +11,6 @@ import { OfferGenerator } from './offer-generator.interface.js'; const PRICE = { min: 100, max: 100000 }; const GUESTS = { min: 1, max: 10 }; const ROOMS = { min: 1, max: 8 }; -const RATING = { min: 0, max: 5 }; const FIRST_WEEK_DAY = 1; const LAST_WEEK_DAY = 7; const LONGITUDE = { min: 2, max: 11 }; @@ -30,17 +29,17 @@ export class TSVOfferGenerator implements OfferGenerator { const previewImage = getRandomItem(this.mockData.previewImages); const images = getRandomItems(this.mockData.offerImages).join(';'); const isPremium = generateRandomBoolean(); - const isFavorite = generateRandomBoolean(); - const rating = generateRandomValue(RATING.min, RATING.max, 1).toString(); const type = getRandomItem(this.mockData.housingTypes); - const rooms = generateRandomValue(ROOMS.min, ROOMS.max, 1).toString(); - const guests = generateRandomValue(GUESTS.min, GUESTS.max, 1).toString(); + const rooms = generateRandomValue(ROOMS.min, ROOMS.max).toString(); + const guests = generateRandomValue(GUESTS.min, GUESTS.max).toString(); const price = generateRandomValue(PRICE.min, PRICE.max).toString(); const facilities = getRandomItems(this.mockData.facilities).join( ';', ); - const name = getRandomItem(this.mockData.users); + const [firstname, lastname] = getRandomItem(this.mockData.users).split(' '); const email = getRandomItem(this.mockData.emails); + const avatarPath = getRandomItem(this.mockData.avatars); + const userType = generateRandomBoolean() ? 'pro' : 'base'; const location = [ generateRandomValue(LATITUDE.min, LATITUDE.max, 6), generateRandomValue(LONGITUDE.min, LONGITUDE.max, 6), @@ -54,15 +53,16 @@ export class TSVOfferGenerator implements OfferGenerator { previewImage, images, isPremium, - isFavorite, - rating, type, rooms, guests, price, facilities, - name, + firstname, + lastname, email, + avatarPath, + userType, location, ].join('\t'); } diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index ed47184..d8a47e9 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -13,3 +13,5 @@ export * from './middleware/validate-objectid.middleware.js'; export * from './middleware/validate-dto.middleware.js'; export * from './middleware/document-exists.middleware.js'; export * from './middleware/upload-file.middleware.js'; +export * from './middleware/parse-token.middleware.js'; +export * from './middleware/private-route.middleware.js'; diff --git a/src/shared/libs/rest/middleware/parse-token.middleware.ts b/src/shared/libs/rest/middleware/parse-token.middleware.ts new file mode 100644 index 0000000..798aef5 --- /dev/null +++ b/src/shared/libs/rest/middleware/parse-token.middleware.ts @@ -0,0 +1,62 @@ +import { NextFunction, Request, Response } from 'express'; +import { jwtVerify } from 'jose'; +import { StatusCodes } from 'http-status-codes'; +import { createSecretKey } from 'node:crypto'; +import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; +import { TokenPayload } from '../../../modules/auth/index.js'; + +function isTokenPayload(payload: unknown): payload is TokenPayload { + return ( + typeof payload === 'object' && + payload !== null && + 'email' in payload && + typeof payload.email === 'string' && + 'firstname' in payload && + typeof payload.firstname === 'string' && + 'lastname' in payload && + typeof payload.lastname === 'string' && + 'id' in payload && + typeof payload.id === 'string' + ); +} + +export class ParseTokenMiddleware implements Middleware { + constructor(private readonly jwtSecret: string) {} + + public async execute( + req: Request, + _res: Response, + next: NextFunction, + ): Promise { + const authorizationHeader = req?.headers?.authorization?.split(' '); + + if (!authorizationHeader) { + return next(); + } + + const [, token] = authorizationHeader; + + try { + const { payload } = await jwtVerify( + token, + createSecretKey(this.jwtSecret, 'utf-8'), + ); + + if (isTokenPayload(payload)) { + req.tokenPayload = { ...payload }; + return next(); + } else { + throw new Error('Bad token'); + } + } catch { + return next( + new HttpError( + StatusCodes.UNAUTHORIZED, + 'Invalid token', + 'AuthenticateMiddleware', + ), + ); + } + } +} diff --git a/src/shared/libs/rest/middleware/private-route.middleware.ts b/src/shared/libs/rest/middleware/private-route.middleware.ts new file mode 100644 index 0000000..3f50bf5 --- /dev/null +++ b/src/shared/libs/rest/middleware/private-route.middleware.ts @@ -0,0 +1,22 @@ +import { StatusCodes } from 'http-status-codes'; +import { NextFunction, Request, Response } from 'express'; +import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; + +export class PrivateRouteMiddleware implements Middleware { + public async execute( + { tokenPayload }: Request, + _res: Response, + next: NextFunction, + ): Promise { + if (!tokenPayload) { + throw new HttpError( + StatusCodes.UNAUTHORIZED, + 'Unauthorized', + 'PrivateRouteMiddleware', + ); + } + + return next(); + } +} diff --git a/src/shared/modules/auth/auth-service.interface.ts b/src/shared/modules/auth/auth-service.interface.ts new file mode 100644 index 0000000..1bb9afc --- /dev/null +++ b/src/shared/modules/auth/auth-service.interface.ts @@ -0,0 +1,6 @@ +import { UserEntity, LoginUserDto } from '../user/index.js'; + +export interface AuthService { + authenticate(user: UserEntity): Promise; + verify(dto: LoginUserDto): Promise; +} diff --git a/src/shared/modules/auth/auth.constant.ts b/src/shared/modules/auth/auth.constant.ts new file mode 100644 index 0000000..269747d --- /dev/null +++ b/src/shared/modules/auth/auth.constant.ts @@ -0,0 +1,2 @@ +export const JWT_ALGORITHM = 'HS256'; +export const JWT_EXPIRED = '2d'; diff --git a/src/shared/modules/auth/auth.container.ts b/src/shared/modules/auth/auth.container.ts new file mode 100644 index 0000000..f2909b4 --- /dev/null +++ b/src/shared/modules/auth/auth.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; +import { AuthService } from './auth-service.interface.js'; +import { Component } from '../../types/index.js'; +import { DefaultAuthService } from './default-auth.service.js'; +import { AuthExceptionFilter } from './auth.exception-filter.js'; + +export function createAuthContainer() { + const authContainer = new Container(); + authContainer + .bind(Component.AuthService) + .to(DefaultAuthService) + .inSingletonScope(); + authContainer + .bind(Component.AuthExceptionFilter) + .to(AuthExceptionFilter) + .inSingletonScope(); + + return authContainer; +} diff --git a/src/shared/modules/auth/auth.exception-filter.ts b/src/shared/modules/auth/auth.exception-filter.ts new file mode 100644 index 0000000..aa587db --- /dev/null +++ b/src/shared/modules/auth/auth.exception-filter.ts @@ -0,0 +1,30 @@ +import { inject, injectable } from 'inversify'; +import { NextFunction, Request, Response } from 'express'; +import { ExceptionFilter } from '../../libs/rest/index.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../../libs/logger/index.js'; +import { BaseUserException } from './errors/base-user.exception.js'; + +@injectable() +export class AuthExceptionFilter implements ExceptionFilter { + constructor(@inject(Component.Logger) private readonly logger: Logger) { + this.logger.info('Register AuthExceptionFilter'); + } + + public catch( + error: Error, + _req: Request, + res: Response, + next: NextFunction, + ): void { + if (!(error instanceof BaseUserException)) { + return next(error); + } + + this.logger.error(`[AuthModule] ${error.message}`, error); + res.status(error.httpStatusCode).json({ + type: 'AUTHORIZATION', + error: error.message, + }); + } +} diff --git a/src/shared/modules/auth/default-auth.service.ts b/src/shared/modules/auth/default-auth.service.ts new file mode 100644 index 0000000..a5a1cb9 --- /dev/null +++ b/src/shared/modules/auth/default-auth.service.ts @@ -0,0 +1,61 @@ +import { inject, injectable } from 'inversify'; +import * as crypto from 'node:crypto'; +import { AuthService } from './auth-service.interface.js'; +import { Logger } from '../../libs/logger/index.js'; +import { Component } from '../../types/index.js'; +import { LoginUserDto, UserEntity, UserService } from '../user/index.js'; +import { Config, RestSchema } from '../../libs/config/index.js'; +import { TokenPayload } from './types/TokenPayload.js'; +import { SignJWT } from 'jose'; +import { JWT_ALGORITHM, JWT_EXPIRED } from './auth.constant.js'; +import { + UserNotFoundException, + UserPasswordIncorrectException, +} from './errors/index.js'; + +@injectable() +export class DefaultAuthService implements AuthService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.UserService) private readonly userService: UserService, + @inject(Component.Config) private readonly config: Config, + ) {} + + public async authenticate(user: UserEntity): Promise { + const jwtSecret = this.config.get('JWT_SECRET'); + const secretKey = crypto.createSecretKey(jwtSecret, 'utf-8'); + + const { email, firstname, lastname, id } = user; + + const tokenPayload: TokenPayload = { + email, + firstname, + lastname, + id, + }; + + this.logger.info(`Create token for ${email}`); + + return new SignJWT(tokenPayload) + .setProtectedHeader({ alg: JWT_ALGORITHM }) + .setIssuedAt() + .setExpirationTime(JWT_EXPIRED) + .sign(secretKey); + } + + public async verify(dto: LoginUserDto): Promise { + const user = await this.userService.findByEmail(dto.email); + + if (!user) { + this.logger.warn(`User with ${dto.email} not found`); + throw new UserNotFoundException(); + } + + if (!user.verifyPassword(dto.password, this.config.get('SALT'))) { + this.logger.warn(`Incorrect password for ${dto.email}`); + throw new UserPasswordIncorrectException(); + } + + return user; + } +} diff --git a/src/shared/modules/auth/errors/base-user.exception.ts b/src/shared/modules/auth/errors/base-user.exception.ts new file mode 100644 index 0000000..27f8f1b --- /dev/null +++ b/src/shared/modules/auth/errors/base-user.exception.ts @@ -0,0 +1,7 @@ +import { HttpError } from '../../../libs/rest/index.js'; + +export class BaseUserException extends HttpError { + constructor(httpStatusCode: number, message: string) { + super(httpStatusCode, message); + } +} diff --git a/src/shared/modules/auth/errors/index.ts b/src/shared/modules/auth/errors/index.ts new file mode 100644 index 0000000..e1154ae --- /dev/null +++ b/src/shared/modules/auth/errors/index.ts @@ -0,0 +1,3 @@ +export * from './base-user.exception.js'; +export * from './user-not-found.exception.js'; +export * from './user-password-incorrect.exception.js'; diff --git a/src/shared/modules/auth/errors/user-not-found.exception.ts b/src/shared/modules/auth/errors/user-not-found.exception.ts new file mode 100644 index 0000000..4ae0b72 --- /dev/null +++ b/src/shared/modules/auth/errors/user-not-found.exception.ts @@ -0,0 +1,8 @@ +import { StatusCodes } from 'http-status-codes'; +import { BaseUserException } from './base-user.exception.js'; + +export class UserNotFoundException extends BaseUserException { + constructor() { + super(StatusCodes.NOT_FOUND, 'User noot found'); + } +} diff --git a/src/shared/modules/auth/errors/user-password-incorrect.exception.ts b/src/shared/modules/auth/errors/user-password-incorrect.exception.ts new file mode 100644 index 0000000..8579750 --- /dev/null +++ b/src/shared/modules/auth/errors/user-password-incorrect.exception.ts @@ -0,0 +1,8 @@ +import { StatusCodes } from 'http-status-codes'; +import { BaseUserException } from './base-user.exception.js'; + +export class UserPasswordIncorrectException extends BaseUserException { + constructor() { + super(StatusCodes.UNAUTHORIZED, 'Incorrect user name or password'); + } +} diff --git a/src/shared/modules/auth/index.ts b/src/shared/modules/auth/index.ts new file mode 100644 index 0000000..ea59cdb --- /dev/null +++ b/src/shared/modules/auth/index.ts @@ -0,0 +1,4 @@ +export * from './auth-service.interface.js'; +export * from './types/TokenPayload.js'; +export * from './default-auth.service.js'; +export * from './auth.container.js'; diff --git a/src/shared/modules/auth/types/TokenPayload.ts b/src/shared/modules/auth/types/TokenPayload.ts new file mode 100644 index 0000000..b4c88cb --- /dev/null +++ b/src/shared/modules/auth/types/TokenPayload.ts @@ -0,0 +1,6 @@ +export type TokenPayload = { + email: string; + firstname: string; + lastname: string; + id: string; +}; diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index 87fb636..750442f 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -2,9 +2,10 @@ import { Response } from 'express'; import { inject, injectable } from 'inversify'; import { BaseController, - DocumentExistsMiddleware, HttpMethod, ValidateDtoMiddleware, + PrivateRouteMiddleware, + ValidateObjectIdMiddleware, } from '../../libs/rest/index.js'; import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; @@ -31,19 +32,24 @@ export default class CommentController extends BaseController { method: HttpMethod.Post, handler: this.create, middlewares: [ + new PrivateRouteMiddleware(), new ValidateDtoMiddleware(CreateCommentDTO), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new ValidateObjectIdMiddleware('offerId'), ], }); } public async create( - { body }: CreateCommentRequest, + { body, tokenPayload }: CreateCommentRequest, res: Response, ): Promise { const { offerId } = body; - const comment = await this.commentService.create(body); + const comment = await this.commentService.create({ + ...body, + userId: tokenPayload.id, + }); await this.offerService.incCommentCount(offerId); + await this.offerService.calcRating(offerId); this.created(res, fillDTO(CommentRdo, comment)); } } diff --git a/src/shared/modules/comment/comment.http b/src/shared/modules/comment/comment.http index a103f2a..4755fc8 100644 --- a/src/shared/modules/comment/comment.http +++ b/src/shared/modules/comment/comment.http @@ -3,10 +3,11 @@ POST http://localhost:3000/comments HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3NTc1NjIzLCJleHAiOjE3Nzc3NDg0MjN9.CDkRYtP0vK_gcJ5CRZwEDj9FVqpQVNt0S0qoHgMPnUM { - "text": "Отличное расположение!", - "rating": 5, + "text": "Грязища, фу!", + "rating": 1, "offerId": "69c98315edbb02aef834bfd1", "userId": "69c982e424f79692fbaad886" } diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index cd8500c..168a851 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -14,6 +14,5 @@ export class CreateCommentDTO { @IsMongoId({ message: CreateCommentMessages.offerId.invalidFormat }) public offerId: string; - @IsMongoId({ message: CreateCommentMessages.userId.invalidFormat }) public userId: string; } diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts new file mode 100644 index 0000000..d6921bb --- /dev/null +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -0,0 +1,75 @@ +import { inject, injectable } from 'inversify'; +import { FavoriteService } from './favorite-service.interface.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../../libs/logger/index.js'; +import { types } from '@typegoose/typegoose'; +import { FavoriteEntity } from './favorite.entity.js'; +import { OfferEntity, OfferWithFavorite } from '../offer/index.js'; + +@injectable() +export class DefaultFavoriteService implements FavoriteService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.FavoriteModel) + private readonly favoriteModel: types.ModelType, + ) {} + + public async add(userId: string, offerId: string): Promise { + await this.favoriteModel.updateOne( + { userId, offerId }, + { $setOnInsert: { userId, offerId } }, + { upsert: true }, + ); + + this.logger.info(`Offer ${offerId} added to favorites for user ${userId}`); + } + + public async remove(userId: string, offerId: string): Promise { + await this.favoriteModel.deleteOne({ userId, offerId }).exec(); + this.logger.info( + `Offer ${offerId} removed from favorites for user ${userId}`, + ); + } + + public async removeByOfferId(offerId: string): Promise { + await this.favoriteModel.deleteMany({ offerId }).exec(); + this.logger.info(`All favorites removed for offer ${offerId}`); + } + + public async getFavoriteStatus( + userId: string, + offerId: string, + ): Promise { + const result = await this.favoriteModel.exists({ userId, offerId }).exec(); + + return result !== null; + } + + public async getOfferIdsByUser(userId: string): Promise { + const favorites = await this.favoriteModel + .find({ userId }) + .select('offerId') + .lean(); + + return favorites.map((item) => item.offerId.toString()); + } + + public async getFavoriteOffers(userId: string): Promise { + const favorites = await this.favoriteModel + .find({ userId }) + .populate({ + path: 'offerId', + populate: { path: 'userId' }, + }) + .lean(); + + return favorites.map((fav) => ({ + ...(fav.offerId as OfferEntity), + isFavorite: true, + })); + } + + public async exists(documentId: string): Promise { + return (await this.favoriteModel.exists({ _id: documentId })) !== null; + } +} diff --git a/src/shared/modules/favorite/favorite-service.interface.ts b/src/shared/modules/favorite/favorite-service.interface.ts new file mode 100644 index 0000000..4aa0b54 --- /dev/null +++ b/src/shared/modules/favorite/favorite-service.interface.ts @@ -0,0 +1,11 @@ +import { OfferWithFavorite } from '../offer/index.js'; + +export interface FavoriteService { + add(userId: string, offerId: string): Promise; + remove(userId: string, offerId: string): Promise; + removeByOfferId(offerId: string): Promise; + getFavoriteStatus(userId: string, offerId: string): Promise; + getOfferIdsByUser(userId: string): Promise; + getFavoriteOffers(userId: string): Promise; + exists(documentId: string): Promise; +} diff --git a/src/shared/modules/favorite/favorite.container.ts b/src/shared/modules/favorite/favorite.container.ts new file mode 100644 index 0000000..94d8fdb --- /dev/null +++ b/src/shared/modules/favorite/favorite.container.ts @@ -0,0 +1,19 @@ +import { Container } from 'inversify'; +import { Component } from '../../types/index.js'; +import { FavoriteService } from './favorite-service.interface.js'; +import { DefaultFavoriteService } from './default-favorite.service.js'; +import { FavoriteModel } from './favorite.entity.js'; + +export function createFavoriteContainer() { + const favoriteContainer = new Container(); + + favoriteContainer + .bind(Component.FavoriteService) + .to(DefaultFavoriteService) + .inSingletonScope(); + favoriteContainer + .bind(Component.FavoriteModel) + .toConstantValue(FavoriteModel); + + return favoriteContainer; +} diff --git a/src/shared/modules/favorite/favorite.entity.ts b/src/shared/modules/favorite/favorite.entity.ts new file mode 100644 index 0000000..499f5b3 --- /dev/null +++ b/src/shared/modules/favorite/favorite.entity.ts @@ -0,0 +1,30 @@ +import { + prop, + getModelForClass, + modelOptions, + defaultClasses, + Ref, +} from '@typegoose/typegoose'; +import { UserEntity } from '../user/index.js'; +import { OfferEntity } from '../offer/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export interface FavoriteEntity extends defaultClasses.Base {} + +@modelOptions({ + schemaOptions: { + collection: 'favorites', + timestamps: true, + }, +}) + +// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging +export class FavoriteEntity extends defaultClasses.TimeStamps { + @prop({ required: true, ref: UserEntity }) + public userId!: Ref; + + @prop({ required: true, ref: OfferEntity }) + public offerId!: Ref; +} + +export const FavoriteModel = getModelForClass(FavoriteEntity); diff --git a/src/shared/modules/favorite/index.ts b/src/shared/modules/favorite/index.ts new file mode 100644 index 0000000..837b17c --- /dev/null +++ b/src/shared/modules/favorite/index.ts @@ -0,0 +1,4 @@ +export * from './favorite-service.interface.js'; +export * from './default-favorite.service.js'; +export * from './favorite.entity.js'; +export * from './favorite.container.js'; diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index acd8683..16d9b82 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -3,7 +3,7 @@ import { OfferService } from './offer-service.interface.js'; import { City, Component, SortType } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; import { types } from '@typegoose/typegoose'; -import { OfferEntity } from './offer.entity.js'; +import { OfferEntity, OfferWithFavorite } from './offer.entity.js'; import { CreateOfferDTO } from './dto/create-offer.dto.js'; import { UpdateOfferDTO } from './dto/update-offer.dto.js'; import { @@ -12,6 +12,7 @@ import { } from './offer.constants.js'; import { CommentEntity } from '../comment/index.js'; import { Types } from 'mongoose'; +import { FavoriteEntity } from '../favorite/index.js'; @injectable() export class DefaultOfferService implements OfferService { @@ -21,6 +22,8 @@ export class DefaultOfferService implements OfferService { private readonly offerModel: types.ModelType, @inject(Component.CommentModel) private readonly commentModel: types.ModelType, + @inject(Component.FavoriteModel) + private readonly favoriteModel: types.ModelType, ) {} public async create( @@ -34,19 +37,57 @@ export class DefaultOfferService implements OfferService { public async findById( offerId: string, - ): Promise | null> { - return this.offerModel.findById(offerId).populate(['userId']).exec(); + userId?: string, + ): Promise { + const offer = await this.offerModel + .findById(offerId) + .populate(['userId']) + .lean() + .exec(); + + if (!offer) { + return null; + } + + if (!userId) { + return { ...offer, isFavorite: false }; + } + + const isFavorite = await this.favoriteModel.exists({ + userId, + offerId, + }); + + return { + ...offer, + isFavorite: isFavorite !== null, + }; } public async find( count?: number, - ): Promise[]> { - return this.offerModel + userId?: string, + ): Promise { + const offers = await this.offerModel .find() - .sort({ createdAt: SortType.Down }) .limit(count ?? DEFAULT_OFFER_COUNT) .populate(['userId']) - .exec(); + .lean(); + + if (!userId) { + return offers.map((offer) => ({ ...offer, isFavorite: false })); + } + + const favoriteIds = await this.favoriteModel.distinct('offerId', { + userId, + }); + + const set = new Set(favoriteIds.map(String)); + + return offers.map((offer) => ({ + ...offer, + isFavorite: set.has(offer._id.toString()), + })); } public async deleteById( @@ -80,7 +121,7 @@ export class DefaultOfferService implements OfferService { .exec(); } - async calcRating(offerId: string): Promise { + public async calcRating(offerId: string): Promise { const ratings = await this.commentModel.aggregate([ { $match: { offerId: new Types.ObjectId(offerId) } }, { $group: { _id: '$offerId', avgRating: { $avg: '$rating' } } }, @@ -107,24 +148,34 @@ export class DefaultOfferService implements OfferService { public async findPremiumByCity( city: City, + userId?: string, count?: number, - ): Promise[]> { + ): Promise { if (!city) { return []; } - return this.offerModel + const premiumOffers = await this.offerModel .find({ city, isPremium: true }) .sort({ createdAt: SortType.Down }) .limit(count ?? DEFAULT_PREMIUM_OFFER_COUNT) .populate(['userId']) + .lean() .exec(); - } - public async findByFavorite(): Promise[]> { - return this.offerModel - .find({ isFavorite: true }) - .populate(['userId']) - .exec(); + if (!userId) { + return premiumOffers.map((offer) => ({ ...offer, isFavorite: false })); + } + + const favoriteIds = await this.favoriteModel.distinct('offerId', { + userId, + }); + + const set = new Set(favoriteIds.map(String)); + + return premiumOffers.map((offer) => ({ + ...offer, + isFavorite: set.has(offer._id.toString()), + })); } } diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 3e22670..011ed7e 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -8,8 +8,6 @@ import { IsEnum, IsIn, IsInt, - IsMongoId, - IsNumber, IsUrl, Max, MaxLength, @@ -68,17 +66,6 @@ export class CreateOfferDTO { @IsBoolean({ message: CreateOfferValidationMessage.isPremium.invalidFormat }) isPremium: boolean; - @IsBoolean({ message: CreateOfferValidationMessage.isFavorite.invalidFormat }) - isFavorite: boolean; - - @IsNumber( - { maxDecimalPlaces: 1 }, - { message: CreateOfferValidationMessage.rating.invalidFormat }, - ) - @Min(0, { message: CreateOfferValidationMessage.rating.minValue }) - @Max(5, { message: CreateOfferValidationMessage.rating.maxValue }) - rating: number; - @IsIn(HousingValues, { message: CreateOfferValidationMessage.type.invalid }) type: Housing; @@ -107,7 +94,6 @@ export class CreateOfferDTO { }) facilities: Facility[]; - @IsMongoId({ message: CreateOfferValidationMessage.userId.invalidId }) userId: string; @ValidateNested({ message: CreateOfferValidationMessage.location.invalid }) diff --git a/src/shared/modules/offer/dto/create-offer.messages.ts b/src/shared/modules/offer/dto/create-offer.messages.ts index 954e5d7..08306e3 100644 --- a/src/shared/modules/offer/dto/create-offer.messages.ts +++ b/src/shared/modules/offer/dto/create-offer.messages.ts @@ -23,14 +23,6 @@ export const CreateOfferValidationMessage = { isPremium: { invalidFormat: 'isPremium must be a boolean', }, - isFavorite: { - invalidFormat: 'isFavorite must be a boolean', - }, - rating: { - invalidFormat: 'rating must be a number with max 1 decimal place', - minValue: 'Minimum rating is 0', - maxValue: 'Maximum rating is 5', - }, type: { invalid: 'type must be one of Housing', }, diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts index a84f13d..1fd63a3 100644 --- a/src/shared/modules/offer/dto/update-offer.dto.ts +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -72,10 +72,6 @@ export class UpdateOfferDTO { @IsBoolean({ message: CreateUpdateOfferMessage.isPremium.invalidFormat }) isPremium: boolean; - @IsOptional() - @IsBoolean({ message: CreateUpdateOfferMessage.isFavorite.invalidFormat }) - isFavorite: boolean; - @IsOptional() @IsNumber( { maxDecimalPlaces: 1 }, diff --git a/src/shared/modules/offer/dto/update-offer.messages.ts b/src/shared/modules/offer/dto/update-offer.messages.ts index 1fb3e24..db2ea42 100644 --- a/src/shared/modules/offer/dto/update-offer.messages.ts +++ b/src/shared/modules/offer/dto/update-offer.messages.ts @@ -23,9 +23,6 @@ export const CreateUpdateOfferMessage = { isPremium: { invalidFormat: 'isPremium must be a boolean', }, - isFavorite: { - invalidFormat: 'isFavorite must be a boolean', - }, rating: { invalidFormat: 'rating must be a number with max 1 decimal place', minValue: 'Minimum rating is 0', diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index bdd6172..b85a07f 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -1,13 +1,13 @@ import { CreateOfferDTO } from './dto/create-offer.dto.js'; import { DocumentType } from '@typegoose/typegoose'; -import { OfferEntity } from './offer.entity.js'; +import { OfferEntity, OfferWithFavorite } from './offer.entity.js'; import { UpdateOfferDTO } from './dto/update-offer.dto.js'; import { City, DocumentExists } from '../../types/index.js'; export interface OfferService extends DocumentExists { create(dto: CreateOfferDTO): Promise>; - findById(offerId: string): Promise | null>; - find(count?: number): Promise[]>; + findById(offerId: string, userId?: string): Promise; + find(count?: number, userId?: string): Promise; deleteById(offerId: string): Promise | null>; updateById( offerId: string, @@ -16,9 +16,9 @@ export interface OfferService extends DocumentExists { incCommentCount(offerId: string): Promise | null>; findPremiumByCity( city: City, + userId?: string, count?: number, - ): Promise[]>; - findByFavorite(): Promise[]>; + ): Promise; calcRating(offerId: string): Promise; exists(documentId: string): Promise; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index bbd39b6..3011793 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -6,6 +6,7 @@ import { DocumentExistsMiddleware, HttpError, HttpMethod, + PrivateRouteMiddleware, RequestQuery, ValidateDtoMiddleware, ValidateObjectIdMiddleware, @@ -21,6 +22,7 @@ import { UpdateOfferDTO } from './dto/update-offer.dto.js'; import { ParamsDictionary } from 'express-serve-static-core'; import { CommentRdo, CommentService } from '../comment/index.js'; import { CreateOfferDTO } from './dto/create-offer.dto.js'; +import { FavoriteService } from '../favorite/index.js'; @injectable() export class OfferController extends BaseController { @@ -30,6 +32,8 @@ export class OfferController extends BaseController { private readonly offerService: OfferService, @inject(Component.CommentService) private readonly commentService: CommentService, + @inject(Component.FavoriteService) + private readonly favoriteService: FavoriteService, ) { super(logger); @@ -44,11 +48,20 @@ export class OfferController extends BaseController { method: HttpMethod.Get, handler: this.getPremiumOffersByCity, }); + this.addRoute({ + path: '/favorites', + method: HttpMethod.Get, + handler: this.getFavorites, + middlewares: [new PrivateRouteMiddleware()], + }); this.addRoute({ path: '/', method: HttpMethod.Post, handler: this.create, - middlewares: [new ValidateDtoMiddleware(CreateOfferDTO)], + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateDtoMiddleware(CreateOfferDTO), + ], }); this.addRoute({ path: '/:offerId', @@ -64,6 +77,7 @@ export class OfferController extends BaseController { method: HttpMethod.Delete, handler: this.delete, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), ], @@ -73,6 +87,7 @@ export class OfferController extends BaseController { method: HttpMethod.Patch, handler: this.update, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(UpdateOfferDTO), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), @@ -87,31 +102,60 @@ export class OfferController extends BaseController { new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), ], }); + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Post, + handler: this.addFavorite, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Delete, + handler: this.removeFavorite, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); } public async show( - { params }: Request, + { params, tokenPayload }: Request, res: Response, ): Promise { const { offerId } = params; - const offer = await this.offerService.findById(offerId); + const userId = tokenPayload?.id; + const offer = await this.offerService.findById(offerId, userId); this.ok(res, fillDTO(OfferRdo, offer)); } public async create( - { body }: CreateOfferRequest, + { body, tokenPayload }: CreateOfferRequest, res: Response, ): Promise { - const result = await this.offerService.create(body); + const result = await this.offerService.create({ + ...body, + userId: tokenPayload.id, + }); const offer = await this.offerService.findById(result.id); this.created(res, fillDTO(OfferRdo, offer)); } public async index( - { query }: Request, + { + query, + tokenPayload, + }: Request, res: Response, ): Promise { - const offers = await this.offerService.find(query?.limit); + const userId = tokenPayload?.id; + const offers = await this.offerService.find(query?.limit, userId); + this.ok(res, fillDTO(OfferRdo, offers)); } @@ -144,10 +188,14 @@ export class OfferController extends BaseController { } public async getPremiumOffersByCity( - { query }: Request, + { + query, + tokenPayload, + }: Request, res: Response, ): Promise { const city = String(query.city).trim() as City; + const userId = tokenPayload?.id; const limit = query.limit && Number(query.limit); if (!city) { @@ -160,8 +208,42 @@ export class OfferController extends BaseController { const premiumOffersByCity = await this.offerService.findPremiumByCity( city, + userId, limit, ); this.ok(res, fillDTO(OfferRdo, premiumOffersByCity)); } + + public async addFavorite( + { params, tokenPayload }: Request, + res: Response, + ): Promise { + const favorite = await this.favoriteService.add( + tokenPayload.id, + params.offerId, + ); + this.noContent(res, favorite); + } + + public async removeFavorite( + { params, tokenPayload }: Request, + res: Response, + ): Promise { + const favorite = await this.favoriteService.remove( + tokenPayload.id, + params.offerId, + ); + this.noContent(res, favorite); + } + + public async getFavorites( + { tokenPayload }: Request, + res: Response, + ): Promise { + const offers = await this.favoriteService.getFavoriteOffers( + tokenPayload.id, + ); + + this.ok(res, fillDTO(OfferRdo, offers)); + } } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index a95f38e..f460f56 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -17,6 +17,9 @@ import { UserEntity } from '../user/index.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface OfferEntity extends defaultClasses.Base {} +export type OfferWithFavorite = OfferEntity & { + isFavorite: boolean; +}; @modelOptions({ schemaOptions: { @@ -47,12 +50,12 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ required: true }) public isPremium!: boolean; - @prop({ required: true }) - public isFavorite!: boolean; - - @prop({ required: true, min: 0, max: 5 }) + @prop({ default: 0 }) public rating!: number; + @prop({ default: 0 }) + public commentCount!: number; + @prop({ required: true, type: () => String, enum: HousingValues }) public type!: Housing; diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index fe4188c..0747110 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -1,17 +1,19 @@ # Получить объявление по идентификатору GET http://localhost:3000/offers/69c98315edbb02aef834bfd1 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE ### ## Получить список объявлений GET http://localhost:3000/offers?limit=10 HTTP/1.1 - +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE ### ## Создать новое объявление POST http://localhost:3000/offers HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE { "title": "Modern apartment in city center", @@ -28,8 +30,6 @@ Content-Type: application/json "https://example.com/images/brussels6.jpg" ], "isPremium": true, - "isFavorite": true, - "rating": 4.7, "type": "apartment", "rooms": 2, "guests": 3, @@ -78,3 +78,24 @@ GET http://localhost:3000/offers/69c98315edbb02aef834bfd1/comments HTTP/1.1 GET http://localhost:3000/offers/premium?city=Paris&limit=10 HTTP/1.1 ### + +## Добавить в избранное + +POST http://localhost:3000/offers/69c98315edbb02aef834bfd1/favorite HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE + +### + +## Удалить из избранного + +DELETE http://localhost:3000/offers/69c98315edbb02aef834bfd1/favorite HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE + +### + +## Получить избранные объявления + +GET http://localhost:3000/offers/favorites HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE + +### diff --git a/src/shared/modules/offer/rdo/offer.rdo.ts b/src/shared/modules/offer/rdo/offer.rdo.ts index b54961e..fa611af 100644 --- a/src/shared/modules/offer/rdo/offer.rdo.ts +++ b/src/shared/modules/offer/rdo/offer.rdo.ts @@ -5,6 +5,7 @@ import { CoordinatesRdo } from './coordinates.rdo.js'; export class OfferRdo { @Expose() + @Transform(({ obj }) => obj.id ?? obj._id?.toString()) public id: string; @Expose() @@ -14,7 +15,9 @@ export class OfferRdo { public description: string; @Expose() - @Transform(({ value }) => value.toISOString()) + @Transform(({ value }) => + value instanceof Date ? value.toISOString() : value, + ) public publishDate: string; @Expose() diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 19328e9..9fcf0b8 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -1,5 +1,6 @@ export * from './user.entity.js'; export * from './dto/create-user.dto.js'; +export * from './dto/login-user.dto.js'; export * from './default-user.service.js'; export * from './user.container.js'; export * from './user-service.interface.js'; diff --git a/src/shared/modules/user/rdo/logged-user.rdo.ts b/src/shared/modules/user/rdo/logged-user.rdo.ts new file mode 100644 index 0000000..543bd7d --- /dev/null +++ b/src/shared/modules/user/rdo/logged-user.rdo.ts @@ -0,0 +1,9 @@ +import { Expose } from 'class-transformer'; + +export class LoggedUserRdo { + @Expose() + public token: string; + + @Expose() + public email: string; +} diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 078fca0..21d0d89 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -19,6 +19,8 @@ import { UserRdo } from './rdo/user.rdo.js'; import { LoginUserRequest } from './login-user-request.type.js'; import { CreateUserDto } from './dto/create-user.dto.js'; import { LoginUserDto } from './dto/login-user.dto.js'; +import { AuthService } from '../auth/index.js'; +import { LoggedUserRdo } from './rdo/logged-user.rdo.js'; @injectable() export class UserController extends BaseController { @@ -27,6 +29,7 @@ export class UserController extends BaseController { @inject(Component.UserService) protected readonly userService: UserService, @inject(Component.Config) protected readonly configService: Config, + @inject(Component.AuthService) private readonly authService: AuthService, ) { super(logger); this.logger.info('Register routes for UserController...'); @@ -42,6 +45,11 @@ export class UserController extends BaseController { handler: this.login, middlewares: [new ValidateDtoMiddleware(LoginUserDto)], }); + this.addRoute({ + path: '/login', + method: HttpMethod.Get, + handler: this.checkAuth, + }); this.addRoute({ path: '/:userId/avatar', method: HttpMethod.Post, @@ -77,30 +85,33 @@ export class UserController extends BaseController { this.created(res, fillDTO(UserRdo, result)); } - public async login( - { body }: LoginUserRequest, - _res: Response, - ): Promise { - const existsUser = await this.userService.findByEmail(body.email); + public async login({ body }: LoginUserRequest, res: Response): Promise { + const user = await this.authService.verify(body); + const token = await this.authService.authenticate(user); + const responseData = fillDTO(LoggedUserRdo, { + email: user.email, + token, + }); + this.ok(res, responseData); + } - if (!existsUser) { + public async uploadAvatar(req: Request, res: Response) { + this.created(res, { + filepath: req.file?.path, + }); + } + + public async checkAuth({ tokenPayload: { email } }: Request, res: Response) { + const user = await this.userService.findByEmail(email); + + if (!user) { throw new HttpError( StatusCodes.UNAUTHORIZED, - `User with email ${body.email} not found.`, + 'Unauthorized', 'UserController', ); } - throw new HttpError( - StatusCodes.NOT_IMPLEMENTED, - 'Not implemented', - 'UserController', - ); - } - - public async uploadAvatar(req: Request, res: Response) { - this.created(res, { - filepath: req.file?.path, - }); + this.ok(res, fillDTO(LoggedUserRdo, user)); } } diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index dedd51f..8c889d4 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -54,6 +54,11 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { public getPassword() { return this.password; } + + public verifyPassword(password: string, salt: string) { + const hashPassword = createSHA256(password, salt); + return hashPassword === this.password; + } } export const UserModel = getModelForClass(UserEntity); diff --git a/src/shared/modules/user/user.http b/src/shared/modules/user/user.http index d4c9be0..d9abad0 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -40,3 +40,11 @@ Content-Type: image/png ------WebKitFormBoundary7MA4YWxkTrZu0gW-- ## + +### + +## Проверить токен пользователя +GET http://localhost:3000/users/login HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3NTc1NjIzLCJleHAiOjE3Nzc3NDg0MjN9.CDkRYtP0vK_gcJ5CRZwEDj9FVqpQVNt0S0qoHgMPnUM + +### diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index 5d591ad..1e11b8b 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -13,4 +13,8 @@ export const Component = { UserController: Symbol.for('UserController'), OfferController: Symbol.for('OfferController'), CommentController: Symbol.for('CommentController'), + AuthService: Symbol.for('AuthService'), + AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), + FavoriteService: Symbol.for('FavoriteService'), + FavoriteModel: Symbol.for('FavoriteModel'), } as const; diff --git a/src/shared/types/offer.type.ts b/src/shared/types/offer.type.ts index ab6608f..b807aaa 100644 --- a/src/shared/types/offer.type.ts +++ b/src/shared/types/offer.type.ts @@ -12,8 +12,6 @@ export type Offer = { previewImage: string; images: string[]; // всегда 6 isPremium: boolean; - isFavorite: boolean; - rating: number; // 0–5 (1 знак после запятой) type: Housing; rooms: number; // 1–8 guests: number; // 1–10 @@ -22,3 +20,8 @@ export type Offer = { host: User; location: Coordinates; }; + +export type OfferWithCalculatedValues = Offer & { + isFavorite: boolean; + rating: number; // 0–5 (1 знак после запятой) +}; diff --git a/tsconfig.json b/tsconfig.json index c0a0a7e..9b2240d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,5 +38,6 @@ ], "ts-node": { "esm": true - } + }, + "files": ["./custom.d.ts"] }