Commit 11e8bbc6 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

feat(challenge_7): add filter, pagination and response Json

parent fb9be9d0
import { Req, Res } from "#interface/IApi";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express" import { Application } from "express"
import { Resource } from "express-automatic-routes" import { Resource } from "express-automatic-routes"
...@@ -29,14 +30,17 @@ export default (_express: Application) => { ...@@ -29,14 +30,17 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/LoginResponse" * $ref: "#/components/schemas/LoginResponse"
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const login = await authService.loginUser(req.body); const login = await authService.loginUser(req.body);
return res.status(200).json(login); return res.sendOk({ data: login });
} catch (error) { } catch (error) {
console.error('Error logging in user:', 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
});
} }
} }
} }
......
import { authenticate } from "#middlewares/authentication"; import { Req, Res } from "#interface/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
...@@ -24,19 +25,21 @@ export default (_express: Application) => { ...@@ -24,19 +25,21 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/logoutResponse" * $ref: "#/components/schemas/logoutResponse"
*/ */
post: { post: {
middleware: [authenticate], middleware: [authMiddleware],
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const userId = (req as any).user.id; const userId = (req as any).user.id;
console.log('User ID to logout:', userId); console.log('User ID to logout:', userId);
return res.status(200).json({ return res.sendOk({ data: null });
message: 'Đăng xuất thành công',
});
} catch (error) { } 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
});
} }
} }
} }
......
import { authenticate } from "#middlewares/authentication"; import { Req, Res } from "#interface/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
...@@ -24,17 +25,28 @@ export default (_express: Application) => { ...@@ -24,17 +25,28 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Profile" * $ref: "#/components/schemas/Profile"
*/ */
get: { get: {
middleware: [authenticate], middleware: [authMiddleware],
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { 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) { } 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
});
} }
} }
} }
......
import { Req, Res } from "#interface/IApi";
import { AuthService } from "#services/authService"; import { AuthService } from "#services/authService";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
...@@ -29,23 +30,35 @@ export default (_express: Application) => { ...@@ -29,23 +30,35 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Register" * $ref: "#/components/schemas/Register"
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const { name, email, password } = req.body; const { name, email, password } = req.body;
if (!name || !email || !password) { 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) { 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); const register = await authService.registerUser(req.body);
return res.status(201).json(register); return res.sendOk({ data: register });
} catch (error) { } catch (error) {
console.error('Error registering user:', 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
});
} }
} }
} }
......
import { authenticate } from "#middlewares/authentication"; import { Req, Res } from "#interface/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService.js"; import { AuthService } from "#services/authService.js";
import { MailService } from "#services/mailService.js"; import { MailService } from "#services/mailService.js";
import { Application } from "express"; import { Application } from "express";
...@@ -28,12 +29,20 @@ export default (_express: Application) => { ...@@ -28,12 +29,20 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/sendOtpResponse" * $ref: "#/components/schemas/sendOtpResponse"
*/ */
post: { post: {
middleware: [authenticate], middleware: [authMiddleware],
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const userId = (req as any).user.id; const userId = req.user?.id;
const email = (req as any).user.email; 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 // get otp
const otp = await authService.otpUser(userId); const otp = await authService.otpUser(userId);
...@@ -42,19 +51,23 @@ export default (_express: Application) => { ...@@ -42,19 +51,23 @@ export default (_express: Application) => {
if (!mailResult || !mailResult.success) { if (!mailResult || !mailResult.success) {
console.error('Gửi mail thất bại:', mailResult?.error); console.error('Gửi mail thất bại:', mailResult?.error);
return res.status(500).json({ return res.sendError({
email: email,
message: 'Không thể gửi email OTP. Vui lòng thử lại sau.', 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.status(200).json({ return res.sendOk({
email: email, data: null,
message: 'OTP đã được gửi đến email của bạn' message: 'OTP đã được gửi đến email của bạn'
}); });
} catch (error) { } 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
});
} }
} }
} }
......
import { authenticate } from "#middlewares/authentication"; import { Req, Res } from "#interface/IApi";
import { authMiddleware } from "#middlewares/authentication";
import { AuthService } from "#services/authService.js"; import { AuthService } from "#services/authService.js";
import { Application } from "express"; import { Application } from "express";
import { Resource } from "express-automatic-routes"; import { Resource } from "express-automatic-routes";
...@@ -32,15 +33,28 @@ export default (_express: Application) => { ...@@ -32,15 +33,28 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/verifyOtpResponse" * $ref: "#/components/schemas/verifyOtpResponse"
*/ */
post: { post: {
middleware: [authenticate], middleware: [authMiddleware],
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { 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); const verifyResult = await authService.verifyOtp(userId, req.body.otp);
res.status(201).json(verifyResult);
return res.sendOk({ data: verifyResult });
} catch (error) { } 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
});
} }
} }
} }
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider"; import { ClassesProvider } from "#providers/ClassesProvider";
import { Req, Res } from "#interface/IApi";
import queryModifier from "#middlewares/request";
export default (_express: Application) => { export default (_express: Application) => {
const classesProvider = new ClassesProvider(); const classesProvider = new ClassesProvider();
...@@ -22,26 +24,23 @@ export default (_express: Application) => { ...@@ -22,26 +24,23 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/ClassListResponse"
* items:
* $ref: "#/components/schemas/Class"
*/ */
get: { get: {
handler: async (req, res) => { middleware: [queryModifier],
try {
const { filters, sort, page, pageSize } = req.query;
const classes = await classesProvider.getAllClasses({ handler: async (req: Req, res: Res) => {
filters: filters as string, try {
sort: sort as string, const classes = await classesProvider.getAllClasses(req.payload);
page: page ? parseInt(page as string) : 1,
pageSize: pageSize ? parseInt(pageSize as string) : 10
});
return res.json(classes); return res.sendOk({ data: classes });
} catch (error) { } catch (error) {
console.error('Error getting classes:', 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) => { ...@@ -64,21 +63,25 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* $ref: "#/components/schemas/Class" * $ref: "#/components/schemas/ClassResponse"
* 400: * 400:
* description: Dữ liệu đầu vào không hợp lệ (Thiếu trường, sai định dạng) * description: Dữ liệu đầu vào không hợp lệ (Thiếu trường, sai định dạng)
* 500: * 500:
* description: Lỗi hệ thống phía Server * description: Lỗi hệ thống phía Server
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const newClass = await classesProvider.createClass(req.body); const newClass = await classesProvider.createClass(req.body);
return res.status(201).json(newClass); return res.sendOk({ data: newClass });
} catch (error) { } catch (error) {
console.error('Error creating class:', 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
});
} }
} }
} }
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { ClassesProvider } from "#providers/ClassesProvider.js"; import { ClassesProvider } from "#providers/ClassesProvider.js";
import { Req, Res } from "#interface/IApi";
export default (_express: Application) => { export default (_express: Application) => {
const classesProvider = new ClassesProvider(); const classesProvider = new ClassesProvider();
...@@ -24,23 +25,29 @@ export default (_express: Application) => { ...@@ -24,23 +25,29 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/ClassResponse"
* items:
* $ref: "#/components/schemas/Class"
*/ */
get: { get: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const { id } = req.params; const { id } = req.params;
if (!id || typeof id !== 'string') { 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 { try {
const classData = await classesProvider.getClassById(id); const classData = await classesProvider.getClassById(id);
return res.json(classData); return res.sendOk({ data: classData });
} catch (error) { } 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) => { ...@@ -70,23 +77,29 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/ClassResponse"
* items:
* $ref: "#/components/schemas/Class"
*/ */
put: { put: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const { id } = req.params; const { id } = req.params;
if (!id || typeof id !== 'string') { 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 { try {
const classData = await classesProvider.updateClassById(id, req.body); const classData = await classesProvider.updateClassById(id, req.body);
return res.json(classData); return res.sendOk({ data: classData });
} catch (error) { } 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) => { ...@@ -110,18 +123,26 @@ export default (_express: Application) => {
* description: Xóa lớp học thành công * description: Xóa lớp học thành công
*/ */
delete: { delete: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const { id } = req.params; const { id } = req.params;
if (!id || typeof id !== 'string') { 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 { try {
const classData = await classesProvider.deleteClassById(id); await classesProvider.deleteClassById(id);
return res.json(classData); return res.sendOk({ data: null });
} catch (error) { } 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
});
} }
} }
} }
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { CoursesProvider } from "#providers/CoursesProvider.js"; import { CoursesProvider } from "#providers/CoursesProvider.js";
import { Req, Res } from "#interface/IApi";
export default (_express: Application) => { export default (_express: Application) => {
const coursesProvider = new CoursesProvider(); const coursesProvider = new CoursesProvider();
...@@ -22,26 +23,21 @@ export default (_express: Application) => { ...@@ -22,26 +23,21 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/CourseListResponse"
* items:
* $ref: "#/components/schemas/Course"
*/ */
get: { get: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const { filters, sort, page, pageSize } = req.query; const courses = await coursesProvider.getAllCourses(req.payload);
const courses = await coursesProvider.getAllCourses({ return res.sendOk({ data: courses });
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);
} catch (error) { } catch (error) {
console.error('Error getting courses:', 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) => { ...@@ -64,19 +60,21 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/CourseResponse"
* items:
* $ref: "#/components/schemas/Course"
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const newCourse = await coursesProvider.createCourse(req.body); const newCourse = await coursesProvider.createCourse(req.body);
return res.status(201).json(newCourse); return res.sendOk({ data: newCourse });
} catch (error) { } catch (error) {
console.error('Error creating course:', 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
});
} }
} }
} }
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { CoursesProvider } from "#providers/CoursesProvider.js"; import { CoursesProvider } from "#providers/CoursesProvider.js";
import { Req, Res } from "#interface/IApi";
export default (_express: Application) => { export default (_express: Application) => {
const coursesProvider = new CoursesProvider(); const coursesProvider = new CoursesProvider();
...@@ -24,20 +25,22 @@ export default (_express: Application) => { ...@@ -24,20 +25,22 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/CourseResponse"
* items:
* $ref: "#/components/schemas/Course"
*/ */
get: { get: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const id = req.params.id; const id = req.params.id;
if (!id || typeof id !== 'string') { 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); const course = await coursesProvider.getCourseById(id);
return res.json(course); return res.sendOk({ data: course });
} }
}, },
...@@ -66,23 +69,29 @@ export default (_express: Application) => { ...@@ -66,23 +69,29 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/CourseResponse"
* items:
* $ref: "#/components/schemas/Course"
*/ */
put: { put: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const { id } = req.params; const { id } = req.params;
if (!id || typeof id !== 'string') { 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 { try {
const updateCourse = await coursesProvider.updateCourse(id, req.body); const updateCourse = await coursesProvider.updateCourse(id, req.body);
return res.json(updateCourse); return res.sendOk({ data: updateCourse });
} catch (error) { } 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) => { ...@@ -105,23 +114,29 @@ export default (_express: Application) => {
* content: * content:
* application/json: * application/json:
* schema: * schema:
* type: array * $ref: "#/components/schemas/CourseResponse"
* items:
* $ref: "#/components/schemas/Course"
*/ */
delete: { delete: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
const { id } = req.params; const { id } = req.params;
if (!id || typeof id !== 'string') { 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 { try {
const deletedCourse = await coursesProvider.deleteCourse(id); const deletedCourse = await coursesProvider.deleteCourse(id);
return res.json(deletedCourse); return res.sendOk({ data: deletedCourse });
} catch (error) { } 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
});
} }
} }
} }
......
import type { Application } from "express"; import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { Req, Res } from "#interface/IApi";
export default (_express: Application) => { export default (_express: Application) => {
const enrollProvider = new EnrollProvider(); const enrollProvider = new EnrollProvider();
...@@ -29,14 +30,18 @@ export default (_express: Application) => { ...@@ -29,14 +30,18 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/AllStudentInClassOutput" * $ref: "#/components/schemas/AllStudentInClassOutput"
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const students = await enrollProvider.getAllStudentInClass(req.body.class_id); const students = await enrollProvider.getAllStudentInClass(req.body.class_id);
return res.status(200).json(students); return res.sendOk({ data: students });
} catch (error) { } catch (error) {
console.error('Error fetching students in class:', 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
});
} }
} }
} }
......
...@@ -2,7 +2,8 @@ import type { Application } from "express"; ...@@ -2,7 +2,8 @@ import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization"; 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) => { export default (_express: Application) => {
const enrollProvider = new EnrollProvider(); const enrollProvider = new EnrollProvider();
...@@ -33,16 +34,29 @@ export default (_express: Application) => { ...@@ -33,16 +34,29 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Enrollment" * $ref: "#/components/schemas/Enrollment"
*/ */
post: { post: {
middleware: [login, authorize("student")], middleware: [authMiddleware, authorize("student")],
handler: async (req, res) => {
handler: async (req: Req, res: Res) => {
try { 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); const enroll = await enrollProvider.enroll(userId, req.body.class_id);
return res.status(201).json(enroll); return res.sendOk({ data: enroll });
} catch (error) { } catch (error) {
console.error('Error creating enrollment:', 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
});
} }
} }
} }
......
...@@ -2,6 +2,7 @@ import type { Application } from "express"; ...@@ -2,6 +2,7 @@ import type { Application } from "express";
import type { Resource } from "express-automatic-routes"; import type { Resource } from "express-automatic-routes";
import { EnrollProvider } from "#providers/EnrollProvider.js"; import { EnrollProvider } from "#providers/EnrollProvider.js";
import { authorize } from "#middlewares/authorization"; import { authorize } from "#middlewares/authorization";
import { Req, Res } from "#interface/IApi";
export default (_express: Application) => { export default (_express: Application) => {
const enrollProvider = new EnrollProvider(); const enrollProvider = new EnrollProvider();
...@@ -30,14 +31,18 @@ export default (_express: Application) => { ...@@ -30,14 +31,18 @@ export default (_express: Application) => {
* $ref: "#/components/schemas/Unenrollment" * $ref: "#/components/schemas/Unenrollment"
*/ */
post: { post: {
handler: async (req, res) => { handler: async (req: Req, res: Res) => {
try { try {
const enroll = await enrollProvider.unEnroll(req.body.userId, req.body.classId); const enroll = await enrollProvider.unEnroll(req.body.userId, req.body.classId);
return res.status(201).json(enroll); return res.sendOk({ data: enroll });
} catch (error) { } catch (error) {
console.error('Error unenrolling:', 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
});
} }
} }
} }
......
...@@ -19,38 +19,33 @@ ...@@ -19,38 +19,33 @@
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Lớp 12A1", "name": "Lớp 12A1",
"description": "Lớp chuyên toán", "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_at": "2026-05-16T08:00:00.000Z",
"created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11", "created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11",
"updated_at": "2026-05-16T09:30:00.000Z", "updated_at": "2026-05-16T09:30:00.000Z",
"updated_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11", "updated_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
"course_id": "7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10",
"status": "active"
}, },
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid"
"example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}, },
"name": { "name": {
"type": "string", "type": "string"
"example": "Lớp 12A1"
}, },
"description": { "description": {
"type": "string", "type": "string",
"nullable": true, "nullable": true
"example": "Lớp chuyên toán"
}, },
"course_id": { "course_id": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
"nullable": true, "nullable": true
"example": "7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10"
}, },
"status": { "status": {
"type": "string", "type": "string",
"nullable": true, "nullable": true
"example": "active"
}, },
"created_at": { "created_at": {
"type": "string", "type": "string",
...@@ -60,8 +55,7 @@ ...@@ -60,8 +55,7 @@
"created_by": { "created_by": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
"nullable": true, "nullable": true
"example": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
}, },
"updated_at": { "updated_at": {
"type": "string", "type": "string",
...@@ -71,8 +65,106 @@ ...@@ -71,8 +65,106 @@
"updated_by": { "updated_by": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
"nullable": true, "nullable": true
"example": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11" }
}
},
"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 @@ ...@@ -138,29 +230,25 @@
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Khóa học 12A1", "name": "Khóa học 12A1",
"description": "Khóa học chuyên toán", "description": "Khóa học chuyên toán",
"status": "active",
"created_at": "2026-05-16T08:00:00.000Z", "created_at": "2026-05-16T08:00:00.000Z",
"created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11", "created_by": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11"
"status": "active"
}, },
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid"
"example": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}, },
"name": { "name": {
"type": "string", "type": "string"
"example": "Khóa học 12A1"
}, },
"description": { "description": {
"type": "string", "type": "string",
"nullable": true, "nullable": true
"example": "Khóa học chuyên toán"
}, },
"status": { "status": {
"type": "string", "type": "string",
"nullable": true, "nullable": true
"example": "active"
}, },
"created_at": { "created_at": {
"type": "string", "type": "string",
...@@ -170,8 +258,106 @@ ...@@ -170,8 +258,106 @@
"created_by": { "created_by": {
"type": "string", "type": "string",
"format": "uuid", "format": "uuid",
"nullable": true, "nullable": true
"example": "2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11" }
}
},
"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 @@ ...@@ -278,34 +464,85 @@
"Profile": { "Profile": {
"type": "object", "type": "object",
"example": { "example": {
"id": "123", "message": "string",
"name": "Pham Quang Bao", "message_en": "string",
"email": "phamquangbao@example.com", "responseData": {
"phone": "+1234567890", "id": "string",
"address": "123 Main St, Anytown, USA" "name": "string",
"email": "string",
"phone": "string",
"address": "string",
"status": "string"
},
"status": "success | fail",
"timeStamp": "2024-02-26 03:12:45",
"violation": [
{}
]
}, },
"properties": { "properties": {
"id": { "message": {
"type": "uuid",
"format": "uuid",
"example": "123"
},
"name": {
"type": "string", "type": "string",
"example": "Pham Quang Bao" "description": "Thông điệp trả về"
}, },
"email": { "message_en": {
"type": "string", "type": "string",
"format": "email", "description": "Thông điệp trả về (bằng tiếng Anh)"
"example": "phamquangbao@example.com"
}, },
"phone": { "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": "string",
"description": "ID của người dùng"
},
"name": {
"type": "string",
"description": "Tên của người dùng"
},
"email": {
"type": "string",
"description": "Email của người dùng"
},
"phone": {
"type": "string",
"description": "Số điện thoại của người dùng"
},
"address": {
"type": "string",
"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", "type": "string",
"example": "+1234567890" "description": "Trạng thái của phản hồi (success hoặc fail)"
}, },
"address": { "timeStamp": {
"type": "string", "type": "string",
"example": "123 Main St, Anytown, USA" "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 @@ ...@@ -691,10 +928,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/ClassResponse"
"items": {
"$ref": "#/components/schemas/Class"
}
} }
} }
} }
...@@ -731,10 +965,7 @@ ...@@ -731,10 +965,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/ClassResponse"
"items": {
"$ref": "#/components/schemas/Class"
}
} }
} }
} }
...@@ -791,10 +1022,7 @@ ...@@ -791,10 +1022,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/ClassListResponse"
"items": {
"$ref": "#/components/schemas/Class"
}
} }
} }
} }
...@@ -822,7 +1050,7 @@ ...@@ -822,7 +1050,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"$ref": "#/components/schemas/Class" "$ref": "#/components/schemas/ClassResponse"
} }
} }
} }
...@@ -857,10 +1085,7 @@ ...@@ -857,10 +1085,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/CourseResponse"
"items": {
"$ref": "#/components/schemas/Course"
}
} }
} }
} }
...@@ -897,10 +1122,7 @@ ...@@ -897,10 +1122,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/CourseResponse"
"items": {
"$ref": "#/components/schemas/Course"
}
} }
} }
} }
...@@ -927,10 +1149,7 @@ ...@@ -927,10 +1149,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/CourseResponse"
"items": {
"$ref": "#/components/schemas/Course"
}
} }
} }
} }
...@@ -963,10 +1182,7 @@ ...@@ -963,10 +1182,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/CourseListResponse"
"items": {
"$ref": "#/components/schemas/Course"
}
} }
} }
} }
...@@ -994,10 +1210,7 @@ ...@@ -994,10 +1210,7 @@
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "array", "$ref": "#/components/schemas/CourseResponse"
"items": {
"$ref": "#/components/schemas/Course"
}
} }
} }
} }
......
...@@ -4,6 +4,7 @@ import _autoroutes from 'express-automatic-routes'; ...@@ -4,6 +4,7 @@ import _autoroutes from 'express-automatic-routes';
import swaggerUi from 'swagger-ui-express'; import swaggerUi from 'swagger-ui-express';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import swaggerFile from '#/docs/swagger/swagger-output.json'; import swaggerFile from '#/docs/swagger/swagger-output.json';
import response from '#middlewares/response';
dotenv.config(); dotenv.config();
...@@ -12,6 +13,8 @@ const port = 3000 ...@@ -12,6 +13,8 @@ const port = 3000
app.use(express.json()); app.use(express.json());
app.use(response as express.RequestHandler);
_autoroutes(app, { _autoroutes(app, {
dir: resolve(__dirname, './controllers'), dir: resolve(__dirname, './controllers'),
log: true log: true
......
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
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Response, NextFunction } from 'express'; import { NextFunction } from 'express';
import { JWTPayload, Req, Res } from '#interface/IApi';
//demo //demo
const JWT_SECRET = process.env.JWT_SECRET || ''; const JWT_SECRET = process.env.JWT_SECRET || '';
export const authenticate = (req: any, res: Response, next: NextFunction) => { export const authMiddleware = (req: Req, res: Res, next: NextFunction) => {
const authHeader = req.headers.authorization; const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) { 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]; const token = authHeader.split(' ')[1];
try { if (!token) {
const decoded = jwt.verify(token, JWT_SECRET) as any; 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; req.user = decoded;
next();
return next();
} catch (error) { } 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
import jwt from 'jsonwebtoken'; import { NextFunction } from 'express';
import { Request, Response, NextFunction } from 'express';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
import { Req, Res } from '#interface/IApi';
export const authorize = (...allowedRoles: string[]) => { 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 roleId = req.user?.role_id || [];
const role = await models.roles.findOne({ const role = await models.roles.findOne({
...@@ -13,9 +13,13 @@ export const authorize = (...allowedRoles: string[]) => { ...@@ -13,9 +13,13 @@ export const authorize = (...allowedRoles: string[]) => {
const hasPermission = allowedRoles.includes(role?.name || ''); const hasPermission = allowedRoles.includes(role?.name || '');
if (!hasPermission) { 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
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
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
import { payload } from '#interface/IApi';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
interface GetAllClassesParams {
filters?: string;
sort?: string;
page?: number;
pageSize?: number;
}
interface CreateClassInput { interface CreateClassInput {
name: string; name: string;
description?: string; description?: string;
...@@ -14,18 +8,20 @@ interface CreateClassInput { ...@@ -14,18 +8,20 @@ interface CreateClassInput {
} }
export class ClassesProvider { export class ClassesProvider {
async getAllClasses(params: payload) {
async getAllClasses(params: GetAllClassesParams) { const { count, rows } = await models.classes.findAndCountAll({
const { filters, sort, page = 1, pageSize = 10 } = params; where: params.filters,
order: params.sortBy ? [[params.sortBy, params.sortOrder]] : [['created_at', 'DESC']],
const offset = (page - 1) * pageSize; limit: params.pageSize,
offset: (params.page - 1) * params.pageSize,
return await models.classes.findAndCountAll({ raw: true
// where
order: sort ? [['created_at', sort]] : [['created_at', 'DESC']],
limit: pageSize,
offset: offset
}); });
return {
count,
page: params.page,
pageSize: params.pageSize,
rows
};
} }
async createClass(data: CreateClassInput) { async createClass(data: CreateClassInput) {
......
import { payload } from '#interface/IApi';
import { models } from '#models/sequelize-config.js'; import { models } from '#models/sequelize-config.js';
interface GetAllCoursesParams {
filters?: string;
sort?: string;
page?: number;
pageSize?: number;
}
interface CreateCourseInput { interface CreateCourseInput {
name: string; name: string;
description: string; description: string;
...@@ -14,17 +8,19 @@ interface CreateCourseInput { ...@@ -14,17 +8,19 @@ interface CreateCourseInput {
} }
export class CoursesProvider { export class CoursesProvider {
async getAllCourses(params: GetAllCoursesParams) { async getAllCourses(params: payload) {
const { filters, sort, page = 1, pageSize = 10 } = params; const { count, rows } = await models.courses.findAndCountAll({
where: params.filters,
const offset = (page - 1) * pageSize; order: params.sortBy ? [[params.sortBy, params.sortOrder]] : [['created_at', 'DESC']],
limit: params.pageSize,
return await models.courses.findAndCountAll({ offset: (params.page - 1) * params.pageSize
// where
order: sort ? [['created_at', sort]] : [['created_at', 'DESC']],
limit: pageSize,
offset: offset
}); });
return {
count,
page: params.page,
pageSize: params.pageSize,
rows
};
} }
async getCourseById(courseId: string) { async getCourseById(courseId: string) {
......
import { models } from "#models/sequelize-config.js"; import { models } from "#models/sequelize-config.js";
export class EnrollProvider { export class EnrollProvider {
async enroll(userId: string, classId: string) { async enroll(userId: string, classId: string) {
const checkEnrollment = await models.enrollments.findOne({ const checkEnrollment = await models.enrollments.findOne({
......
...@@ -88,7 +88,7 @@ export class AuthService { ...@@ -88,7 +88,7 @@ export class AuthService {
} }
} }
async profileUser(userId: number) { async profileUser(userId: string) {
const user = await models.users.findByPk(userId); const user = await models.users.findByPk(userId);
if (!user) { if (!user) {
......
...@@ -2,35 +2,86 @@ const authProfileSchemas = { ...@@ -2,35 +2,86 @@ const authProfileSchemas = {
Profile: { Profile: {
type: 'object', type: 'object',
example: { example: {
id: '123', message: "string",
name: 'Pham Quang Bao', message_en: "string",
email: 'phamquangbao@example.com', responseData: {
phone: '+1234567890', id: "string",
address: '123 Main St, Anytown, USA', name: "string",
email: "string",
phone: "string",
address: "string",
status: "string",
},
status: "success | fail",
timeStamp: "2024-02-26 03:12:45",
violation: [
{}
]
}, },
properties: { properties: {
id: { message: {
type: 'uuid',
format: 'uuid',
example: '123',
},
name: {
type: 'string', type: 'string',
example: 'Pham Quang Bao', description: 'Thông điệp trả về'
}, },
email: { message_en: {
type: 'string', type: 'string',
format: 'email', description: 'Thông điệp trả về (bằng tiếng Anh)'
example: 'phamquangbao@example.com', },
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: 'string',
description: 'ID của người dùng'
},
name: {
type: 'string',
description: 'Tên của người dùng'
},
email: {
type: 'string',
description: 'Email của người dùng'
},
phone: {
type: 'string',
description: 'Số điện thoại của người dùng'
},
address: {
type: 'string',
description: 'Địa chỉ của người dùng'
},
status: {
type: 'string',
description: 'Trạng thái của người dùng'
}
}
}, },
phone: { status: {
type: 'string', type: 'string',
example: '+1234567890', description: 'Trạng thái của phản hồi (success hoặc fail)'
}, },
address: { timeStamp: {
type: 'string', type: 'string',
example: '123 Main St, Anytown, USA', 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'
}
}
}
}
}, },
}, },
}; };
......
const classSchemas = { const classSchemas = {
// get schema
Class: { Class: {
type: 'object', type: 'object',
example: { example: {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Lớp 12A1', name: 'Lớp 12A1',
description: 'Lớp chuyên toán', 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_at: '2026-05-16T08:00:00.000Z',
created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11', created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11',
updated_at: '2026-05-16T09:30:00.000Z', updated_at: '2026-05-16T09:30:00.000Z',
updated_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11', updated_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11',
course_id: '7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10',
status: 'active',
}, },
properties: { properties: {
id: { id: { type: 'string', format: 'uuid' },
type: 'string', name: { type: 'string' },
format: 'uuid', description: { type: 'string', nullable: true },
example: '3fa85f64-5717-4562-b3fc-2c963f66afa6', course_id: { type: 'string', format: 'uuid', nullable: true },
}, status: { type: 'string', nullable: true },
name: { created_at: { type: 'string', format: 'date-time', nullable: true },
type: 'string', created_by: { type: 'string', format: 'uuid', nullable: true },
example: 'Lớp 12A1', updated_at: { type: 'string', format: 'date-time', nullable: true },
}, updated_by: { type: 'string', format: 'uuid', nullable: true },
description: { },
},
// get schema
ClassResponse: {
type: 'object',
properties: {
message: {
type: 'string', type: 'string',
nullable: true, nullable: true,
example: 'Lớp chuyên toán',
}, },
course_id: { message_en: {
type: 'string', type: 'string',
format: 'uuid',
nullable: true, nullable: true,
example: '7c1a4d9c-3a2b-4c58-9df4-8e2c2d9a3c10', },
responseData: {
$ref: '#/components/schemas/Class',
}, },
status: { status: {
type: 'string', type: 'string',
nullable: true, example: 'success',
example: 'active', },
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,
},
},
},
}, },
created_at: { },
},
ClassListResponse: {
type: 'object',
properties: {
message: {
type: 'string', type: 'string',
format: 'date-time',
nullable: true, nullable: true,
}, },
created_by: { message_en: {
type: 'string', type: 'string',
format: 'uuid',
nullable: true, nullable: true,
example: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11',
}, },
updated_at: { responseData: {
type: 'string', type: 'object',
format: 'date-time', properties: {
nullable: true, count: {
type: 'integer',
},
page: {
type: 'integer',
},
pageSize: {
type: 'integer',
},
rows: {
type: 'array',
items: {
$ref: '#/components/schemas/Class',
},
},
},
}, },
updated_by: { status: {
type: 'string', type: 'string',
format: 'uuid', example: 'success',
nullable: true, },
example: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11', 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,
},
},
},
}, },
}, },
}, },
......
...@@ -5,41 +5,117 @@ const courseSchemas = { ...@@ -5,41 +5,117 @@ const courseSchemas = {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Khóa học 12A1', name: 'Khóa học 12A1',
description: 'Khóa học chuyên toán', description: 'Khóa học chuyên toán',
status: 'active',
created_at: '2026-05-16T08:00:00.000Z', created_at: '2026-05-16T08:00:00.000Z',
created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11', created_by: '2b4f6f8e-19f6-4c5d-93c2-4d7a7c3d1e11',
status: 'active',
}, },
properties: { properties: {
id: { id: { type: 'string', format: 'uuid' },
type: 'string', name: { type: 'string' },
format: 'uuid', description: { type: 'string', nullable: true },
example: '3fa85f64-5717-4562-b3fc-2c963f66afa6', status: { type: 'string', nullable: true },
}, created_at: { type: 'string', format: 'date-time', nullable: true },
name: { created_by: { type: 'string', format: 'uuid', nullable: true },
},
},
CourseResponse: {
type: 'object',
properties: {
message: {
type: 'string', type: 'string',
example: 'Khóa học 12A1', nullable: true,
}, },
description: { message_en: {
type: 'string', type: 'string',
nullable: true, nullable: true,
example: 'Khóa học chuyên toán', },
responseData: {
$ref: '#/components/schemas/Course',
}, },
status: { status: {
type: 'string', type: 'string',
nullable: true, example: 'success',
example: 'active',
}, },
created_at: { 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', type: 'string',
format: 'date-time',
nullable: true, nullable: true,
}, },
created_by: { message_en: {
type: 'string', type: 'string',
format: 'uuid',
nullable: true, 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,
},
},
},
},
}, },
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment