Merge branch 'feat/storage-endpoint' into development

This commit is contained in:
Methapon2001 2023-12-12 13:10:24 +07:00
commit f0346901df
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
7 changed files with 1537 additions and 24 deletions

View file

@ -1,15 +1,25 @@
PUBLIC_KEY=
MINIO_HOST=localhost
MINIO_PORT=9000
# Keycloak public key
PUBLIC_KEY=keycloak.public.key
REALM_URL=https://keycloak.local/realms/EDM
PREFERRED_AUTH=online
MANAGEMENT_ROLE=doc-management
# App port
PORT=25570
# Real host name must be used.
# Sdk will generate presigned url based on host name.
MINIO_HOST=minio.local
MINIO_PORT=443
MINIO_SSL=true
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
# Bucket notification event needed to be configured
# Can use prepare script to create bucket
MINIO_BUCKET=dev
ELASTICSEARCH_PROTOCOL=http
ELASTICSEARCH_HOST=localhost
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_PORT=3001
# Can use prepare script
ELASTICSEARCH_INDEX=dev-index
AMQ_URL=amqp://admin:1234@localhost:3002
AMQ_QUEUE=queue-name
AMQ_URL=amqp://admin:1234@localhost:9999
AMQ_QUEUE=queue
AUTH_BYPASS=false # MUST NOT TURN THIS ON IN PRODUCTION

View file

@ -42,14 +42,8 @@ const io = new Server(server, {
});
setInstance(io);
io.on("connection", (socket) => {
console.log("User Connected");
socket.on("disconnected", () => {
console.log("User Disconnected");
});
});
io.on("connection", () => console.log("[Socket.IO] User connected."));
io.on("disconnected", () => console.log("[Socket.IO] User disconnected."));
server.listen(PORT, "0.0.0.0", () =>
console.log(`[APP] Application is running on http://localhost:${PORT}`),

View file

@ -0,0 +1,614 @@
import {
Body,
Controller,
Delete,
Example,
Post,
Put,
Request,
Route,
Security,
SuccessResponse,
Tags,
} from "tsoa";
import minioClient from "../minio";
import esClient from "../elasticsearch";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
import { copyCond } from "../utils/minio";
import * as io from "../lib/websocket";
if (!process.env.MINIO_BUCKET) throw Error("Default MinIO bucket must be specified.");
if (!process.env.ELASTICSEARCH_INDEX) throw Error("Default ElasticSearch index must be specified.");
const DEFAULT_BUCKET = process.env.MINIO_BUCKET;
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
const MINIO_ERROR_MESSAGE = "เกิดข้อผิดพลาดกับระบบจัดการไฟล์";
const PATH_NOT_FOUND_MESSAGE = "ไม่พบตำแหน่งที่ต้องการนำข้อมูลเข้า";
const PATH_ALREADY_EXIST = "ตำแหน่งดังกล่าวมีในระบบแล้ว";
interface ListRequestBody {
operation: "folder" | "file";
path: string[];
}
interface FolderBody {
/** @example ["แฟ้ม 1", "แฟ้ม 2"] */
path: string[];
/** @example "แฟ้ม 3" */
name: string;
}
interface PutFolderBody {
from: {
/** @example ["แฟ้ม 1", "แฟ้ม 2"] */
path: string[];
/** @example "แฟ้ม 3" */
name: string;
};
to: {
/** @example ["แฟ้ม 1", "แฟ้ม 2"] */
path: string[];
/** @example "แฟ้ม 3 แก้ไข" */
name: string;
};
}
interface DeleteFolderBody {
/** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3 แก้ไข"] */
path: string[];
}
interface FileBody {
/** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
path: string[];
/** @example "ไฟล์ 1.xlsx" */
file: string;
/** @example "การเงิน" */
title?: string;
/** @example "การเงิน" */
description?: string;
/** @example ["การเงิน", "รายงาน"] */
category?: string[];
/** @example ["การเงิน", "รายรับ", "รายจ่าย"] */
keyword?: string[];
}
interface PutFileBody extends Omit<FileBody, "file" | "path"> {
/** หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง */
from: {
/** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
path: string[];
/** @example "ไฟล์ 1.xlsx" */
file: string;
};
to?: {
/** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
path: string[];
/** @example "ไฟล์ 1 แก้ไข.xlsx" */
file: string;
};
/** @example false */
upload?: boolean;
}
interface DeleteFileBody {
/** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */
path: string[];
/** @example "ไฟล์ 1 แก้ไข.xlsx" */
file: string;
}
async function listFolder(path: string[]) {
const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
const item: { pathname: string; name: string }[] = [];
const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, path.join("/") + "/");
stream.on("data", (v) => {
if (v && v.prefix)
item.push({
pathname: v.prefix,
name: v.prefix.split("/").filter(Boolean).at(-1)!,
});
});
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
});
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(DEFAULT_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 StorageFolder;
}),
);
return folder.filter((v: StorageFolder | undefined): v is StorageFolder => !!v);
}
async function listFile(path: string[]) {
const result = await esClient
.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX,
sort: [{ pathname: "asc" }],
query: { match: { path: path.join("/") + "/" } },
size: 10000,
})
.then((r) => r.hits.hits);
const records = result
.map((v) => {
if (v._source) {
const { attachment, ...rest } = v._source;
return rest satisfies StorageFile;
}
})
.filter((v: StorageFile | undefined): v is StorageFile => !!v);
return records;
}
async function checkPathExist(bucket: string, path: string) {
return await checkFileExist(bucket, `${path}/.keep`);
}
async function checkFileExist(bucket: string, pathname: string) {
return Boolean(
await minioClient.statObject(bucket, pathname).catch((e) => {
if (e.code === "NotFound") return false;
console.error(`Storage Error: ${e}`);
throw new Error(MINIO_ERROR_MESSAGE);
}),
);
}
@Route("storage")
export class StorageController extends Controller {
@Post("list")
@Example([
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1",
name: "แฟ้ม 1",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
},
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2",
name: "แฟ้ม 2",
createdAt: "2022-01-23T16:05:02.114Z",
createdBy: "admin",
},
])
@Example([
{
pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
title: "เอกสาร",
description: "เอกสารการเงิน",
category: ["บัญชี"],
keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
upload: false,
fileName: "เอกสาร 1.pdf",
fileSize: 10240,
fileType: "application/pdf",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
updatedAt: "2021-07-20T12:33:13.018Z",
updatedBy: "admin",
},
])
@Tags("Storage Folder", "Storage File")
@Security("bearerAuth")
public async getList(@Body() body: ListRequestBody) {
const path = body.path.filter(Boolean);
if (body.operation === "folder") return await listFolder(path);
if (body.operation === "file") return await listFile(path);
}
@Post("folder")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async postFolder(
@Request() request: { user: { preferred_username: string } },
@Body() body: FolderBody,
) {
const { path, name } = body;
if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
const meta = {
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
};
const created = await minioClient
.putObject(
DEFAULT_BUCKET,
`${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/.keep`,
"",
0,
meta,
)
.catch((e) => console.error(`MinIO Error: ${e}`));
if (!created) throw new Error(MINIO_ERROR_MESSAGE);
io.getInstance()?.emit("FolderCreate", {
pathname: `${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/`,
name: name.replace(/[/\\?%*:|"<>#]/g, "-").trim(),
...meta,
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* Folder Folder (Path) Folder (Path)
*/
@Put("folder")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async moveFolder(@Body() body: PutFolderBody) {
const src = `${body.from.path.join("/")}/${body.from.name}`;
const dst = `${body.to.path.join("/")}/${body.to.name}`;
if (!(await checkPathExist(DEFAULT_BUCKET, src))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
if (await checkPathExist(DEFAULT_BUCKET, dst)) {
throw new HttpError(HttpStatusCode.CONFLICT, PATH_ALREADY_EXIST);
}
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
}
const list = await new Promise<{ pathname: string }[]>((resolve, reject) => {
const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, `${src}/`, true);
const item: { pathname: string }[] = [];
stream.on("data", (v) => {
if (v && v.name) item.push({ pathname: v.name });
});
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
});
await Promise.all(
list.map(async (v) => {
const from = `/${DEFAULT_BUCKET}/${v.pathname}`;
const to = `${dst}/${v.pathname.slice(`${src}/`.length)}`;
const result = await minioClient
.copyObject(DEFAULT_BUCKET, to, from, copyCond)
.catch((e) => {
console.error(`MinIO Error: ${e}`);
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
if (result) {
if (v.pathname.includes(".keep")) {
return await minioClient
.removeObject(DEFAULT_BUCKET, v.pathname)
.catch((e) => console.error(`MinIO Error: ${e}`));
}
const search = await esClient.search<
StorageFile & { attachment: Record<string, string> }
>({
index: DEFAULT_INDEX!,
query: { match: { pathname: v.pathname } },
});
if (search && search.hits.hits.length === 0) {
await minioClient
.removeObject(DEFAULT_BUCKET, v.pathname)
.catch((e) => console.error(`MinIO Error: ${e}`));
return console.log(
`Trying to update file in storage but not exist in database: ${from} > ${to}`,
);
}
const data = search.hits.hits[0];
await esClient
.update({
index: DEFAULT_INDEX!,
id: data._id,
doc: {
pathname: to,
path: to.split("/").slice(0, -1).join("/") + "/",
},
refresh: "wait_for",
})
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
await minioClient
.removeObject(DEFAULT_BUCKET, v.pathname)
.catch((e) => console.error(`MinIO Error: ${e}`));
}
}),
);
io.getInstance()?.emit("FolderMove", {
from: `${src}/`,
to: `${dst}/`,
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* Folder File
*/
@Delete("folder")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteStorage(@Body() body: DeleteFolderBody) {
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, body.path.join("/") + "/", true);
stream.on("data", (v) => v && v.name && objects.push(v.name));
stream.on("close", async () => {
resolve(await minioClient.removeObjects(DEFAULT_BUCKET, objects));
});
stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE)));
});
io.getInstance()?.emit("FolderDelete", { pathname: body.path.join("/") + "/" });
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* URL
*/
@Post("file")
@Tags("Storage File")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async postFile(
@Request() request: { user: { preferred_username: string } },
@Body() body: FileBody,
) {
const { path, file } = body;
if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
const result = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_INDEX!,
query: { match: { pathname: path.join("/") + "/" } },
})
.catch((e) => console.error(`MinIO Error: ${e}`));
const metadata: StorageFile = {
path: path.join("/") + "/",
pathname: path.join("/") + "/" + file.replace(/[/\\?%*:|"<>#]/g, "-").trim(),
fileName: file.replace(/[/\\?%*:|"<>#]/g, "-").trim(),
fileSize: 0, // Will be get by minio object storage after file is uploaded
fileType: "", // Will be determined by minio object storage after file is uploaded
title: body.title ?? file.replace(/[/\\?%*:|"<>#]/g, "-").trim(), // default to same as filename
description: body.description ?? "",
category: body.category ?? [],
keyword: body.keyword ?? [],
upload: false, // flag
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username,
};
// Pathname is unique and should not have multiple record with same path
if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) {
await esClient
.delete({
index: DEFAULT_INDEX!,
id: result.hits.hits[0]._id,
})
.catch((e) => console.error(`MinIO Error: ${e}`));
const rec = result.hits.hits[0]._source;
// File exist get created at and created by field but all other will be replaced
// by provided infomation or blank if not provided
metadata.createdAt = rec.createdAt;
metadata.createdBy = rec.createdBy;
}
await esClient.index({
index: DEFAULT_INDEX!,
document: metadata,
refresh: "wait_for", // Must have or else it doesn't wait for updated index resulted in data not found on fetch
});
io.getInstance()?.emit("FileUploadRequest", metadata);
const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, metadata.pathname);
return { ...metadata, uploadUrl: presignedUrl };
}
@Put("file")
@Tags("Storage File")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async moveFile(
@Request() request: { user: { preferred_username: string } },
@Body() body: PutFileBody,
) {
const search = await esClient
.search<StorageFile & { attachment: Record<string, any> }>({
index: DEFAULT_INDEX,
query: {
match: { pathname: body.from.path.join("/") + `/${body.from.file}` },
},
})
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
if (!search) {
throw new Error("เกิดข้อผิดพลาดกับระบบฐานข้อมูล กรุณาลองใหม่ในภายหลัง");
}
if (search && search.hits.hits.length === 0) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
}
if (!(await checkFileExist(DEFAULT_BUCKET, body.from.path.join("/") + `/${body.from.file}`))) {
await esClient.delete({
index: DEFAULT_INDEX,
id: search.hits.hits[0]._id,
});
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
}
if (body.to && !(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
}
if (
body.to &&
(await checkFileExist(DEFAULT_BUCKET, body.to.path.join("/") + `/${body.to.file}`))
) {
throw new HttpError(
HttpStatusCode.PRECONDITION_FAILED,
"พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้",
);
}
if (!search.hits.hits[0]._source) {
// This should not possible.
// Just in case the result found with no associated data.
await esClient.delete({
index: DEFAULT_INDEX,
id: search.hits.hits[0]._id,
});
throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
}
const id = search.hits.hits[0]._id;
const { attachment: _, ...source } = search.hits.hits[0]._source;
const { to, from, upload, ...metadata } = body;
const dateMeta = {
updatedAt: new Date().toISOString(),
updatedBy: request.user.preferred_username,
};
if (from && to) {
const src = [DEFAULT_BUCKET, ...from.path, ""].join("/") + from.file;
const dst = to.path.join("/") + `/${to.file}`;
const result = await minioClient.copyObject(DEFAULT_BUCKET, dst, src, copyCond).catch((e) => {
console.error(`MinIO Error: ${e}`);
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
if (result) {
await esClient
.update({
index: DEFAULT_INDEX,
id: id,
doc: {
...metadata,
path: to.path.join("/") + "/",
pathname: dst,
...dateMeta,
},
refresh: "wait_for",
})
.then(async () => await minioClient.removeObject(DEFAULT_INDEX, src))
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
io.getInstance()?.emit("FileMove", {
from: source,
to: {
...source,
...metadata,
path: to.path.join("/") + "/",
pathname: dst,
...dateMeta,
},
});
if (upload) {
const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, dst);
return { uploadUrl: presignedUrl };
}
}
}
if (from) {
await esClient
.update({
index: DEFAULT_INDEX,
id: id,
doc: {
...metadata,
...dateMeta,
},
refresh: "wait_for",
})
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
io.getInstance()?.emit("FileMove", {
from: source,
to: {
...source,
...metadata,
...dateMeta,
},
});
if (upload) {
const src = from.path.join("/") + `/${from.file}`;
const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, src);
return { uploadUrl: presignedUrl };
}
}
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
@Delete("file")
@Tags("Storage File")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFile(@Body() body: DeleteFileBody) {
const pathname = body.path.join("/") + body.file;
await minioClient
.removeObject(DEFAULT_BUCKET, pathname)
.catch((e) => console.error(`MinIO Error: ${e}`));
await esClient
.deleteByQuery({
index: DEFAULT_INDEX,
query: { match: { pathname } },
})
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
io.getInstance()?.emit("FileDelete", { pathname });
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -9,3 +9,5 @@ export function setInstance(server: Server) {
export function getInstance() {
return io;
}
export default { getInstance, setInstance };

View file

@ -13,6 +13,8 @@ import { FolderController } from './controllers/folderController';
// 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
import { SearchController } from './controllers/searchController';
// 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
import { StorageController } from './controllers/storageController';
// 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
import { SubFolderController } from './controllers/subFolderController';
// 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
import { SubFolderFileController } from './controllers/subFolderFileController';
@ -65,6 +67,77 @@ const models: TsoaRoute.Models = {
"additionalProperties": false,
},
// 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
"ListRequestBody": {
"dataType": "refObject",
"properties": {
"operation": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["folder"]},{"dataType":"enum","enums":["file"]}],"required":true},
"path": {"dataType":"array","array":{"dataType":"string"},"required":true},
},
"additionalProperties": false,
},
// 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
"FolderBody": {
"dataType": "refObject",
"properties": {
"path": {"dataType":"array","array":{"dataType":"string"},"required":true},
"name": {"dataType":"string","required":true},
},
"additionalProperties": false,
},
// 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
"PutFolderBody": {
"dataType": "refObject",
"properties": {
"from": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},
"to": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},
},
"additionalProperties": false,
},
// 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
"DeleteFolderBody": {
"dataType": "refObject",
"properties": {
"path": {"dataType":"array","array":{"dataType":"string"},"required":true},
},
"additionalProperties": false,
},
// 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
"FileBody": {
"dataType": "refObject",
"properties": {
"path": {"dataType":"array","array":{"dataType":"string"},"required":true},
"file": {"dataType":"string","required":true},
"title": {"dataType":"string"},
"description": {"dataType":"string"},
"category": {"dataType":"array","array":{"dataType":"string"}},
"keyword": {"dataType":"array","array":{"dataType":"string"}},
},
"additionalProperties": false,
},
// 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
"PutFileBody": {
"dataType": "refObject",
"properties": {
"title": {"dataType":"string"},
"description": {"dataType":"string"},
"category": {"dataType":"array","array":{"dataType":"string"}},
"keyword": {"dataType":"array","array":{"dataType":"string"}},
"from": {"dataType":"nestedObjectLiteral","nestedProperties":{"file":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true},
"to": {"dataType":"nestedObjectLiteral","nestedProperties":{"file":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}}},
"upload": {"dataType":"boolean"},
},
"additionalProperties": false,
},
// 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
"DeleteFileBody": {
"dataType": "refObject",
"properties": {
"path": {"dataType":"array","array":{"dataType":"string"},"required":true},
"file": {"dataType":"string","required":true},
},
"additionalProperties": false,
},
// 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
};
const validationService = new ValidationService(models);
@ -656,6 +729,181 @@ 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('/storage/list',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.getList)),
function StorageController_getList(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"ListRequestBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.getList.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, undefined, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/folder',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.postFolder)),
function StorageController_postFolder(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"FolderBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.postFolder.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/folder',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.moveFolder)),
function StorageController_moveFolder(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"PutFolderBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.moveFolder.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/folder',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.deleteStorage)),
function StorageController_deleteStorage(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"DeleteFolderBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.deleteStorage.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/file',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.postFile)),
function StorageController_postFile(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"FileBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.postFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/file',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.moveFile)),
function StorageController_moveFile(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"PutFileBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.moveFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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('/storage/file',
...(fetchMiddlewares<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(StorageController.prototype.deleteFile)),
function StorageController_deleteFile(request: any, response: any, next: any) {
const args = {
body: {"in":"body","name":"body","required":true,"ref":"DeleteFileBody"},
};
// 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
let validatedArgs: any[] = [];
try {
validatedArgs = getValidatedArgs(args, request, response);
const controller = new StorageController();
const promise = controller.deleteFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 204, next);
} catch (err) {
return next(err);
}
});
// 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<RequestHandler>(SubFolderController)),

View file

@ -174,6 +174,299 @@
},
"type": "object",
"additionalProperties": false
},
"ListRequestBody": {
"properties": {
"operation": {
"type": "string",
"enum": [
"folder",
"file"
]
},
"path": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"operation",
"path"
],
"type": "object",
"additionalProperties": false
},
"FolderBody": {
"properties": {
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2"
]
},
"name": {
"type": "string",
"example": "แฟ้ม 3"
}
},
"required": [
"path",
"name"
],
"type": "object",
"additionalProperties": false
},
"PutFolderBody": {
"properties": {
"from": {
"properties": {
"name": {
"type": "string",
"example": "แฟ้ม 3"
},
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2"
]
}
},
"required": [
"name",
"path"
],
"type": "object"
},
"to": {
"properties": {
"name": {
"type": "string",
"example": "แฟ้ม 3 แก้ไข"
},
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2"
]
}
},
"required": [
"name",
"path"
],
"type": "object"
}
},
"required": [
"from",
"to"
],
"type": "object",
"additionalProperties": false
},
"DeleteFolderBody": {
"properties": {
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2",
"แฟ้ม 3 แก้ไข"
]
}
},
"required": [
"path"
],
"type": "object",
"additionalProperties": false
},
"FileBody": {
"properties": {
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2",
"แฟ้ม 3"
]
},
"file": {
"type": "string",
"example": "ไฟล์ 1.xlsx"
},
"title": {
"type": "string",
"example": "การเงิน"
},
"description": {
"type": "string",
"example": "การเงิน"
},
"category": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"การเงิน",
"รายงาน"
]
},
"keyword": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"การเงิน",
"รายรับ",
"รายจ่าย"
]
}
},
"required": [
"path",
"file"
],
"type": "object",
"additionalProperties": false
},
"PutFileBody": {
"properties": {
"title": {
"type": "string",
"example": "การเงิน"
},
"description": {
"type": "string",
"example": "การเงิน"
},
"category": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"การเงิน",
"รายงาน"
]
},
"keyword": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"การเงิน",
"รายรับ",
"รายจ่าย"
]
},
"from": {
"properties": {
"file": {
"type": "string",
"example": "ไฟล์ 1.xlsx"
},
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2",
"แฟ้ม 3"
]
}
},
"required": [
"file",
"path"
],
"type": "object",
"description": "หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง"
},
"to": {
"properties": {
"file": {
"type": "string",
"example": "ไฟล์ 1 แก้ไข.xlsx"
},
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2",
"แฟ้ม 3"
]
}
},
"required": [
"file",
"path"
],
"type": "object"
},
"upload": {
"type": "boolean",
"example": false
}
},
"required": [
"from"
],
"type": "object",
"additionalProperties": false
},
"DeleteFileBody": {
"properties": {
"path": {
"items": {
"type": "string"
},
"type": "array",
"example": [
"แฟ้ม 1",
"แฟ้ม 2",
"แฟ้ม 3"
]
},
"file": {
"type": "string",
"example": "ไฟล์ 1 แก้ไข.xlsx"
}
},
"required": [
"path",
"file"
],
"type": "object",
"additionalProperties": false
}
},
"securitySchemes": {
@ -1662,6 +1955,356 @@
}
}
},
"/storage/list": {
"post": {
"operationId": "GetList",
"responses": {
"200": {
"description": "Ok",
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"items": {
"$ref": "#/components/schemas/StorageFolder"
},
"type": "array"
},
{
"items": {
"$ref": "#/components/schemas/StorageFile"
},
"type": "array"
}
]
},
"examples": {
"Example 1": {
"value": [
{
"path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1",
"name": "แฟ้ม 1",
"createdAt": "2021-07-20T12:33:13.018Z",
"createdBy": "admin"
},
{
"path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2",
"name": "แฟ้ม 2",
"createdAt": "2022-01-23T16:05:02.114Z",
"createdBy": "admin"
}
]
},
"Example 2": {
"value": [
{
"pathname": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
"path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
"title": "เอกสาร",
"description": "เอกสารการเงิน",
"category": [
"บัญชี"
],
"keyword": [
"เงิน",
"บัญชี",
"รายจ่าย",
"รายรับ"
],
"upload": false,
"fileName": "เอกสาร 1.pdf",
"fileSize": 10240,
"fileType": "application/pdf",
"createdAt": "2021-07-20T12:33:13.018Z",
"createdBy": "admin",
"updatedAt": "2021-07-20T12:33:13.018Z",
"updatedBy": "admin"
}
]
}
}
}
}
}
},
"tags": [
"Storage Folder",
"Storage File"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ListRequestBody"
}
}
}
}
}
},
"/storage/folder": {
"post": {
"operationId": "PostFolder",
"responses": {
"204": {
"description": "สำเร็จ"
}
},
"tags": [
"Storage Folder"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FolderBody"
}
}
}
}
},
"put": {
"operationId": "MoveFolder",
"responses": {
"204": {
"description": "สำเร็จ"
}
},
"description": "ยา้ย Folder ภายใต้ Folder (Path) หนึ่ง ไปภายใน Folder (Path) หนึ่งและสามารถเปลี่ยนชื่อได้",
"tags": [
"Storage Folder"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutFolderBody"
}
}
}
}
},
"delete": {
"operationId": "DeleteStorage",
"responses": {
"204": {
"description": "สำเร็จ"
}
},
"description": "ลบ Folder หรือ File ออกจากระบบ",
"tags": [
"Storage Folder"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteFolderBody"
}
}
}
}
}
},
"/storage/file": {
"post": {
"operationId": "PostFile",
"responses": {
"204": {
"description": "สำเร็จ",
"content": {
"application/json": {
"schema": {
"properties": {
"createdBy": {
"type": "string"
},
"createdAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "string",
"format": "date-time"
}
]
},
"updatedBy": {
"type": "string"
},
"updatedAt": {
"anyOf": [
{
"type": "string"
},
{
"type": "string",
"format": "date-time"
}
]
},
"upload": {
"type": "boolean"
},
"path": {
"type": "string"
},
"keyword": {
"items": {
"type": "string"
},
"type": "array"
},
"category": {
"items": {
"type": "string"
},
"type": "array"
},
"description": {
"type": "string"
},
"title": {
"type": "string"
},
"fileType": {
"type": "string"
},
"fileSize": {
"type": "number",
"format": "double"
},
"fileName": {
"type": "string"
},
"pathname": {
"type": "string"
},
"uploadUrl": {
"type": "string"
}
},
"required": [
"createdBy",
"createdAt",
"updatedBy",
"updatedAt",
"upload",
"path",
"keyword",
"category",
"description",
"title",
"fileType",
"fileSize",
"fileName",
"pathname",
"uploadUrl"
],
"type": "object"
}
}
}
}
},
"description": "ร้องขอการอัพโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด",
"tags": [
"Storage File"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileBody"
}
}
}
}
},
"put": {
"operationId": "MoveFile",
"responses": {
"204": {
"description": "สำเร็จ",
"content": {
"application/json": {
"schema": {
"anyOf": [
{},
{
"properties": {
"uploadUrl": {
"type": "string"
}
},
"required": [
"uploadUrl"
],
"type": "object"
}
]
}
}
}
}
},
"tags": [
"Storage File"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutFileBody"
}
}
}
}
},
"delete": {
"operationId": "DeleteFile",
"responses": {
"204": {
"description": "สำเร็จ"
}
},
"tags": [
"Storage File"
],
"security": [],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeleteFileBody"
}
}
}
}
}
},
"/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": {
"get": {
"operationId": "ListFolder",

View file

@ -28,7 +28,9 @@ export async function expressAuthentication(
securityName: string,
scopes?: string[],
) {
if (process.env.AUTH_BYPASS) return { preferred_username: "bypassed" };
if (process.env.NODE_ENV !== "production" && process.env.AUTH_BYPASS) {
return { preferred_username: "bypassed" };
}
if (securityName !== "bearerAuth") throw new Error("Unknown authentication method.");
@ -36,7 +38,7 @@ export async function expressAuthentication(
? request.headers["authorization"].split(" ")[1]
: null;
if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.");
if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่พบข้อมูลสำหัรบบืนบันตัวตน");
let payload: JwtPayload = {};
@ -60,7 +62,7 @@ export async function expressAuthentication(
.map((v) => (v === "management-role" ? process.env.MANAGEMENT_ROLE : v))
.every((v) => !payload.resource_access[payload.azp].roles.includes(v))
) {
throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action.");
throw new HttpError(HttpStatusCode.FORBIDDEN, "คุณไม่มีสิทธิในเข้าถึงข้อมูลนี้");
}
return payload;
@ -68,7 +70,7 @@ export async function expressAuthentication(
async function verifyOffline(token: string) {
const payload = await jwtVerify(token).catch((_) => null);
if (!payload) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
if (!payload) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
return payload;
}
@ -77,8 +79,8 @@ async function verifyOnline(token: string) {
headers: { authorization: `Bearer ${token}` },
}).catch((e) => console.error(e));
if (!res) throw new Error("Cannot connect to auth service.");
if (!res.ok) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
if (!res) throw new Error("ไม่สามารถเข้าถึงระบบยืนยันตัวตน");
if (!res.ok) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "ไม่สามารถยืนยันตัวตนได้");
return await jwtDecode(token);
}