Commit 9fccec2f authored by Nguyễn Thị Nguyệt Quế's avatar Nguyễn Thị Nguyệt Quế

Merge branch 'feat/upload_image_and_add_export_excel' into 'develop'

feat: add api cloudinary for upload image and add import/export excel

See merge request !14
parents 1bb3e639 8487c9d2
...@@ -5,4 +5,8 @@ HOSTNAMEDB = localhost ...@@ -5,4 +5,8 @@ HOSTNAMEDB = localhost
PORTDB = 5432 PORTDB = 5432
NAMEDB = mydatabase NAMEDB = mydatabase
USERNAMEDB = myuser USERNAMEDB = myuser
PASSWORDDB = example PASSWORDDB = example
\ No newline at end of file
CLOUD_NAME = cloudinary
API_KEY = api_key
API_SECRET = api_secret
\ No newline at end of file
...@@ -57,6 +57,7 @@ ...@@ -57,6 +57,7 @@
"@fast-csv/format": "^5.0.7", "@fast-csv/format": "^5.0.7",
"@types/node-cron": "^3.0.11", "@types/node-cron": "^3.0.11",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cloudinary": "^2.10.0",
"csv-parser": "^3.2.1", "csv-parser": "^3.2.1",
"dotenv": "^17.4.2", "dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
...@@ -67,12 +68,13 @@ ...@@ -67,12 +68,13 @@
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"nodemailer": "^8.0.7", "nodemailer": "^8.0.7",
"pg": "^8.20.0", "pg": "^8.20.0",
"tsx": "^4.21.0",
"pg-hstore": "^2.3.4", "pg-hstore": "^2.3.4",
"sequelize": "^6.37.8", "sequelize": "^6.37.8",
"sequelize-auto": "^0.8.8", "sequelize-auto": "^0.8.8",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1" "swagger-ui-express": "^5.0.1",
"tsx": "^4.21.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/bcrypt": "^6.0.0", "@types/bcrypt": "^6.0.0",
......
...@@ -17,6 +17,9 @@ importers: ...@@ -17,6 +17,9 @@ importers:
bcrypt: bcrypt:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
cloudinary:
specifier: ^2.10.0
version: 2.10.0
csv-parser: csv-parser:
specifier: ^3.2.1 specifier: ^3.2.1
version: 3.2.1 version: 3.2.1
...@@ -56,6 +59,9 @@ importers: ...@@ -56,6 +59,9 @@ importers:
sequelize-auto: sequelize-auto:
specifier: ^0.8.8 specifier: ^0.8.8
version: 0.8.8(sequelize@6.37.8(pg-hstore@2.3.4)(pg@8.20.0)) version: 0.8.8(sequelize@6.37.8(pg-hstore@2.3.4)(pg@8.20.0))
streamifier:
specifier: ^0.1.1
version: 0.1.1
swagger-jsdoc: swagger-jsdoc:
specifier: ^6.2.8 specifier: ^6.2.8
version: 6.2.8(openapi-types@12.1.3) version: 6.2.8(openapi-types@12.1.3)
...@@ -65,6 +71,9 @@ importers: ...@@ -65,6 +71,9 @@ importers:
tsx: tsx:
specifier: ^4.21.0 specifier: ^4.21.0
version: 4.21.0 version: 4.21.0
xlsx:
specifier: ^0.18.5
version: 0.18.5
devDependencies: devDependencies:
'@types/bcrypt': '@types/bcrypt':
specifier: ^6.0.0 specifier: ^6.0.0
...@@ -543,6 +552,10 @@ packages: ...@@ -543,6 +552,10 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
adler-32@1.3.1:
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
engines: {node: '>=0.8'}
ansi-regex@5.0.1: ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -664,6 +677,10 @@ packages: ...@@ -664,6 +677,10 @@ packages:
call-me-maybe@1.0.2: call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
cfb@1.2.2:
resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==}
engines: {node: '>=0.8'}
chokidar@1.7.0: chokidar@1.7.0:
resolution: {integrity: sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==} resolution: {integrity: sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==}
...@@ -674,6 +691,14 @@ packages: ...@@ -674,6 +691,14 @@ packages:
cliui@7.0.4: cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
cloudinary@2.10.0:
resolution: {integrity: sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==}
engines: {node: '>=9'}
codepage@1.15.0:
resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==}
engines: {node: '>=0.8'}
collection-visit@1.0.0: collection-visit@1.0.0:
resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -734,6 +759,11 @@ packages: ...@@ -734,6 +759,11 @@ packages:
resolution: {integrity: sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==} resolution: {integrity: sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==}
hasBin: true hasBin: true
crc-32@1.2.2:
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
engines: {node: '>=0.8'}
hasBin: true
create-require@1.1.1: create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
...@@ -921,6 +951,10 @@ packages: ...@@ -921,6 +951,10 @@ packages:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
frac@1.1.2:
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
engines: {node: '>=0.8'}
fragment-cache@0.2.1: fragment-cache@0.2.1:
resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -1646,6 +1680,10 @@ packages: ...@@ -1646,6 +1680,10 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
ssf@0.11.2:
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
engines: {node: '>=0.8'}
static-extend@0.1.2: static-extend@0.1.2:
resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
...@@ -1654,6 +1692,10 @@ packages: ...@@ -1654,6 +1692,10 @@ packages:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
streamifier@0.1.1:
resolution: {integrity: sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==}
engines: {node: '>=0.10'}
streamsearch@1.1.0: streamsearch@1.1.0:
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
...@@ -1799,6 +1841,14 @@ packages: ...@@ -1799,6 +1841,14 @@ packages:
wkx@0.5.0: wkx@0.5.0:
resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==} resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==}
wmf@1.0.2:
resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
engines: {node: '>=0.8'}
word@0.3.0:
resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==}
engines: {node: '>=0.8'}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
...@@ -1806,6 +1856,11 @@ packages: ...@@ -1806,6 +1856,11 @@ packages:
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
xlsx@0.18.5:
resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==}
engines: {node: '>=0.8'}
hasBin: true
xtend@4.0.2: xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'} engines: {node: '>=0.4'}
...@@ -2131,6 +2186,8 @@ snapshots: ...@@ -2131,6 +2186,8 @@ snapshots:
acorn@8.16.0: {} acorn@8.16.0: {}
adler-32@1.3.1: {}
ansi-regex@5.0.1: {} ansi-regex@5.0.1: {}
ansi-styles@4.3.0: ansi-styles@4.3.0:
...@@ -2271,6 +2328,11 @@ snapshots: ...@@ -2271,6 +2328,11 @@ snapshots:
call-me-maybe@1.0.2: {} call-me-maybe@1.0.2: {}
cfb@1.2.2:
dependencies:
adler-32: 1.3.1
crc-32: 1.2.2
chokidar@1.7.0: chokidar@1.7.0:
dependencies: dependencies:
anymatch: 1.3.2 anymatch: 1.3.2
...@@ -2299,6 +2361,12 @@ snapshots: ...@@ -2299,6 +2361,12 @@ snapshots:
strip-ansi: 6.0.1 strip-ansi: 6.0.1
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
cloudinary@2.10.0:
dependencies:
lodash: 4.18.1
codepage@1.15.0: {}
collection-visit@1.0.0: collection-visit@1.0.0:
dependencies: dependencies:
map-visit: 1.0.0 map-visit: 1.0.0
...@@ -2356,6 +2424,8 @@ snapshots: ...@@ -2356,6 +2424,8 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
crc-32@1.2.2: {}
create-require@1.1.1: {} create-require@1.1.1: {}
csv-parser@3.2.1: {} csv-parser@3.2.1: {}
...@@ -2609,6 +2679,8 @@ snapshots: ...@@ -2609,6 +2679,8 @@ snapshots:
forwarded@0.2.0: {} forwarded@0.2.0: {}
frac@1.1.2: {}
fragment-cache@0.2.1: fragment-cache@0.2.1:
dependencies: dependencies:
map-cache: 0.2.2 map-cache: 0.2.2
...@@ -3349,6 +3421,10 @@ snapshots: ...@@ -3349,6 +3421,10 @@ snapshots:
split2@4.2.0: {} split2@4.2.0: {}
ssf@0.11.2:
dependencies:
frac: 1.1.2
static-extend@0.1.2: static-extend@0.1.2:
dependencies: dependencies:
define-property: 0.2.5 define-property: 0.2.5
...@@ -3356,6 +3432,8 @@ snapshots: ...@@ -3356,6 +3432,8 @@ snapshots:
statuses@2.0.2: {} statuses@2.0.2: {}
streamifier@0.1.1: {}
streamsearch@1.1.0: {} streamsearch@1.1.0: {}
string-width@4.2.3: string-width@4.2.3:
...@@ -3504,6 +3582,10 @@ snapshots: ...@@ -3504,6 +3582,10 @@ snapshots:
dependencies: dependencies:
'@types/node': 25.7.0 '@types/node': 25.7.0
wmf@1.0.2: {}
word@0.3.0: {}
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0
...@@ -3512,6 +3594,16 @@ snapshots: ...@@ -3512,6 +3594,16 @@ snapshots:
wrappy@1.0.2: {} wrappy@1.0.2: {}
xlsx@0.18.5:
dependencies:
adler-32: 1.3.1
cfb: 1.2.2
codepage: 1.15.0
crc-32: 1.2.2
ssf: 0.11.2
wmf: 1.0.2
word: 0.3.0
xtend@4.0.2: {} xtend@4.0.2: {}
y18n@5.0.8: {} y18n@5.0.8: {}
......
import { v2 as cloudinary } from "cloudinary";
import "dotenv/config";
cloudinary.config({
cloud_name: process.env.CLOUD_NAME! as string,
api_key: process.env.API_KEY! as string,
api_secret: process.env.API_SECRET! as string,
});
export default cloudinary;
\ No newline at end of file
...@@ -2,11 +2,11 @@ import multer from "multer"; ...@@ -2,11 +2,11 @@ import multer from "multer";
const memoryStorage = multer.memoryStorage(); const memoryStorage = multer.memoryStorage();
const uploadCSV = multer({ const upload = multer({
storage: memoryStorage, storage: memoryStorage,
limits: { limits: {
fileSize: 2 * 1024 * 1024, // 2 MB fileSize: 2 * 1024 * 1024, // 2 MB
}, },
}); });
export default uploadCSV; export default upload;
\ No newline at end of file \ No newline at end of file
...@@ -25,9 +25,7 @@ export default (_express: Application) => { ...@@ -25,9 +25,7 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/LoginResponse"
* items:
* $ref: "#/components/schemas/LoginResponse"
*/ */
post: { post: {
handler: async (req: Req, res: Res) => { handler: async (req: Req, res: Res) => {
......
import { Req, Res } from "#interfaces/IApi";
import { AuthService } from "#services/authService";
import { Application } from "express"
import { Resource } from "express-automatic-routes"
export default (_express: Application) => {
const authService = new AuthService();
return <Resource>{
/**
* @openapi
* /api/v1.0/auth/refresh:
* post:
* tags: [Auth]
* description: lấy token mới bằng refresh token
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/RefreshInput"
* responses:
* 200:
* description: Lấy token mới thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/RefreshResponse"
*/
post: {
handler: async (req: Req, res: Res) => {
try {
const result = await authService.refresh(req.body.refresh_token);
return res.sendOk({ data: result });
} catch (error) {
console.error('Error refreshing token:', error);
return res.sendError({
message: "Làm mới token thất bại",
message_en: "Failed to refresh token",
status: 500
});
}
}
}
}
}
\ No newline at end of file
...@@ -18,16 +18,14 @@ export default (_express: Application) => { ...@@ -18,16 +18,14 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: "#/components/schemas/CreateUserInput" * $ref: "#/components/schemas/RegisterInput"
* responses: * responses:
* 201: * 201:
* description: Tạo người dùng mới thành công * description: Tạo người dùng mới thành công
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/RegisterResponse"
* items:
* $ref: "#/components/schemas/Register"
*/ */
post: { post: {
handler: async (req: Req, res: Res) => { handler: async (req: Req, res: Res) => {
......
...@@ -24,9 +24,7 @@ export default (_express: Application) => { ...@@ -24,9 +24,7 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/sendOtpResponse"
* items:
* $ref: "#/components/schemas/sendOtpResponse"
*/ */
post: { post: {
middleware: [authMiddleware], middleware: [authMiddleware],
......
...@@ -28,9 +28,7 @@ export default (_express: Application) => { ...@@ -28,9 +28,7 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/verifyOtpResponse"
* items:
* $ref: "#/components/schemas/verifyOtpResponse"
*/ */
post: { post: {
middleware: [authMiddleware], middleware: [authMiddleware],
......
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
import * as fastcsv from '@fast-csv/format';
import { Req, Res } from "#interfaces/IApi"; import { Req, Res } from "#interfaces/IApi";
import { CoursesProvider } from "#providers/CoursesProvider"; import { CoursesProvider } from "#providers/CoursesProvider";
import queryModifier from "#middlewares/request"; import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication"; import { DownloadService } from "#services/downloadService";
import { authorize } from "#middlewares/authorization";
export default (_express: Application) => { export default (_express: Application) => {
const coursesProvider = new CoursesProvider(); const coursesProvider = new CoursesProvider();
const downloadService = new DownloadService();
return <Resource>{ return <Resource>{
/** /**
* @openapi * @openapi
* /api/v1.0/courses/export: * /api/v1.0/courses/export/csv:
* get: * get:
* tags: [Courses] * tags: [Courses]
* description: Xuất danh sách các khoá học ra file CSV. * description: Xuất danh sách các khoá học ra file CSV.
...@@ -31,36 +30,13 @@ export default (_express: Application) => { ...@@ -31,36 +30,13 @@ export default (_express: Application) => {
handler: async (req: Req, res: Res) => { handler: async (req: Req, res: Res) => {
try { try {
const filename = `courses-export-${Date.now()}.csv`;
const courses = await coursesProvider.exportCourses(req.payload); const courses = await coursesProvider.exportCourses(req.payload);
const { filename, buffer } = await downloadService.downloadCsv(courses);
res.setHeader('Content-Type', 'text/csv; charset=utf-8'); res.setHeader('Content-Type', 'text/csv; charset=utf-8');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.write('\uFEFF'); return res.send(buffer);
const csvStream = fastcsv.format({
headers: true,
alwaysWriteHeaders: true
});
csvStream.pipe(res);
for (const course of courses) {
const rowData = {
"Id": course.id,
"Name": course.name,
"Description": course.description,
"Status": course.status,
"Created At": course.created_at,
"Created By": course.created_by
};
csvStream.write(rowData);
}
csvStream.end();
} catch (error) { } catch (error) {
console.error('Error exporting courses:', error); console.error('Error exporting courses:', error);
return res.sendError({ return res.sendError({
...@@ -70,6 +46,6 @@ export default (_express: Application) => { ...@@ -70,6 +46,6 @@ export default (_express: Application) => {
}); });
} }
} }
} },
} }
} }
\ No newline at end of file
import { Application } from "express";
import { Resource } from "express-automatic-routes";
import { Req, Res } from "#interfaces/IApi";
import { CoursesProvider } from "#providers/CoursesProvider";
import queryModifier from "#middlewares/request";
import { DownloadService } from "#services/downloadService";
export default (_express: Application) => {
const coursesProvider = new CoursesProvider();
const downloadService = new DownloadService();
return <Resource>{
/**
* @openapi
* /api/v1.0/courses/export/excel:
* get:
* tags: [Courses]
* description: Xuất danh sách các khoá học ra file Excel.
* parameters:
* - $ref: '#/components/parameters/filters'
* - $ref: '#/components/parameters/sort'
* - $ref: '#/components/parameters/page'
* - $ref: '#/components/parameters/pageSize'
* responses:
* 200:
* description: File Excel được tạo thành công
*/
get: {
middleware: [queryModifier],
handler: async (req: Req, res: Res) => {
try {
const courses = await coursesProvider.exportCourses(req.payload);
const { filename, buffer } = await downloadService.downloadExcel(courses);
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
return res.send(buffer);
} catch (error) {
console.error('Error exporting courses:', error);
return res.sendError({
message: "Lỗi khi xuất khóa học!",
message_en: "Error occurred while exporting courses!",
status: 500,
});
}
}
},
}
}
\ No newline at end of file
import { Application } from "express"; import { Application } from "express";
import { Readable } from 'stream'; import { Readable } from 'stream';
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
import uploadCSV from "#config/multer.config"; import upload from "#config/multer.config";
import csv from 'csv-parser'; import csv from 'csv-parser';
import { Req, Res } from "#interfaces/IApi"; import { Req, Res } from "#interfaces/IApi";
import { CoursesProvider } from "#providers/CoursesProvider"; import { CoursesProvider } from "#providers/CoursesProvider";
import * as XLSX from 'xlsx';
export default (_express: Application) => { export default (_express: Application) => {
const coursesProvider = new CoursesProvider(); const coursesProvider = new CoursesProvider();
...@@ -38,7 +39,7 @@ export default (_express: Application) => { ...@@ -38,7 +39,7 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/CourseResponse" * $ref: "#/components/schemas/CourseResponse"
*/ */
post: { post: {
middleware: [uploadCSV.single('file')], middleware: [upload.single('file')],
handler: async (req: Req, res: Res) => { handler: async (req: Req, res: Res) => {
try { try {
...@@ -50,30 +51,63 @@ export default (_express: Application) => { ...@@ -50,30 +51,63 @@ export default (_express: Application) => {
}); });
} }
// csv đọc file từ buffer và chuyển đổi const mimeType = req.file.mimetype;
const results = await new Promise<any[]>((resolve, reject) => { let parsedData: any[] = [];
const rows: any[] = [];
const stream = Readable.from(req.file!.buffer);
stream // check csv
.pipe(csv()) if (mimeType === 'text/csv' || mimeType === 'application/vnd.ms-excel') {
.on('data', (row) => { parsedData = await new Promise<any[]>((resolve, reject) => {
rows.push(row); const rows: any[] = [];
}) const stream = Readable.from(req.file!.buffer);
.on('end', () => {
resolve(rows); stream
}) .pipe(csv())
.on('error', (error) => { .on('data', (row) => rows.push(row))
reject(error); .on('end', () => resolve(rows))
.on('error', (error) => reject(error));
});
}
// check excel
else if (mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
const workbook = XLSX.read(req.file.buffer, { type: 'buffer' });
const firstSheetName = workbook.SheetNames[0];
if (!firstSheetName) {
return res.sendError({
message: "Đã có lỗi xảy ra khi đọc file!",
message_en: "Error occurred while reading the file!",
status: 400
}); });
}); }
const worksheet = workbook.Sheets[firstSheetName];
if (!worksheet) {
return res.sendError({
message: "Đã có lỗi xảy ra khi đọc file!",
message_en: "Error occurred while reading the file!",
status: 400
});
}
parsedData = XLSX.utils.sheet_to_json(worksheet);
}
else {
return res.sendError({
message: "Định dạng file không được hỗ trợ! Vui lòng tải lên file .csv hoặc .xlsx",
message_en: "Unsupported file format! Please upload .csv or .xlsx file.",
status: 400
});
}
const importedCourses = await coursesProvider.importCourses(results); const results = await coursesProvider.importCourses(parsedData);
return res.sendOk({ return res.sendOk({
message: "Khóa học đã được import thành công!", message: "Khóa học đã được import thành công!",
message_en: "Courses imported successfully!", message_en: "Courses imported successfully!",
data: importedCourses data: results
}); });
} catch (error) { } catch (error) {
......
import { Req, Res } from "#interfaces/IApi";
import { Application } from "express";
import { Resource } from "express-automatic-routes";
import queryModifier from "#middlewares/request";
import { authMiddleware } from "#middlewares/authentication";
import upload from "#config/multer.config";
import { UploadService } from "#services/uploadService";
export default (_express: Application) => {
const uploadService = new UploadService();
return <Resource>{
/**
* @openapi
* /api/v1.0/upload:
* post:
* tags: [Upload]
* security:
* - bearerAuth: []
* description: Upload file ảnh.
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* required:
* - image
* properties:
* image:
* type: string
* format: binary
* description: File ảnh (Giới hạn 2MB)
* responses:
* 201:
* description: Upload ảnh thành công
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/UploadResponse"
*/
post: {
middleware: [authMiddleware, queryModifier, upload.single("image")],
handler: async (req: Req, res: Res) => {
try {
if (!req.file) {
return res.sendError({
message: "Không có file nào được tải lên!",
message_en: "No file uploaded!",
status: 400
});
}
if (!req.file.mimetype.startsWith("image/")) {
return res.sendError({
message: "Chỉ cho phép upload ảnh!",
message_en: "Only image uploads are allowed!",
status: 500
});
}
const result = await uploadService.uploadImage(req.file.buffer);
res.sendOk({
data: { width: result?.width, height: result?.height, bytes: result?.bytes, url: result?.url },
message: "Upload ảnh thành công",
message_en: "Image uploaded successfully",
status: 200
});
} catch (error) {
console.error("Error uploading image:", error);
res.sendError({
message: "Upload ảnh thất bại",
message_en: "Failed to upload image",
status: 500
});
}
}
}
};
};
\ No newline at end of file
...@@ -93,21 +93,7 @@ ...@@ -93,21 +93,7 @@
"example": "2024-02-26 03:12:45" "example": "2024-02-26 03:12:45"
}, },
"violations": { "violations": {
"type": "array", "type": "array"
"items": {
"type": "object",
"properties": {
"code": {
"type": "number"
},
"message": {
"type": "string"
},
"action": {
"nullable": true
}
}
}
} }
} }
}, },
...@@ -319,6 +305,7 @@ ...@@ -319,6 +305,7 @@
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Khóa học 12A1", "name": "Khóa học 12A1",
"description": "Khóa học chuyên toán", "description": "Khóa học chuyên toán",
"thumbnail_url": "https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg",
"status": "active", "status": "active",
"created_at": "2026-05-16T08:00:00.000Z", "created_at": "2026-05-16T08:00:00.000Z",
"created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11" "created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
...@@ -335,6 +322,11 @@ ...@@ -335,6 +322,11 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
}, },
"thumbnail_url": {
"type": "string",
"format": "url",
"nullable": true
},
"status": { "status": {
"type": "string", "type": "string",
"nullable": true "nullable": true
...@@ -450,6 +442,12 @@ ...@@ -450,6 +442,12 @@
"nullable": true, "nullable": true,
"example": "Khóa học chuyên toán" "example": "Khóa học chuyên toán"
}, },
"thumbnail_url": {
"type": "string",
"format": "url",
"nullable": true,
"example": "https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg"
},
"status": { "status": {
"type": "string", "type": "string",
"nullable": true, "nullable": true,
...@@ -457,7 +455,7 @@ ...@@ -457,7 +455,7 @@
} }
} }
}, },
"Register": { "RegisterResponse": {
"type": "object", "type": "object",
"example": { "example": {
"id": "123", "id": "123",
...@@ -465,23 +463,47 @@ ...@@ -465,23 +463,47 @@
"email": "phamquangbao@example.com" "email": "phamquangbao@example.com"
}, },
"properties": { "properties": {
"id": { "message": {
"type": "uuid", "type": "string",
"format": "uuid", "nullable": true
"example": "123"
}, },
"name": { "message_en": {
"type": "string", "type": "string",
"example": "Pham Quang Bao" "nullable": true
}, },
"email": { "responseData": {
"type": "object",
"properties": {
"id": {
"type": "uuid",
"format": "uuid",
"example": "123"
},
"name": {
"type": "string",
"example": "Pham Quang Bao"
},
"email": {
"type": "string",
"format": "email",
"example": "phamquangbao@example.com"
}
}
},
"status": {
"type": "string", "type": "string",
"format": "email", "example": "success"
"example": "phamquangbao@example.com" },
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
} }
} }
}, },
"CreateUserInput": { "RegisterInput": {
"type": "object", "type": "object",
"example": { "example": {
"name": "Pham Quang Bao", "name": "Pham Quang Bao",
...@@ -506,13 +528,38 @@ ...@@ -506,13 +528,38 @@
}, },
"LoginResponse": { "LoginResponse": {
"type": "object", "type": "object",
"example": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567"
},
"properties": { "properties": {
"accessToken": { "message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"responseData": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
"example": "string"
},
"refresh_token": {
"type": "string",
"example": "string"
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string", "type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567" "example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
} }
} }
}, },
...@@ -549,9 +596,7 @@ ...@@ -549,9 +596,7 @@
}, },
"status": "success | fail", "status": "success | fail",
"timeStamp": "2024-02-26 03:12:45", "timeStamp": "2024-02-26 03:12:45",
"violation": [ "violation": []
{}
]
}, },
"properties": { "properties": {
"message": { "message": {
...@@ -914,6 +959,114 @@ ...@@ -914,6 +959,114 @@
"type": "array" "type": "array"
} }
} }
},
"RefreshInput": {
"type": "object",
"example": {
"refresh_token": "string"
},
"properties": {
"refresh_token": {
"type": "uuid",
"format": "uuid",
"example": "string"
}
}
},
"RefreshResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"responseData": {
"type": "object",
"properties": {
"access_token": {
"type": "string",
"example": "string"
},
"refresh_token": {
"type": "string",
"example": "string"
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
}
}
},
"UploadResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"nullable": true
},
"message_en": {
"type": "string",
"nullable": true
},
"responseData": {
"type": "object",
"properties": {
"width": {
"type": "number",
"example": 800
},
"height": {
"type": "number",
"example": 600
},
"bytes": {
"type": "number",
"example": 150000
},
"url": {
"type": "string",
"example": "https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg"
}
}
},
"status": {
"type": "string",
"example": "success"
},
"timeStamp": {
"type": "string",
"example": "2024-02-26 03:12:45"
},
"violations": {
"type": "array"
}
}
},
"UploadInput": {
"type": "object",
"example": {
"file": "image.jpg"
},
"properties": {
"file": {
"type": "string",
"format": "binary",
"example": "image.jpg"
}
}
} }
}, },
"parameters": { "parameters": {
...@@ -974,10 +1127,7 @@ ...@@ -974,10 +1127,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/LoginResponse"
"items": {
"$ref": "#/components/schemas/LoginResponse"
}
} }
} }
} }
...@@ -1035,6 +1185,36 @@ ...@@ -1035,6 +1185,36 @@
} }
} }
}, },
"/api/v1.0/auth/refresh": {
"post": {
"tags": [
"Auth"
],
"description": "lấy token mới bằng refresh token",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshInput"
}
}
}
},
"responses": {
"200": {
"description": "Lấy token mới thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RefreshResponse"
}
}
}
}
}
}
},
"/api/v1.0/auth/register": { "/api/v1.0/auth/register": {
"post": { "post": {
"tags": [ "tags": [
...@@ -1046,7 +1226,7 @@ ...@@ -1046,7 +1226,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/CreateUserInput" "$ref": "#/components/schemas/RegisterInput"
} }
} }
} }
...@@ -1057,10 +1237,7 @@ ...@@ -1057,10 +1237,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/RegisterResponse"
"items": {
"$ref": "#/components/schemas/Register"
}
} }
} }
} }
...@@ -1085,10 +1262,7 @@ ...@@ -1085,10 +1262,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/sendOtpResponse"
"items": {
"$ref": "#/components/schemas/sendOtpResponse"
}
} }
} }
} }
...@@ -1123,10 +1297,7 @@ ...@@ -1123,10 +1297,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/verifyOtpResponse"
"items": {
"$ref": "#/components/schemas/verifyOtpResponse"
}
} }
} }
} }
...@@ -1460,7 +1631,7 @@ ...@@ -1460,7 +1631,7 @@
} }
} }
}, },
"/api/v1.0/courses/export": { "/api/v1.0/courses/export/csv": {
"get": { "get": {
"tags": [ "tags": [
"Courses" "Courses"
...@@ -1487,6 +1658,33 @@ ...@@ -1487,6 +1658,33 @@
} }
} }
}, },
"/api/v1.0/courses/export/excel": {
"get": {
"tags": [
"Courses"
],
"description": "Xuất danh sách các khoá học ra file Excel.",
"parameters": [
{
"$ref": "#/components/parameters/filters"
},
{
"$ref": "#/components/parameters/sort"
},
{
"$ref": "#/components/parameters/page"
},
{
"$ref": "#/components/parameters/pageSize"
}
],
"responses": {
"200": {
"description": "File Excel được tạo thành công"
}
}
}
},
"/api/v1.0/courses/import": { "/api/v1.0/courses/import": {
"post": { "post": {
"tags": [ "tags": [
...@@ -1767,6 +1965,51 @@ ...@@ -1767,6 +1965,51 @@
} }
} }
} }
},
"/api/v1.0/upload": {
"post": {
"tags": [
"Upload"
],
"security": [
{
"bearerAuth": []
}
],
"description": "Upload file ảnh.",
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"required": [
"image"
],
"properties": {
"image": {
"type": "string",
"format": "binary",
"description": "File ảnh (Giới hạn 2MB)"
}
}
}
}
}
},
"responses": {
"201": {
"description": "Upload ảnh thành công",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UploadResponse"
}
}
}
}
}
}
} }
}, },
"tags": [] "tags": []
......
...@@ -24,5 +24,5 @@ emailCronJob.start(); ...@@ -24,5 +24,5 @@ emailCronJob.start();
app.use('/swagger/index', swaggerUi.serve, swaggerUi.setup(swaggerFile)); app.use('/swagger/index', swaggerUi.serve, swaggerUi.setup(swaggerFile));
app.listen(port, () => { app.listen(port, () => {
console.log(`App listening on port ${port}`) console.log(`App listening on http://localhost:${port}/swagger/index/`)
}) })
...@@ -5,7 +5,6 @@ import { JWTPayload, Req, Res } from '#interfaces/IApi'; ...@@ -5,7 +5,6 @@ import { JWTPayload, Req, Res } from '#interfaces/IApi';
export const authMiddleware = (req: Req, res: Res, next: NextFunction) => { export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
const JWT_SECRET = process.env.JWT_SECRET || ''; const JWT_SECRET = process.env.JWT_SECRET || '';
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { if (!authHeader || !authHeader.startsWith('Bearer ')) {
...@@ -17,7 +16,6 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => { ...@@ -17,7 +16,6 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
} }
const token = authHeader.split(' ')[1]; const token = authHeader.split(' ')[1];
if (!token) { if (!token) {
return res.sendError({ return res.sendError({
message: 'Không tìm thấy token', message: 'Không tìm thấy token',
...@@ -28,14 +26,15 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => { ...@@ -28,14 +26,15 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
try { try {
const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload; const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload;
req.user = decoded;
req.user = decoded;
return next(); return next();
} catch (error) { } catch (error) {
return res.sendError({ return res.sendError({
message: 'Token không hợp lệ hoặc đã hết hạn', message: 'Token không hợp lệ hoặc đã hết hạn',
message_en: 'Invalid or expired token', message_en: 'Invalid or expired token',
status: 403 status: 401
}); });
} }
}; };
\ No newline at end of file
...@@ -9,11 +9,12 @@ export interface coursesAttributes { ...@@ -9,11 +9,12 @@ export interface coursesAttributes {
created_at?: Date; created_at?: Date;
created_by?: string; created_by?: string;
status?: string; status?: string;
thumbnail_url?: string;
} }
export type coursesPk = "id"; export type coursesPk = "id";
export type coursesId = courses[coursesPk]; export type coursesId = courses[coursesPk];
export type coursesOptionalAttributes = "id" | "name" | "description" | "created_at" | "created_by" | "status"; export type coursesOptionalAttributes = "id" | "name" | "description" | "created_at" | "created_by" | "status" | "thumbnail_url";
export type coursesCreationAttributes = Optional<coursesAttributes, coursesOptionalAttributes>; export type coursesCreationAttributes = Optional<coursesAttributes, coursesOptionalAttributes>;
export class courses extends Model<coursesAttributes, coursesCreationAttributes> implements coursesAttributes { export class courses extends Model<coursesAttributes, coursesCreationAttributes> implements coursesAttributes {
...@@ -23,6 +24,7 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes> ...@@ -23,6 +24,7 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes>
created_at?: Date; created_at?: Date;
created_by?: string; created_by?: string;
status?: string; status?: string;
thumbnail_url?: string;
// courses hasMany classes via course_id // courses hasMany classes via course_id
classes!: classes[]; classes!: classes[];
...@@ -65,6 +67,10 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes> ...@@ -65,6 +67,10 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes>
status: { status: {
type: DataTypes.STRING(50), type: DataTypes.STRING(50),
allowNull: true allowNull: true
},
thumbnail_url: {
type: DataTypes.TEXT,
allowNull: true
} }
}, { }, {
tableName: 'courses', tableName: 'courses',
......
...@@ -5,6 +5,8 @@ import { courses as _courses } from "./courses"; ...@@ -5,6 +5,8 @@ import { courses as _courses } from "./courses";
import type { coursesAttributes, coursesCreationAttributes } from "./courses"; import type { coursesAttributes, coursesCreationAttributes } from "./courses";
import { enrollments as _enrollments } from "./enrollments"; import { enrollments as _enrollments } from "./enrollments";
import type { enrollmentsAttributes, enrollmentsCreationAttributes } from "./enrollments"; import type { enrollmentsAttributes, enrollmentsCreationAttributes } from "./enrollments";
import { refresh_token as _refresh_token } from "./refresh_token";
import type { refresh_tokenAttributes, refresh_tokenCreationAttributes } from "./refresh_token";
import { roles as _roles } from "./roles"; import { roles as _roles } from "./roles";
import type { rolesAttributes, rolesCreationAttributes } from "./roles"; import type { rolesAttributes, rolesCreationAttributes } from "./roles";
import { user_auth as _user_auth } from "./user_auth"; import { user_auth as _user_auth } from "./user_auth";
...@@ -16,6 +18,7 @@ export { ...@@ -16,6 +18,7 @@ export {
_classes as classes, _classes as classes,
_courses as courses, _courses as courses,
_enrollments as enrollments, _enrollments as enrollments,
_refresh_token as refresh_token,
_roles as roles, _roles as roles,
_user_auth as user_auth, _user_auth as user_auth,
_users as users, _users as users,
...@@ -28,6 +31,8 @@ export type { ...@@ -28,6 +31,8 @@ export type {
coursesCreationAttributes, coursesCreationAttributes,
enrollmentsAttributes, enrollmentsAttributes,
enrollmentsCreationAttributes, enrollmentsCreationAttributes,
refresh_tokenAttributes,
refresh_tokenCreationAttributes,
rolesAttributes, rolesAttributes,
rolesCreationAttributes, rolesCreationAttributes,
user_authAttributes, user_authAttributes,
...@@ -40,6 +45,7 @@ export function initModels(sequelize: Sequelize) { ...@@ -40,6 +45,7 @@ export function initModels(sequelize: Sequelize) {
const classes = _classes.initModel(sequelize); const classes = _classes.initModel(sequelize);
const courses = _courses.initModel(sequelize); const courses = _courses.initModel(sequelize);
const enrollments = _enrollments.initModel(sequelize); const enrollments = _enrollments.initModel(sequelize);
const refresh_token = _refresh_token.initModel(sequelize);
const roles = _roles.initModel(sequelize); const roles = _roles.initModel(sequelize);
const user_auth = _user_auth.initModel(sequelize); const user_auth = _user_auth.initModel(sequelize);
const users = _users.initModel(sequelize); const users = _users.initModel(sequelize);
...@@ -50,6 +56,8 @@ export function initModels(sequelize: Sequelize) { ...@@ -50,6 +56,8 @@ export function initModels(sequelize: Sequelize) {
courses.hasMany(classes, { as: "classes", foreignKey: "course_id"}); courses.hasMany(classes, { as: "classes", foreignKey: "course_id"});
users.belongsTo(roles, { as: "role", foreignKey: "role_id"}); users.belongsTo(roles, { as: "role", foreignKey: "role_id"});
roles.hasMany(users, { as: "users", foreignKey: "role_id"}); roles.hasMany(users, { as: "users", foreignKey: "role_id"});
refresh_token.belongsTo(user_auth, { as: "user_auth", foreignKey: "user_auth_id"});
user_auth.hasMany(refresh_token, { as: "refresh_tokens", foreignKey: "user_auth_id"});
enrollments.belongsTo(users, { as: "user", foreignKey: "user_id"}); enrollments.belongsTo(users, { as: "user", foreignKey: "user_id"});
users.hasMany(enrollments, { as: "enrollments", foreignKey: "user_id"}); users.hasMany(enrollments, { as: "enrollments", foreignKey: "user_id"});
user_auth.belongsTo(users, { as: "user", foreignKey: "user_id"}); user_auth.belongsTo(users, { as: "user", foreignKey: "user_id"});
...@@ -59,6 +67,7 @@ export function initModels(sequelize: Sequelize) { ...@@ -59,6 +67,7 @@ export function initModels(sequelize: Sequelize) {
classes: classes, classes: classes,
courses: courses, courses: courses,
enrollments: enrollments, enrollments: enrollments,
refresh_token: refresh_token,
roles: roles, roles: roles,
user_auth: user_auth, user_auth: user_auth,
users: users, users: users,
......
import * as Sequelize from 'sequelize';
import { DataTypes, Model, Optional } from 'sequelize';
import type { user_auth, user_authId } from './user_auth';
export interface refresh_tokenAttributes {
id: string;
refresh_tokens: string;
device_info?: string;
user_auth_id: string;
expires_at: Date;
}
export type refresh_tokenPk = "id";
export type refresh_tokenId = refresh_token[refresh_tokenPk];
export type refresh_tokenOptionalAttributes = "id" | "device_info";
export type refresh_tokenCreationAttributes = Optional<refresh_tokenAttributes, refresh_tokenOptionalAttributes>;
export class refresh_token extends Model<refresh_tokenAttributes, refresh_tokenCreationAttributes> implements refresh_tokenAttributes {
id!: string;
refresh_tokens!: string;
device_info?: string;
user_auth_id!: string;
expires_at!: Date;
// refresh_token belongsTo user_auth via user_auth_id
user_auth!: user_auth;
getUser_auth!: Sequelize.BelongsToGetAssociationMixin<user_auth>;
setUser_auth!: Sequelize.BelongsToSetAssociationMixin<user_auth, user_authId>;
createUser_auth!: Sequelize.BelongsToCreateAssociationMixin<user_auth>;
static initModel(sequelize: Sequelize.Sequelize): typeof refresh_token {
return sequelize.define('refresh_token', {
id: {
type: DataTypes.UUID,
allowNull: false,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
refresh_tokens: {
type: DataTypes.TEXT,
allowNull: false
},
device_info: {
type: DataTypes.STRING(255),
allowNull: true
},
user_auth_id: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'user_auth',
key: 'id'
}
},
expires_at: {
type: DataTypes.DATE,
allowNull: false
}
}, {
tableName: 'refresh_token',
schema: 'public',
timestamps: false
}) as typeof refresh_token;
}
}
import * as Sequelize from 'sequelize'; import * as Sequelize from 'sequelize';
import { DataTypes, Model, Optional } from 'sequelize'; import { DataTypes, Model, Optional } from 'sequelize';
import type { refresh_token, refresh_tokenId } from './refresh_token';
import type { users, usersId } from './users'; import type { users, usersId } from './users';
export interface user_authAttributes { export interface user_authAttributes {
...@@ -26,6 +27,18 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri ...@@ -26,6 +27,18 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri
otp_expiry?: Date; otp_expiry?: Date;
active?: boolean; active?: boolean;
// user_auth hasMany refresh_token via user_auth_id
refresh_tokens!: refresh_token[];
getRefresh_tokens!: Sequelize.HasManyGetAssociationsMixin<refresh_token>;
setRefresh_tokens!: Sequelize.HasManySetAssociationsMixin<refresh_token, refresh_tokenId>;
addRefresh_token!: Sequelize.HasManyAddAssociationMixin<refresh_token, refresh_tokenId>;
addRefresh_tokens!: Sequelize.HasManyAddAssociationsMixin<refresh_token, refresh_tokenId>;
createRefresh_token!: Sequelize.HasManyCreateAssociationMixin<refresh_token>;
removeRefresh_token!: Sequelize.HasManyRemoveAssociationMixin<refresh_token, refresh_tokenId>;
removeRefresh_tokens!: Sequelize.HasManyRemoveAssociationsMixin<refresh_token, refresh_tokenId>;
hasRefresh_token!: Sequelize.HasManyHasAssociationMixin<refresh_token, refresh_tokenId>;
hasRefresh_tokens!: Sequelize.HasManyHasAssociationsMixin<refresh_token, refresh_tokenId>;
countRefresh_tokens!: Sequelize.HasManyCountAssociationsMixin;
// user_auth belongsTo users via user_id // user_auth belongsTo users via user_id
user!: users; user!: users;
getUser!: Sequelize.BelongsToGetAssociationMixin<users>; getUser!: Sequelize.BelongsToGetAssociationMixin<users>;
......
...@@ -4,6 +4,7 @@ import { models } from '#models/sequelize-config'; ...@@ -4,6 +4,7 @@ import { models } from '#models/sequelize-config';
interface CreateCourseInput { interface CreateCourseInput {
name: string; name: string;
description: string; description: string;
thumbnail_url?: string;
status?: string; status?: string;
} }
...@@ -32,6 +33,7 @@ export class CoursesProvider { ...@@ -32,6 +33,7 @@ export class CoursesProvider {
async createCourse(input: CreateCourseInput) { async createCourse(input: CreateCourseInput) {
const course = await models.courses.create( const course = await models.courses.create(
{ {
thumbnail_url: input.thumbnail_url || '',
name: input.name, name: input.name,
description: input.description, description: input.description,
status: input.status || "active" status: input.status || "active"
...@@ -45,6 +47,7 @@ export class CoursesProvider { ...@@ -45,6 +47,7 @@ export class CoursesProvider {
{ {
name: data.name, name: data.name,
description: data.description, description: data.description,
thumbnail_url: data.thumbnail_url || '',
status: data.status || "active" status: data.status || "active"
}, },
{ {
...@@ -69,7 +72,7 @@ export class CoursesProvider { ...@@ -69,7 +72,7 @@ export class CoursesProvider {
async exportCourses(params: payload) { async exportCourses(params: payload) {
const exportedCourses = await models.courses.findAll({ const exportedCourses = await models.courses.findAll({
attributes: ['id', 'name', 'description', 'status', 'created_at', 'created_by'], attributes: ['id', 'thumbnail_url', 'name', 'description', 'status', 'created_at', 'created_by'],
where: params.filters, where: params.filters,
order: params.sortBy ? [[params.sortBy, params.sortOrder]] : [['created_at', 'DESC']], order: params.sortBy ? [[params.sortBy, params.sortOrder]] : [['created_at', 'DESC']],
raw: true, raw: true,
......
...@@ -15,11 +15,10 @@ interface RegisterInput { ...@@ -15,11 +15,10 @@ interface RegisterInput {
} }
export class AuthService { export class AuthService {
// demo
private readonly JWT_SECRET = process.env.JWT_SECRET || ''; private readonly JWT_SECRET = process.env.JWT_SECRET || '';
async loginUser(input: LoginInput) { async loginUser(input: LoginInput) {
const user = await models.users.findOne({ const user = await models.users.findOne({
where: { email: input.email }, where: { email: input.email },
}); });
...@@ -32,6 +31,10 @@ export class AuthService { ...@@ -32,6 +31,10 @@ export class AuthService {
where: { user_id: user?.id } where: { user_id: user?.id }
}); });
if (!auth) {
throw new Error('Nguời dùng không tồn tại');
}
const passwordMatch = await bcrypt.compare(input.password, auth?.password_hash || ''); const passwordMatch = await bcrypt.compare(input.password, auth?.password_hash || '');
if (!passwordMatch) { if (!passwordMatch) {
...@@ -44,11 +47,31 @@ export class AuthService { ...@@ -44,11 +47,31 @@ export class AuthService {
role_id: user.role_id role_id: user.role_id
}; };
const token = jwt.sign(payload, this.JWT_SECRET, { const tokenAT = jwt.sign(payload, this.JWT_SECRET, {
expiresIn: '1h', expiresIn: '1h'
}); });
return { accessToken: token }; const tokenRT = jwt.sign({ id: user.id }, this.JWT_SECRET, {
expiresIn: '30d'
});
try {
await models.refresh_token.create({
user_auth_id: auth.id,
refresh_tokens: tokenRT,
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
device_info: 'Unknown Device'
});
}
catch (error) {
console.error('Lỗi lưu Refresh Token vào Database:', error);
throw new Error('Hệ thống đang bận, vui lòng thử lại sau ít phút');
}
return {
access_token: tokenAT,
refresh_token: tokenRT
};
} }
async registerUser(input: RegisterInput) { async registerUser(input: RegisterInput) {
...@@ -156,4 +179,59 @@ export class AuthService { ...@@ -156,4 +179,59 @@ export class AuthService {
throw new Error('Failed to verify OTP'); throw new Error('Failed to verify OTP');
} }
} }
async refresh(refreshToken: string) {
try {
const checkToken = await models.refresh_token.findOne({
where: { refresh_tokens: refreshToken },
});
if (!checkToken) {
throw new Error('Invalid refresh token');
}
if (checkToken.expires_at && checkToken.expires_at < new Date()) {
throw new Error('Refresh token has expired');
}
const auth = await models.user_auth.findByPk(checkToken.user_auth_id);
if (!auth) {
throw new Error('User authentication not found');
}
const user = await models.users.findByPk(auth.user_id);
if (!user) {
throw new Error('User not found');
}
const payload = {
id: user.id,
email: user.email,
role_id: user.role_id
};
const tokenAT = jwt.sign(payload, this.JWT_SECRET, {
expiresIn: '1h'
});
const tokenRT = jwt.sign({ id: user.id }, this.JWT_SECRET, {
expiresIn: '30d'
});
await checkToken.update({
refresh_tokens: tokenRT,
expires_at: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
});
return {
access_token: tokenAT,
refresh_token: tokenRT
};
}
catch (error) {
console.error('Lỗi lưu Refresh Token vào Database:', error);
throw new Error('Hệ thống đang bận, vui lòng thử lại sau ít phút');
}
}
} }
\ No newline at end of file
import * as XLSX from 'xlsx';
export class DownloadService {
async downloadExcel(courses: any[]) {
const filename = `courses-export-${Date.now()}.xlsx`;
const excelData = courses.map((course: any) => ({
"Id": course.id,
"Name": course.name,
"Description": course.description,
"Status": course.status,
"Thumbnail URL": course.thumbnail_url,
"Created At": course.created_at,
"Created By": course.created_by
}));
const worksheet = XLSX.utils.json_to_sheet(excelData);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, "Courses");
const excelBuffer = XLSX.write(workbook, {
bookType: 'xlsx',
type: 'buffer'
});
return { filename, buffer: excelBuffer };
}
async downloadCsv(courses: any[]) {
const filename = `courses-export-${Date.now()}.csv`;
const csvData = courses.map((course: any) => ({
"Id": course.id,
"Name": course.name,
"Description": course.description,
"Status": course.status,
"Thumbnail URL": course.thumbnail_url,
"Created At": course.created_at,
"Created By": course.created_by
}));
const worksheet = XLSX.utils.json_to_sheet(csvData);
const csvContent = XLSX.utils.sheet_to_csv(worksheet);
const csvBuffer = Buffer.from('\uFEFF' + csvContent, 'utf-8');
return { filename, buffer: csvBuffer };
}
}
\ No newline at end of file
import { Readable } from "stream";
import cloudinary from "#config/cloudinary.config";
type UploadResult = {
width?: number;
height?: number;
bytes?: number;
url?: string;
[key: string]: any;
};
export class UploadService {
async uploadImage(buffer: Buffer): Promise<UploadResult | undefined> {
return new Promise((resolve, reject) => {
const stream = cloudinary.uploader.upload_stream(
{ folder: "images" },
(error, result) => {
if (error) return reject(error);
resolve(result as UploadResult);
}
);
Readable.from(buffer).pipe(stream);
});
}
}
\ No newline at end of file
...@@ -50,20 +50,6 @@ const classSchemas = { ...@@ -50,20 +50,6 @@ const classSchemas = {
}, },
violations: { violations: {
type: 'array', type: 'array',
items: {
type: 'object',
properties: {
code: {
type: 'number',
},
message: {
type: 'string',
},
action: {
nullable: true,
},
},
},
}, },
}, },
}, },
......
...@@ -5,12 +5,14 @@ import courseSchemas from './courses/schemas'; ...@@ -5,12 +5,14 @@ import courseSchemas from './courses/schemas';
import type { Options } from 'swagger-jsdoc'; import type { Options } from 'swagger-jsdoc';
import registerSchemas from './register/schemas'; import registerSchemas from './register/schemas';
import loginSchemas from './login/schemas'; import loginSchemas from './login/schemas';
import authProfileSchemas from './authProfile/schema'; import authProfileSchemas from './profile/schema';
import sendOTPSchemas from './sendOTP/schema'; import sendOTPSchemas from './sendOTP/schema';
import verifyOTPSchemas from './verifyOTP/schema'; import verifyOTPSchemas from './verifyOTP/schema';
import logoutSchemas from './logout/schema'; import logoutSchemas from './logout/schema';
import enrollmentSchemas from './enrollment/schema'; import enrollmentSchemas from './enrollment/schema';
import rolesSchemas from './roles/schema'; import rolesSchemas from './roles/schema';
import refreshTokenSchemas from './refresh/schemas';
import uploadSchemas from './upload/schemas';
const swaggerOptions: Options = { const swaggerOptions: Options = {
definition: { definition: {
...@@ -39,6 +41,8 @@ const swaggerOptions: Options = { ...@@ -39,6 +41,8 @@ const swaggerOptions: Options = {
...logoutSchemas, ...logoutSchemas,
...enrollmentSchemas, ...enrollmentSchemas,
...rolesSchemas, ...rolesSchemas,
...refreshTokenSchemas,
...uploadSchemas,
}, },
parameters: { parameters: {
filters: { filters: {
......
...@@ -5,6 +5,7 @@ const courseSchemas = { ...@@ -5,6 +5,7 @@ const courseSchemas = {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Khóa học 12A1', name: 'Khóa học 12A1',
description: 'Khóa học chuyên toán', description: 'Khóa học chuyên toán',
thumbnail_url: 'https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg',
status: 'active', status: 'active',
created_at: '2026-05-16T08:00:00.000Z', created_at: '2026-05-16T08:00:00.000Z',
created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11', created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11',
...@@ -21,6 +22,11 @@ const courseSchemas = { ...@@ -21,6 +22,11 @@ const courseSchemas = {
type: 'string', type: 'string',
nullable: true nullable: true
}, },
thumbnail_url: {
type: 'string',
format: 'url',
nullable: true
},
status: { status: {
type: 'string', type: 'string',
nullable: true nullable: true
...@@ -138,6 +144,12 @@ const courseSchemas = { ...@@ -138,6 +144,12 @@ const courseSchemas = {
nullable: true, nullable: true,
example: 'Khóa học chuyên toán', example: 'Khóa học chuyên toán',
}, },
thumbnail_url: {
type: 'string',
format: 'url',
nullable: true,
example: 'https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg',
},
status: { status: {
type: 'string', type: 'string',
nullable: true, nullable: true,
......
const loginSchemas = { const loginSchemas = {
LoginResponse: { LoginResponse: {
type: 'object', type: 'object',
example: {
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567',
},
properties: { properties: {
accessToken: { message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
responseData: {
type: 'object',
properties: {
access_token: {
type: 'string',
example: 'string',
},
refresh_token: {
type: 'string',
example: 'string',
},
},
},
status: {
type: 'string', type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567', example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
}, },
}, },
}, },
......
const authProfileSchemas = { const profileSchemas = {
Profile: { Profile: {
type: 'object', type: 'object',
example: { example: {
...@@ -14,9 +14,7 @@ const authProfileSchemas = { ...@@ -14,9 +14,7 @@ const authProfileSchemas = {
}, },
status: "success | fail", status: "success | fail",
timeStamp: "2024-02-26 03:12:45", timeStamp: "2024-02-26 03:12:45",
violation: [ violation: []
{}
]
}, },
properties: { properties: {
message: { message: {
...@@ -86,4 +84,4 @@ const authProfileSchemas = { ...@@ -86,4 +84,4 @@ const authProfileSchemas = {
}, },
}; };
export default authProfileSchemas; export default profileSchemas;
\ No newline at end of file \ No newline at end of file
const refreshSchemas = {
RefreshInput: {
type: 'object',
example: {
refresh_token: 'string',
},
properties: {
refresh_token: {
type: 'uuid',
format: 'uuid',
example: 'string',
},
},
},
RefreshResponse: {
type: 'object',
properties: {
message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
responseData: {
type: 'object',
properties: {
access_token: {
type: 'string',
example: 'string',
},
refresh_token: {
type: 'string',
example: 'string',
},
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
},
},
},
};
export default refreshSchemas;
\ No newline at end of file
const registerSchemas = { const registerSchemas = {
Register: { RegisterResponse: {
type: 'object', type: 'object',
example: { example: {
id: '123', id: '123',
...@@ -7,24 +7,48 @@ const registerSchemas = { ...@@ -7,24 +7,48 @@ const registerSchemas = {
email: 'phamquangbao@example.com', email: 'phamquangbao@example.com',
}, },
properties: { properties: {
id: { message: {
type: 'uuid', type: 'string',
format: 'uuid', nullable: true,
example: '123',
}, },
name: { message_en: {
type: 'string', type: 'string',
example: 'Pham Quang Bao', nullable: true,
}, },
email: { responseData: {
type: 'object',
properties: {
id: {
type: 'uuid',
format: 'uuid',
example: '123',
},
name: {
type: 'string',
example: 'Pham Quang Bao',
},
email: {
type: 'string',
format: 'email',
example: 'phamquangbao@example.com',
},
},
},
status: {
type: 'string', type: 'string',
format: 'email', example: 'success',
example: 'phamquangbao@example.com', },
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
}, },
}, },
}, },
CreateUserInput: { RegisterInput: {
type: 'object', type: 'object',
example: { example: {
name: 'Pham Quang Bao', name: 'Pham Quang Bao',
......
const uploadSchemas = {
UploadResponse: {
type: 'object',
properties: {
message: {
type: 'string',
nullable: true,
},
message_en: {
type: 'string',
nullable: true,
},
responseData: {
type: 'object',
properties: {
width: {
type: 'number',
example: 800,
},
height: {
type: 'number',
example: 600,
},
bytes: {
type: 'number',
example: 150000,
},
url: {
type: 'string',
example: 'https://res.cloudinary.com/demo/image/upload/v1312461204/sample.jpg',
},
},
},
status: {
type: 'string',
example: 'success',
},
timeStamp: {
type: 'string',
example: '2024-02-26 03:12:45',
},
violations: {
type: 'array',
},
},
},
UploadInput: {
type: 'object',
example: {
file: 'image.jpg',
},
properties: {
file: {
type: 'string',
format: 'binary',
example: 'image.jpg',
},
},
},
};
export default uploadSchemas;
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment