From 3fc70daed072e6a791f36af7961c19dd8c12d65b Mon Sep 17 00:00:00 2001 From: Methapon2001 <61303214+Methapon2001@users.noreply.github.com> Date: Mon, 27 Nov 2023 09:45:30 +0700 Subject: [PATCH] refactor: rabbitmq implement --- Services/server/src/app.ts | 4 + .../src/controllers/cabinetController.ts | 130 +++---- .../src/controllers/drawerController.ts | 153 ++++---- .../src/controllers/folderController.ts | 164 ++++---- .../src/controllers/subFolderController.ts | 168 ++++---- .../server/src/{storage => minio}/index.ts | 0 Services/server/src/rabbitmq/handler.ts | 36 +- Services/server/src/rabbitmq/index.ts | 12 +- Services/server/src/routes.ts | 43 +-- Services/server/src/swagger.json | 365 +++++++++++++----- Services/server/src/utils/auth.ts | 34 +- Services/server/src/utils/minio.ts | 112 +++--- 12 files changed, 676 insertions(+), 545 deletions(-) rename Services/server/src/{storage => minio}/index.ts (100%) diff --git a/Services/server/src/app.ts b/Services/server/src/app.ts index efefe83..24231e7 100644 --- a/Services/server/src/app.ts +++ b/Services/server/src/app.ts @@ -5,8 +5,10 @@ import cors from "cors"; import { RegisterRoutes } from "./routes"; import errorHandler from "./middlewares/exception"; +import rabbitmq from "./rabbitmq"; import swaggerSpecs from "./swagger.json"; +import { handler as amqHandler } from "./rabbitmq/handler"; const PORT = +(process.env.PORT || 80); @@ -28,3 +30,5 @@ app.use(errorHandler); app.listen(PORT, "0.0.0.0", () => console.log(`Application is running on http://localhost:${PORT}`), ); + +rabbitmq.init(amqHandler).catch((e) => console.error(e)); diff --git a/Services/server/src/controllers/cabinetController.ts b/Services/server/src/controllers/cabinetController.ts index 2aeab4d..fadac3d 100644 --- a/Services/server/src/controllers/cabinetController.ts +++ b/Services/server/src/controllers/cabinetController.ts @@ -11,104 +11,109 @@ import { SuccessResponse, Tags, Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import minioClient from "../storage"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, replaceIllegalChars } from "../utils/minio"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("cabinet") export class CabinetController extends Controller { @Get("/") @Tags("Cabinet") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listCabinet(): Promise { - const list = await listFolder().catch((e) => console.error(`Error List Folder: ${e}`)); - - if (!list) { - throw new Error("Error listing folder"); - } - + const list = await listFolder(DEFAULT_BUCKET!).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) + throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง"); return list; } @Post("/") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createCabinet( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, ) { - const uploaded = await minioClient - .putObject("ehr", `${replaceIllegalChars(body.name)}/.keep`, "", 0, { + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); - if (!uploaded) throw new Error("Object storage error occured."); + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{cabinetName}") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT, "Success") + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editCabinet( @Path() cabinetName: string, @Body() body: { name: string }, ): Promise { - const list = await listItem(`${cabinetName}/`, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { if (!current.name) return; - const destination = `${replaceIllegalChars(body.name)}/${current.name.slice( - cabinetName.length + 1, - )}`; - const source = `/ehr/${current.name}`; + const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -118,44 +123,23 @@ export class CabinetController extends Controller { @Delete("/{cabinetName}") @Tags("Cabinet") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteCabinet(@Path() cabinetName: string) { await new Promise((resolve, reject) => { const objects: string[] = []; - const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true); + const stream = minioClient.listObjectsV2(DEFAULT_BUCKET!, `${cabinetName}/`, true); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index e9fcf83..6afb601 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -6,120 +6,133 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import minioClient from "../storage"; + +import minioClient from "../minio"; +import esClient from "../elasticsearch"; + +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; import HttpStatusCode from "../interfaces/http-status"; -import HttpError from "../interfaces/http-error"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import esClient from "../elasticsearch"; import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); @Route("/cabinet/{cabinetName}/drawer") export class DrawerController extends Controller { @Get("/") @Tags("Drawer") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listDrawer(@Path() cabinetName: string): Promise { - const fullpath = [cabinetName, ""].join("/"); - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); - } - - return listFolder(fullpath); + const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) + throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createDrawer( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, @Body() body: { name: string }, ) { - if (!(await pathExist(`${cabinetName}/`))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found."); + const basePath = `${cabinetName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก"); } - const uploaded = await minioClient - .putObject("ehr", `${cabinetName}/${replaceIllegalChars(body.name)}/.keep`, "", 0, { + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{drawerName}") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editDrawer( @Path() cabinetName: string, @Path() drawerName: string, @Body() body: { name: string }, ): Promise { - const fullpath = `${cabinetName}/${drawerName}/`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { if (!current.name) return; const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice( - fullpath.length, + path.length, )}`; - const source = `/ehr/${current.name}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -129,44 +142,26 @@ export class DrawerController extends Controller { @Delete("/{drawerName}") @Tags("Drawer") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) { await new Promise((resolve, reject) => { const objects: string[] = []; - const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true); + const stream = minioClient.listObjectsV2( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/`, + true, + ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index fd79c91..f838b6c 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -6,88 +6,100 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import HttpError from "../interfaces/http-error"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import minioClient from "../storage"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder") export class FolderController extends Controller { @Get("/") @Tags("Folder") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, ): Promise { - const fullpath = [cabinetName, drawerName, ""].join("/"); - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); - } - - return listFolder(fullpath); + const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}`).catch((e) => + console.error(`Error List Folder: ${e}`), + ); + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, ) { - if (!(await pathExist(`${cabinetName}/${drawerName}/`))) { - throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet or drawer cannot be found."); + const basePath = `${cabinetName}/${drawerName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); } - const uploaded = await minioClient - .putObject( - "ehr", - `${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/.keep`, - "", - 0, - { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }, - ) + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{folderName}") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( @Body() body: { name: string }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ) { - const fullpath = `${cabinetName}/${drawerName}/${folderName}/`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/${folderName}`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { @@ -95,42 +107,36 @@ export class FolderController extends Controller { const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars( body.name, - )}/${current.name.slice(fullpath.length)}`; - const source = `/ehr/${current.name}`; + )}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -140,8 +146,8 @@ export class FolderController extends Controller { @Delete("/{folderName}") @Tags("Folder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( @Path() cabinetName: string, @Path() drawerName: string, @@ -150,42 +156,20 @@ export class FolderController extends Controller { await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}`, true, ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/controllers/subFolderController.ts b/Services/server/src/controllers/subFolderController.ts index f54a79c..376bae8 100644 --- a/Services/server/src/controllers/subFolderController.ts +++ b/Services/server/src/controllers/subFolderController.ts @@ -6,44 +6,58 @@ import { Path, Post, Put, - Request, Route, Security, SuccessResponse, Tags, + Request, + Response, } from "tsoa"; -import * as Minio from "minio"; -import HttpError from "../interfaces/http-error"; -import HttpStatusCode from "../interfaces/http-status"; -import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio"; -import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; -import minioClient from "../storage"; +import minioClient from "../minio"; import esClient from "../elasticsearch"; +import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio"; + +import HttpStatusCode from "../interfaces/http-status"; +import { EhrFile, EhrFolder } from "../interfaces/ehr-fs"; +import HttpError from "../interfaces/http-error"; + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); + @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder") export class SubFolderController extends Controller { @Get("/") @Tags("SubFolder") - @SuccessResponse(HttpStatusCode.OK) + @Security("bearerAuth") + @Response( + HttpStatusCode.INTERNAL_SERVER_ERROR, + "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง", + ) + @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async listFolder( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ): Promise { - const fullpath = [cabinetName, drawerName, folderName, ""].join("/"); - - if (!(await pathExist(fullpath))) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist."); - } - - return listFolder(fullpath); + const list = await listFolder( + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}`, + ).catch((e) => console.error(`Error List Folder: ${e}`)); + if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง"); + return list; } @Post("/") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.CREATED) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") + @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @Request() request: { user: { preferred_username: string } }, @Body() body: { name: string }, @@ -51,37 +65,36 @@ export class SubFolderController extends Controller { @Path() drawerName: string, @Path() folderName: string, ) { - if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}`))) { - throw new HttpError( - HttpStatusCode.PRECONDITION_FAILED, - "Cabinet, drawer or folder cannot be found.", - ); + const basePath = `${cabinetName}/${drawerName}/${folderName}/`; + + if ( + !Boolean( + await minioClient.statObject(DEFAULT_BUCKET!, `${basePath}.keep`).catch((e) => { + if (e.code === "NotFound") return false; + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ) + ) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); } - const uploaded = await minioClient - .putObject( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/.keep`, - "", - 0, - { - createdAt: new Date().toISOString(), - createdBy: request.user.preferred_username, - }, - ) + const created = await minioClient + .putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, { + createdAt: new Date().toISOString(), + createdBy: request.user.preferred_username, + }) .catch((e) => console.error(e)); - if (!uploaded) { - throw new Error("Object storage error occured."); - } + if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); return this.setStatus(HttpStatusCode.CREATED); } @Put("/{subFolderName}") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async editFolder( @Body() body: { name: string }, @Path() cabinetName: string, @@ -89,11 +102,8 @@ export class SubFolderController extends Controller { @Path() folderName: string, @Path() subFolderName: string, ) { - const fullpath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`; - - const list = await listItem(fullpath, true); - - const cond = new Minio.CopyConditions(); + const path = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`; + const list = await listItem(DEFAULT_BUCKET!, path, true); await Promise.all( list.map(async (current) => { @@ -101,42 +111,36 @@ export class SubFolderController extends Controller { const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars( body.name, - )}/${current.name.slice(fullpath.length)}`; - const source = `/ehr/${current.name}`; + )}/${current.name.slice(path.length)}`; + const source = `/${DEFAULT_BUCKET}/${current.name}`; return await minioClient - .copyObject("ehr", destination, source, cond) + .copyObject(DEFAULT_BUCKET!, destination, source, copyCond) .then(async () => { - if (!current.name) return; - - await minioClient.removeObject("ehr", current.name); - - if (current.name.includes(".keep")) return; + if (current.name.includes(".keep")) { + return await minioClient.removeObject(DEFAULT_BUCKET!, current.name); + } const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - match: { - pathname: current.name, - }, - }, + index: DEFAULT_INDEX!, + query: { match: { pathname: current.name } }, }); - if (search && search.hits.hits.length === 0) { - throw new Error("Data cannot be found in database."); - } + if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); const data = search.hits.hits[0]; await esClient.update({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", + index: DEFAULT_INDEX!, id: data._id, doc: { pathname: destination }, }); + + await minioClient.removeObject(DEFAULT_BUCKET!, current.name); }) .catch((e) => { console.error(e); - throw new Error("Failed to move."); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); }); }), ); @@ -146,8 +150,8 @@ export class SubFolderController extends Controller { @Delete("/{subFolderName}") @Tags("SubFolder") - @Security("bearerAuth") - @SuccessResponse(HttpStatusCode.NO_CONTENT) + @Security("bearerAuth", ["admin"]) + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteFolder( @Path() cabinetName: string, @Path() drawerName: string, @@ -157,42 +161,20 @@ export class SubFolderController extends Controller { await new Promise((resolve, reject) => { const objects: string[] = []; const stream = minioClient.listObjectsV2( - "ehr", - `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`, + DEFAULT_BUCKET!, + `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, true, ); stream.on("data", (v) => { - if (!(v && v.name)) return; - - objects.push(v.name); + if (v && v.name) objects.push(v.name); }); - - stream.on("close", async () => { - minioClient.removeObjects("ehr", objects); - resolve(); - }); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("close", async () => + resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)), + ); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้"))); }); - const searchResult = await esClient.search({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - query: { - prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}` }, - }, - }); - - await Promise.all( - searchResult.hits.hits.map(async (v) => { - return esClient - .delete({ - index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", - id: v._id, - }) - .catch((e) => console.error(`ElasticSearch Error: ${e}`)); - }), - ); - return this.setStatus(HttpStatusCode.NO_CONTENT); } } diff --git a/Services/server/src/storage/index.ts b/Services/server/src/minio/index.ts similarity index 100% rename from Services/server/src/storage/index.ts rename to Services/server/src/minio/index.ts diff --git a/Services/server/src/rabbitmq/handler.ts b/Services/server/src/rabbitmq/handler.ts index 6ae21c1..2a74aa6 100644 --- a/Services/server/src/rabbitmq/handler.ts +++ b/Services/server/src/rabbitmq/handler.ts @@ -1,17 +1,25 @@ import { EhrFile } from "../interfaces/ehr-fs"; import esClient from "../elasticsearch"; -import minioClient from "../storage"; +import minioClient from "../minio"; + +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; + +if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); // for failed queue that will come later const cachedBuffer: Record = {}; const cachedMetadata: Record = {}; -export async function handler(key: string): Promise { - console.info(`[AMQ] Messages received - key: ${key}`); +export async function handler(key: string, event: string): Promise { + console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`); const [bucket, ...fragment] = key.split("/"); const pathname = fragment.join("/"); + if (event === "s3:ObjectRemoved:Delete") { + return await ensureDelete(pathname); + } + if (!cachedBuffer[key]) { const stream = await minioClient.getObject(bucket, pathname); const buffer = Buffer.concat(await stream.toArray()); @@ -41,7 +49,7 @@ export async function handler(key: string): Promise { async function popInfo(pathname: string) { const result = await esClient .search }>({ - index: "my-test-index", + index: DEFAULT_INDEX!, query: { match: { pathname } }, }) .catch((e) => console.error(e)); @@ -50,7 +58,7 @@ async function popInfo(pathname: string) { if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { await esClient .delete({ - index: "my-test-index", + index: DEFAULT_INDEX!, id: result.hits.hits[0]._id, }) .catch((e) => console.error(e)); @@ -63,6 +71,20 @@ async function popInfo(pathname: string) { return false; } +/** + * If there is data in database then delete it + */ +async function ensureDelete(pathname: string) { + await esClient + .deleteByQuery({ + index: DEFAULT_INDEX!, + query: { match: { pathname } }, + conflicts: "proceed", + }) + .catch((e) => console.error(e)); + return true; +} + /** * Handle when record in elasticsearch cannot be found. * This will insert empty metadata. @@ -94,7 +116,7 @@ async function handleNotFoundRecord( const result = await esClient .index({ pipeline: "attachment", - index: "my-test-index", + index: DEFAULT_INDEX!, document: { data: base64, ...metadata }, }) .catch((e) => console.error(e)); @@ -116,7 +138,7 @@ async function handleFoundRecord( const result = await esClient .index({ pipeline: "attachment", - index: "my-test-index", + index: DEFAULT_INDEX!, document: { data: Buffer.from(buffer).toString("base64"), ...metadata }, }) .catch((e) => console.error(e)); diff --git a/Services/server/src/rabbitmq/index.ts b/Services/server/src/rabbitmq/index.ts index 2115eaf..8f5a1f1 100644 --- a/Services/server/src/rabbitmq/index.ts +++ b/Services/server/src/rabbitmq/index.ts @@ -1,6 +1,6 @@ import amqp from "amqplib"; -export async function init(cb: (key: string) => boolean | Promise) { +export async function init(cb: (key: string, event: string) => boolean | Promise) { if (!process.env.AMQ_URL || !process.env.AMQ_QUEUE) return; const { AMQ_URL: url, AMQ_QUEUE: queue } = process.env; @@ -22,10 +22,14 @@ export async function init(cb: (key: string) => boolean | Promise) { const parsed: Record = JSON.parse(msg.content.toString()); if (typeof parsed.Key !== "string" || parsed.Key.includes(".keep")) return channel.ack(msg); + if (typeof parsed.EventName !== "string" || parsed.EventName.includes("Copy")) { + return channel.ack(msg); + } const key = parsed.Key; + const event = parsed.EventName; - if (await cb(key)) return channel.ack(msg); + if (await cb(key, event)) return channel.ack(msg); return await new Promise((resolve) => setTimeout(() => resolve(channel.nack(msg)), 3000)); }, @@ -33,6 +37,4 @@ export async function init(cb: (key: string) => boolean | Promise) { ); } -export default { - init, -}; +export default { init }; diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 2ee984d..689bd83 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -48,6 +48,7 @@ const models: TsoaRoute.Models = { "description": {"dataType":"string","required":true}, "category": {"dataType":"array","array":{"dataType":"string"},"required":true}, "keyword": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "upload": {"dataType":"boolean","required":true}, "updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true}, "updatedBy": {"dataType":"string","required":true}, "createdAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true}, @@ -76,6 +77,7 @@ export function RegisterRoutes(app: Router) { // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa // ########################################################################################################### app.get('/cabinet', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.listCabinet)), @@ -100,7 +102,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/cabinet', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.createCabinet)), @@ -127,7 +129,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.put('/cabinet/:cabinetName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.editCabinet)), @@ -154,7 +156,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.delete('/cabinet/:cabinetName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(CabinetController)), ...(fetchMiddlewares(CabinetController.prototype.deleteCabinet)), @@ -180,6 +182,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/cabinet/:cabinetName/drawer', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.listDrawer)), @@ -205,7 +208,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/cabinet/:cabinetName/drawer', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.createDrawer)), @@ -233,7 +236,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.put('/cabinet/:cabinetName/drawer/:drawerName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.editDrawer)), @@ -261,7 +264,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.delete('/cabinet/:cabinetName/drawer/:drawerName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(DrawerController)), ...(fetchMiddlewares(DrawerController.prototype.deleteDrawer)), @@ -289,18 +292,13 @@ export function RegisterRoutes(app: Router) { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file', authenticateMiddleware([{"bearerAuth":[]}]), - upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.uploadFile)), function FileController_uploadFile(request: any, response: any, next: any) { const args = { request: {"in":"request","name":"request","required":true,"dataType":"object"}, - file: {"in":"formData","name":"file","required":true,"dataType":"file"}, - title: {"in":"formData","name":"title","required":true,"dataType":"string"}, - description: {"in":"formData","name":"description","required":true,"dataType":"string"}, - keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"}, - category: {"in":"formData","name":"category","required":true,"dataType":"string"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string","required":true},"category":{"dataType":"string","required":true},"description":{"dataType":"string","required":true},"title":{"dataType":"string","required":true},"file":{"dataType":"string","required":true}}}, cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"}, drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, @@ -351,7 +349,6 @@ export function RegisterRoutes(app: Router) { // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName', authenticateMiddleware([{"bearerAuth":[]}]), - upload.single('file'), ...(fetchMiddlewares(FileController)), ...(fetchMiddlewares(FileController.prototype.updateFile)), @@ -362,11 +359,7 @@ export function RegisterRoutes(app: Router) { drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"}, folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"}, fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"}, - file: {"in":"formData","name":"file","dataType":"file"}, - title: {"in":"formData","name":"title","dataType":"string"}, - description: {"in":"formData","name":"description","dataType":"string"}, - keyword: {"in":"formData","name":"keyword","dataType":"string"}, - category: {"in":"formData","name":"category","dataType":"string"}, + body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"dataType":"string"}}}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -443,6 +436,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/cabinet/:cabinetName/drawer/:drawerName/folder', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.listFolder)), @@ -469,7 +463,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/cabinet/:cabinetName/drawer/:drawerName/folder', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.createFolder)), @@ -498,7 +492,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.editFolder)), @@ -527,7 +521,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(FolderController)), ...(fetchMiddlewares(FolderController.prototype.deleteFolder)), @@ -580,6 +574,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', + authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.listFolder)), @@ -607,7 +602,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.createFolder)), @@ -637,7 +632,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.editFolder)), @@ -667,7 +662,7 @@ export function RegisterRoutes(app: Router) { }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName', - authenticateMiddleware([{"bearerAuth":[]}]), + authenticateMiddleware([{"bearerAuth":["admin"]}]), ...(fetchMiddlewares(SubFolderController)), ...(fetchMiddlewares(SubFolderController.prototype.deleteFolder)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index d9bde06..6b57bcb 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -79,6 +79,9 @@ }, "type": "array" }, + "upload": { + "type": "boolean" + }, "updatedAt": { "anyOf": [ { @@ -117,6 +120,7 @@ "description", "category", "keyword", + "upload", "updatedAt", "updatedBy", "createdAt", @@ -178,9 +182,9 @@ } }, "info": { - "title": "BMA EHR - Test Service API", - "version": "0.0.1", - "description": "Best practice for initialize express project", + "title": "Enterprise Document Management(EDM) - API", + "version": "0.0.2", + "description": "Open API Specfication for Enterprise Document Management ", "license": { "name": "by Frappet", "url": "https://frappet.com" @@ -193,7 +197,7 @@ "operationId": "ListCabinet", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -204,19 +208,9 @@ } } } - } - }, - "tags": [ - "Cabinet" - ], - "security": [], - "parameters": [] - }, - "post": { - "operationId": "CreateCabinet", - "responses": { - "201": { - "description": "" + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ @@ -227,6 +221,28 @@ "bearerAuth": [] } ], + "parameters": [] + }, + "post": { + "operationId": "CreateCabinet", + "responses": { + "201": { + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" + } + }, + "tags": [ + "Cabinet" + ], + "security": [ + { + "bearerAuth": [ + "admin" + ] + } + ], "parameters": [], "requestBody": { "required": true, @@ -253,7 +269,10 @@ "operationId": "EditCabinet", "responses": { "204": { - "description": "Success" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -261,7 +280,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -297,12 +318,10 @@ "operationId": "DeleteCabinet", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้" } }, "tags": [ @@ -310,7 +329,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -330,7 +351,7 @@ "operationId": "ListDrawer", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -341,12 +362,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "Drawer" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -362,7 +390,13 @@ "operationId": "CreateDrawer", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบลิ้นชัก" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -370,7 +404,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -408,7 +444,10 @@ "operationId": "EditDrawer", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -416,7 +455,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -460,12 +501,7 @@ "operationId": "DeleteDrawer", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -473,7 +509,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -501,7 +539,74 @@ "operationId": "UploadFile", "responses": { "201": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "properties": { + "keyword": { + "type": "string" + }, + "category": { + "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" + }, + "upload": { + "type": "string" + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + } + }, + "required": [ + "keyword", + "category", + "description", + "title", + "file", + "upload", + "updatedBy", + "updatedAt", + "createdBy", + "createdAt" + ], + "type": "object" + } + } + } } }, "tags": [ @@ -541,34 +646,33 @@ "requestBody": { "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } }, "required": [ - "file", - "title", - "description", "keyword", - "category" - ] + "category", + "description", + "title", + "file" + ], + "type": "object" } } } @@ -628,7 +732,27 @@ "operationId": "UpdateFile", "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "properties": { + "upload": { + "type": "string" + } + }, + "required": [ + "upload" + ], + "type": "object" + } + ] + } + } + } } }, "tags": [ @@ -674,29 +798,28 @@ } ], "requestBody": { - "required": false, + "required": true, "content": { - "multipart/form-data": { + "application/json": { "schema": { - "type": "object", "properties": { - "file": { - "type": "string", - "format": "binary" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, "keyword": { "type": "string" }, "category": { "type": "string" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "file": { + "type": "string" } - } + }, + "type": "object" } } } @@ -789,6 +912,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -829,6 +955,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", @@ -890,7 +1017,7 @@ "operationId": "ListFolder", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -901,12 +1028,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "Folder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -930,7 +1064,13 @@ "operationId": "CreateFolder", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบของแฟ้ม" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -938,7 +1078,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -984,7 +1126,10 @@ "operationId": "EditFolder", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -992,7 +1137,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1044,12 +1191,7 @@ "operationId": "DeleteFolder", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -1057,7 +1199,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1128,7 +1272,7 @@ "operationId": "ListFolder", "responses": { "200": { - "description": "", + "description": "สำเร็จ", "content": { "application/json": { "schema": { @@ -1139,12 +1283,19 @@ } } } + }, + "500": { + "description": "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง" } }, "tags": [ "SubFolder" ], - "security": [], + "security": [ + { + "bearerAuth": [] + } + ], "parameters": [ { "in": "path", @@ -1176,7 +1327,13 @@ "operationId": "CreateFolder", "responses": { "201": { - "description": "" + "description": "สำเร็จ" + }, + "404": { + "description": "ไม่พบของแฟ้ม" + }, + "500": { + "description": "เกิดข้อผิดพลาดกับระบบจัดการไฟล์" } }, "tags": [ @@ -1184,7 +1341,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1238,7 +1397,10 @@ "operationId": "EditFolder", "responses": { "204": { - "description": "" + "description": "สำเร็จ" + }, + "500": { + "description": "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้" } }, "tags": [ @@ -1246,7 +1408,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1306,12 +1470,7 @@ "operationId": "DeleteFolder", "responses": { "204": { - "description": "", - "content": { - "application/json": { - "schema": {} - } - } + "description": "สำเร็จ" } }, "tags": [ @@ -1319,7 +1478,9 @@ ], "security": [ { - "bearerAuth": [] + "bearerAuth": [ + "admin" + ] } ], "parameters": [ @@ -1482,6 +1643,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -1519,6 +1683,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", @@ -1758,6 +1923,9 @@ } ] }, + "upload": { + "type": "boolean" + }, "keyword": { "items": { "type": "string" @@ -1798,6 +1966,7 @@ "createdAt", "updatedBy", "updatedAt", + "upload", "keyword", "category", "description", diff --git a/Services/server/src/utils/auth.ts b/Services/server/src/utils/auth.ts index 20cf11f..5d71bec 100644 --- a/Services/server/src/utils/auth.ts +++ b/Services/server/src/utils/auth.ts @@ -14,32 +14,30 @@ const jwtVerify = createVerifier({ }, }); -export function expressAuthentication( +export async function expressAuthentication( request: express.Request, securityName: string, scopes?: string[], ) { - return new Promise(async (resolve, reject) => { - if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method.")); + if (process.env.AUTH_BYPASS) return { preferred_username: "bypassed" }; - const token = request.headers["authorization"]?.includes("Bearer ") - ? request.headers["authorization"].split(" ")[1] - : null; + if (securityName !== "bearerAuth") throw new Error("Unknown authentication method."); - if (!token) return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.")); + const token = request.headers["authorization"]?.includes("Bearer ") + ? request.headers["authorization"].split(" ")[1] + : null; - const payload = await jwtVerify(token).catch((_) => null); + if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided."); - if (!payload) { - return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.")); - } + const payload = await jwtVerify(token).catch((_) => null); - if (scopes && !scopes.every((v) => payload.resource_access[payload.azp].roles.includes(v))) { - return reject( - new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."), - ); - } + if (!payload) { + throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided."); + } - return resolve(payload); - }); + if (scopes && !scopes.some((v) => payload.resource_access[payload.azp].roles.includes(v))) { + throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action."); + } + + return payload; } diff --git a/Services/server/src/utils/minio.ts b/Services/server/src/utils/minio.ts index 7d6be02..53cf2b2 100644 --- a/Services/server/src/utils/minio.ts +++ b/Services/server/src/utils/minio.ts @@ -1,38 +1,27 @@ import { EhrFolder } from "../interfaces/ehr-fs"; import * as Minio from "minio"; -import minioClient from "../storage"; +import minioClient from "../minio"; /** - * Remove slash at the start and ensure slash at the end of the path - * @param path - path to be check and ensure - * @returns path without / at start and end with trailing slash - */ -function safePath(path: string) { - return path.replace(/^\/|\/$/g, "") + "/"; -} - -/** - * Replace illegal character eg. ? % < > / \ : | that can't be in path with "-". - * Used when create folder / dir through api + * Replace illegal character eg. ? % < > / \ : | that can't be in path with other char with dash by default. * @param path - string to check and replace - * @returns path with illegal character replaced with "-" + * @param replace - string to replace illegal character + * @returns illegal character replaced path */ -export function replaceIllegalChars(path: string, replaceChar = "-") { - return path.replace(/[/\\?%*:|"<>]/g, replaceChar); +export function replaceIllegalChars(path: string, replace = "-") { + return path.replace(/[/\\?%*:|"<>]/g, replace); } /** - * Utility function to check for .keep file if it is exist or not. - * @returns true if .keep exist, false otherwise + * Check if folder really exist by using ".keep" object. */ export async function pathExist(path: string): Promise { - return await minioClient - .statObject("ehr", `${safePath(path)}.keep`) - .then((_) => true) - .catch((e) => { + return Boolean( + await minioClient.statObject("ehr", `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => { if (e.code === "NotFound") return false; - throw new Error("Object Storage Error"); - }); + throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); + }), + ); } /** @@ -40,55 +29,62 @@ export async function pathExist(path: string): Promise { * @param path - path to list * @return list of folder with metadata */ -export function listFolder(path?: string): Promise { - if (path) path = safePath(path); +export async function listFolder(bucket: string, path?: string): Promise { + if (path) path = `${path.replace(/^\/|\/$/g, "")}/`; - return new Promise((resolve, reject) => { - const folder: EhrFolder[] = []; - - const stream = minioClient.listObjectsV2("ehr", path ?? ""); + const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { + const item: { pathname: string; name: string }[] = []; + const stream = minioClient.listObjectsV2(bucket, path ?? ""); stream.on("data", (v) => { - if (!(v && v.prefix)) return; - - folder.push({ - pathname: v.prefix, - name: v.prefix.slice(path?.length).split("/")[0], - createdAt: "N/A", - createdBy: "N/A", - }); - }); - - stream.on("end", async () => { - for (let i = 0; i < folder.length; i++) { - const stat = await minioClient - .statObject("ehr", `${folder[i].pathname}.keep`) - .catch((e) => console.error(`Error List Folder: ${folder[i].pathname}`, e)); - - if (!stat) continue; - - folder[i] = { - ...folder[i], - createdAt: stat.metaData.createdat ?? "N/A", - createdBy: stat.metaData.createdby ?? "N/A", - }; + if (v && v.prefix) { + item.push({ + pathname: v.prefix, + name: v.prefix.slice(path?.length).split("/")[0], + }); } - resolve(folder); }); - - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"))); }); + + const folder = await Promise.all( + list.map(async (v) => { + // Get stat from hidden object that used to mark as folder as minio doesn't really have folder + const stat = await minioClient + .statObject(bucket, `${v.pathname}.keep`) + .catch((e) => console.error(`MinIO Error: ${e}`)); + + if (!stat) return undefined; + + const { createdat, createdby } = stat.metaData; + + return { + ...v, + createdAt: createdat ?? "n/a", + createdBy: createdby ?? "n/a", + } satisfies EhrFolder; + }), + ); + + return folder.filter((v: (typeof folder)[number]): v is EhrFolder => !!v); } -export async function listItem(path: string, recursive = false): Promise { +export async function listItem( + bucket: string, + path: string, + recursive = false, +): Promise { return new Promise((resolve, reject) => { - const stream = minioClient.listObjectsV2("ehr", path, recursive); + const stream = minioClient.listObjectsV2(bucket, path, recursive); const item: Minio.BucketItem[] = []; stream.on("data", (v) => { if (v && v.name) item.push(v); }); stream.on("end", () => resolve(item)); - stream.on("error", () => reject(new Error("Object storage error occured."))); + stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"))); }); } + +export const copyCond = new Minio.CopyConditions();