Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
B
BACKEND CHALLENGES
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Phạm Quang Bảo
BACKEND CHALLENGES
Commits
8487c9d2
Commit
8487c9d2
authored
Jun 01, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add api cloudinary for upload image and add import/export excel
parent
1bb3e639
Changes
34
Hide whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
1114 additions
and
168 deletions
+1114
-168
.env.example
backend/.env.example
+5
-1
package.json
backend/package.json
+4
-2
pnpm-lock.yaml
backend/pnpm-lock.yaml
+92
-0
cloudinary.config.ts
backend/src/config/cloudinary.config.ts
+10
-0
multer.config.ts
backend/src/config/multer.config.ts
+2
-2
index.ts
backend/src/controllers/api/v1.0/auth/login/index.ts
+1
-3
index.ts
backend/src/controllers/api/v1.0/auth/refresh/index.ts
+46
-0
index.ts
backend/src/controllers/api/v1.0/auth/register/index.ts
+2
-4
index.ts
backend/src/controllers/api/v1.0/auth/send-otp/index.ts
+1
-3
index.ts
backend/src/controllers/api/v1.0/auth/verify-otp/index.ts
+1
-3
index.ts
backend/src/controllers/api/v1.0/courses/export/csv/index.ts
+6
-30
index.ts
...nd/src/controllers/api/v1.0/courses/export/excel/index.ts
+51
-0
index.ts
backend/src/controllers/api/v1.0/courses/import/index.ts
+53
-19
index.ts
backend/src/controllers/api/v1.0/upload/index.ts
+82
-0
swagger-output.json
backend/src/docs/swagger/swagger-output.json
+295
-52
index.ts
backend/src/index.ts
+1
-1
authentication.ts
backend/src/middlewares/authentication.ts
+3
-4
courses.ts
backend/src/models/courses.ts
+7
-1
init-models.ts
backend/src/models/init-models.ts
+9
-0
refresh_token.ts
backend/src/models/refresh_token.ts
+65
-0
user_auth.ts
backend/src/models/user_auth.ts
+13
-0
CoursesProvider.ts
backend/src/providers/CoursesProvider.ts
+4
-1
authService.ts
backend/src/services/authService.ts
+83
-5
downloadService.ts
backend/src/services/downloadService.ts
+48
-0
uploadService.ts
backend/src/services/uploadService.ts
+26
-0
schemas.ts
backend/src/templates/swagger/classes/schemas.ts
+0
-14
config.ts
backend/src/templates/swagger/config.ts
+5
-1
schemas.ts
backend/src/templates/swagger/courses/schemas.ts
+12
-0
schemas.ts
backend/src/templates/swagger/login/schemas.ts
+30
-5
schema.ts
backend/src/templates/swagger/profile/schema.ts
+3
-5
schemas.ts
backend/src/templates/swagger/refresh/schemas.ts
+55
-0
schemas.ts
backend/src/templates/swagger/register/schemas.ts
+35
-11
schemas.ts
backend/src/templates/swagger/upload/schemas.ts
+63
-0
tsconfig.tsbuildinfo
backend/tsconfig.tsbuildinfo
+1
-1
No files found.
backend/.env.example
View file @
8487c9d2
...
...
@@ -5,4 +5,8 @@ HOSTNAMEDB = localhost
PORTDB = 5432
NAMEDB = mydatabase
USERNAMEDB = myuser
PASSWORDDB = example
\ No newline at end of file
PASSWORDDB = example
CLOUD_NAME = cloudinary
API_KEY = api_key
API_SECRET = api_secret
\ No newline at end of file
backend/package.json
View file @
8487c9d2
...
...
@@ -57,6 +57,7 @@
"@fast-csv/format"
:
"^5.0.7"
,
"@types/node-cron"
:
"^3.0.11"
,
"bcrypt"
:
"^6.0.0"
,
"cloudinary"
:
"^2.10.0"
,
"csv-parser"
:
"^3.2.1"
,
"dotenv"
:
"^17.4.2"
,
"express"
:
"^5.2.1"
,
...
...
@@ -67,12 +68,13 @@
"node-cron"
:
"^4.2.1"
,
"nodemailer"
:
"^8.0.7"
,
"pg"
:
"^8.20.0"
,
"tsx"
:
"^4.21.0"
,
"pg-hstore"
:
"^2.3.4"
,
"sequelize"
:
"^6.37.8"
,
"sequelize-auto"
:
"^0.8.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"
:
{
"@types/bcrypt"
:
"^6.0.0"
,
...
...
backend/pnpm-lock.yaml
View file @
8487c9d2
...
...
@@ -17,6 +17,9 @@ importers:
bcrypt
:
specifier
:
^6.0.0
version
:
6.0.0
cloudinary
:
specifier
:
^2.10.0
version
:
2.10.0
csv-parser
:
specifier
:
^3.2.1
version
:
3.2.1
...
...
@@ -56,6 +59,9 @@ importers:
sequelize-auto
:
specifier
:
^0.8.8
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
:
specifier
:
^6.2.8
version
:
6.2.8(openapi-types@12.1.3)
...
...
@@ -65,6 +71,9 @@ importers:
tsx
:
specifier
:
^4.21.0
version
:
4.21.0
xlsx
:
specifier
:
^0.18.5
version
:
0.18.5
devDependencies
:
'
@types/bcrypt'
:
specifier
:
^6.0.0
...
...
@@ -543,6 +552,10 @@ packages:
engines
:
{
node
:
'
>=0.4.0'
}
hasBin
:
true
adler-32@1.3.1
:
resolution
:
{
integrity
:
sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
}
engines
:
{
node
:
'
>=0.8'
}
ansi-regex@5.0.1
:
resolution
:
{
integrity
:
sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
}
engines
:
{
node
:
'
>=8'
}
...
...
@@ -664,6 +677,10 @@ packages:
call-me-maybe@1.0.2
:
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
:
resolution
:
{
integrity
:
sha512-mk8fAWcRUOxY7btlLtitj3A45jOwSAxH4tOFOoEGbVsl6cL6pPMWUy7dwZ/canfj3QEdP6FHSnf/l1c6/WkzVg==
}
...
...
@@ -674,6 +691,14 @@ packages:
cliui@7.0.4
:
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
:
resolution
:
{
integrity
:
sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==
}
engines
:
{
node
:
'
>=0.10.0'
}
...
...
@@ -734,6 +759,11 @@ packages:
resolution
:
{
integrity
:
sha512-jHTjZhsbg9xWgsP2vuNW2jnnzBX+p4T+vNI9Lbjzs1n4KhOfa22bQppiFYLsWQKd8TzmL5aSP/Me3yfsCwXbDA==
}
hasBin
:
true
crc-32@1.2.2
:
resolution
:
{
integrity
:
sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
}
engines
:
{
node
:
'
>=0.8'
}
hasBin
:
true
create-require@1.1.1
:
resolution
:
{
integrity
:
sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
}
...
...
@@ -921,6 +951,10 @@ packages:
resolution
:
{
integrity
:
sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
}
engines
:
{
node
:
'
>=
0.6'
}
frac@1.1.2
:
resolution
:
{
integrity
:
sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
}
engines
:
{
node
:
'
>=0.8'
}
fragment-cache@0.2.1
:
resolution
:
{
integrity
:
sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==
}
engines
:
{
node
:
'
>=0.10.0'
}
...
...
@@ -1646,6 +1680,10 @@ packages:
resolution
:
{
integrity
:
sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
}
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
:
resolution
:
{
integrity
:
sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==
}
engines
:
{
node
:
'
>=0.10.0'
}
...
...
@@ -1654,6 +1692,10 @@ packages:
resolution
:
{
integrity
:
sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
}
engines
:
{
node
:
'
>=
0.8'
}
streamifier@0.1.1
:
resolution
:
{
integrity
:
sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==
}
engines
:
{
node
:
'
>=0.10'
}
streamsearch@1.1.0
:
resolution
:
{
integrity
:
sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
}
engines
:
{
node
:
'
>=10.0.0'
}
...
...
@@ -1799,6 +1841,14 @@ packages:
wkx@0.5.0
:
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
:
resolution
:
{
integrity
:
sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
}
engines
:
{
node
:
'
>=10'
}
...
...
@@ -1806,6 +1856,11 @@ packages:
wrappy@1.0.2
:
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
:
resolution
:
{
integrity
:
sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
}
engines
:
{
node
:
'
>=0.4'
}
...
...
@@ -2131,6 +2186,8 @@ snapshots:
acorn@8.16.0
:
{}
adler-32@1.3.1
:
{}
ansi-regex@5.0.1
:
{}
ansi-styles@4.3.0
:
...
...
@@ -2271,6 +2328,11 @@ snapshots:
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
:
dependencies
:
anymatch
:
1.3.2
...
...
@@ -2299,6 +2361,12 @@ snapshots:
strip-ansi
:
6.0.1
wrap-ansi
:
7.0.0
cloudinary@2.10.0
:
dependencies
:
lodash
:
4.18.1
codepage@1.15.0
:
{}
collection-visit@1.0.0
:
dependencies
:
map-visit
:
1.0.0
...
...
@@ -2356,6 +2424,8 @@ snapshots:
transitivePeerDependencies
:
-
supports-color
crc-32@1.2.2
:
{}
create-require@1.1.1
:
{}
csv-parser@3.2.1
:
{}
...
...
@@ -2609,6 +2679,8 @@ snapshots:
forwarded@0.2.0
:
{}
frac@1.1.2
:
{}
fragment-cache@0.2.1
:
dependencies
:
map-cache
:
0.2.2
...
...
@@ -3349,6 +3421,10 @@ snapshots:
split2@4.2.0
:
{}
ssf@0.11.2
:
dependencies
:
frac
:
1.1.2
static-extend@0.1.2
:
dependencies
:
define-property
:
0.2.5
...
...
@@ -3356,6 +3432,8 @@ snapshots:
statuses@2.0.2
:
{}
streamifier@0.1.1
:
{}
streamsearch@1.1.0
:
{}
string-width@4.2.3
:
...
...
@@ -3504,6 +3582,10 @@ snapshots:
dependencies
:
'
@types/node'
:
25.7.0
wmf@1.0.2
:
{}
word@0.3.0
:
{}
wrap-ansi@7.0.0
:
dependencies
:
ansi-styles
:
4.3.0
...
...
@@ -3512,6 +3594,16 @@ snapshots:
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
:
{}
y18n@5.0.8
:
{}
...
...
backend/src/config/cloudinary.config.ts
0 → 100644
View file @
8487c9d2
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
backend/src/config/multer.config.ts
View file @
8487c9d2
...
...
@@ -2,11 +2,11 @@ import multer from "multer";
const
memoryStorage
=
multer
.
memoryStorage
();
const
upload
CSV
=
multer
({
const
upload
=
multer
({
storage
:
memoryStorage
,
limits
:
{
fileSize
:
2
*
1024
*
1024
,
// 2 MB
},
});
export
default
uploadCSV
;
\ No newline at end of file
export
default
upload
;
\ No newline at end of file
backend/src/controllers/api/v1.0/auth/login/index.ts
View file @
8487c9d2
...
...
@@ -25,9 +25,7 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/LoginResponse"
* $ref: "#/components/schemas/LoginResponse"
*/
post
:
{
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
...
...
backend/src/controllers/api/v1.0/auth/refresh/index.ts
0 → 100644
View file @
8487c9d2
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
backend/src/controllers/api/v1.0/auth/register/index.ts
View file @
8487c9d2
...
...
@@ -18,16 +18,14 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/
CreateUs
erInput"
* $ref: "#/components/schemas/
Regist
erInput"
* responses:
* 201:
* description: Tạo người dùng mới thành công
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Register"
* $ref: "#/components/schemas/RegisterResponse"
*/
post
:
{
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
...
...
backend/src/controllers/api/v1.0/auth/send-otp/index.ts
View file @
8487c9d2
...
...
@@ -24,9 +24,7 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/sendOtpResponse"
* $ref: "#/components/schemas/sendOtpResponse"
*/
post
:
{
middleware
:
[
authMiddleware
],
...
...
backend/src/controllers/api/v1.0/auth/verify-otp/index.ts
View file @
8487c9d2
...
...
@@ -28,9 +28,7 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/verifyOtpResponse"
* $ref: "#/components/schemas/verifyOtpResponse"
*/
post
:
{
middleware
:
[
authMiddleware
],
...
...
backend/src/controllers/api/v1.0/courses/export/index.ts
→
backend/src/controllers/api/v1.0/courses/export/
csv/
index.ts
View file @
8487c9d2
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
*
as
fastcsv
from
'@fast-csv/format'
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
CoursesProvider
}
from
"#providers/CoursesProvider"
;
import
queryModifier
from
"#middlewares/request"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
authorize
}
from
"#middlewares/authorization"
;
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:
* /api/v1.0/courses/export
/csv
:
* get:
* tags: [Courses]
* description: Xuất danh sách các khoá học ra file CSV.
...
...
@@ -31,36 +30,13 @@ export default (_express: Application) => {
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
filename
=
`courses-export-
${
Date
.
now
()}
.csv`
;
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-Disposition'
,
`attachment; filename="
${
filename
}
"`
);
res
.
write
(
'
\
uFEFF'
);
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
();
return
res
.
send
(
buffer
);
}
catch
(
error
)
{
console
.
error
(
'Error exporting courses:'
,
error
);
return
res
.
sendError
({
...
...
@@ -70,6 +46,6 @@ export default (_express: Application) => {
});
}
}
}
}
,
}
}
\ No newline at end of file
backend/src/controllers/api/v1.0/courses/export/excel/index.ts
0 → 100644
View file @
8487c9d2
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
backend/src/controllers/api/v1.0/courses/import/index.ts
View file @
8487c9d2
import
{
Application
}
from
"express"
;
import
{
Readable
}
from
'stream'
;
import
{
Resource
}
from
"express-automatic-routes"
;
import
upload
CSV
from
"#config/multer.config"
;
import
upload
from
"#config/multer.config"
;
import
csv
from
'csv-parser'
;
import
{
Req
,
Res
}
from
"#interfaces/IApi"
;
import
{
CoursesProvider
}
from
"#providers/CoursesProvider"
;
import
*
as
XLSX
from
'xlsx'
;
export
default
(
_express
:
Application
)
=>
{
const
coursesProvider
=
new
CoursesProvider
();
...
...
@@ -38,7 +39,7 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/CourseResponse"
*/
post
:
{
middleware
:
[
upload
CSV
.
single
(
'file'
)],
middleware
:
[
upload
.
single
(
'file'
)],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
...
...
@@ -50,30 +51,63 @@ export default (_express: Application) => {
});
}
// csv đọc file từ buffer và chuyển đổi
const
results
=
await
new
Promise
<
any
[]
>
((
resolve
,
reject
)
=>
{
const
rows
:
any
[]
=
[];
const
stream
=
Readable
.
from
(
req
.
file
!
.
buffer
);
const
mimeType
=
req
.
file
.
mimetype
;
let
parsedData
:
any
[]
=
[];
stream
.
pipe
(
csv
())
.
on
(
'data'
,
(
row
)
=>
{
rows
.
push
(
row
);
})
.
on
(
'end'
,
()
=>
{
resolve
(
rows
);
})
.
on
(
'error'
,
(
error
)
=>
{
reject
(
error
);
// check csv
if
(
mimeType
===
'text/csv'
||
mimeType
===
'application/vnd.ms-excel'
)
{
parsedData
=
await
new
Promise
<
any
[]
>
((
resolve
,
reject
)
=>
{
const
rows
:
any
[]
=
[];
const
stream
=
Readable
.
from
(
req
.
file
!
.
buffer
);
stream
.
pipe
(
csv
())
.
on
(
'data'
,
(
row
)
=>
rows
.
push
(
row
))
.
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
({
message
:
"Khóa học đã được import thành công!"
,
message_en
:
"Courses imported successfully!"
,
data
:
importedCourse
s
data
:
result
s
});
}
catch
(
error
)
{
...
...
backend/src/controllers/api/v1.0/upload/index.ts
0 → 100644
View file @
8487c9d2
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
backend/src/docs/swagger/swagger-output.json
View file @
8487c9d2
...
...
@@ -93,21 +93,7 @@
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"number"
},
"message"
:
{
"type"
:
"string"
},
"action"
:
{
"nullable"
:
true
}
}
}
"type"
:
"array"
}
}
},
...
...
@@ -319,6 +305,7 @@
"id"
:
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
,
"name"
:
"Khóa học 12A1"
,
"description"
:
"Khóa học chuyên toán"
,
"thumbnail_url"
:
"https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg"
,
"status"
:
"active"
,
"created_at"
:
"2026-05-16T08:00:00.000Z"
,
"created_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
...
...
@@ -335,6 +322,11 @@
"type"
:
"string"
,
"nullable"
:
true
},
"thumbnail_url"
:
{
"type"
:
"string"
,
"format"
:
"url"
,
"nullable"
:
true
},
"status"
:
{
"type"
:
"string"
,
"nullable"
:
true
...
...
@@ -450,6 +442,12 @@
"nullable"
:
true
,
"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"
:
{
"type"
:
"string"
,
"nullable"
:
true
,
...
...
@@ -457,7 +455,7 @@
}
}
},
"Register"
:
{
"Register
Response
"
:
{
"type"
:
"object"
,
"example"
:
{
"id"
:
"123"
,
...
...
@@ -465,23 +463,47 @@
"email"
:
"phamquangbao@example.com"
},
"properties"
:
{
"id"
:
{
"type"
:
"uuid"
,
"format"
:
"uuid"
,
"example"
:
"123"
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"
name
"
:
{
"
message_en
"
:
{
"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"
,
"format"
:
"email"
,
"example"
:
"phamquangbao@example.com"
"example"
:
"success"
},
"timeStamp"
:
{
"type"
:
"string"
,
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
}
}
},
"
CreateUs
erInput"
:
{
"
Regist
erInput"
:
{
"type"
:
"object"
,
"example"
:
{
"name"
:
"Pham Quang Bao"
,
...
...
@@ -506,13 +528,38 @@
},
"LoginResponse"
:
{
"type"
:
"object"
,
"example"
:
{
"accessToken"
:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567"
},
"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"
,
"example"
:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567"
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
}
}
},
...
...
@@ -549,9 +596,7 @@
},
"status"
:
"success | fail"
,
"timeStamp"
:
"2024-02-26 03:12:45"
,
"violation"
:
[
{}
]
"violation"
:
[]
},
"properties"
:
{
"message"
:
{
...
...
@@ -914,6 +959,114 @@
"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"
:
{
...
...
@@ -974,10 +1127,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/LoginResponse"
}
"$ref"
:
"#/components/schemas/LoginResponse"
}
}
}
...
...
@@ -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"
:
{
"post"
:
{
"tags"
:
[
...
...
@@ -1046,7 +1226,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/
CreateUs
erInput"
"$ref"
:
"#/components/schemas/
Regist
erInput"
}
}
}
...
...
@@ -1057,10 +1237,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Register"
}
"$ref"
:
"#/components/schemas/RegisterResponse"
}
}
}
...
...
@@ -1085,10 +1262,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/sendOtpResponse"
}
"$ref"
:
"#/components/schemas/sendOtpResponse"
}
}
}
...
...
@@ -1123,10 +1297,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/verifyOtpResponse"
}
"$ref"
:
"#/components/schemas/verifyOtpResponse"
}
}
}
...
...
@@ -1460,7 +1631,7 @@
}
}
},
"/api/v1.0/courses/export"
:
{
"/api/v1.0/courses/export
/csv
"
:
{
"get"
:
{
"tags"
:
[
"Courses"
...
...
@@ -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"
:
{
"post"
:
{
"tags"
:
[
...
...
@@ -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"
:
[]
...
...
backend/src/index.ts
View file @
8487c9d2
...
...
@@ -24,5 +24,5 @@ emailCronJob.start();
app
.
use
(
'/swagger/index'
,
swaggerUi
.
serve
,
swaggerUi
.
setup
(
swaggerFile
));
app
.
listen
(
port
,
()
=>
{
console
.
log
(
`App listening on
port
${
port
}
`
)
console
.
log
(
`App listening on
http://localhost:
${
port
}
/swagger/index/
`
)
})
backend/src/middlewares/authentication.ts
View file @
8487c9d2
...
...
@@ -5,7 +5,6 @@ import { JWTPayload, Req, Res } from '#interfaces/IApi';
export
const
authMiddleware
=
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
)
=>
{
const
JWT_SECRET
=
process
.
env
.
JWT_SECRET
||
''
;
const
authHeader
=
req
.
headers
.
authorization
;
if
(
!
authHeader
||
!
authHeader
.
startsWith
(
'Bearer '
))
{
...
...
@@ -17,7 +16,6 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
}
const
token
=
authHeader
.
split
(
' '
)[
1
];
if
(
!
token
)
{
return
res
.
sendError
({
message
:
'Không tìm thấy token'
,
...
...
@@ -28,14 +26,15 @@ export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
try
{
const
decoded
=
jwt
.
verify
(
token
,
JWT_SECRET
)
as
JWTPayload
;
req
.
user
=
decoded
;
req
.
user
=
decoded
;
return
next
();
}
catch
(
error
)
{
return
res
.
sendError
({
message
:
'Token không hợp lệ hoặc đã hết hạn'
,
message_en
:
'Invalid or expired token'
,
status
:
40
3
status
:
40
1
});
}
};
\ No newline at end of file
backend/src/models/courses.ts
View file @
8487c9d2
...
...
@@ -9,11 +9,12 @@ export interface coursesAttributes {
created_at
?:
Date
;
created_by
?:
string
;
status
?:
string
;
thumbnail_url
?:
string
;
}
export
type
coursesPk
=
"id"
;
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
class
courses
extends
Model
<
coursesAttributes
,
coursesCreationAttributes
>
implements
coursesAttributes
{
...
...
@@ -23,6 +24,7 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes>
created_at
?:
Date
;
created_by
?:
string
;
status
?:
string
;
thumbnail_url
?:
string
;
// courses hasMany classes via course_id
classes
!
:
classes
[];
...
...
@@ -65,6 +67,10 @@ export class courses extends Model<coursesAttributes, coursesCreationAttributes>
status
:
{
type
:
DataTypes
.
STRING
(
50
),
allowNull
:
true
},
thumbnail_url
:
{
type
:
DataTypes
.
TEXT
,
allowNull
:
true
}
},
{
tableName
:
'courses'
,
...
...
backend/src/models/init-models.ts
View file @
8487c9d2
...
...
@@ -5,6 +5,8 @@ import { courses as _courses } from "./courses";
import
type
{
coursesAttributes
,
coursesCreationAttributes
}
from
"./courses"
;
import
{
enrollments
as
_enrollments
}
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
type
{
rolesAttributes
,
rolesCreationAttributes
}
from
"./roles"
;
import
{
user_auth
as
_user_auth
}
from
"./user_auth"
;
...
...
@@ -16,6 +18,7 @@ export {
_classes
as
classes
,
_courses
as
courses
,
_enrollments
as
enrollments
,
_refresh_token
as
refresh_token
,
_roles
as
roles
,
_user_auth
as
user_auth
,
_users
as
users
,
...
...
@@ -28,6 +31,8 @@ export type {
coursesCreationAttributes
,
enrollmentsAttributes
,
enrollmentsCreationAttributes
,
refresh_tokenAttributes
,
refresh_tokenCreationAttributes
,
rolesAttributes
,
rolesCreationAttributes
,
user_authAttributes
,
...
...
@@ -40,6 +45,7 @@ export function initModels(sequelize: Sequelize) {
const
classes
=
_classes
.
initModel
(
sequelize
);
const
courses
=
_courses
.
initModel
(
sequelize
);
const
enrollments
=
_enrollments
.
initModel
(
sequelize
);
const
refresh_token
=
_refresh_token
.
initModel
(
sequelize
);
const
roles
=
_roles
.
initModel
(
sequelize
);
const
user_auth
=
_user_auth
.
initModel
(
sequelize
);
const
users
=
_users
.
initModel
(
sequelize
);
...
...
@@ -50,6 +56,8 @@ export function initModels(sequelize: Sequelize) {
courses
.
hasMany
(
classes
,
{
as
:
"classes"
,
foreignKey
:
"course_id"
});
users
.
belongsTo
(
roles
,
{
as
:
"role"
,
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"
});
users
.
hasMany
(
enrollments
,
{
as
:
"enrollments"
,
foreignKey
:
"user_id"
});
user_auth
.
belongsTo
(
users
,
{
as
:
"user"
,
foreignKey
:
"user_id"
});
...
...
@@ -59,6 +67,7 @@ export function initModels(sequelize: Sequelize) {
classes
:
classes
,
courses
:
courses
,
enrollments
:
enrollments
,
refresh_token
:
refresh_token
,
roles
:
roles
,
user_auth
:
user_auth
,
users
:
users
,
...
...
backend/src/models/refresh_token.ts
0 → 100644
View file @
8487c9d2
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
;
}
}
backend/src/models/user_auth.ts
View file @
8487c9d2
import
*
as
Sequelize
from
'sequelize'
;
import
{
DataTypes
,
Model
,
Optional
}
from
'sequelize'
;
import
type
{
refresh_token
,
refresh_tokenId
}
from
'./refresh_token'
;
import
type
{
users
,
usersId
}
from
'./users'
;
export
interface
user_authAttributes
{
...
...
@@ -26,6 +27,18 @@ export class user_auth extends Model<user_authAttributes, user_authCreationAttri
otp_expiry
?:
Date
;
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
!
:
users
;
getUser
!
:
Sequelize
.
BelongsToGetAssociationMixin
<
users
>
;
...
...
backend/src/providers/CoursesProvider.ts
View file @
8487c9d2
...
...
@@ -4,6 +4,7 @@ import { models } from '#models/sequelize-config';
interface
CreateCourseInput
{
name
:
string
;
description
:
string
;
thumbnail_url
?:
string
;
status
?:
string
;
}
...
...
@@ -32,6 +33,7 @@ export class CoursesProvider {
async
createCourse
(
input
:
CreateCourseInput
)
{
const
course
=
await
models
.
courses
.
create
(
{
thumbnail_url
:
input
.
thumbnail_url
||
''
,
name
:
input
.
name
,
description
:
input
.
description
,
status
:
input
.
status
||
"active"
...
...
@@ -45,6 +47,7 @@ export class CoursesProvider {
{
name
:
data
.
name
,
description
:
data
.
description
,
thumbnail_url
:
data
.
thumbnail_url
||
''
,
status
:
data
.
status
||
"active"
},
{
...
...
@@ -69,7 +72,7 @@ export class CoursesProvider {
async
exportCourses
(
params
:
payload
)
{
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
,
order
:
params
.
sortBy
?
[[
params
.
sortBy
,
params
.
sortOrder
]]
:
[[
'created_at'
,
'DESC'
]],
raw
:
true
,
...
...
backend/src/services/authService.ts
View file @
8487c9d2
...
...
@@ -15,11 +15,10 @@ interface RegisterInput {
}
export
class
AuthService
{
// demo
private
readonly
JWT_SECRET
=
process
.
env
.
JWT_SECRET
||
''
;
async
loginUser
(
input
:
LoginInput
)
{
const
user
=
await
models
.
users
.
findOne
({
where
:
{
email
:
input
.
email
},
});
...
...
@@ -32,6 +31,10 @@ export class AuthService {
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
||
''
);
if
(
!
passwordMatch
)
{
...
...
@@ -44,11 +47,31 @@ export class AuthService {
role_id
:
user
.
role_id
};
const
token
=
jwt
.
sign
(
payload
,
this
.
JWT_SECRET
,
{
expiresIn
:
'1h'
,
const
token
AT
=
jwt
.
sign
(
payload
,
this
.
JWT_SECRET
,
{
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
)
{
...
...
@@ -156,4 +179,59 @@ export class AuthService {
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
backend/src/services/downloadService.ts
0 → 100644
View file @
8487c9d2
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
backend/src/services/uploadService.ts
0 → 100644
View file @
8487c9d2
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
backend/src/templates/swagger/classes/schemas.ts
View file @
8487c9d2
...
...
@@ -50,20 +50,6 @@ const classSchemas = {
},
violations
:
{
type
:
'array'
,
items
:
{
type
:
'object'
,
properties
:
{
code
:
{
type
:
'number'
,
},
message
:
{
type
:
'string'
,
},
action
:
{
nullable
:
true
,
},
},
},
},
},
},
...
...
backend/src/templates/swagger/config.ts
View file @
8487c9d2
...
...
@@ -5,12 +5,14 @@ import courseSchemas from './courses/schemas';
import
type
{
Options
}
from
'swagger-jsdoc'
;
import
registerSchemas
from
'./register/schemas'
;
import
loginSchemas
from
'./login/schemas'
;
import
authProfileSchemas
from
'./
authP
rofile/schema'
;
import
authProfileSchemas
from
'./
p
rofile/schema'
;
import
sendOTPSchemas
from
'./sendOTP/schema'
;
import
verifyOTPSchemas
from
'./verifyOTP/schema'
;
import
logoutSchemas
from
'./logout/schema'
;
import
enrollmentSchemas
from
'./enrollment/schema'
;
import
rolesSchemas
from
'./roles/schema'
;
import
refreshTokenSchemas
from
'./refresh/schemas'
;
import
uploadSchemas
from
'./upload/schemas'
;
const
swaggerOptions
:
Options
=
{
definition
:
{
...
...
@@ -39,6 +41,8 @@ const swaggerOptions: Options = {
...
logoutSchemas
,
...
enrollmentSchemas
,
...
rolesSchemas
,
...
refreshTokenSchemas
,
...
uploadSchemas
,
},
parameters
:
{
filters
:
{
...
...
backend/src/templates/swagger/courses/schemas.ts
View file @
8487c9d2
...
...
@@ -5,6 +5,7 @@ const courseSchemas = {
id
:
'3fa85f64-5717-4562-b3fc-2c963f66afa6'
,
name
:
'Khóa học 12A1'
,
description
:
'Khóa học chuyên toán'
,
thumbnail_url
:
'https://res.cloudinary.com/dx9s9n4eo/image/upload/v1700000000/images/sample.jpg'
,
status
:
'active'
,
created_at
:
'2026-05-16T08:00:00.000Z'
,
created_by
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
...
...
@@ -21,6 +22,11 @@ const courseSchemas = {
type
:
'string'
,
nullable
:
true
},
thumbnail_url
:
{
type
:
'string'
,
format
:
'url'
,
nullable
:
true
},
status
:
{
type
:
'string'
,
nullable
:
true
...
...
@@ -138,6 +144,12 @@ const courseSchemas = {
nullable
:
true
,
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
:
{
type
:
'string'
,
nullable
:
true
,
...
...
backend/src/templates/swagger/login/schemas.ts
View file @
8487c9d2
const
loginSchemas
=
{
LoginResponse
:
{
type
:
'object'
,
example
:
{
accessToken
:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567'
,
},
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
:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsIm5hbWUiOiJQaGFtIFF1YW5nIEJhbyIsImVtYWlsIjoicGhhbXF1YW5nYmFvQGV4YW1wbGUuY29tIiwiaWF0IjoxNjg4ODg3MDYyfQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz567'
,
example
:
'success'
,
},
timeStamp
:
{
type
:
'string'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
},
},
},
...
...
backend/src/templates/swagger/
authP
rofile/schema.ts
→
backend/src/templates/swagger/
p
rofile/schema.ts
View file @
8487c9d2
const
authP
rofileSchemas
=
{
const
p
rofileSchemas
=
{
Profile
:
{
type
:
'object'
,
example
:
{
...
...
@@ -14,9 +14,7 @@ const authProfileSchemas = {
},
status
:
"success | fail"
,
timeStamp
:
"2024-02-26 03:12:45"
,
violation
:
[
{}
]
violation
:
[]
},
properties
:
{
message
:
{
...
...
@@ -86,4 +84,4 @@ const authProfileSchemas = {
},
};
export
default
authProfileSchemas
;
\ No newline at end of file
export
default
profileSchemas
;
\ No newline at end of file
backend/src/templates/swagger/refresh/schemas.ts
0 → 100644
View file @
8487c9d2
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
backend/src/templates/swagger/register/schemas.ts
View file @
8487c9d2
const
registerSchemas
=
{
Register
:
{
Register
Response
:
{
type
:
'object'
,
example
:
{
id
:
'123'
,
...
...
@@ -7,24 +7,48 @@ const registerSchemas = {
email
:
'phamquangbao@example.com'
,
},
properties
:
{
id
:
{
type
:
'uuid'
,
format
:
'uuid'
,
example
:
'123'
,
message
:
{
type
:
'string'
,
nullable
:
true
,
},
name
:
{
message_en
:
{
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'
,
format
:
'email'
,
example
:
'phamquangbao@example.com'
,
example
:
'success'
,
},
timeStamp
:
{
type
:
'string'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
},
},
},
CreateUs
erInput
:
{
Regist
erInput
:
{
type
:
'object'
,
example
:
{
name
:
'Pham Quang Bao'
,
...
...
backend/src/templates/swagger/upload/schemas.ts
0 → 100644
View file @
8487c9d2
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
backend/tsconfig.tsbuildinfo
View file @
8487c9d2
This source diff could not be displayed because it is too large. You can
view the blob
instead.
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment