From 03022576bdab98a7c96810fa151bb1ed4fa8dbe9 Mon Sep 17 00:00:00 2001 From: Shibly Teknologi Solusi Date: Mon, 18 May 2026 15:25:01 +0700 Subject: [PATCH] update --- src/controllers/file/file.ts | 182 +++++++++++++++++++++++++++++++++++ src/routes/private.ts | 4 + src/swagger/builder.js | 23 ----- 3 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 src/controllers/file/file.ts diff --git a/src/controllers/file/file.ts b/src/controllers/file/file.ts new file mode 100644 index 0000000..3a2b9f9 --- /dev/null +++ b/src/controllers/file/file.ts @@ -0,0 +1,182 @@ +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 config from "config"; +import fs from "fs"; +import path from "path"; + +const log: Logger = new Logger({ name: '[FileController]', type: 'pretty' }); + +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)" + }, + path: { + type: "string", + format: "text", + description: "Path File" + }, + }, + required: ["file", "path"] + } + } + } + } + */ + try { + const schema = Joi.object().keys({ + path: Joi.string().required().label('File Path'), + }); + + const param: { path: string } = await schema.validateAsync(req.body); + + if (param.path.includes('..')) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid path"); + } + + // 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']; + if (!allowedMimes.includes(file.mimetype)) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_insert, "Invalid file type. Only JPG, PNG, and WEBP are allowed"); + } + param.path = param.path[param.path.length - 1] !== '/' ? param.path + '/' : param.path; + + if (!param.path.startsWith('uploads/')) { + param.path = 'uploads/' + param.path.replace(/^\/+/, ''); + } + + const ext = file.name.split('.'); + const name = uuidv4() + '.' + ext[ext.length - 1]; + + const result: { file: string } = { + file: "", + }; + + const folder_path = param.path; + 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" }, + path: { type: "string", description: "Folder path (e.g. patient-photos or uploads/patient-photos/)" } + }, + required: ["file_name", "path"] + } + } + } + } + */ + + try { + const schema = Joi.object().keys({ + file_name: Joi.string().required().label('File Name'), + path: Joi.string().required().label('File Path'), + }); + + const param: { file_name: string; path: string } = await schema.validateAsync(req.body); + + if (param.path.includes('..')) { + return ReturnHelper.errorResponse(res, 400, 400, Language.lang.failed_delete, "Invalid path"); + } + + param.path = param.path[param.path.length - 1] !== '/' ? param.path + '/' : param.path; + + if (!param.path.startsWith('uploads/')) { + param.path = 'uploads/' + param.path.replace(/^\/+/, ''); + } + + const file_path = path.join(param.path, 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); + } + } +} \ No newline at end of file diff --git a/src/routes/private.ts b/src/routes/private.ts index 6d8de72..0c069bc 100644 --- a/src/routes/private.ts +++ b/src/routes/private.ts @@ -70,6 +70,7 @@ import { InpatientRoomController } from "../controllers/inpatient_room"; import { SurgeryRoomController } from "../controllers/surgery_room"; import { SurgeryTypeController } from "../controllers/surgery_type"; import { FosterCareHistoryController } from "../controllers/fostercarehistory"; +import { FileController } from "../controllers/file/file"; export class RoutePrivate { static setup(app: express.Application) { @@ -84,6 +85,9 @@ export class RoutePrivate { app.delete("/api/menu/delete/:id/:hard", MenuController.delete); app.put("/api/menu/restore/:id", MenuController.restore); + app.post('/api/upload', FileController.upload) + app.delete('/api/delete', FileController.delete) + app.get("/api/administrativu/list", AdministrativuController.list); app.get("/api/administrativu/export", AdministrativuController.export); app.post("/api/administrativu/create", AdministrativuController.create); diff --git a/src/swagger/builder.js b/src/swagger/builder.js index 46a8018..358e58d 100644 --- a/src/swagger/builder.js +++ b/src/swagger/builder.js @@ -140,29 +140,6 @@ const doc = { $department_id: "uuid-string", $status: "Y", }, - inpatient_room_bed: { - $inpatient_room_id: "34fdda87-9b42-44d3-8cab-2f2032481d42", - $number: 1, - $status: { - "@enum": ["booked", "empty", "progress", "occupied"], - }, - $bed_detail: { - $photo_before: "base64 or url string", - $photo_after: "base64 or url string", - $electrical: true, - $telephone: false, - $lighting: true, - $wall: false, - $ceiling: false, - $floor: false, - $leakage: false, - $temperature: false, - $electromedical_equipment: false, - $available: true, - $online_inpatient_referral: false, - $cohort: false, - }, - }, // pharmacy: { // $room: "Room name", // $code: "R001",