diff --git a/server/middleware/validators.js b/server/middleware/validators.js index 94172cb..f18b914 100644 --- a/server/middleware/validators.js +++ b/server/middleware/validators.js @@ -4,7 +4,7 @@ const codes = require('../routes/http-codes'); // library to check image dimensions from file buffer -var sizeOf = require('buffer-image-size'); +const sizeOf = require('buffer-image-size'); /** * check if id has valid UUID format @@ -27,8 +27,6 @@ const idValidator = (req, res, next) => { const imageDimensionValidator = (imageFileBuf, maxWidth, maxHeight) => { const dimensions = sizeOf(imageFileBuf); - console.log(dimensions.width) - console.log(dimensions.height) if (dimensions.width > maxWidth || dimensions.height > maxHeight) { let err = new Error(`Image exceeds maximum dimensions of ${maxWidth}px width and ${maxHeight}px height`); err.status = codes.wrongrequest; diff --git a/server/resource/decoration/591c78741ee62711cfc18f27.png b/server/resource/decoration/591c78741ee62711cfc18f27.png index 03c59e5..513e845 100644 Binary files a/server/resource/decoration/591c78741ee62711cfc18f27.png and b/server/resource/decoration/591c78741ee62711cfc18f27.png differ diff --git a/server/routes/decorations.js b/server/routes/decorations.js index 6bd782a..e2ad4e7 100644 --- a/server/routes/decorations.js +++ b/server/routes/decorations.js @@ -13,6 +13,7 @@ const codes = require('./http-codes'); const apiAuthenticationMiddleware = require('../middleware/auth-middleware'); const checkHl = require('../middleware/permission-check').checkHl; +const genericGetById = require('./_generic').genericGetById; const routerHandling = require('../middleware/router-handling'); const idValidator = require('../middleware/validators').idValidator; const resourceLocation = require('../middleware/resource-location').resourceLocation().concat('/decoration/'); @@ -22,140 +23,175 @@ const DecorationModel = require('../models/decoration'); const AwardingsModel = require('../models/awarding'); // util -const genericGetById = require('./_generic').genericGetById; +const imageDimensionValidator = require('../middleware/validators').imageDimensionValidator; +const MAX_MEDAL_IMAGE_WIDTH = 100; +const MAX_MEDAL_IMAGE_HEIGHT = 150; +const MAX_RIBBON_IMAGE_WIDTH = 150; +const MAX_RIBBON_IMAGE_HEIGHT = 50; -const decoration = new express.Router(); +const decorationRouter = new express.Router(); // routes ********************** -decoration.route('/') - .get((req, res, next) => { - const filter = {}; - if (req.query.fractFilter) { - filter.fraction = req.query.fractFilter.toUpperCase(); - } - if (req.query.q) { - filter.name = {$regex: req.query.q, $options: 'i'}; - } - DecorationModel.find(filter, {}, { - sort: { - fraction: 'asc', - isMedal: 'asc', - sortingNumber: 'asc', - name: 'asc', - }, - }, (err, items) => { - if (err) { - err.status = codes.servererror; - return next(err); - } - if (items && items.length > 0) { - res.locals.items = items; - } else { - res.locals.items = []; - } - res.locals.processed = true; - next(); - }); - }) +decorationRouter.route('/') + .get((req, res, next) => { + const filter = {}; + if (req.query.fractFilter) { + filter.fraction = req.query.fractFilter.toUpperCase(); + } + if (req.query.q) { + filter.name = {$regex: req.query.q, $options: 'i'}; + } + DecorationModel.find(filter, {}, { + sort: { + fraction: 'asc', + isMedal: 'asc', + sortingNumber: 'asc', + name: 'asc', + }, + }, (err, items) => { + if (err) { + err.status = codes.servererror; + return next(err); + } + if (items && items.length > 0) { + res.locals.items = items; + } else { + res.locals.items = []; + } + res.locals.processed = true; + next(); + }); + }) - .post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => { - const decoration = new DecorationModel(req.body); - // timestamp and default are set automatically by Mongoose Schema Validation - decoration.save((err) => { - if (err) { - err.status = codes.wrongrequest; - err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors); - return next(err); - } - res.status(codes.created); - res.locals.items = decoration; - fs.appendFile(resourceLocation + decoration._id + '.png', - new Buffer(req.file.buffer), - (err) => { - if (err) next(err); - }); - next(); - }); - }) + .post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => { + if (req.file) { + const decoration = new DecorationModel(req.body); - .all( - routerHandling.httpMethodNotAllowed - ); + const imageFileBuffer = req.file.buffer; + const err = imageDimensionValidator(imageFileBuffer, + (decoration.isMedal ? MAX_MEDAL_IMAGE_WIDTH : MAX_RIBBON_IMAGE_WIDTH), + (decoration.isMedal ? MAX_MEDAL_IMAGE_HEIGHT : MAX_RIBBON_IMAGE_HEIGHT)); + if (err) { + return next(err); + } -decoration.route('/:id') - .get(idValidator, (req, res, next) => { - return genericGetById(req, res, next, DecorationModel); - }) + // timestamp and default are set automatically by Mongoose Schema Validation + decoration.save((err) => { + if (err) { + err.status = codes.wrongrequest; + err.message += ' in fields: ' + Object.getOwnPropertyNames(err.errors); + return next(err); + } + res.status(codes.created); + res.locals.items = decoration; + fs.appendFile(resourceLocation + decoration._id + '.png', + new Buffer(req.file.buffer), + (err) => { + if (err) next(err); + }); + next(); + }); + } else { + const err = new Error('no image file provided'); + err.status = codes.wrongmediasend; + next(err); + } + }) - .patch(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => { - if (!req.body || (req.body._id && req.body._id !== req.params.id)) { - // little bit different as in PUT. :id does not need to be in data, but if the _id and url id must match - const err = new Error('id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' + - req.body._id); - err.status = codes.notfound; - next(err); - return; // prevent node to process this function further after next() has finished. - } + .all( + routerHandling.httpMethodNotAllowed + ); - // optional task 3: increment version manually as we do not use .save(.) - req.body.updatedAt = new Date(); - req.body.$inc = {__v: 1}; +decorationRouter.route('/:id') + .get(idValidator, (req, res, next) => { + return genericGetById(req, res, next, DecorationModel); + }) - if (req.file) { - const file = resourceLocation + req.params.id + '.png'; - fs.unlink(file, (err) => { - if (err) next(err); - fs.appendFile(file, new Buffer(req.file.buffer), (err) => { - if (err) next(err); - }); - }); - } + .patch(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => { + if (!req.body || (req.body._id && req.body._id !== req.params.id)) { + // little bit different as in PUT. :id does not need to be in data, but if the _id and url id must + // match + const err = new Error( + 'id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' + + req.body._id); + err.status = codes.notfound; + next(err); + return; // prevent node to process this function further after next() has finished. + } - // PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no need - // to reset attributes that are missing. - DecorationModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => { - if (err) { - err.status = codes.wrongrequest; - } else if (!item) { - err = new Error('item not found'); - err.status = codes.notfound; - } else { - res.locals.items = item; - } - next(err); - }); - }) + // optional task 3: increment version manually as we do not use .save(.) + req.body.updatedAt = new Date(); + req.body.$inc = {__v: 1}; - .delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => { - const id = req.params.id; - DecorationModel.findByIdAndRemove(id, (err, item) => { - if (err) { - err.status = codes.wrongrequest; - } else if (!item) { - err = new Error('item not found'); - err.status = codes.notfound; - } + DecorationModel.findById(req.body._id, (err, item) => { + if (err) { + return next(err); + } + if (req.file) { + const imageFileBuffer = req.file.buffer; + const imageDimensionError = imageDimensionValidator(imageFileBuffer, + (item.isMedal ? MAX_MEDAL_IMAGE_WIDTH : MAX_RIBBON_IMAGE_WIDTH), + (item.isMedal ? MAX_MEDAL_IMAGE_HEIGHT : MAX_RIBBON_IMAGE_HEIGHT)); + if (imageDimensionError) { + return next(imageDimensionError); + } - // deleted all awardings linked to this decoration - AwardingsModel.find({decorationId: id}).remove().exec(); + const file = resourceLocation + req.params.id + '.png'; + fs.unlink(file, (err) => { + if (err) next(err); + fs.appendFile(file, new Buffer(req.file.buffer), (err) => { + if (err) next(err); + }); + }); + } - // delete graphic - fs.unlink(resourceLocation.concat(id).concat('.png'), - (err) => { - // we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last handler - res.locals.processed = true; - next(err); - }); - }); - }) + // PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no + // need to reset attributes that are missing. + DecorationModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => { + if (err) { + err.status = codes.wrongrequest; + } else if (!item) { + err = new Error('item not found'); + err.status = codes.notfound; + } else { + res.locals.items = item; + } + next(err); + }); + }); + }) - .all( - routerHandling.httpMethodNotAllowed - ); + .delete(apiAuthenticationMiddleware, checkHl, (req, res, next) => { + const id = req.params.id; + DecorationModel.findByIdAndRemove(id, (err, item) => { + if (err) { + err.status = codes.wrongrequest; + } else if (!item) { + err = new Error('item not found'); + err.status = codes.notfound; + } + + // deleted all awardings linked to this decoration + AwardingsModel.find({decorationId: id}).remove().exec(); + + // delete graphic + fs.unlink(resourceLocation.concat(id).concat('.png'), + (err) => { + // we don't set res.locals.items and thus it will send a 204 (no content) at the end. see last + // handler + res.locals.processed = true; + next(err); + }); + }); + }) + + .all( + routerHandling.httpMethodNotAllowed + ); // this middleware function can be used, if you like or remove it // it looks for object(s) in res.locals.items and if they exist, they are send to the client as json -decoration.use(routerHandling.emptyResponse); +decorationRouter.use(routerHandling.emptyResponse); -module.exports = decoration; +module.exports = decorationRouter; diff --git a/server/routes/ranks.js b/server/routes/ranks.js index b1461c3..bf2652b 100644 --- a/server/routes/ranks.js +++ b/server/routes/ranks.js @@ -56,27 +56,32 @@ ranks.route('/') }) .post(apiAuthenticationMiddleware, checkHl, upload.single('image'), (req, res, next) => { - const rank = new RankModel(req.body); - const imageFileBuffer = req.file.buffer; - - const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); - if(err) { - return next(err); - } - - // timestamp and default are set automatically by Mongoose Schema Validation - rank.save((err) => { + if (req.file) { + const imageFileBuffer = req.file.buffer; + const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); if (err) { - err.status = codes.wrongrequest; - next(err); + return next(err); } - res.status(codes.created); - res.locals.items = rank; - fs.appendFile(resourceLocation + rank._id + '.png', new Buffer(imageFileBuffer), - (err) => { + + const rank = new RankModel(req.body); + // timestamp and default are set automatically by Mongoose Schema Validation + rank.save((err) => { + if (err) { + err.status = codes.wrongrequest; next(err); - }); - }); + } + res.status(codes.created); + res.locals.items = rank; + fs.appendFile(resourceLocation + rank._id + '.png', new Buffer(imageFileBuffer), + (err) => { + next(err); + }); + }); + } else { + const err = new Error('no image file provided'); + err.status = codes.wrongmediasend; + next(err); + } }) .all( @@ -106,7 +111,7 @@ ranks.route('/:id') if (req.file) { const imageFileBuffer = req.file.buffer; const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); - if(err) { + if (err) { return next(err); } diff --git a/server/routes/squads.js b/server/routes/squads.js index 8764f4f..bb824ca 100644 --- a/server/routes/squads.js +++ b/server/routes/squads.js @@ -22,6 +22,9 @@ const SquadModel = require('../models/squad'); // util const genericGetById = require('./_generic').genericGetById; +const imageDimensionValidator = require('../middleware/validators').imageDimensionValidator; +const MAX_IMAGE_WIDTH = 150; +const MAX_IMAGE_HEIGHT = 150; const squads = new express.Router(); @@ -54,6 +57,12 @@ squads.route('/') const squad = new SquadModel(req.body); // timestamp and default are set automatically by Mongoose Schema Validation if (req.file) { + const imageFileBuffer = req.file.buffer; + const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); + if (err) { + return next(err); + } + squad.save((err) => { if (err) { err.status = codes.wrongrequest; @@ -62,10 +71,9 @@ squads.route('/') } res.status(codes.created); res.locals.items = squad; - fs.appendFile(resourceLocation.concat(squad._id).concat('.png'), - new Buffer(req.file.buffer), (err) => { - next(err); - }); + fs.appendFile(resourceLocation.concat(squad._id).concat('.png'), new Buffer(imageFileBuffer), (err) => { + next(err); + }); }); } else { const err = new Error('no image file provided'); @@ -98,11 +106,16 @@ squads.route('/:id') req.body.$inc = {__v: 1}; if (req.file) { + const imageFileBuffer = req.file.buffer; + const err = imageDimensionValidator(imageFileBuffer, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); + if (err) { + return next(err); + } const file = resourceLocation.concat(req.params.id) .concat('.png'); fs.unlink(file, (err) => { if (err) next(err); - fs.appendFile(file, new Buffer(req.file.buffer), (err) => { + fs.appendFile(file, new Buffer(imageFileBuffer), (err) => { if (err) next(err); }); }); diff --git a/static/src/app/manage/decorations/edit-decoration/edit-decoration.component.ts b/static/src/app/manage/decorations/edit-decoration/edit-decoration.component.ts index ff86c88..e248905 100644 --- a/static/src/app/manage/decorations/edit-decoration/edit-decoration.component.ts +++ b/static/src/app/manage/decorations/edit-decoration/edit-decoration.component.ts @@ -66,7 +66,7 @@ export class EditDecorationComponent implements OnInit, OnDestroy { if (this.fileList) { file = this.fileList[0]; this.decorationService.submitDecoration(this.decoration, file) - .subscribe(rank => { + .subscribe(decoration => { this.router.navigate(['..'], {relativeTo: this.route}); }); } else { @@ -83,12 +83,15 @@ export class EditDecorationComponent implements OnInit, OnDestroy { } delete this.decoration['__v']; this.decorationService.submitDecoration(this.decoration, file) - .subscribe(rank => { + .subscribe(decoration => { setTimeout(() => { this.imagePreviewSrc = 'resource/decoration/' + this.decoration._id + '.png?' + Date.now(); }, 300); fileInput.value = ''; this.snackBarService.showSuccess('generic.save.success'); + }, error => { + const errorMsg = error._body ? JSON.parse(error._body).error.message : error.error.error.message; + this.snackBarService.showError('Error: '.concat(errorMsg), 15000); }); } } diff --git a/static/src/app/manage/squads/edit-squad/edit-squad.component.ts b/static/src/app/manage/squads/edit-squad/edit-squad.component.ts index 0d36c29..4eeb022 100644 --- a/static/src/app/manage/squads/edit-squad/edit-squad.component.ts +++ b/static/src/app/manage/squads/edit-squad/edit-squad.component.ts @@ -69,7 +69,7 @@ export class EditSquadComponent implements OnInit, OnDestroy { if (this.fileList) { file = this.fileList[0]; this.squadService.submitSquad(this.squad, file) - .subscribe(rank => { + .subscribe(squad => { this.saved = true; this.router.navigate(['..'], {relativeTo: this.route}); }); @@ -87,12 +87,15 @@ export class EditSquadComponent implements OnInit, OnDestroy { } delete this.squad['__v']; this.squadService.submitSquad(this.squad, file) - .subscribe(rank => { + .subscribe(squad => { setTimeout(() => { this.imagePreviewSrc = 'resource/squad/' + this.squad._id + '.png?' + Date.now(); }, 300); fileInput.value = ''; this.snackBarService.showSuccess('generic.save.success'); + }, error => { + const errorMsg = error._body ? JSON.parse(error._body).error.message : error.error.error.message; + this.snackBarService.showError('Error: '.concat(errorMsg), 15000); }); } }