diff --git a/package-lock.json b/package-lock.json index 1a348fd..a2dc9a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,17 +24,21 @@ "@types/hapi__joi": "^17.1.15", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", + "@types/uuid": "^10.0.0", + "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", "joi": "^17.13.3", + "multer": "^1.4.5-lts.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.15.6", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -46,6 +50,7 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.1", "@types/jest": "^29.5.14", + "@types/multer": "^1.4.12", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "eslint": "^9.26.0", @@ -3688,6 +3693,16 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz", + "integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.15.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", @@ -3812,6 +3827,12 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "license": "MIT" + }, "node_modules/@types/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", @@ -5197,6 +5218,78 @@ "dev": true, "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -5635,6 +5728,24 @@ "node": ">=14.16" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6329,6 +6440,23 @@ "node": ">=10" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7355,6 +7483,21 @@ "dev": true, "license": "ISC" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -7848,6 +7991,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -7864,7 +8019,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8086,6 +8240,22 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8093,6 +8263,18 @@ "dev": true, "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -8138,6 +8320,24 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -8187,6 +8387,24 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -8200,6 +8418,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9011,6 +9244,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/joi": { "version": "17.13.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", @@ -10540,6 +10782,15 @@ "node": ">=4" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postgres-array": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", @@ -10728,6 +10979,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11098,12 +11358,35 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -11241,6 +11524,23 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -12807,6 +13107,35 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12831,7 +13160,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -13154,6 +13482,27 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -13277,6 +13626,28 @@ "dev": true, "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 4afaa7f..6c10482 100644 --- a/package.json +++ b/package.json @@ -35,17 +35,21 @@ "@types/hapi__joi": "^17.1.15", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", + "@types/uuid": "^10.0.0", + "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", "joi": "^17.13.3", + "multer": "^1.4.5-lts.2", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pg": "^8.15.6", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -57,6 +61,7 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.1", "@types/jest": "^29.5.14", + "@types/multer": "^1.4.12", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "eslint": "^9.26.0", diff --git a/src/app.module.ts b/src/app.module.ts index 803089d..02b112e 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import {UsersModule} from "./users/user.module"; import {AuthenticationModule} from "./authentication/authentication.module"; import { PostsModule } from './posts/posts.module'; import { CategoriesModule } from './categories/categories.module'; +import {FilesModule} from "./files/file.module"; @Module({ imports: [ @@ -32,12 +33,16 @@ import { CategoriesModule } from './categories/categories.module'; validationSchema: Joi.object({ JWT_SECRET: Joi.string().required(), JWT_EXPIRATION_TIME: Joi.string().required(), + S3_BUCKET: Joi.string().required(), + S3_ACCESS_KEY: Joi.string().required(), + S3_ENDPOINT: Joi.string().required(), }) }), UsersModule, AuthenticationModule, PostsModule, - CategoriesModule + CategoriesModule, + FilesModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/authentication/local.strategy.ts b/src/authentication/local.strategy.ts index 6ade241..33c63cf 100644 --- a/src/authentication/local.strategy.ts +++ b/src/authentication/local.strategy.ts @@ -2,7 +2,7 @@ import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; -import User from '../users/user.entity'; +import User from '../users/entities/user.entity'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { diff --git a/src/authentication/requestWithUser.interface.ts b/src/authentication/requestWithUser.interface.ts index f942e8d..64cfd81 100644 --- a/src/authentication/requestWithUser.interface.ts +++ b/src/authentication/requestWithUser.interface.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import User from '../users/user.entity'; +import User from '../users/entities/user.entity'; interface RequestWithUser extends Request { user: User; diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts index 3499b76..2874846 100644 --- a/src/categories/categories.module.ts +++ b/src/categories/categories.module.ts @@ -1,9 +1,16 @@ -import { Module } from '@nestjs/common'; -import { CategoriesService } from './categories.service'; -import { CategoriesController } from './categories.controller'; +import {Module} from '@nestjs/common'; +import {CategoriesService} from './categories.service'; +import {CategoriesController} from './categories.controller'; +import {TypeOrmModule} from "@nestjs/typeorm"; +import Post from "../posts/entities/post.entity"; +import Category from "./entities/category.entity"; @Module({ - controllers: [CategoriesController], - providers: [CategoriesService], + controllers: [CategoriesController], + imports: [TypeOrmModule.forFeature([Category])], + + providers: [CategoriesService], + exports: [CategoriesService], }) -export class CategoriesModule {} +export class CategoriesModule { +} diff --git a/src/core/file.interface.ts b/src/core/file.interface.ts new file mode 100644 index 0000000..fdbf9ce --- /dev/null +++ b/src/core/file.interface.ts @@ -0,0 +1,11 @@ +export interface File { + fieldname: string; + originalname: string; + encoding: string; + mimetype: string; + size: number; + destination: string; + filename: string; + path: string; + buffer: Buffer; +} \ No newline at end of file diff --git a/src/core/s3-config.ts b/src/core/s3-config.ts new file mode 100644 index 0000000..9ebc443 --- /dev/null +++ b/src/core/s3-config.ts @@ -0,0 +1,21 @@ +export const BUCKET = 'nest-reno'; + + +export const IMAGES = 'images'; +export const FILES = 'files'; +export const THUMBNAILS = 'thumbnails'; + +export const ACCESSKEY = '004a8f97aa7c9b90000000004'; +export const SECRETKEY = 'K004lokMgbjY5cfVoFwrzl2gD4AW6yk'; + +const aws = require('aws-sdk'); + +aws.config.update({ + accessKeyId: ACCESSKEY, + secretAccessKey: SECRETKEY +}); + +const spacesEndpoint = new aws.Endpoint('s3.us-west-004.backblazeb2.com'); +export const s3 = new aws.S3({ + endpoint: spacesEndpoint +}); \ No newline at end of file diff --git a/src/files/file.module.ts b/src/files/file.module.ts new file mode 100644 index 0000000..1070a17 --- /dev/null +++ b/src/files/file.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { FilesService } from './files.service'; +import { ConfigModule } from '@nestjs/config'; +import PublicFile from './publicFile.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([PublicFile]), ConfigModule], + providers: [FilesService], + exports: [FilesService], +}) +export class FilesModule {} \ No newline at end of file diff --git a/src/files/files.service.ts b/src/files/files.service.ts new file mode 100644 index 0000000..d8064b8 --- /dev/null +++ b/src/files/files.service.ts @@ -0,0 +1,78 @@ +import {Injectable} from '@nestjs/common'; +import {InjectRepository} from '@nestjs/typeorm'; +import {Repository, QueryRunner} from 'typeorm'; +import PublicFile from './publicFile.entity'; +import {S3} from 'aws-sdk'; +import {ConfigService} from '@nestjs/config'; +import {v4 as uuid} from 'uuid'; +import {File} from "../core/file.interface"; +import {s3} from "../core/s3-config"; + +@Injectable() +export class FilesService { + constructor( + @InjectRepository(PublicFile) + private publicFilesRepository: Repository, + private readonly configService: ConfigService, + ) { + } + + async uploadPublicFile(dataBuffer: Buffer, filename: string) { + const uploadResult = await s3.upload({ + ACL: 'public-read', + Bucket: 'nest-reno', + Body: dataBuffer, + Key: filename, + }) + .promise(); + + + const newFile = this.publicFilesRepository.create({ + key: uploadResult.Key, + url: uploadResult.Location, + }); + await this.publicFilesRepository.save(newFile); + return newFile; + + } + + async uploadFile(file: File, folder: String) { + const uploadResult = await s3.upload({ + ACL: 'public-read', + Bucket: folder, + Body: file.buffer, + Key: file.originalname, + ContentType: file.mimetype, + }) + .promise(); + return uploadResult['Location']; + } + + async deletePublicFile(fileId: number) { + const file = await this.publicFilesRepository.findOneBy({id: fileId}); + await s3 + .deleteObject({ + Bucket: this.configService.get('S3_BUCKET'), + Key: file.key, + }) + .promise(); + await this.publicFilesRepository.delete(fileId); + } + + async deletePublicFileWithQueryRunner( + fileId: number, + queryRunner: QueryRunner, + ) { + const file = await queryRunner.manager.findOneBy(PublicFile, { + id: fileId, + }); + const s3 = new S3(); + await s3 + .deleteObject({ + Bucket: this.configService.get('S3_BUCKET'), + Key: file.key, + }) + .promise(); + await queryRunner.manager.delete(PublicFile, fileId); + } +} \ No newline at end of file diff --git a/src/files/publicFile.entity.ts b/src/files/publicFile.entity.ts new file mode 100644 index 0000000..8b28d3c --- /dev/null +++ b/src/files/publicFile.entity.ts @@ -0,0 +1,15 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +class PublicFile { + @PrimaryGeneratedColumn() + public id: number; + + @Column() + public url: string; + + @Column() + public key: string; +} + +export default PublicFile; \ No newline at end of file diff --git a/src/posts/entities/post.entity.ts b/src/posts/entities/post.entity.ts index 7f4ba36..eaa5bdc 100644 --- a/src/posts/entities/post.entity.ts +++ b/src/posts/entities/post.entity.ts @@ -1,5 +1,5 @@ import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, ManyToMany, JoinTable} from 'typeorm'; -import User from "../../users/user.entity"; +import User from "../../users/entities/user.entity"; import Category from "../../categories/entities/category.entity"; @Entity() diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 0a83378..d4bf308 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -1,9 +1,15 @@ -import { Module } from '@nestjs/common'; -import { PostsService } from './posts.service'; -import { PostsController } from './posts.controller'; +import {Module} from '@nestjs/common'; +import {PostsService} from './posts.service'; +import {PostsController} from './posts.controller'; +import {TypeOrmModule} from "@nestjs/typeorm"; +import User from "../users/entities/user.entity"; +import Post from "./entities/post.entity"; @Module({ - controllers: [PostsController], - providers: [PostsService], + controllers: [PostsController], + imports: [TypeOrmModule.forFeature([Post])], + providers: [PostsService], + exports: [PostsService], }) -export class PostsModule {} +export class PostsModule { +} diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index 52a6481..59f3da4 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -1,7 +1,7 @@ import {Injectable} from '@nestjs/common'; import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; -import User from "../users/user.entity"; +import User from "../users/entities/user.entity"; import {InjectRepository} from "@nestjs/typeorm"; import {Repository} from "typeorm"; import Post from './entities/post.entity'; diff --git a/src/users/address.entity.ts b/src/users/entities/address.entity.ts similarity index 100% rename from src/users/address.entity.ts rename to src/users/entities/address.entity.ts diff --git a/src/users/user.entity.ts b/src/users/entities/user.entity.ts similarity index 75% rename from src/users/user.entity.ts rename to src/users/entities/user.entity.ts index 8c29aff..2f21cdb 100644 --- a/src/users/user.entity.ts +++ b/src/users/entities/user.entity.ts @@ -1,7 +1,8 @@ import {Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany} from 'typeorm'; import {Exclude, Expose} from 'class-transformer'; import Address from "./address.entity"; -import Post from "../posts/entities/post.entity"; +import Post from "../../posts/entities/post.entity"; +import PublicFile from "../../files/publicFile.entity"; @Entity() class User { @@ -30,6 +31,15 @@ class User { @OneToMany(() => Post, (post: Post) => post.author) public posts: Post[]; + @JoinColumn() + @OneToOne( + () => PublicFile, + { + eager: true, + nullable: true + } + ) + public avatar?: PublicFile; @Column({ nullable: true, diff --git a/src/users/user.controller.ts b/src/users/user.controller.ts new file mode 100644 index 0000000..d7294ee --- /dev/null +++ b/src/users/user.controller.ts @@ -0,0 +1,21 @@ +import { Controller, Post, Req, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; +import JwtAuthenticationGuard from '../authentication/jwt-authentication.guard'; +import RequestWithUser from '../authentication/requestWithUser.interface'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { Express } from 'express'; +import {UsersService} from "./user.service"; +import 'multer'; + +@Controller('user') +export class UsersController { + constructor( + private readonly usersService: UsersService, + ) {} + + @Post('avatar') + @UseGuards(JwtAuthenticationGuard) + @UseInterceptors(FileInterceptor('file')) + async addAvatar(@Req() request: RequestWithUser, @UploadedFile() file: Express.Multer.File) { + return this.usersService.addAvatar(request.user.id, file.buffer, file.originalname); + } +} \ No newline at end of file diff --git a/src/users/user.module.ts b/src/users/user.module.ts index 87e399d..49e06e3 100644 --- a/src/users/user.module.ts +++ b/src/users/user.module.ts @@ -1,11 +1,20 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import User from './user.entity'; -import { UsersService } from './user.service'; +import User from './entities/user.entity'; +import { ConfigModule } from '@nestjs/config'; +import {UsersService} from "./user.service"; +import {UsersController} from "./user.controller"; +import {FilesModule} from "../files/file.module"; +import Address from "./entities/address.entity"; @Module({ - imports: [TypeOrmModule.forFeature([User])], + imports: [ + TypeOrmModule.forFeature([User, Address]), + ConfigModule, + FilesModule, + ], providers: [UsersService], exports: [UsersService], + controllers: [UsersController], }) -export class UsersModule {} +export class UsersModule {} \ No newline at end of file diff --git a/src/users/user.service.ts b/src/users/user.service.ts index 580ae4d..907635b 100644 --- a/src/users/user.service.ts +++ b/src/users/user.service.ts @@ -1,53 +1,66 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import User from './user.entity'; +import {HttpException, HttpStatus, Injectable} from '@nestjs/common'; +import {InjectRepository} from '@nestjs/typeorm'; +import {Repository} from 'typeorm'; +import User from './entities/user.entity'; import CreateUserDto from './dto/createUser.dto'; import * as bcrypt from 'bcrypt'; +import {FilesService} from "../files/files.service"; @Injectable() export class UsersService { - constructor( - @InjectRepository(User) - private usersRepository: Repository, - ) {} - - async getByEmail(email: string) { - const user = await this.usersRepository.findOne({ where: { email } }); - if (user) { - return user; + constructor( + @InjectRepository(User) + private usersRepository: Repository, + private readonly filesService: FilesService + ) { } - throw new HttpException( - 'User with this email does not exist', - HttpStatus.NOT_FOUND, - ); - } - async getById(id: number) { - const user = await this.usersRepository.findOne({ where: { id } }); - if (user) { - return user; + async getByEmail(email: string) { + const user = await this.usersRepository.findOne({where: {email}}); + if (user) { + return user; + } + throw new HttpException( + 'User with this email does not exist', + HttpStatus.NOT_FOUND, + ); } - throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND); - } - async create(userData: CreateUserDto) { - const newUser = this.usersRepository.create(userData); - await this.usersRepository.save(newUser); - return newUser; - } + async getById(id: number) { + const user = await this.usersRepository.findOne({where: {id}}); + if (user) { + return user; + } + throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND); + } + + async addAvatar(userId: number, imageBuffer: Buffer, filename: string) { + const avatar = await this.filesService.uploadPublicFile(imageBuffer, filename); + const user = await this.getById(userId); + await this.usersRepository.update(userId, { + ...user, + avatar + }); + return avatar; + } + + async create(userData: CreateUserDto) { + const newUser = this.usersRepository.create(userData); + await this.usersRepository.save(newUser); + return newUser; + } async setCurrentRefreshToken(refreshToken: string, userId: number) { - const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); - await this.usersRepository.update(userId, { - currentHashedRefreshToken, - }); - } + const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); + await this.usersRepository.update(userId, { + currentHashedRefreshToken, + }); + } async removeRefreshToken(userId: number) { - return this.usersRepository.update(userId, { - currentHashedRefreshToken: null, - }); - } + return this.usersRepository.update(userId, { + currentHashedRefreshToken: null, + }); + } }