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

Merge branch 'feat/upload_image_and_add_export_excel' into 'develop'

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

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