From 1c6a067db7d38499ca3ad2da5178270dadc721a8 Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Thu, 23 Apr 2020 21:02:17 +0200 Subject: [PATCH] feat: Media attachment system --- server/config/config.js | 7 + .../20200423201600_create_media_table.js | 12 ++ server/src/http/controllers/Authentication.js | 2 +- server/src/http/controllers/Media.js | 146 ++++++++++++++++++ server/src/http/index.js | 2 + server/src/models/Media.js | 10 ++ 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 server/src/database/migrations/20200423201600_create_media_table.js create mode 100644 server/src/http/controllers/Media.js create mode 100644 server/src/models/Media.js diff --git a/server/config/config.js b/server/config/config.js index dc91a1953..02d9122d0 100644 --- a/server/config/config.js +++ b/server/config/config.js @@ -22,4 +22,11 @@ module.exports = { superUser: 'root', superPassword: '123123123', }, + mail: { + host: 'smtp.mailtrap.io', + port: 587, + secure: false, + username: '842f331d3dc005', + password: '172f97b34f1a17', + } }; diff --git a/server/src/database/migrations/20200423201600_create_media_table.js b/server/src/database/migrations/20200423201600_create_media_table.js new file mode 100644 index 000000000..64ffc3940 --- /dev/null +++ b/server/src/database/migrations/20200423201600_create_media_table.js @@ -0,0 +1,12 @@ + +exports.up = function(knex) { + return knex.schema.createTable('media', (table) => { + table.increments(); + table.string('attachment_file'); + table.timestamps(); + }); +}; + +exports.down = function(knex) { + return knex.schema.dropTableIfExists('media'); +}; diff --git a/server/src/http/controllers/Authentication.js b/server/src/http/controllers/Authentication.js index 79af8d14c..0b6b9d653 100644 --- a/server/src/http/controllers/Authentication.js +++ b/server/src/http/controllers/Authentication.js @@ -147,11 +147,11 @@ export default { const hashedPassword = await hashPassword(form.password); const userInsert = { ...pick(form, ['first_name', 'last_name', 'email', 'phone_number']), - password: hashedPassword, active: true, }; const registeredUser = await SystemUser.query().insert({ ...userInsert, + password: hashedPassword, tenant_id: tenantOrganization.id, }); await dbManager.createDb(`bigcapital_tenant_${organizationId}`); diff --git a/server/src/http/controllers/Media.js b/server/src/http/controllers/Media.js new file mode 100644 index 000000000..ec898ebc3 --- /dev/null +++ b/server/src/http/controllers/Media.js @@ -0,0 +1,146 @@ + +import express from 'express'; +import { check, param, query, validationResult } from 'express-validator'; +import fs from 'fs'; +import asyncMiddleware from '@/http/middleware/asyncMiddleware'; +import TenancyMiddleware from '@/http/middleware/TenancyMiddleware'; +import jwtAuth from '@/http/middleware/jwtAuth'; +import Logger from '@/services/Logger'; + +const fsPromises = fs.promises; + +export default { + /** + * Router constructor. + */ + router() { + const router = express.Router(); + + router.use(jwtAuth); + router.use(TenancyMiddleware); + + router.post('/upload', + this.upload.validation, + asyncMiddleware(this.upload.handler)); + + router.delete('/delete/:id', + this.delete.validation, + asyncMiddleware(this.delete.handler)); + + router.get('/', + this.get.validation, + asyncMiddleware(this.get.handler)); + + return router; + }, + + /** + * Retrieve all or the given attachment ids. + */ + get: { + validation: [ + query('ids'), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const { Media } = req.models; + const media = await Media.query().onBuild((builder) => { + + if (req.query.ids) { + const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids]; + builder.whereIn('id', ids); + } + }); + + return res.status(200).send({ media }); + }, + }, + + /** + * Uploads the given attachment file. + */ + upload: { + validation: [ + // check('attachment').exists(), + ], + async handler(req, res) { + if (!req.files.attachment) { + return res.status(400).send({ + errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }], + }); + } + const publicPath = 'storage/app/public/'; + const attachmentsMimes = ['image/png', 'image/jpeg']; + const { attachment } = req.files; + const { Media } = req.models; + + const errorReasons = []; + + // Validate the attachment. + if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) { + errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 }); + } + // Catch all error reasons to response 400. + if (errorReasons.length > 0) { + return res.status(400).send({ errors: errorReasons }); + } + + try { + await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`); + Logger.log('info', 'Attachment uploaded successfully'); + } catch (error) { + Logger.log('info', 'Attachment uploading failed.', { error }); + } + + const media = await Media.query().insert({ + attachment_file: `${attachment.md5}.png`, + }); + return res.status(200).send({ media }); + }, + }, + + /** + * Deletes the given attachment ids from file system and database. + */ + delete: { + validation: [ + param('id').exists().isNumeric().toInt(), + ], + async handler(req, res) { + const validationErrors = validationResult(req); + + if (!validationErrors.isEmpty()) { + return res.boom.badData(null, { + code: 'validation_error', ...validationErrors, + }); + } + const { Media } = req.models; + const { id } = req.params; + const media = await Media.query().where('id', id).first(); + + if (!media) { + return res.status(400).send({ + errors: [{ type: 'MEDIA.ID.NOT.FOUND', code: 200 }], + }); + } + const publicPath = 'storage/app/public/'; + const tenantPath = `${publicPath}${req.organizationId}`; + + try { + await fsPromises.unlink(`${tenantPath}/${media.attachmentFile}`); + Logger.log('error', 'Attachment file has been deleted.'); + } catch (error) { + Logger.log('error', 'Delete item attachment file delete failed.', { error }); + } + await Media.query().where('id', media.id).delete(); + + return res.status(200).send(); + }, + }, +}; diff --git a/server/src/http/index.js b/server/src/http/index.js index 5a10ed6d8..0eb645fe9 100644 --- a/server/src/http/index.js +++ b/server/src/http/index.js @@ -25,6 +25,7 @@ import Resources from './controllers/Resources'; import ExchangeRates from '@/http/controllers/ExchangeRates'; // import SalesReports from '@/http/controllers/SalesReports'; // import PurchasesReports from '@/http/controllers/PurchasesReports'; +import Media from '@/http/controllers/Media'; export default (app) => { // app.use('/api/oauth2', OAuth2.router()); @@ -51,6 +52,7 @@ export default (app) => { // app.use('/api/budget', Budget.router()); app.use('/api/resources', Resources.router()); app.use('/api/exchange_rates', ExchangeRates.router()); + app.use('/api/media', Media.router()); // app.use('/api/currency_adjustment', CurrencyAdjustment.router()); // app.use('/api/reports/sales', SalesReports.router()); // app.use('/api/reports/purchases', PurchasesReports.router()); diff --git a/server/src/models/Media.js b/server/src/models/Media.js new file mode 100644 index 000000000..c814d4655 --- /dev/null +++ b/server/src/models/Media.js @@ -0,0 +1,10 @@ +import TenantModel from '@/models/TenantModel'; + +export default class Media extends TenantModel { + /** + * Table name + */ + static get tableName() { + return 'media'; + } +}