From e62ffb590679f3503aeb629a549160ad16382777 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 + server/src/app.js | 17 +- .../20200423201600_create_media_table.js | 12 ++ server/src/http/controllers/Authentication.js | 5 +- server/src/http/controllers/Media.js | 146 ++++++++++++++++++ server/src/http/index.js | 2 + .../FilterRolesDynamicFilter.js | 6 +- server/src/models/Media.js | 10 ++ 8 files changed, 189 insertions(+), 16 deletions(-) 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/app.js b/server/src/app.js index 29f749873..00baa037c 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -12,19 +12,13 @@ global.rootPath = rootPath.path; const app = express(); -// i18n.configure({ -// // setup some locales - other locales default to en silently -// locales: ['en'], - -// // sets a custom cookie name to parse locale settings from. -// cookie: 'yourcookiename', - -// // where to store json files - defaults to './locales' -// directory: `${__dirname}/resources/locale`, -// }); +i18n.configure({ + locales: ['en'], + directory: `${__dirname}/resources/locale`, +}); // i18n init parses req for language headers, cookies, etc. -// app.use(i18n.init); +app.use(i18n.init); // Express configuration app.set('port', process.env.PORT || 3000); @@ -34,7 +28,6 @@ app.use(boom()); app.use(express.json()); app.use(fileUpload({ createParentPath: true, - // safeFileNames: true, })); routes(app); 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..ca3a6bfeb 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}`); @@ -225,6 +225,7 @@ export default { mail.sendMail(mailOptions, (error) => { if (error) { Logger.log('error', 'Failed send reset password mail', { error, form }); + return; } Logger.log('info', 'User has been sent reset password email successfuly.', { form }); }); @@ -253,6 +254,7 @@ export default { code: 'validation_error', ...validationErrors, }); } + Logger.log('info', 'User trying to reset password.'); const { token } = req.params; const { password } = req.body; @@ -284,6 +286,7 @@ export default { // Delete the reset password token. await PasswordReset.query().where('token', token).delete(); + Logger.log('info', 'User password has been reset successfully.'); return res.status(200).send({}); }, 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/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js b/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js index 076a7e378..978abb53d 100644 --- a/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js +++ b/server/src/lib/ViewRolesBuilder/FilterRolesDynamicFilter.js @@ -7,8 +7,8 @@ import { export default class ViewRolesDynamicFilter extends DynamicFilterRoleAbstructor { /** * Constructor method. - * @param {*} filterRoles - * @param {*} logicExpression + * @param {*} filterRoles - + * @param {*} logicExpression - */ constructor(filterRoles, logicExpression) { super(); @@ -41,4 +41,4 @@ export default class ViewRolesDynamicFilter extends DynamicFilterRoleAbstructor buildFilterQuery(this.tableName, this.filterRoles, this.logicExpression)(builder); }; } -} \ No newline at end of file +} 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'; + } +}