From 25f2f3180c9f5a018c6c6b8ef5539445bd995254 Mon Sep 17 00:00:00 2001 From: Shibly Teknologi Solusi Date: Sat, 23 May 2026 01:07:28 +0700 Subject: [PATCH] update --- .gitignore | 3 +- src/controllers/file/file.ts | 223 +++++++++++++++++++++++++++++++++++ src/controllers/user.ts | 4 +- src/routes/private.ts | 5 + 4 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 src/controllers/file/file.ts diff --git a/.gitignore b/.gitignore index f186f66..7d73d75 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ package-lock.json src/swagger/swagger.json swagger.json config/ferro.json -uploads/* \ No newline at end of file +uploads/* +assets/saude/ \ No newline at end of file diff --git a/src/controllers/file/file.ts b/src/controllers/file/file.ts new file mode 100644 index 0000000..083c32d --- /dev/null +++ b/src/controllers/file/file.ts @@ -0,0 +1,223 @@ +import { Response, NextFunction } from "express"; +import { Request } from "express-jwt"; +import { ReturnHelper } from "../../helpers/express/return"; +import Joi from "joi"; +import { ILogObj, Logger } from "tslog"; +import { Language } from "../../langs/lang"; +import fileUpload from "express-fileupload"; +import { v4 as uuidv4 } from 'uuid'; +import fs from "fs"; +import path from "path"; + +const log: Logger = new Logger({ name: '[FileController]', type: 'pretty' }); +const STORAGE_DIR = 'assets/saude/'; + +export class FileController { + static async upload(req: Request, res: Response, next: NextFunction): Promise { + /* #swagger.tags = ['Handle File'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.consumes = ['multipart/form-data'] + #swagger.requestBody = { + required: true, + content: { + "multipart/form-data": { + schema: { + type: "object", + properties: { + file: { + type: "string", + format: "binary", + description: "Image file (max 1MB, jpg/png/webp)" + }, + }, + required: ["file"] + } + } + } + } + */ + try { + // Check if file exists + if (!req.files || !req.files.file) { + return ReturnHelper.errorResponse(res, 400, 404, Language.lang.failed_insert, "File not found"); + } + + const file = req.files.file as fileUpload.UploadedFile; + + // Validate file size (max 1MB) + const maxSize = 10 * 1024 * 1024; // 10MB + if (file.size > maxSize) { + return ReturnHelper.errorResponse( + res, + 400, + 400, + Language.lang.failed_insert, + "File size exceeds 10MB limit" + ); + } + + // Validate file type + const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/svg+xml', 'image/svg']; + if (!allowedMimes.includes(file.mimetype)) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid file type. Only JPG, PNG, WEBP, and SVG are allowed"); + } + const ext = file.name.split('.'); + const name = uuidv4() + '.' + ext[ext.length - 1]; + + const result: { file: string } = { + file: "", + }; + + const folder_path = STORAGE_DIR; + const file_path = path.join(folder_path, name); + + // Create directory if it doesn't exist + if (!fs.existsSync(folder_path)) { + fs.mkdirSync(folder_path, { recursive: true }); + } + + // Move file to destination + await file.mv(file_path); + + result.file = name; + + log.info(`File uploaded successfully: ${file_path}`); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_insert, result); + + } catch (e: unknown) { + // Proper error logging to avoid tslog serialization issues + if (e instanceof Error) { + log.error("File upload failed:", { + message: e.message, + stack: e.stack, + name: e.name + }); + return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, e.message); + } else { + log.error("File upload failed with unknown error:", String(e)); + return ReturnHelper.errorResponse(res, 500, 500, Language.lang.failed_insert, "Unknown error occurred"); + } + } + } + + static async delete(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Handle File'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.requestBody = { + required: true, + content: { + "application/json": { + schema: { + type: "object", + properties: { + file_name: { type: "string", description: "File name to delete" } + }, + required: ["file_name"] + } + } + } + } + */ + + try { + const schema = Joi.object().keys({ + file_name: Joi.string().required().label('File Name'), + }); + + const param: { file_name: string } = await schema.validateAsync(req.body); + + if (param.file_name.includes('..') || param.file_name.includes('/') || param.file_name.includes('\\')) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_delete, "Invalid file name"); + } + + const file_path = path.join(STORAGE_DIR, param.file_name); + + // Check if file exists + if (!fs.existsSync(file_path)) { + return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_not_found, "File not found"); + } + + // Delete file + fs.unlinkSync(file_path); + + return ReturnHelper.successResponseAny(res, 200, Language.lang.success_delete, {}); + } catch (e: unknown) { + log.error(e); + const err = e as Error; + + return ReturnHelper.errorResponse(res, 500, 402, Language.lang.failed_delete, err.message); + } + } + + static async download(req: Request, res: Response, next: NextFunction): Promise { + /* + #swagger.tags = ['Handle File'] + #swagger.security = [{ "bearerAuth": [] }] + #swagger.parameters['name'] = { + in: 'query', + required: true, + type: 'string', + description: 'File name' + } + #swagger.parameters['token'] = { + in: 'query', + required: true, + type: 'string', + description: 'Token' + } + */ + try { + const schema = Joi.object({ + name: Joi.string().required().label("File Name"), + token: Joi.string().required().label("Token"), + }); + + const param: { name: string } = await schema.validateAsync(req.query); + + if (param.name.includes('..') || param.name.includes('/') || param.name.includes('\\')) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_not_found, "Invalid file name"); + } + + const file_path = path.join(STORAGE_DIR, param.name); + + if (!fs.existsSync(file_path)) { + return ReturnHelper.errorResponse(res, 404, 404, Language.lang.failed_not_found, "File not found"); + } + + const buffer = fs.readFileSync(file_path); + const fileName = param.name; + const ext = fileName.split(".").pop()?.toLowerCase(); + + const mimeMap: Record = { + jpg: "image/jpeg", + jpeg: "image/jpeg", + png: "image/png", + webp: "image/webp", + gif: "image/gif", + pdf: "application/pdf", + svg: "image/svg+xml", + }; + + const contentType = mimeMap[ext ?? ""] ?? "application/octet-stream"; + + res.setHeader("Content-Type", contentType); + res.setHeader("Content-Length", buffer.length); + + const isInline = contentType.startsWith("image/") || contentType === "application/pdf"; + + if (isInline) { + res.setHeader("Content-Disposition", `inline; filename="${encodeURIComponent(fileName)}"`); + } else { + res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(fileName)}"`); + } + + return res.end(buffer); + } catch (e: unknown) { + log.error(e); + const err = e as Error; + return ReturnHelper.errorResponse(res, 400, 401, Language.lang.failed_not_found, err.message); + } + } +} \ No newline at end of file diff --git a/src/controllers/user.ts b/src/controllers/user.ts index 2494459..6af2223 100644 --- a/src/controllers/user.ts +++ b/src/controllers/user.ts @@ -878,10 +878,10 @@ export class UserController { const data = await userRepository.findOneBy({ id: param.id }); if (data != null) { - const doctorprofile = await UserController.uploadFile(req.files, "profile_picture", "uploads/doctor-profile"); + const doctorprofile = await UserController.uploadFile(req.files, "", "assets/saude"); if (doctorprofile) { if (data.profile_picture) { - const oldFilePath = path.join("uploads/doctor-profile", data.profile_picture); + const oldFilePath = path.join("assets/saude", data.profile_picture); await UserController.deleteFile(oldFilePath); } data.profile_picture = doctorprofile; diff --git a/src/routes/private.ts b/src/routes/private.ts index 78a7c48..91082f5 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -5,6 +5,7 @@ import { UserActivityController } from '../controllers/user_activity'; import { UserRefreshTokenController } from '../controllers/user_refresh_token'; import JwtHelper from '../helpers/jwt'; import { UserRoleController } from '../controllers/user_roles'; +import { FileController } from '../controllers/file/file'; export class RoutePrivate { static setup(app: express.Application) { @@ -42,5 +43,9 @@ export class RoutePrivate { app.get('/api/refresh_token/list', UserRefreshTokenController.list) app.put('/api/refresh_token/force_logout/:refresh_token', UserRefreshTokenController.forceLogout) + + app.post('/api/upload', FileController.upload) + app.delete('/api/delete', FileController.delete) + app.get('/api/download', FileController.download) } } \ No newline at end of file