mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 16:19:49 +00:00
feat: wip upload documents
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"bigcapital": "./bin/bigcapital.js"
|
"bigcapital": "./bin/bigcapital.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-s3": "^3.576.0",
|
||||||
"@casl/ability": "^5.4.3",
|
"@casl/ability": "^5.4.3",
|
||||||
"@hapi/boom": "^7.4.3",
|
"@hapi/boom": "^7.4.3",
|
||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
@@ -73,12 +74,14 @@
|
|||||||
"lru-cache": "^6.0.0",
|
"lru-cache": "^6.0.0",
|
||||||
"mathjs": "^9.4.0",
|
"mathjs": "^9.4.0",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
|
"mime-types": "^2.1.35",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"moment-range": "^4.0.2",
|
"moment-range": "^4.0.2",
|
||||||
"moment-timezone": "^0.5.43",
|
"moment-timezone": "^0.5.43",
|
||||||
"mongodb": "^6.1.0",
|
"mongodb": "^6.1.0",
|
||||||
"mongoose": "^5.10.0",
|
"mongoose": "^5.10.0",
|
||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
|
"multer-s3": "^3.0.1",
|
||||||
"mustache": "^3.0.3",
|
"mustache": "^3.0.3",
|
||||||
"mysql": "^2.17.1",
|
"mysql": "^2.17.1",
|
||||||
"mysql2": "^1.6.5",
|
"mysql2": "^1.6.5",
|
||||||
@@ -113,6 +116,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.14.158",
|
"@types/lodash": "^4.14.158",
|
||||||
|
"@types/multer": "^1.4.11",
|
||||||
"@types/ramda": "^0.27.64",
|
"@types/ramda": "^0.27.64",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||||
"@typescript-eslint/parser": "^5.50.0",
|
"@typescript-eslint/parser": "^5.50.0",
|
||||||
|
|||||||
114
packages/server/src/api/controllers/AttachmentsController.ts
Normal file
114
packages/server/src/api/controllers/AttachmentsController.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import mime from 'mime-types';
|
||||||
|
import { Service, Inject } from 'typedi';
|
||||||
|
import { Router, Response } from 'express';
|
||||||
|
import { param } from 'express-validator';
|
||||||
|
import BaseController from '@/api/controllers/BaseController';
|
||||||
|
import { Request } from 'express-validator/src/base';
|
||||||
|
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AttachmentsController extends BaseController {
|
||||||
|
@Inject()
|
||||||
|
private attachmentsApplication: AttachmentsApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Router constructor.
|
||||||
|
*/
|
||||||
|
public router() {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
this.attachmentsApplication.uploadPipeline.single('file'),
|
||||||
|
this.uploadAttachment.bind(this)
|
||||||
|
);
|
||||||
|
router.delete(
|
||||||
|
'/:id',
|
||||||
|
[param('id').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.deleteAttachment.bind(this)
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
'/:id',
|
||||||
|
[param('id').exists()],
|
||||||
|
this.validationResult,
|
||||||
|
this.getAttachment.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads the attachments to S3 and store the file metadata to DB.
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param {Function} next
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async uploadAttachment(req: Request, res: Response, next: Function) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const file = req.file;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.attachmentsApplication.upload(tenantId, file);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
status: 200,
|
||||||
|
message: 'The document has uploaded successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Request} req
|
||||||
|
* @param {Response} res
|
||||||
|
* @param next
|
||||||
|
*/
|
||||||
|
private async getAttachment(req: Request, res: Response, next: Function) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.attachmentsApplication.get(tenantId, id);
|
||||||
|
|
||||||
|
const byte = await data.Body.transformToByteArray();
|
||||||
|
const extension = mime.extension(data.ContentType);
|
||||||
|
const buffer = Buffer.from(byte);
|
||||||
|
|
||||||
|
res.set(
|
||||||
|
'Content-Disposition',
|
||||||
|
`filename="${req.params.id}.${extension}"`
|
||||||
|
);
|
||||||
|
res.set('Content-Type', data.ContentType);
|
||||||
|
res.send(buffer);
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @param next
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private async deleteAttachment(req: Request, res: Response, next: Function) {
|
||||||
|
const { tenantId } = req;
|
||||||
|
const { id: documentId } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.attachmentsApplication.delete(tenantId, documentId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
status: 200,
|
||||||
|
message: 'The document has been delete successfully.',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ import { ImportController } from './controllers/Import/ImportController';
|
|||||||
import { BankingController } from './controllers/Banking/BankingController';
|
import { BankingController } from './controllers/Banking/BankingController';
|
||||||
import { Webhooks } from './controllers/Webhooks/Webhooks';
|
import { Webhooks } from './controllers/Webhooks/Webhooks';
|
||||||
import { ExportController } from './controllers/Export/ExportController';
|
import { ExportController } from './controllers/Export/ExportController';
|
||||||
|
import { AttachmentsController } from './controllers/AttachmentsController';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const app = Router();
|
const app = Router();
|
||||||
@@ -79,6 +80,7 @@ export default () => {
|
|||||||
app.use('/jobs', Container.get(Jobs).router());
|
app.use('/jobs', Container.get(Jobs).router());
|
||||||
app.use('/account', Container.get(Account).router());
|
app.use('/account', Container.get(Account).router());
|
||||||
app.use('/webhooks', Container.get(Webhooks).router());
|
app.use('/webhooks', Container.get(Webhooks).router());
|
||||||
|
app.use('/attachments', Container.get(AttachmentsController).router());
|
||||||
|
|
||||||
// - Dashboard routes.
|
// - Dashboard routes.
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
@@ -142,7 +144,7 @@ export default () => {
|
|||||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||||
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
||||||
dashboard.use('/import', Container.get(ImportController).router());
|
dashboard.use('/import', Container.get(ImportController).router());
|
||||||
dashboard.use('/export', Container.get(ExportController).router())
|
dashboard.use('/export', Container.get(ExportController).router());
|
||||||
|
|
||||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||||
|
|||||||
@@ -157,7 +157,10 @@ module.exports = {
|
|||||||
* Sign-up email confirmation
|
* Sign-up email confirmation
|
||||||
*/
|
*/
|
||||||
signupConfirmation: {
|
signupConfirmation: {
|
||||||
enabled: parseBoolean<boolean>(process.env.SIGNUP_EMAIL_CONFIRMATION, false),
|
enabled: parseBoolean<boolean>(
|
||||||
|
process.env.SIGNUP_EMAIL_CONFIRMATION,
|
||||||
|
false
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,4 +228,15 @@ module.exports = {
|
|||||||
defaultTo(process.env.HOSTED_ON_BIGCAPITAL_CLOUD, false),
|
defaultTo(process.env.HOSTED_ON_BIGCAPITAL_CLOUD, false),
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 for documents.
|
||||||
|
*/
|
||||||
|
s3: {
|
||||||
|
region: process.env.AWS_REGION,
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
endpoint: process.env.AWS_ENDPOINT,
|
||||||
|
bucket: process.env.AWS_BUCKET,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
exports.up = function (knex) {
|
exports.up = function (knex) {
|
||||||
return knex.schema.createTable('storage', (table) => {
|
return knex.schema.createTable('documents', (table) => {
|
||||||
table.increments('id').primary();
|
table.increments('id').primary();
|
||||||
table.string('key').notNullable();
|
table.string('key').notNullable();
|
||||||
table.string('path').notNullable();
|
|
||||||
table.string('extension').notNullable();
|
table.string('extension').notNullable();
|
||||||
table.integer('expire_in');
|
|
||||||
table.timestamps();
|
table.timestamps();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.down = function (knex) {
|
exports.down = function (knex) {
|
||||||
return knex.schema.dropTableIfExists('storage');
|
return knex.schema.dropTableIfExists('documents');
|
||||||
};
|
};
|
||||||
10
packages/server/src/lib/S3/S3.ts
Normal file
10
packages/server/src/lib/S3/S3.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { S3Client } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
export const s3 = new S3Client({
|
||||||
|
region: process.env.AWS_REGION,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
|
||||||
|
},
|
||||||
|
endpoint: process.env.AWS_ENDPOINT,
|
||||||
|
});
|
||||||
@@ -11,7 +11,7 @@ export default class Attachment extends mixin(TenantModel, [
|
|||||||
* Table name
|
* Table name
|
||||||
*/
|
*/
|
||||||
static get tableName() {
|
static get tableName() {
|
||||||
return 'storage';
|
return 'documents';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import { UploadDocument } from './UploadDocument';
|
||||||
|
import { DeleteAttachment } from './DeleteAttachment';
|
||||||
|
import { GetAttachment } from './GetAttachment';
|
||||||
|
import { AttachmentUploadPipeline } from './S3UploadPipeline';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AttachmentsApplication {
|
||||||
|
@Inject()
|
||||||
|
private uploadDocumentService: UploadDocument;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private deleteDocumentService: DeleteAttachment;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private getDocumentService: GetAttachment;
|
||||||
|
|
||||||
|
private uploadPipelineService: AttachmentUploadPipeline;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
get uploadPipeline() {
|
||||||
|
return this.uploadPipelineService.uploadPipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {} file
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public upload(tenantId: number, file: any) {
|
||||||
|
return this.uploadDocumentService.upload(tenantId, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the give file attachment file key.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} documentKey
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public delete(tenantId: number, documentKey: string) {
|
||||||
|
return this.deleteDocumentService.delete(tenantId, documentKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the document data.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} documentKey
|
||||||
|
*/
|
||||||
|
public get(tenantId: number, documentKey: string) {
|
||||||
|
return this.getDocumentService.getAttachment(tenantId, documentKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/server/src/services/Attachments/DeleteAttachment.ts
Normal file
19
packages/server/src/services/Attachments/DeleteAttachment.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { DeleteObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
import { s3 } from '@/lib/S3/S3';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class DeleteAttachment {
|
||||||
|
/**
|
||||||
|
* Deletes the give file attachment file key.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} filekey
|
||||||
|
*/
|
||||||
|
async delete(tenantId: number, filekey: string): Promise<void> {
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.AWS_BUCKET,
|
||||||
|
Key: filekey,
|
||||||
|
};
|
||||||
|
await s3.send(new DeleteObjectCommand(params));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/server/src/services/Attachments/GetAttachment.ts
Normal file
21
packages/server/src/services/Attachments/GetAttachment.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { s3 } from '@/lib/S3/S3';
|
||||||
|
import { GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class GetAttachment {
|
||||||
|
/**
|
||||||
|
* Retrieves data of the given document key.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} filekey
|
||||||
|
*/
|
||||||
|
async getAttachment(tenantId: number, filekey: string) {
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.AWS_BUCKET,
|
||||||
|
Key: filekey,
|
||||||
|
};
|
||||||
|
const data = await s3.send(new GetObjectCommand(params));
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/server/src/services/Attachments/S3UploadPipeline.ts
Normal file
23
packages/server/src/services/Attachments/S3UploadPipeline.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import multer from 'multer';
|
||||||
|
import multerS3 from 'multer-s3';
|
||||||
|
import { s3 } from '@/lib/S3/S3';
|
||||||
|
import { Service } from 'typedi';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class AttachmentUploadPipeline {
|
||||||
|
uploadPipeline() {
|
||||||
|
return multer({
|
||||||
|
storage: multerS3({
|
||||||
|
s3,
|
||||||
|
bucket: process.env.AWS_BUCKET,
|
||||||
|
contentType: multerS3.AUTO_CONTENT_TYPE,
|
||||||
|
metadata: function (req, file, cb) {
|
||||||
|
cb(null, { fieldName: file.fieldname });
|
||||||
|
},
|
||||||
|
key: function (req, file, cb) {
|
||||||
|
cb(null, Date.now().toString());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
10
packages/server/src/services/Attachments/UploadDocument.ts
Normal file
10
packages/server/src/services/Attachments/UploadDocument.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class UploadDocument {
|
||||||
|
|
||||||
|
async upload(tenantId: number, file: any) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
12277
pnpm-lock.yaml
generated
12277
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user