diff --git a/.gitignore b/.gitignore index 7b57d2e..c618081 100755 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ configs/**/credential.* # Generated files /build /.deploy -/src/public/uploads +/src/public/tmp /src/public/users /src/native/styles/index.js my-release-key.keystore diff --git a/configs/project/client.js b/configs/project/client.js index 4efd779..2200840 100755 --- a/configs/project/client.js +++ b/configs/project/client.js @@ -9,4 +9,15 @@ module.exports = { }, }, recaptcha: require('./recaptcha/client'), + fileUpload: { + avatar: { + maxSize: 1024 * 1024, // in bytes + // MIME type + validMIMETypes: [ + 'image/jpeg', + 'image/png', + 'image/gif', + ], + }, + }, }; diff --git a/src/common/components/forms/user/AvatarForm.js b/src/common/components/forms/user/AvatarForm.js index b642e2b..865460f 100644 --- a/src/common/components/forms/user/AvatarForm.js +++ b/src/common/components/forms/user/AvatarForm.js @@ -16,10 +16,33 @@ const initialValues = { storage: 'local', }; -const validate = (values) => { +/** + * Test server side validation with Postman: + * 1. Setup the method and url `POST http://localhost:3000/api/users/me/avatar` + * 2. Select `Body` tab + * 3. Select `form-data` type + * 4. Add new key `avatar` and select some invalid file on purpose + * 5. Send + */ +export let validate = (values) => { const errors = {}; + if (!values.avatar || values.avatar.length !== 1) { errors.avatar = 'Required'; + } else { + let { size, type, mimetype } = values.avatar[0]; + let { maxSize, validMIMETypes } = configs.fileUpload.avatar; + + if (size > maxSize) { + errors.avatar = ( + `Your file(${Math.floor(size / 1024)} Kb) ` + + `exceeds the limit size(${Math.floor(maxSize / 1024)} Kb).` + ); + } + // we check the key `type` for client side and `mimetype` for server side + if (validMIMETypes.indexOf(type || mimetype) < 0) { + errors.avatar = 'Invalid type. Please upload .jpg, .png or .gif file.'; + } } return errors; diff --git a/src/server/controllers/user.js b/src/server/controllers/user.js index 11b91d5..f1bba60 100644 --- a/src/server/controllers/user.js +++ b/src/server/controllers/user.js @@ -1,7 +1,10 @@ +import fs from 'fs'; +import path from 'path'; +import mkdirp from 'mkdirp'; import assign from 'object-assign'; import configs from '../../../configs/project/server'; import Errors from '../../common/constants/Errors'; -import { handleDbError } from '../decorators/handleError'; +import handleError, { handleDbError } from '../decorators/handleError'; import User from '../models/User'; import filterAttribute from '../utils/filterAttribute'; import { loginUser } from '../../common/actions/userActions'; @@ -213,8 +216,19 @@ export default { uploadAvatar(req, res) { // use `req.file` to access the avatar file // and use `req.body` to access other fileds - res.json({ - downloadURL: `/users/${req.user._id}/${req.file.filename}`, - }); + let { filename } = req.files.avatar[0]; + let tmpPath = req.files.avatar[0].path; + let targetDir = path.join( + __dirname, '../../public', 'users', req.user._id.toString() + ); + let targetPath = path.join(targetDir, filename); + + mkdirp(targetDir, handleError(res)(() => { + fs.rename(tmpPath, targetPath, handleError(res)(() => { + res.json({ + downloadURL: `/users/${req.user._id}/${filename}`, + }); + })); + })); }, }; diff --git a/src/server/middlewares/validate.js b/src/server/middlewares/validate.js index 5da47d3..34d5384 100644 --- a/src/server/middlewares/validate.js +++ b/src/server/middlewares/validate.js @@ -8,7 +8,10 @@ import User from '../models/User'; export default { form: (formPath, onlyFields = []) => (req, res, next) => { let { validate } = require(`../../common/components/forms/${formPath}`); - let errors = validate(req.body); + let errors = validate({ + ...req.body, + ...req.files, + }); if (onlyFields.length > 0) { let newErrors = {}; diff --git a/src/server/routes/api.js b/src/server/routes/api.js index 4e54ff5..2ff4a45 100644 --- a/src/server/routes/api.js +++ b/src/server/routes/api.js @@ -78,9 +78,10 @@ export default ({ app }) => { app.post('/api/users/me/avatar', authRequired, fileUpload.disk({ - destination: 'users/{userId}', + destination: 'tmp/{userId}', filename: 'avatar.jpg', - }).single('avatar'), + }).fields([{ name: 'avatar' }]), + validate.form('user/AvatarForm'), userController.uploadAvatar); // form