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
11e8bbc6
Commit
11e8bbc6
authored
May 20, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(challenge_7): add filter, pagination and response Json
parent
fb9be9d0
Changes
27
Show whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
1051 additions
and
328 deletions
+1051
-328
index.ts
code/src/controllers/api/v1.0/auth/login/index.ts
+8
-4
index.ts
code/src/controllers/api/v1.0/auth/logout/index.ts
+10
-7
index.ts
code/src/controllers/api/v1.0/auth/profile/index.ts
+19
-7
index.ts
code/src/controllers/api/v1.0/auth/register/index.ts
+18
-5
index.ts
code/src/controllers/api/v1.0/auth/send-otp/index.ts
+24
-11
index.ts
code/src/controllers/api/v1.0/auth/verify-otp/index.ts
+20
-6
index.ts
code/src/controllers/api/v1.0/classes/index.ts
+21
-18
{id}.ts
code/src/controllers/api/v1.0/classes/{id}.ts
+40
-19
index.ts
code/src/controllers/api/v1.0/courses/index.ts
+18
-20
{id}.ts
code/src/controllers/api/v1.0/courses/{id}.ts
+35
-20
index.ts
...ollers/api/v1.0/enrollments/all-student-in-class/index.ts
+8
-3
index.ts
code/src/controllers/api/v1.0/enrollments/enroll/index.ts
+20
-6
index.ts
code/src/controllers/api/v1.0/enrollments/unenroll/index.ts
+8
-3
swagger-output.json
code/src/docs/swagger/swagger-output.json
+294
-81
index.ts
code/src/index.ts
+3
-0
IApi.ts
code/src/interface/IApi.ts
+77
-0
authentication.ts
code/src/middlewares/authentication.ts
+24
-7
authorization.ts
code/src/middlewares/authorization.ts
+9
-5
request.ts
code/src/middlewares/request.ts
+72
-0
response.ts
code/src/middlewares/response.ts
+36
-0
ClassesProvider.ts
code/src/providers/ClassesProvider.ts
+14
-18
CoursesProvider.ts
code/src/providers/CoursesProvider.ts
+13
-17
EnrollProvider.ts
code/src/providers/EnrollProvider.ts
+0
-1
authService.ts
code/src/services/authService.ts
+1
-1
schema.ts
code/src/templates/swagger/authProfile/schema.ts
+70
-19
schemas.ts
code/src/templates/swagger/classes/schemas.ts
+95
-32
schemas.ts
code/src/templates/swagger/courses/schemas.ts
+94
-18
No files found.
code/src/controllers/api/v1.0/auth/login/index.ts
View file @
11e8bbc6
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
import
{
Resource
}
from
"express-automatic-routes"
...
...
@@ -29,14 +30,17 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/LoginResponse"
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
login
=
await
authService
.
loginUser
(
req
.
body
);
return
res
.
status
(
200
).
json
(
login
);
return
res
.
sendOk
({
data
:
login
});
}
catch
(
error
)
{
console
.
error
(
'Error logging in user:'
,
error
);
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Đăng nhập thất bại"
,
message_en
:
"Login failed"
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/auth/logout/index.ts
View file @
11e8bbc6
import
{
authenticate
}
from
"#middlewares/authentication"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
@@ -24,19 +25,21 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/logoutResponse"
*/
post
:
{
middleware
:
[
auth
enticat
e
],
middleware
:
[
auth
Middlewar
e
],
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
userId
=
(
req
as
any
).
user
.
id
;
console
.
log
(
'User ID to logout:'
,
userId
);
return
res
.
status
(
200
).
json
({
message
:
'Đăng xuất thành công'
,
});
return
res
.
sendOk
({
data
:
null
});
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Đăng xuất thất bại"
,
message_en
:
"Logout failed"
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/auth/profile/index.ts
View file @
11e8bbc6
import
{
authenticate
}
from
"#middlewares/authentication"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
@@ -24,17 +25,28 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Profile"
*/
get
:
{
middleware
:
[
auth
enticat
e
],
middleware
:
[
auth
Middlewar
e
],
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
userId
=
(
req
as
any
).
user
.
id
;
const
userId
=
req
.
user
?
.
id
;
const
profile
=
await
authService
.
profileUser
(
userId
);
if
(
!
userId
)
{
return
res
.
sendError
({
message
:
"Không tìm thấy thông tin người dùng"
,
message_en
:
"User information not found"
,
status
:
401
});
}
return
res
.
status
(
200
).
json
(
profile
);
const
profile
=
await
authService
.
profileUser
(
userId
);
return
res
.
sendOk
({
data
:
profile
});
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lấy thông tin hồ sơ thất bại"
,
message_en
:
"Failed to fetch profile"
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/auth/register/index.ts
View file @
11e8bbc6
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
AuthService
}
from
"#services/authService"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
@@ -29,23 +30,35 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Register"
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
{
name
,
email
,
password
}
=
req
.
body
;
if
(
!
name
||
!
email
||
!
password
)
{
return
res
.
status
(
400
).
json
({
error
:
'Vui lòng nhập đầy đủ thông tin'
});
return
res
.
sendError
({
message
:
'Vui lòng nhập đầy đủ thông tin'
,
message_en
:
'Please provide all required fields'
,
status
:
400
});
}
if
(
password
.
length
<
6
)
{
return
res
.
status
(
400
).
json
({
error
:
'Mật khẩu phải có ít nhất 6 ký tự'
});
return
res
.
sendError
({
message
:
'Mật khẩu phải có ít nhất 6 ký tự'
,
message_en
:
'Password must be at least 6 characters long'
,
status
:
400
});
}
const
register
=
await
authService
.
registerUser
(
req
.
body
);
return
res
.
s
tatus
(
201
).
json
(
register
);
return
res
.
s
endOk
({
data
:
register
}
);
}
catch
(
error
)
{
console
.
error
(
'Error registering user:'
,
error
);
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
'Đăng ký người dùng thất bại'
,
message_en
:
'User registration failed'
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/auth/send-otp/index.ts
View file @
11e8bbc6
import
{
authenticate
}
from
"#middlewares/authentication"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService.js"
;
import
{
MailService
}
from
"#services/mailService.js"
;
import
{
Application
}
from
"express"
;
...
...
@@ -28,12 +29,20 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/sendOtpResponse"
*/
post
:
{
middleware
:
[
auth
enticat
e
],
middleware
:
[
auth
Middlewar
e
],
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
userId
=
(
req
as
any
).
user
.
id
;
const
email
=
(
req
as
any
).
user
.
email
;
const
userId
=
req
.
user
?.
id
;
const
email
=
req
.
user
?.
email
;
if
(
!
userId
||
!
email
)
{
return
res
.
sendError
({
message
:
'Thông tin người dùng không hợp lệ'
,
message_en
:
'Invalid user information'
,
status
:
400
});
}
// get otp
const
otp
=
await
authService
.
otpUser
(
userId
);
...
...
@@ -42,19 +51,23 @@ export default (_express: Application) => {
if
(
!
mailResult
||
!
mailResult
.
success
)
{
console
.
error
(
'Gửi mail thất bại:'
,
mailResult
?.
error
);
return
res
.
status
(
500
).
json
({
email
:
email
,
return
res
.
sendError
({
message
:
'Không thể gửi email OTP. Vui lòng thử lại sau.'
,
error
:
mailResult
?.
error
message_en
:
'Failed to send OTP email. Please try again later.'
,
status
:
500
});
}
return
res
.
s
tatus
(
200
).
json
({
email
:
emai
l
,
return
res
.
s
endOk
({
data
:
nul
l
,
message
:
'OTP đã được gửi đến email của bạn'
});
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
'Đã xảy ra lỗi khi gửi OTP'
,
message_en
:
'An error occurred while sending OTP'
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/auth/verify-otp/index.ts
View file @
11e8bbc6
import
{
authenticate
}
from
"#middlewares/authentication"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
AuthService
}
from
"#services/authService.js"
;
import
{
Application
}
from
"express"
;
import
{
Resource
}
from
"express-automatic-routes"
;
...
...
@@ -32,15 +33,28 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/verifyOtpResponse"
*/
post
:
{
middleware
:
[
auth
enticat
e
],
middleware
:
[
auth
Middlewar
e
],
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
userId
=
(
req
as
any
).
user
.
id
;
const
userId
=
req
.
user
?.
id
;
if
(
!
userId
)
{
return
res
.
sendError
({
message
:
'Thông tin người dùng không hợp lệ'
,
message_en
:
'Invalid user information'
,
status
:
400
});
}
const
verifyResult
=
await
authService
.
verifyOtp
(
userId
,
req
.
body
.
otp
);
res
.
status
(
201
).
json
(
verifyResult
);
return
res
.
sendOk
({
data
:
verifyResult
});
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
'Đã xảy ra lỗi khi xác minh OTP'
,
message_en
:
'An error occurred while verifying OTP'
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/classes/index.ts
View file @
11e8bbc6
import
type
{
Application
}
from
"express"
;
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
ClassesProvider
}
from
"#providers/ClassesProvider"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
queryModifier
from
"#middlewares/request"
;
export
default
(
_express
:
Application
)
=>
{
const
classesProvider
=
new
ClassesProvider
();
...
...
@@ -22,26 +24,23 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Class"
* $ref: "#/components/schemas/ClassListResponse"
*/
get
:
{
handler
:
async
(
req
,
res
)
=>
{
try
{
const
{
filters
,
sort
,
page
,
pageSize
}
=
req
.
query
;
middleware
:
[
queryModifier
],
const
classes
=
await
classesProvider
.
getAllClasses
({
filters
:
filters
as
string
,
sort
:
sort
as
string
,
page
:
page
?
parseInt
(
page
as
string
)
:
1
,
pageSize
:
pageSize
?
parseInt
(
pageSize
as
string
)
:
10
});
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
classes
=
await
classesProvider
.
getAllClasses
(
req
.
payload
);
return
res
.
json
(
classes
);
return
res
.
sendOk
({
data
:
classes
}
);
}
catch
(
error
)
{
console
.
error
(
'Error getting classes:'
,
error
);
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
'Đã xảy ra lỗi khi lấy danh sách lớp học'
,
message_en
:
'An error occurred while fetching class list'
,
status
:
500
});
}
}
},
...
...
@@ -64,21 +63,25 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* $ref: "#/components/schemas/Class
"
* $ref: "#/components/schemas/ClassResponse
"
* 400:
* description: Dữ liệu đầu vào không hợp lệ (Thiếu trường, sai định dạng)
* 500:
* description: Lỗi hệ thống phía Server
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
newClass
=
await
classesProvider
.
createClass
(
req
.
body
);
return
res
.
s
tatus
(
201
).
json
(
newClass
);
return
res
.
s
endOk
({
data
:
newClass
}
);
}
catch
(
error
)
{
console
.
error
(
'Error creating class:'
,
error
);
return
res
.
status
(
400
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
'Đã xảy ra lỗi khi tạo lớp học mới'
,
message_en
:
'An error occurred while creating the class'
,
status
:
400
});
}
}
}
...
...
code/src/controllers/api/v1.0/classes/{id}.ts
View file @
11e8bbc6
import
type
{
Application
}
from
"express"
;
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
ClassesProvider
}
from
"#providers/ClassesProvider.js"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
classesProvider
=
new
ClassesProvider
();
...
...
@@ -24,23 +25,29 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Class"
* $ref: "#/components/schemas/ClassResponse"
*/
get
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
{
id
}
=
req
.
params
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
try
{
const
classData
=
await
classesProvider
.
getClassById
(
id
);
return
res
.
json
(
classData
);
return
res
.
sendOk
({
data
:
classData
}
);
}
catch
(
error
)
{
return
res
.
status
(
404
).
json
({
error
:
"Class not found"
});
return
res
.
sendError
({
message
:
"Lớp học không tồn tại"
,
message_en
:
"Class not found"
,
status
:
404
});
}
}
},
...
...
@@ -70,23 +77,29 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Class"
* $ref: "#/components/schemas/ClassResponse"
*/
put
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
{
id
}
=
req
.
params
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
try
{
const
classData
=
await
classesProvider
.
updateClassById
(
id
,
req
.
body
);
return
res
.
json
(
classData
);
return
res
.
sendOk
({
data
:
classData
}
);
}
catch
(
error
)
{
return
res
.
status
(
404
).
json
({
error
:
"Class not found"
});
return
res
.
sendError
({
message
:
"Lớp học không tồn tại"
,
message_en
:
"Class not found"
,
status
:
404
});
}
}
},
...
...
@@ -110,18 +123,26 @@ export default (_express: Application) => {
* description: Xóa lớp học thành công
*/
delete
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
{
id
}
=
req
.
params
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
try
{
const
classData
=
await
classesProvider
.
deleteClassById
(
id
);
return
res
.
json
(
classData
);
await
classesProvider
.
deleteClassById
(
id
);
return
res
.
sendOk
({
data
:
null
}
);
}
catch
(
error
)
{
return
res
.
status
(
404
).
json
({
error
:
"Class not found"
});
return
res
.
sendError
({
message
:
"Lớp học không tồn tại"
,
message_en
:
"Class not found"
,
status
:
404
});
}
}
}
...
...
code/src/controllers/api/v1.0/courses/index.ts
View file @
11e8bbc6
import
type
{
Application
}
from
"express"
;
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
CoursesProvider
}
from
"#providers/CoursesProvider.js"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
coursesProvider
=
new
CoursesProvider
();
...
...
@@ -22,26 +23,21 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Course"
* $ref: "#/components/schemas/CourseListResponse"
*/
get
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
{
filters
,
sort
,
page
,
pageSize
}
=
req
.
query
;
const
courses
=
await
coursesProvider
.
getAllCourses
(
req
.
payload
)
;
const
courses
=
await
coursesProvider
.
getAllCourses
({
filters
:
filters
as
string
,
sort
:
sort
as
string
,
page
:
page
?
parseInt
(
page
as
string
)
:
1
,
pageSize
:
pageSize
?
parseInt
(
pageSize
as
string
)
:
10
});
return
res
.
json
(
courses
);
return
res
.
sendOk
({
data
:
courses
});
}
catch
(
error
)
{
console
.
error
(
'Error getting courses:'
,
error
);
return
res
.
status
(
500
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lỗi khi lấy danh sách khóa học!"
,
message_en
:
"Error occurred while fetching courses!"
,
status
:
500
});
}
}
},
...
...
@@ -64,19 +60,21 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Course"
* $ref: "#/components/schemas/CourseResponse"
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
newCourse
=
await
coursesProvider
.
createCourse
(
req
.
body
);
return
res
.
s
tatus
(
201
).
json
(
newCourse
);
return
res
.
s
endOk
({
data
:
newCourse
}
);
}
catch
(
error
)
{
console
.
error
(
'Error creating course:'
,
error
);
return
res
.
status
(
400
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lỗi khi tạo khóa học mới!"
,
message_en
:
"Error occurred while creating course!"
,
status
:
400
});
}
}
}
...
...
code/src/controllers/api/v1.0/courses/{id}.ts
View file @
11e8bbc6
import
type
{
Application
}
from
"express"
;
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
CoursesProvider
}
from
"#providers/CoursesProvider.js"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
coursesProvider
=
new
CoursesProvider
();
...
...
@@ -24,20 +25,22 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Course"
* $ref: "#/components/schemas/CourseResponse"
*/
get
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
id
=
req
.
params
.
id
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
const
course
=
await
coursesProvider
.
getCourseById
(
id
);
return
res
.
json
(
course
);
return
res
.
sendOk
({
data
:
course
}
);
}
},
...
...
@@ -66,23 +69,29 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Course"
* $ref: "#/components/schemas/CourseResponse"
*/
put
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
{
id
}
=
req
.
params
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
try
{
const
updateCourse
=
await
coursesProvider
.
updateCourse
(
id
,
req
.
body
);
return
res
.
json
(
updateCourse
);
return
res
.
sendOk
({
data
:
updateCourse
}
);
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
"Lỗi khi cập nhật khóa học!"
});
return
res
.
sendError
({
message
:
"Lỗi khi cập nhật khóa học!"
,
message_en
:
"Error occurred while updating course!"
,
status
:
500
});
}
}
},
...
...
@@ -105,23 +114,29 @@ export default (_express: Application) => {
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: "#/components/schemas/Course"
* $ref: "#/components/schemas/CourseResponse"
*/
delete
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
const
{
id
}
=
req
.
params
;
if
(
!
id
||
typeof
id
!==
'string'
)
{
return
res
.
status
(
400
).
json
({
error
:
"ID không hợp lệ!"
});
return
res
.
sendError
({
message
:
"ID không hợp lệ!"
,
message_en
:
"Invalid ID!"
,
status
:
400
});
}
try
{
const
deletedCourse
=
await
coursesProvider
.
deleteCourse
(
id
);
return
res
.
json
(
deletedCourse
);
return
res
.
sendOk
({
data
:
deletedCourse
}
);
}
catch
(
error
)
{
return
res
.
status
(
500
).
json
({
error
:
"Lỗi khi xóa khóa học!"
});
return
res
.
sendError
({
message
:
"Lỗi khi xóa khóa học!"
,
message_en
:
"Error occurred while deleting course!"
,
status
:
500
});
}
}
}
...
...
code/src/controllers/api/v1.0/enrollments/all-student-in-class/index.ts
View file @
11e8bbc6
import
type
{
Application
}
from
"express"
;
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
EnrollProvider
}
from
"#providers/EnrollProvider.js"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
enrollProvider
=
new
EnrollProvider
();
...
...
@@ -29,14 +30,18 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/AllStudentInClassOutput"
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
students
=
await
enrollProvider
.
getAllStudentInClass
(
req
.
body
.
class_id
);
return
res
.
s
tatus
(
200
).
json
(
students
);
return
res
.
s
endOk
({
data
:
students
}
);
}
catch
(
error
)
{
console
.
error
(
'Error fetching students in class:'
,
error
);
return
res
.
status
(
400
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lỗi khi lấy danh sách học sinh trong lớp!"
,
message_en
:
"Error occurred while fetching students in class!"
,
status
:
400
});
}
}
}
...
...
code/src/controllers/api/v1.0/enrollments/enroll/index.ts
View file @
11e8bbc6
...
...
@@ -2,7 +2,8 @@ import type { Application } from "express";
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
EnrollProvider
}
from
"#providers/EnrollProvider.js"
;
import
{
authorize
}
from
"#middlewares/authorization"
;
import
{
login
}
from
"#middlewares/authentication"
;
import
{
authMiddleware
}
from
"#middlewares/authentication"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
enrollProvider
=
new
EnrollProvider
();
...
...
@@ -33,16 +34,29 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Enrollment"
*/
post
:
{
middleware
:
[
login
,
authorize
(
"student"
)],
handler
:
async
(
req
,
res
)
=>
{
middleware
:
[
authMiddleware
,
authorize
(
"student"
)],
handler
:
async
(
req
:
Req
,
res
:
Res
)
=>
{
try
{
const
userId
=
(
req
as
any
).
user
.
id
;
const
userId
=
req
.
user
?.
id
;
if
(
!
userId
)
{
return
res
.
sendError
({
message
:
"Thông tin người dùng không hợp lệ!"
,
message_en
:
"Invalid user information!"
,
status
:
400
});
}
const
enroll
=
await
enrollProvider
.
enroll
(
userId
,
req
.
body
.
class_id
);
return
res
.
s
tatus
(
201
).
json
(
enroll
);
return
res
.
s
endOk
({
data
:
enroll
}
);
}
catch
(
error
)
{
console
.
error
(
'Error creating enrollment:'
,
error
);
return
res
.
status
(
400
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lỗi khi đăng ký khóa học!"
,
message_en
:
"Error occurred while enrolling in course!"
,
status
:
400
});
}
}
}
...
...
code/src/controllers/api/v1.0/enrollments/unenroll/index.ts
View file @
11e8bbc6
...
...
@@ -2,6 +2,7 @@ import type { Application } from "express";
import
type
{
Resource
}
from
"express-automatic-routes"
;
import
{
EnrollProvider
}
from
"#providers/EnrollProvider.js"
;
import
{
authorize
}
from
"#middlewares/authorization"
;
import
{
Req
,
Res
}
from
"#interface/IApi"
;
export
default
(
_express
:
Application
)
=>
{
const
enrollProvider
=
new
EnrollProvider
();
...
...
@@ -30,14 +31,18 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Unenrollment"
*/
post
:
{
handler
:
async
(
req
,
r
es
)
=>
{
handler
:
async
(
req
:
Req
,
res
:
R
es
)
=>
{
try
{
const
enroll
=
await
enrollProvider
.
unEnroll
(
req
.
body
.
userId
,
req
.
body
.
classId
);
return
res
.
s
tatus
(
201
).
json
(
enroll
);
return
res
.
s
endOk
({
data
:
enroll
}
);
}
catch
(
error
)
{
console
.
error
(
'Error unenrolling:'
,
error
);
return
res
.
status
(
400
).
json
({
error
:
(
error
as
Error
).
message
});
return
res
.
sendError
({
message
:
"Lỗi khi hủy đăng ký khóa học!"
,
message_en
:
"Error occurred while unenrolling from course!"
,
status
:
400
});
}
}
}
...
...
code/src/docs/swagger/swagger-output.json
View file @
11e8bbc6
...
...
@@ -19,38 +19,33 @@
"id"
:
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
,
"name"
:
"Lớp 12A1"
,
"description"
:
"Lớp chuyên toán"
,
"course_id"
:
"7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10"
,
"status"
:
"active"
,
"created_at"
:
"2026-05-16T08:00:00.000Z"
,
"created_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
,
"updated_at"
:
"2026-05-16T09:30:00.000Z"
,
"updated_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
,
"course_id"
:
"7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10"
,
"status"
:
"active"
"updated_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
},
"properties"
:
{
"id"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"example"
:
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
"format"
:
"uuid"
},
"name"
:
{
"type"
:
"string"
,
"example"
:
"Lớp 12A1"
"type"
:
"string"
},
"description"
:
{
"type"
:
"string"
,
"nullable"
:
true
,
"example"
:
"Lớp chuyên toán"
"nullable"
:
true
},
"course_id"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"nullable"
:
true
,
"example"
:
"7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10"
"nullable"
:
true
},
"status"
:
{
"type"
:
"string"
,
"nullable"
:
true
,
"example"
:
"active"
"nullable"
:
true
},
"created_at"
:
{
"type"
:
"string"
,
...
...
@@ -60,8 +55,7 @@
"created_by"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"nullable"
:
true
,
"example"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
"nullable"
:
true
},
"updated_at"
:
{
"type"
:
"string"
,
...
...
@@ -71,8 +65,106 @@
"updated_by"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"nullable"
:
true
,
"example"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
"nullable"
:
true
}
}
},
"ClassResponse"
:
{
"type"
:
"object"
,
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"message_en"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"responseData"
:
{
"$ref"
:
"#/components/schemas/Class"
},
"status"
:
{
"type"
:
"string"
,
"example"
:
"success"
},
"timeStamp"
:
{
"type"
:
"string"
,
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"number"
},
"message"
:
{
"type"
:
"string"
},
"action"
:
{
"nullable"
:
true
}
}
}
}
}
},
"ClassListResponse"
:
{
"type"
:
"object"
,
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"message_en"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"responseData"
:
{
"type"
:
"object"
,
"properties"
:
{
"count"
:
{
"type"
:
"integer"
},
"page"
:
{
"type"
:
"integer"
},
"pageSize"
:
{
"type"
:
"integer"
},
"rows"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Class"
}
}
}
},
"status"
:
{
"type"
:
"string"
,
"example"
:
"success"
},
"timeStamp"
:
{
"type"
:
"string"
,
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"number"
},
"message"
:
{
"type"
:
"string"
},
"action"
:
{
"nullable"
:
true
}
}
}
}
}
},
...
...
@@ -138,29 +230,25 @@
"id"
:
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
,
"name"
:
"Khóa học 12A1"
,
"description"
:
"Khóa học chuyên toán"
,
"status"
:
"active"
,
"created_at"
:
"2026-05-16T08:00:00.000Z"
,
"created_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
,
"status"
:
"active"
"created_by"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
},
"properties"
:
{
"id"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"example"
:
"3fa85f64-5717-4562-b3fc-2c963f66afa6"
"format"
:
"uuid"
},
"name"
:
{
"type"
:
"string"
,
"example"
:
"Khóa học 12A1"
"type"
:
"string"
},
"description"
:
{
"type"
:
"string"
,
"nullable"
:
true
,
"example"
:
"Khóa học chuyên toán"
"nullable"
:
true
},
"status"
:
{
"type"
:
"string"
,
"nullable"
:
true
,
"example"
:
"active"
"nullable"
:
true
},
"created_at"
:
{
"type"
:
"string"
,
...
...
@@ -170,8 +258,106 @@
"created_by"
:
{
"type"
:
"string"
,
"format"
:
"uuid"
,
"nullable"
:
true
,
"example"
:
"2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
"nullable"
:
true
}
}
},
"CourseResponse"
:
{
"type"
:
"object"
,
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"message_en"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"responseData"
:
{
"$ref"
:
"#/components/schemas/Course"
},
"status"
:
{
"type"
:
"string"
,
"example"
:
"success"
},
"timeStamp"
:
{
"type"
:
"string"
,
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"number"
},
"message"
:
{
"type"
:
"string"
},
"action"
:
{
"nullable"
:
true
}
}
}
}
}
},
"CourseListResponse"
:
{
"type"
:
"object"
,
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"message_en"
:
{
"type"
:
"string"
,
"nullable"
:
true
},
"responseData"
:
{
"type"
:
"object"
,
"properties"
:
{
"count"
:
{
"type"
:
"integer"
},
"page"
:
{
"type"
:
"integer"
},
"pageSize"
:
{
"type"
:
"integer"
},
"rows"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
}
}
},
"status"
:
{
"type"
:
"string"
,
"example"
:
"success"
},
"timeStamp"
:
{
"type"
:
"string"
,
"example"
:
"2024-02-26 03:12:45"
},
"violations"
:
{
"type"
:
"array"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"code"
:
{
"type"
:
"number"
},
"message"
:
{
"type"
:
"string"
},
"action"
:
{
"nullable"
:
true
}
}
}
}
}
},
...
...
@@ -278,34 +464,85 @@
"Profile"
:
{
"type"
:
"object"
,
"example"
:
{
"id"
:
"123"
,
"name"
:
"Pham Quang Bao"
,
"email"
:
"phamquangbao@example.com"
,
"phone"
:
"+1234567890"
,
"address"
:
"123 Main St, Anytown, USA"
"message"
:
"string"
,
"message_en"
:
"string"
,
"responseData"
:
{
"id"
:
"string"
,
"name"
:
"string"
,
"email"
:
"string"
,
"phone"
:
"string"
,
"address"
:
"string"
,
"status"
:
"string"
},
"status"
:
"success | fail"
,
"timeStamp"
:
"2024-02-26 03:12:45"
,
"violation"
:
[
{}
]
},
"properties"
:
{
"message"
:
{
"type"
:
"string"
,
"description"
:
"Thông điệp trả về"
},
"message_en"
:
{
"type"
:
"string"
,
"description"
:
"Thông điệp trả về (bằng tiếng Anh)"
},
"responseData"
:
{
"type"
:
"object"
,
"description"
:
"Dữ liệu phản hồi chứa thông tin hồ sơ người dùng"
,
"properties"
:
{
"id"
:
{
"type"
:
"uuid"
,
"format"
:
"uuid"
,
"example"
:
"123"
"type"
:
"string"
,
"description"
:
"ID của người dùng"
},
"name"
:
{
"type"
:
"string"
,
"example"
:
"Pham Quang Bao
"
"description"
:
"Tên của người dùng
"
},
"email"
:
{
"type"
:
"string"
,
"format"
:
"email"
,
"example"
:
"phamquangbao@example.com"
"description"
:
"Email của người dùng"
},
"phone"
:
{
"type"
:
"string"
,
"example"
:
"+1234567890
"
"description"
:
"Số điện thoại của người dùng
"
},
"address"
:
{
"type"
:
"string"
,
"example"
:
"123 Main St, Anytown, USA"
"description"
:
"Địa chỉ của người dùng"
},
"status"
:
{
"type"
:
"string"
,
"description"
:
"Trạng thái của người dùng"
}
}
},
"status"
:
{
"type"
:
"string"
,
"description"
:
"Trạng thái của phản hồi (success hoặc fail)"
},
"timeStamp"
:
{
"type"
:
"string"
,
"description"
:
"Thời gian phản hồi được tạo ra"
},
"violation"
:
{
"type"
:
"array"
,
"description"
:
"Danh sách các vi phạm (nếu có)"
,
"items"
:
{
"type"
:
"object"
,
"properties"
:
{
"field"
:
{
"type"
:
"string"
,
"description"
:
"Tên trường vi phạm"
},
"message"
:
{
"type"
:
"string"
,
"description"
:
"Thông điệp lỗi liên quan đến trường vi phạm"
}
}
}
}
}
},
...
...
@@ -691,10 +928,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Class"
}
"$ref"
:
"#/components/schemas/ClassResponse"
}
}
}
...
...
@@ -731,10 +965,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Class"
}
"$ref"
:
"#/components/schemas/ClassResponse"
}
}
}
...
...
@@ -791,10 +1022,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Class"
}
"$ref"
:
"#/components/schemas/ClassListResponse"
}
}
}
...
...
@@ -822,7 +1050,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"$ref"
:
"#/components/schemas/Class"
"$ref"
:
"#/components/schemas/Class
Response
"
}
}
}
...
...
@@ -857,10 +1085,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
"$ref"
:
"#/components/schemas/CourseResponse"
}
}
}
...
...
@@ -897,10 +1122,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
"$ref"
:
"#/components/schemas/CourseResponse"
}
}
}
...
...
@@ -927,10 +1149,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
"$ref"
:
"#/components/schemas/CourseResponse"
}
}
}
...
...
@@ -963,10 +1182,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
"$ref"
:
"#/components/schemas/CourseListResponse"
}
}
}
...
...
@@ -994,10 +1210,7 @@
"content"
:
{
"application/json"
:
{
"schema"
:
{
"type"
:
"array"
,
"items"
:
{
"$ref"
:
"#/components/schemas/Course"
}
"$ref"
:
"#/components/schemas/CourseResponse"
}
}
}
...
...
code/src/index.ts
View file @
11e8bbc6
...
...
@@ -4,6 +4,7 @@ import _autoroutes from 'express-automatic-routes';
import
swaggerUi
from
'swagger-ui-express'
;
import
dotenv
from
'dotenv'
;
import
swaggerFile
from
'#/docs/swagger/swagger-output.json'
;
import
response
from
'#middlewares/response'
;
dotenv
.
config
();
...
...
@@ -12,6 +13,8 @@ const port = 3000
app
.
use
(
express
.
json
());
app
.
use
(
response
as
express
.
RequestHandler
);
_autoroutes
(
app
,
{
dir
:
resolve
(
__dirname
,
'./controllers'
),
log
:
true
...
...
code/src/interface/IApi.ts
0 → 100644
View file @
11e8bbc6
import
{
IncomingHttpHeaders
}
from
"http"
;
import
{
Request
,
Response
}
from
"express"
;
import
type
{
WhereOptions
}
from
"sequelize"
;
export
interface
JWTPayload
{
id
:
string
;
email
:
string
;
username
?:
string
;
roles
?:
string
[];
role_id
?:
string
;
permissions
?:
string
[];
roleIds
?:
string
[];
departmentIds
?:
string
[];
type
?:
string
;
iat
?:
number
;
exp
?:
number
;
}
export
type
OkParams
=
{
data
:
unknown
;
message
?:
string
;
message_en
?:
string
;
status
?:
number
;
};
export
type
ErrorParams
=
{
message
:
string
;
message_en
:
string
;
status
:
number
;
error
?:
Error
;
};
export
type
ResponseDTOParams
=
{
data
?:
unknown
;
message
?:
string
;
message_en
?:
string
;
violations
?:
ViolationDTO
[];
};
export
interface
Req
extends
Request
{
user
?:
JWTPayload
;
headers
:
IncomingHttpHeaders
;
payload
:
payload
;
}
export
interface
payload
{
page
:
number
;
pageSize
:
number
;
sortBy
:
string
;
sortOrder
:
string
;
filters
:
WhereOptions
;
}
export
interface
Res
extends
Response
{
sendOk
:
(
params
:
OkParams
)
=>
void
;
sendError
:
(
params
:
ErrorParams
)
=>
void
;
}
export
const
ResponseDTO
=
({
data
,
message
,
message_en
,
violations
}:
ResponseDTOParams
)
=>
({
message
:
!
message
?
null
:
message
,
message_en
:
!
message_en
?
null
:
message_en
,
responseData
:
!
data
?
null
:
data
,
status
:
violations
===
undefined
||
violations
===
null
||
violations
.
length
==
0
?
"success"
:
"fail"
,
timeStamp
:
new
Date
().
toISOString
().
replace
(
/T/
,
" "
).
replace
(
/
\.
.+/
,
""
),
violations
:
!
violations
?
null
:
violations
,
});
export
class
ViolationDTO
{
private
code
:
number
;
private
message
:
string
;
private
action
:
unknown
;
constructor
(
code
:
number
,
message
:
string
,
action
:
unknown
=
null
)
{
this
.
code
=
code
;
this
.
message
=
message
;
this
.
action
=
action
;
}
}
\ No newline at end of file
code/src/middlewares/authentication.ts
View file @
11e8bbc6
import
jwt
from
'jsonwebtoken'
;
import
{
Response
,
NextFunction
}
from
'express'
;
import
{
NextFunction
}
from
'express'
;
import
{
JWTPayload
,
Req
,
Res
}
from
'#interface/IApi'
;
//demo
const
JWT_SECRET
=
process
.
env
.
JWT_SECRET
||
''
;
export
const
auth
enticate
=
(
req
:
any
,
res
:
Response
,
next
:
NextFunction
)
=>
{
export
const
auth
Middleware
=
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
)
=>
{
const
authHeader
=
req
.
headers
.
authorization
;
if
(
!
authHeader
||
!
authHeader
.
startsWith
(
'Bearer '
))
{
return
res
.
status
(
401
).
json
({
error
:
'Không tìm thấy token'
});
return
res
.
sendError
({
message
:
'Authorization header không hợp lệ'
,
message_en
:
'Invalid authorization header'
,
status
:
401
});
}
const
token
=
authHeader
.
split
(
' '
)[
1
];
try
{
const
decoded
=
jwt
.
verify
(
token
,
JWT_SECRET
)
as
any
;
if
(
!
token
)
{
return
res
.
sendError
({
message
:
'Không tìm thấy token'
,
message_en
:
'Token not found'
,
status
:
401
});
}
try
{
const
decoded
=
jwt
.
verify
(
token
,
JWT_SECRET
)
as
JWTPayload
;
req
.
user
=
decoded
;
next
();
return
next
();
}
catch
(
error
)
{
return
res
.
status
(
403
).
json
({
error
:
'Token không hợp lệ hoặc đã hết hạn'
});
return
res
.
sendError
({
message
:
'Token không hợp lệ hoặc đã hết hạn'
,
message_en
:
'Invalid or expired token'
,
status
:
403
});
}
};
\ No newline at end of file
code/src/middlewares/authorization.ts
View file @
11e8bbc6
import
jwt
from
'jsonwebtoken'
;
import
{
Request
,
Response
,
NextFunction
}
from
'express'
;
import
{
NextFunction
}
from
'express'
;
import
{
models
}
from
'#models/sequelize-config.js'
;
import
{
Req
,
Res
}
from
'#interface/IApi'
;
export
const
authorize
=
(...
allowedRoles
:
string
[])
=>
{
return
async
(
req
:
any
,
res
:
Response
,
next
:
NextFunction
)
=>
{
return
async
(
req
:
Req
,
res
:
Res
,
next
:
NextFunction
)
=>
{
const
roleId
=
req
.
user
?.
role_id
||
[];
const
role
=
await
models
.
roles
.
findOne
({
...
...
@@ -13,9 +13,13 @@ export const authorize = (...allowedRoles: string[]) => {
const
hasPermission
=
allowedRoles
.
includes
(
role
?.
name
||
''
);
if
(
!
hasPermission
)
{
return
res
.
status
(
403
).
json
({
error
:
'Bạn không có quyền truy cập tài nguyên này'
});
return
res
.
sendError
({
message
:
'Bạn không có quyền truy cập tài nguyên này'
,
message_en
:
'You do not have permission to access this resource'
,
status
:
403
});
}
next
();
return
next
();
};
};
\ No newline at end of file
code/src/middlewares/request.ts
0 → 100644
View file @
11e8bbc6
import
{
Req
,
Res
}
from
"#interface/IApi"
;
import
{
NextFunction
}
from
"express"
;
import
{
Op
}
from
"sequelize"
;
export
default
function
queryModifier
(
req
:
Req
,
_res
:
Res
,
next
:
NextFunction
)
{
const
pageSize
=
parseInt
(
req
.
query
.
pageSize
?.
toString
()
||
"10"
)
const
page
=
parseInt
(
req
.
query
.
page
?.
toString
()
||
"1"
)
const
sortBy
=
req
.
query
.
sortBy
?.
toString
()
||
'created_at'
const
sortOrder
=
req
.
query
.
sortOrder
?.
toString
()
||
'desc'
const
filters
=
req
.
query
.
filters
?.
toString
()
||
""
const
conditionCheckedChild
:
any
[]
=
[];
if
(
filters
)
{
// Tách các điều kiện bằng dấu phẩy ","
const
arrFilters
=
filters
.
split
(
","
);
arrFilters
.
forEach
((
element
)
=>
{
// Mỗi phần tử sẽ có dạng: "status|=|published" hoặc "title|like|NodeJS"
const
parts
=
element
.
split
(
"|"
);
// Phải đủ 3 thành phần: [Tên cột, Toán tử, Giá trị]
if
(
parts
.
length
===
3
)
{
const
[
field
,
operator
,
value
]
=
parts
as
[
string
,
string
,
string
];
let
condition
:
any
=
{};
// Dịch toán tử từ URL sang toán tử Sequelize tương ứng
switch
(
operator
.
toLowerCase
())
{
case
"="
:
condition
[
field
]
=
{
[
Op
.
eq
]:
value
};
break
;
case
"like"
:
condition
[
field
]
=
{
[
Op
.
iLike
]:
`%
${
value
}
%`
};
break
;
case
">"
:
condition
[
field
]
=
{
[
Op
.
gt
]:
value
};
break
;
case
"<"
:
condition
[
field
]
=
{
[
Op
.
lt
]:
value
};
break
;
case
">="
:
condition
[
field
]
=
{
[
Op
.
gte
]:
value
};
break
;
case
"<="
:
condition
[
field
]
=
{
[
Op
.
lte
]:
value
};
break
;
default
:
condition
[
field
]
=
{
[
Op
.
eq
]:
value
};
}
conditionCheckedChild
.
push
(
condition
);
}
});
}
// Kết hợp tất cả điều kiện với nhau bằng Op.and
const
finalFilters
=
conditionCheckedChild
.
length
>
0
?
{
[
Op
.
and
]:
conditionCheckedChild
}
:
{};
req
.
payload
=
{
page
,
pageSize
,
sortBy
,
sortOrder
,
filters
:
finalFilters
};
next
();
}
\ No newline at end of file
code/src/middlewares/response.ts
0 → 100644
View file @
11e8bbc6
import
{
ErrorParams
,
OkParams
,
Req
,
Res
,
ResponseDTO
,
ViolationDTO
}
from
"#interface/IApi"
;
import
{
NextFunction
}
from
"express"
;
export
default
function
(
_req
:
Req
,
res
:
Res
,
next
:
NextFunction
)
{
res
.
sendOk
=
function
({
data
,
message
=
'Thành công'
,
message_en
=
'Success'
,
status
}:
OkParams
)
{
res
.
status
(
status
??
200
).
json
(
ResponseDTO
({
data
,
message
,
message_en
,
violations
:
[]
})
);
};
res
.
sendError
=
function
({
message
=
'Đã xảy ra lỗi'
,
message_en
=
'An error occurred'
,
status
=
500
,
}:
ErrorParams
)
{
res
.
status
(
status
).
json
(
ResponseDTO
({
message
,
message_en
,
violations
:
[
new
ViolationDTO
(
status
,
message
,
''
)]
})
);
};
next
();
}
\ No newline at end of file
code/src/providers/ClassesProvider.ts
View file @
11e8bbc6
import
{
payload
}
from
'#interface/IApi'
;
import
{
models
}
from
'#models/sequelize-config.js'
;
interface
GetAllClassesParams
{
filters
?:
string
;
sort
?:
string
;
page
?:
number
;
pageSize
?:
number
;
}
interface
CreateClassInput
{
name
:
string
;
description
?:
string
;
...
...
@@ -14,18 +8,20 @@ interface CreateClassInput {
}
export
class
ClassesProvider
{
async
getAllClasses
(
params
:
GetAllClassesParams
)
{
const
{
filters
,
sort
,
page
=
1
,
pageSize
=
10
}
=
params
;
const
offset
=
(
page
-
1
)
*
pageSize
;
return
await
models
.
classes
.
findAndCountAll
({
// where
order
:
sort
?
[[
'created_at'
,
sort
]]
:
[[
'created_at'
,
'DESC'
]],
limit
:
pageSize
,
offset
:
offset
async
getAllClasses
(
params
:
payload
)
{
const
{
count
,
rows
}
=
await
models
.
classes
.
findAndCountAll
({
where
:
params
.
filters
,
order
:
params
.
sortBy
?
[[
params
.
sortBy
,
params
.
sortOrder
]]
:
[[
'created_at'
,
'DESC'
]],
limit
:
params
.
pageSize
,
offset
:
(
params
.
page
-
1
)
*
params
.
pageSize
,
raw
:
true
});
return
{
count
,
page
:
params
.
page
,
pageSize
:
params
.
pageSize
,
rows
};
}
async
createClass
(
data
:
CreateClassInput
)
{
...
...
code/src/providers/CoursesProvider.ts
View file @
11e8bbc6
import
{
payload
}
from
'#interface/IApi'
;
import
{
models
}
from
'#models/sequelize-config.js'
;
interface
GetAllCoursesParams
{
filters
?:
string
;
sort
?:
string
;
page
?:
number
;
pageSize
?:
number
;
}
interface
CreateCourseInput
{
name
:
string
;
description
:
string
;
...
...
@@ -14,17 +8,19 @@ interface CreateCourseInput {
}
export
class
CoursesProvider
{
async
getAllCourses
(
params
:
GetAllCoursesParams
)
{
const
{
filters
,
sort
,
page
=
1
,
pageSize
=
10
}
=
params
;
const
offset
=
(
page
-
1
)
*
pageSize
;
return
await
models
.
courses
.
findAndCountAll
({
// where
order
:
sort
?
[[
'created_at'
,
sort
]]
:
[[
'created_at'
,
'DESC'
]],
limit
:
pageSize
,
offset
:
offset
async
getAllCourses
(
params
:
payload
)
{
const
{
count
,
rows
}
=
await
models
.
courses
.
findAndCountAll
({
where
:
params
.
filters
,
order
:
params
.
sortBy
?
[[
params
.
sortBy
,
params
.
sortOrder
]]
:
[[
'created_at'
,
'DESC'
]],
limit
:
params
.
pageSize
,
offset
:
(
params
.
page
-
1
)
*
params
.
pageSize
});
return
{
count
,
page
:
params
.
page
,
pageSize
:
params
.
pageSize
,
rows
};
}
async
getCourseById
(
courseId
:
string
)
{
...
...
code/src/providers/EnrollProvider.ts
View file @
11e8bbc6
import
{
models
}
from
"#models/sequelize-config.js"
;
export
class
EnrollProvider
{
async
enroll
(
userId
:
string
,
classId
:
string
)
{
const
checkEnrollment
=
await
models
.
enrollments
.
findOne
({
...
...
code/src/services/authService.ts
View file @
11e8bbc6
...
...
@@ -88,7 +88,7 @@ export class AuthService {
}
}
async
profileUser
(
userId
:
number
)
{
async
profileUser
(
userId
:
string
)
{
const
user
=
await
models
.
users
.
findByPk
(
userId
);
if
(
!
user
)
{
...
...
code/src/templates/swagger/authProfile/schema.ts
View file @
11e8bbc6
...
...
@@ -2,35 +2,86 @@ const authProfileSchemas = {
Profile
:
{
type
:
'object'
,
example
:
{
id
:
'123'
,
name
:
'Pham Quang Bao'
,
email
:
'phamquangbao@example.com'
,
phone
:
'+1234567890'
,
address
:
'123 Main St, Anytown, USA'
,
message
:
"string"
,
message_en
:
"string"
,
responseData
:
{
id
:
"string"
,
name
:
"string"
,
email
:
"string"
,
phone
:
"string"
,
address
:
"string"
,
status
:
"string"
,
},
status
:
"success | fail"
,
timeStamp
:
"2024-02-26 03:12:45"
,
violation
:
[
{}
]
},
properties
:
{
message
:
{
type
:
'string'
,
description
:
'Thông điệp trả về'
},
message_en
:
{
type
:
'string'
,
description
:
'Thông điệp trả về (bằng tiếng Anh)'
},
responseData
:
{
type
:
'object'
,
description
:
'Dữ liệu phản hồi chứa thông tin hồ sơ người dùng'
,
properties
:
{
id
:
{
type
:
'uuid'
,
format
:
'uuid'
,
example
:
'123'
,
type
:
'string'
,
description
:
'ID của người dùng'
},
name
:
{
type
:
'string'
,
example
:
'Pham Quang Bao'
,
description
:
'Tên của người dùng'
},
email
:
{
type
:
'string'
,
format
:
'email'
,
example
:
'phamquangbao@example.com'
,
description
:
'Email của người dùng'
},
phone
:
{
type
:
'string'
,
example
:
'+1234567890'
,
description
:
'Số điện thoại của người dùng'
},
address
:
{
type
:
'string'
,
example
:
'123 Main St, Anytown, USA'
,
description
:
'Địa chỉ của người dùng'
},
status
:
{
type
:
'string'
,
description
:
'Trạng thái của người dùng'
}
}
},
status
:
{
type
:
'string'
,
description
:
'Trạng thái của phản hồi (success hoặc fail)'
},
timeStamp
:
{
type
:
'string'
,
description
:
'Thời gian phản hồi được tạo ra'
},
violation
:
{
type
:
'array'
,
description
:
'Danh sách các vi phạm (nếu có)'
,
items
:
{
type
:
'object'
,
properties
:
{
field
:
{
type
:
'string'
,
description
:
'Tên trường vi phạm'
},
message
:
{
type
:
'string'
,
description
:
'Thông điệp lỗi liên quan đến trường vi phạm'
}
}
}
}
},
},
};
...
...
code/src/templates/swagger/classes/schemas.ts
View file @
11e8bbc6
const
classSchemas
=
{
// get schema
Class
:
{
type
:
'object'
,
example
:
{
id
:
'3fa85f64-5717-4562-b3fc-2c963f66afa6'
,
name
:
'Lớp 12A1'
,
description
:
'Lớp chuyên toán'
,
course_id
:
'7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10'
,
status
:
'active'
,
created_at
:
'2026-05-16T08:00:00.000Z'
,
created_by
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
updated_at
:
'2026-05-16T09:30:00.000Z'
,
updated_by
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
course_id
:
'7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10'
,
status
:
'active'
,
},
properties
:
{
id
:
{
type
:
'string'
,
format
:
'uuid'
,
example
:
'3fa85f64-5717-4562-b3fc-2c963f66afa6'
,
id
:
{
type
:
'string'
,
format
:
'uuid'
},
name
:
{
type
:
'string'
},
description
:
{
type
:
'string'
,
nullable
:
true
},
course_id
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
},
status
:
{
type
:
'string'
,
nullable
:
true
},
created_at
:
{
type
:
'string'
,
format
:
'date-time'
,
nullable
:
true
},
created_by
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
},
updated_at
:
{
type
:
'string'
,
format
:
'date-time'
,
nullable
:
true
},
updated_by
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
},
},
name
:
{
type
:
'string'
,
example
:
'Lớp 12A1'
,
},
description
:
{
// get schema
ClassResponse
:
{
type
:
'object'
,
properties
:
{
message
:
{
type
:
'string'
,
nullable
:
true
,
example
:
'Lớp chuyên toán'
,
},
course_id
:
{
message_en
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
,
example
:
'7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10'
,
},
responseData
:
{
$ref
:
'#/components/schemas/Class'
,
},
status
:
{
type
:
'string'
,
nullable
:
true
,
example
:
'active'
,
example
:
'success'
,
},
created_at
:
{
timeStamp
:
{
type
:
'string'
,
format
:
'date-time'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
items
:
{
type
:
'object'
,
properties
:
{
code
:
{
type
:
'number'
,
},
message
:
{
type
:
'string'
,
},
action
:
{
nullable
:
true
,
},
created_by
:
{
},
},
},
},
},
ClassListResponse
:
{
type
:
'object'
,
properties
:
{
message
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
,
example
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
},
updated_at
:
{
message_en
:
{
type
:
'string'
,
format
:
'date-time'
,
nullable
:
true
,
},
updated_by
:
{
responseData
:
{
type
:
'object'
,
properties
:
{
count
:
{
type
:
'integer'
,
},
page
:
{
type
:
'integer'
,
},
pageSize
:
{
type
:
'integer'
,
},
rows
:
{
type
:
'array'
,
items
:
{
$ref
:
'#/components/schemas/Class'
,
},
},
},
},
status
:
{
type
:
'string'
,
format
:
'uuid'
,
example
:
'success'
,
},
timeStamp
:
{
type
:
'string'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
items
:
{
type
:
'object'
,
properties
:
{
code
:
{
type
:
'number'
,
},
message
:
{
type
:
'string'
,
},
action
:
{
nullable
:
true
,
example
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
},
},
},
},
},
},
...
...
code/src/templates/swagger/courses/schemas.ts
View file @
11e8bbc6
...
...
@@ -5,41 +5,117 @@ const courseSchemas = {
id
:
'3fa85f64-5717-4562-b3fc-2c963f66afa6'
,
name
:
'Khóa học 12A1'
,
description
:
'Khóa học chuyên toán'
,
status
:
'active'
,
created_at
:
'2026-05-16T08:00:00.000Z'
,
created_by
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
status
:
'active'
,
},
properties
:
{
id
:
{
type
:
'string'
,
format
:
'uuid'
,
example
:
'3fa85f64-5717-4562-b3fc-2c963f66afa6'
,
id
:
{
type
:
'string'
,
format
:
'uuid'
},
name
:
{
type
:
'string'
},
description
:
{
type
:
'string'
,
nullable
:
true
},
status
:
{
type
:
'string'
,
nullable
:
true
},
created_at
:
{
type
:
'string'
,
format
:
'date-time'
,
nullable
:
true
},
created_by
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
},
},
name
:
{
},
CourseResponse
:
{
type
:
'object'
,
properties
:
{
message
:
{
type
:
'string'
,
example
:
'Khóa học 12A1'
,
nullable
:
true
,
},
descriptio
n
:
{
message_e
n
:
{
type
:
'string'
,
nullable
:
true
,
example
:
'Khóa học chuyên toán'
,
},
responseData
:
{
$ref
:
'#/components/schemas/Course'
,
},
status
:
{
type
:
'string'
,
example
:
'success'
,
},
timeStamp
:
{
type
:
'string'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
items
:
{
type
:
'object'
,
properties
:
{
code
:
{
type
:
'number'
,
},
message
:
{
type
:
'string'
,
},
action
:
{
nullable
:
true
,
example
:
'active'
,
},
created_at
:
{
},
},
},
},
},
CourseListResponse
:
{
type
:
'object'
,
properties
:
{
message
:
{
type
:
'string'
,
format
:
'date-time'
,
nullable
:
true
,
},
created_by
:
{
message_en
:
{
type
:
'string'
,
format
:
'uuid'
,
nullable
:
true
,
example
:
'2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11'
,
}
},
responseData
:
{
type
:
'object'
,
properties
:
{
count
:
{
type
:
'integer'
,
},
page
:
{
type
:
'integer'
,
},
pageSize
:
{
type
:
'integer'
,
},
rows
:
{
type
:
'array'
,
items
:
{
$ref
:
'#/components/schemas/Course'
,
},
},
},
},
status
:
{
type
:
'string'
,
example
:
'success'
,
},
timeStamp
:
{
type
:
'string'
,
example
:
'2024-02-26 03:12:45'
,
},
violations
:
{
type
:
'array'
,
items
:
{
type
:
'object'
,
properties
:
{
code
:
{
type
:
'number'
,
},
message
:
{
type
:
'string'
,
},
action
:
{
nullable
:
true
,
},
},
},
},
},
},
...
...
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