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