upload file/avatar

This commit is contained in:
2025-05-15 16:33:51 +07:00
parent d97dfd25e2
commit ae7f2cd114
19 changed files with 648 additions and 64 deletions

377
package-lock.json generated
View File

@@ -24,17 +24,21 @@
"@types/hapi__joi": "^17.1.15", "@types/hapi__joi": "^17.1.15",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/uuid": "^10.0.0",
"aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"joi": "^17.13.3", "joi": "^17.13.3",
"multer": "^1.4.5-lts.2",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.15.6", "pg": "^8.15.6",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
@@ -46,6 +50,7 @@
"@swc/core": "^1.10.7", "@swc/core": "^1.10.7",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.26.0", "eslint": "^9.26.0",
@@ -3688,6 +3693,16 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "22.15.17", "version": "22.15.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz",
@@ -3812,6 +3827,12 @@
"@types/superagent": "^8.1.0" "@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": { "node_modules/@types/validator": {
"version": "13.15.0", "version": "13.15.0",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.0.tgz",
@@ -5197,6 +5218,78 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/b4a": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@@ -5635,6 +5728,24 @@
"node": ">=14.16" "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": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -6329,6 +6440,23 @@
"node": ">=10" "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -7355,6 +7483,21 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -7848,6 +7991,18 @@
"node": ">=8" "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": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -7864,7 +8019,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -8086,6 +8240,22 @@
"node": ">= 0.10" "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": { "node_modules/is-arrayish": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -8093,6 +8263,18 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -8138,6 +8320,24 @@
"node": ">=6" "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": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -8187,6 +8387,24 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "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": { "node_modules/is-stream": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
@@ -8200,6 +8418,21 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/is-unicode-supported": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "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" "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": { "node_modules/joi": {
"version": "17.13.3", "version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
@@ -10540,6 +10782,15 @@
"node": ">=4" "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": { "node_modules/postgres-array": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
@@ -10728,6 +10979,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -11098,12 +11358,35 @@
], ],
"license": "MIT" "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": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "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": { "node_modules/schema-utils": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@@ -11241,6 +11524,23 @@
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC" "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": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -12807,6 +13107,35 @@
"punycode": "^2.1.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -12831,7 +13160,6 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"uuid": "dist/esm/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
@@ -13154,6 +13482,27 @@
"node": ">= 8" "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": { "node_modules/wide-align": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -13277,6 +13626,28 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -35,17 +35,21 @@
"@types/hapi__joi": "^17.1.15", "@types/hapi__joi": "^17.1.15",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/uuid": "^10.0.0",
"aws-sdk": "^2.1692.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"joi": "^17.13.3", "joi": "^17.13.3",
"multer": "^1.4.5-lts.2",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.15.6", "pg": "^8.15.6",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/eslintrc": "^3.2.0",
@@ -57,6 +61,7 @@
"@swc/core": "^1.10.7", "@swc/core": "^1.10.7",
"@types/express": "^5.0.1", "@types/express": "^5.0.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.26.0", "eslint": "^9.26.0",

View File

@@ -8,6 +8,7 @@ import {UsersModule} from "./users/user.module";
import {AuthenticationModule} from "./authentication/authentication.module"; import {AuthenticationModule} from "./authentication/authentication.module";
import { PostsModule } from './posts/posts.module'; import { PostsModule } from './posts/posts.module';
import { CategoriesModule } from './categories/categories.module'; import { CategoriesModule } from './categories/categories.module';
import {FilesModule} from "./files/file.module";
@Module({ @Module({
imports: [ imports: [
@@ -32,12 +33,16 @@ import { CategoriesModule } from './categories/categories.module';
validationSchema: Joi.object({ validationSchema: Joi.object({
JWT_SECRET: Joi.string().required(), JWT_SECRET: Joi.string().required(),
JWT_EXPIRATION_TIME: 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, UsersModule,
AuthenticationModule, AuthenticationModule,
PostsModule, PostsModule,
CategoriesModule CategoriesModule,
FilesModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@@ -2,7 +2,7 @@ import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AuthenticationService } from './authentication.service'; import { AuthenticationService } from './authentication.service';
import User from '../users/user.entity'; import User from '../users/entities/user.entity';
@Injectable() @Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {

View File

@@ -1,5 +1,5 @@
import { Request } from 'express'; import { Request } from 'express';
import User from '../users/user.entity'; import User from '../users/entities/user.entity';
interface RequestWithUser extends Request { interface RequestWithUser extends Request {
user: User; user: User;

View File

@@ -1,9 +1,16 @@
import { Module } from '@nestjs/common'; import {Module} from '@nestjs/common';
import { CategoriesService } from './categories.service'; import {CategoriesService} from './categories.service';
import { CategoriesController } from './categories.controller'; import {CategoriesController} from './categories.controller';
import {TypeOrmModule} from "@nestjs/typeorm";
import Post from "../posts/entities/post.entity";
import Category from "./entities/category.entity";
@Module({ @Module({
controllers: [CategoriesController], controllers: [CategoriesController],
providers: [CategoriesService], imports: [TypeOrmModule.forFeature([Category])],
providers: [CategoriesService],
exports: [CategoriesService],
}) })
export class CategoriesModule {} export class CategoriesModule {
}

View File

@@ -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;
}

21
src/core/s3-config.ts Normal file
View File

@@ -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
});

12
src/files/file.module.ts Normal file
View File

@@ -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 {}

View File

@@ -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<PublicFile>,
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);
}
}

View File

@@ -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;

View File

@@ -1,5 +1,5 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn, ManyToMany, JoinTable} from 'typeorm'; 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"; import Category from "../../categories/entities/category.entity";
@Entity() @Entity()

View File

@@ -1,9 +1,15 @@
import { Module } from '@nestjs/common'; import {Module} from '@nestjs/common';
import { PostsService } from './posts.service'; import {PostsService} from './posts.service';
import { PostsController } from './posts.controller'; import {PostsController} from './posts.controller';
import {TypeOrmModule} from "@nestjs/typeorm";
import User from "../users/entities/user.entity";
import Post from "./entities/post.entity";
@Module({ @Module({
controllers: [PostsController], controllers: [PostsController],
providers: [PostsService], imports: [TypeOrmModule.forFeature([Post])],
providers: [PostsService],
exports: [PostsService],
}) })
export class PostsModule {} export class PostsModule {
}

View File

@@ -1,7 +1,7 @@
import {Injectable} from '@nestjs/common'; import {Injectable} from '@nestjs/common';
import {CreatePostDto} from './dto/create-post.dto'; import {CreatePostDto} from './dto/create-post.dto';
import {UpdatePostDto} from './dto/update-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 {InjectRepository} from "@nestjs/typeorm";
import {Repository} from "typeorm"; import {Repository} from "typeorm";
import Post from './entities/post.entity'; import Post from './entities/post.entity';

View File

@@ -1,7 +1,8 @@
import {Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany} from 'typeorm'; import {Column, Entity, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany} from 'typeorm';
import {Exclude, Expose} from 'class-transformer'; import {Exclude, Expose} from 'class-transformer';
import Address from "./address.entity"; 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() @Entity()
class User { class User {
@@ -30,6 +31,15 @@ class User {
@OneToMany(() => Post, (post: Post) => post.author) @OneToMany(() => Post, (post: Post) => post.author)
public posts: Post[]; public posts: Post[];
@JoinColumn()
@OneToOne(
() => PublicFile,
{
eager: true,
nullable: true
}
)
public avatar?: PublicFile;
@Column({ @Column({
nullable: true, nullable: true,

View File

@@ -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);
}
}

View File

@@ -1,11 +1,20 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import User from './user.entity'; import User from './entities/user.entity';
import { UsersService } from './user.service'; 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({ @Module({
imports: [TypeOrmModule.forFeature([User])], imports: [
TypeOrmModule.forFeature([User, Address]),
ConfigModule,
FilesModule,
],
providers: [UsersService], providers: [UsersService],
exports: [UsersService], exports: [UsersService],
controllers: [UsersController],
}) })
export class UsersModule {} export class UsersModule {}

View File

@@ -1,53 +1,66 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import {HttpException, HttpStatus, Injectable} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import {InjectRepository} from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import {Repository} from 'typeorm';
import User from './user.entity'; import User from './entities/user.entity';
import CreateUserDto from './dto/createUser.dto'; import CreateUserDto from './dto/createUser.dto';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
import {FilesService} from "../files/files.service";
@Injectable() @Injectable()
export class UsersService { export class UsersService {
constructor( constructor(
@InjectRepository(User) @InjectRepository(User)
private usersRepository: Repository<User>, private usersRepository: Repository<User>,
) {} private readonly filesService: FilesService
) {
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,
);
}
async getById(id: number) { async getByEmail(email: string) {
const user = await this.usersRepository.findOne({ where: { id } }); const user = await this.usersRepository.findOne({where: {email}});
if (user) { if (user) {
return 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) { async getById(id: number) {
const newUser = this.usersRepository.create(userData); const user = await this.usersRepository.findOne({where: {id}});
await this.usersRepository.save(newUser); if (user) {
return newUser; 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) { async setCurrentRefreshToken(refreshToken: string, userId: number) {
const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10);
await this.usersRepository.update(userId, { await this.usersRepository.update(userId, {
currentHashedRefreshToken, currentHashedRefreshToken,
}); });
} }
async removeRefreshToken(userId: number) { async removeRefreshToken(userId: number) {
return this.usersRepository.update(userId, { return this.usersRepository.update(userId, {
currentHashedRefreshToken: null, currentHashedRefreshToken: null,
}); });
} }
} }