diff --git a/package-lock.json b/package-lock.json index 5e7ecc5..d2e6f0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -164,6 +164,29 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "aws-sdk": { + "version": "2.186.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.186.0.tgz", + "integrity": "sha1-ZOzOpb8ESYEDI8MT2ctBwqSihQ0=", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.17", + "xmlbuilder": "4.2.1" + }, + "dependencies": { + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + } + } + }, "aws-sign2": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", @@ -179,11 +202,24 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz", + "integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==" + }, "base64url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" }, + "basic-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", @@ -327,6 +363,16 @@ "repeat-element": "1.1.2" } }, + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "1.2.1", + "ieee754": "1.1.8", + "isarray": "1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -802,6 +848,11 @@ } } }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -2099,6 +2150,11 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -2302,8 +2358,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -2325,6 +2380,11 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -2433,6 +2493,11 @@ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", "optional": true }, + "lodash": { + "version": "4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, "lodash._baseassign": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", @@ -2666,6 +2731,28 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" }, + "morgan": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", + "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", + "requires": { + "basic-auth": "2.0.0", + "debug": "2.6.9", + "depd": "1.1.1", + "on-finished": "2.3.0", + "on-headers": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2992,6 +3079,11 @@ "strict-uri-encode": "1.1.0" } }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", @@ -3173,7 +3265,7 @@ "stringstream": "0.0.5", "tough-cookie": "2.3.2", "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "uuid": "3.2.1" }, "dependencies": { "qs": { @@ -3215,6 +3307,11 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", @@ -3740,6 +3837,22 @@ "xdg-basedir": "3.0.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -3761,9 +3874,9 @@ "integrity": "sha1-ApT7kiu5N1FTVBxPcJYjHyh8ivg=" }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "vary": { "version": "1.1.1", @@ -3838,6 +3951,23 @@ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", "dev": true }, + "xml2js": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.17.tgz", + "integrity": "sha1-F76T6q4/O3eTWceVtBlwWogX6Gg=", + "requires": { + "sax": "1.2.1", + "xmlbuilder": "4.2.1" + } + }, + "xmlbuilder": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz", + "integrity": "sha1-qlijBBoGb5DqoWwvU4n/GfP0YaU=", + "requires": { + "lodash": "4.17.4" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/package.json b/package.json index c49daa1..9e62294 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "homepage": "https://github.com/foundersandcoders/OTP-Data-Entry#readme", "dependencies": { + "aws-sdk": "^2.186.0", "body-parser": "^1.18.0", "compression": "^1.7.1", "cookie-parser": "^1.4.3", @@ -31,8 +32,10 @@ "express": "^4.15.3", "express-handlebars": "^3.0.0", "jsonwebtoken": "^8.1.0", + "morgan": "^1.9.0", "query-string": "^5.0.0", - "request": "^2.81.0" + "request": "^2.81.0", + "uuid": "^3.2.1" }, "devDependencies": { "nodemon": "^1.12.0", diff --git a/public/image-upload.js b/public/image-upload.js new file mode 100644 index 0000000..7bb99ad --- /dev/null +++ b/public/image-upload.js @@ -0,0 +1,42 @@ +(function() { + var fileInput = + document.getElementById('eventFileInput') || + document.getElementById('placeImageInput'); + var hiddenFileInput = document.getElementById('hiddenFileInput'); + var fileErrorMessage = document.getElementById('fileErrorMessage'); + var spinner = document.getElementById('spinner'); + var imagePreview = document.getElementById('imagePreview'); + + fileInput.onchange = function() { + var fileInputFiles = fileInput.files; + var file = fileInputFiles[0]; + imagePreview.src = imagePreview.src && ''; + spinner.classList.toggle('dn'); + imagePreview.classList.add('dn'); + getSignedRequest(file) + .then(function(res) { + imagePreview.src = res.data.url; + hiddenFileInput.value = res.data.url; + return uploadFile(res.data.signedRequest, file); + }) + .then(function() { + spinner.classList.toggle('dn'); + imagePreview.classList.remove('dn'); + }) + .catch(function(err) { + imagePreview.src = ''; + hiddenFileInput.value = ''; + fileErrorMessage.textContent = 'Could not upload file'; + }); + + function getSignedRequest(file) { + return axios.get( + '/sign-s3?file-name=' + file.name + '&file-type=' + file.type, + ); + } + + function uploadFile(signedRequest, file) { + return axios.put(signedRequest, file); + } + }; +})(); diff --git a/public/style.css b/public/style.css index 3b3119f..63c0d58 100644 --- a/public/style.css +++ b/public/style.css @@ -1,27 +1,56 @@ .bg-turquoise { - background-color: #409A93; + background-color: #409a93; } .bg-main { - background-color: #F2F2F2; + background-color: #f2f2f2; } - .light-text { - color: #95989A; - } +.light-text { + color: #95989a; +} + +.lato { + font-family: 'lato', sans-serif; +} - .lato { - font-family: 'lato', sans-serif; - } +.dark-text { + color: #5c5c5c; +} - .dark-text { - color: #5C5C5C; - } +.b--turquoise { + border-color: #409a93; +} - .b--turquoise { - border-color: #409A93; - } +.text-turquoise { + color: #409a93; +} - .text-turquoise { - color: #409A93; - } +#spinner { + border: 0.2rem solid #f3f3f3; + border-radius: 75%; + border-top: 0.2rem solid #3498db; + width: 2rem; + height: 2rem; + -webkit-animation: spin 2s linear infinite; /* Safari */ + animation: spin 2s linear infinite; +} + +/* Safari */ +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + } +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/public/validate-event-form.js b/public/validate-event-form.js index 061525a..8fd4547 100644 --- a/public/validate-event-form.js +++ b/public/validate-event-form.js @@ -21,10 +21,20 @@ description_en: elements.description_en.value, description_ar: elements.description_ar.value, eventPlace: elements.eventPlace.value, - imageUrl: elements.imageUrl.value, cost: elements.cost.value, }; + // checks if a file is being uploaded + if ( + document.getElementById('spinner').classList.value.indexOf('dn') === -1 + ) { + return notValid('*Image did not finish upload'); + } + + data.imageUrl = elements.s3Url + ? elements.s3Url.value + : elements.imageUrl.value; + // set time in the data object data.startTime = new Date( new Date( diff --git a/public/validate-place-form.js b/public/validate-place-form.js index e1f0e63..f879ad1 100644 --- a/public/validate-place-form.js +++ b/public/validate-place-form.js @@ -26,9 +26,19 @@ website: elements.website.value, phone: elements.phone.value, email: elements.email.value, - imageUrl: elements.imageUrl.value, }; + // checks if a file is being uploaded + if ( + document.getElementById('spinner').classList.value.indexOf('dn') === -1 + ) { + return notValid('*Image did not finish upload'); + } + + data.imageUrl = elements.s3Url + ? elements.s3Url.value + : elements.imageUrl.value; + // Check if place name input were filled if (!data.name_en && !data.name_ar) { return notValid('*Please input a name'); diff --git a/src/app.js b/src/app.js index c2cfcc1..b6c37c8 100644 --- a/src/app.js +++ b/src/app.js @@ -11,6 +11,7 @@ const checkedDropDown = require('./helpers/check_dropdown_option.js'); const iterate = require('./helpers/iterate.js'); const cookieParser = require('cookie-parser'); const compression = require('compression'); +const morgan = require('morgan'); const app = express(); @@ -20,6 +21,11 @@ app.use(cookieParser()); app.use(express.static('public')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); +app.use( + morgan('dev', { + skip: (req, res) => res.statusCode < 400, + }), +); // Set up local languages app.locals.en = languages.en; diff --git a/src/controllers/router.js b/src/controllers/router.js index e229be1..707c480 100644 --- a/src/controllers/router.js +++ b/src/controllers/router.js @@ -7,6 +7,7 @@ const oauthToken = require('./OAuth/token.js'); const checkLoggedIn = require('../middleware/checkLoggedIn.js'); router.get('/', require('./home.js')); +router.get('/sign-s3', require('./sign_s3.js')); router.get('/:lang/content', require('../content.js')); // places @@ -14,17 +15,34 @@ router.get('/:lang/places', placeController.getAll); router.get('/:lang/place/:id', placeController.getSpecific); router.get('/:lang/add-place', placeController.renderForm); router.post('/:lang/add-place', placeController.addPlace); -router.get('/:lang/edit-place/:id', checkLoggedIn, placeController.renderEditForm); +router.get( + '/:lang/edit-place/:id', + checkLoggedIn, + placeController.renderEditForm, +); router.post('/:lang/edit-place/:id', checkLoggedIn, placeController.addPlace); -router.get('/:lang/delete-place/:id', checkLoggedIn, placeController.deletePlace); +router.get( + '/:lang/delete-place/:id', + checkLoggedIn, + placeController.deletePlace, +); // events router.get('/:lang/events', eventsController.getAll); router.get('/:lang/event/:id', eventsController.getSpecific); -router.get('/:lang/delete-event/:id', checkLoggedIn, eventsController.deleteEvent); +router.get( + '/:lang/delete-event/:id', + checkLoggedIn, + eventsController.deleteEvent, +); router.get('/:lang/add-event', placesList, eventsController.renderForm); router.post('/:lang/add-event', eventsController.addEvent); -router.get('/:lang/edit-event/:id', checkLoggedIn, placesList, eventsController.renderEditForm); +router.get( + '/:lang/edit-event/:id', + checkLoggedIn, + placesList, + eventsController.renderEditForm, +); router.post('/:lang/edit-event/:id', checkLoggedIn, eventsController.addEvent); router.get('/:lang/login', oauthCode); diff --git a/src/controllers/sign_s3.js b/src/controllers/sign_s3.js new file mode 100644 index 0000000..ae92c80 --- /dev/null +++ b/src/controllers/sign_s3.js @@ -0,0 +1,33 @@ +const aws = require('aws-sdk'); +require('env2')('./config.env'); +const uuidv4 = require('uuid/v4'); +const S3_BUCKET = process.env.S3_BUCKET; + +// set the region of the S3 bucket +aws.config.region = 'eu-west-2'; + +module.exports = (req, res) => { + const s3 = new aws.S3(); + const fileName = req.query['file-name']; + const fileType = req.query['file-type']; + const s3Params = { + Bucket: S3_BUCKET, + Key: `${fileName}:${uuidv4()}`, + Expires: 60, + ContentType: fileType, + ACL: 'public-read', + }; + + s3.getSignedUrl('putObject', s3Params, (err, data) => { + if (err) { + res.status(500).end(); + } + + res.status(200).end( + JSON.stringify({ + signedRequest: data, + url: `https://${S3_BUCKET}.s3.amazonaws.com/${fileName}`, + }), + ); + }); +}; diff --git a/src/middleware/langError.js b/src/middleware/langError.js index d726a16..7a26192 100644 --- a/src/middleware/langError.js +++ b/src/middleware/langError.js @@ -3,12 +3,12 @@ module.exports = (req, res, next) => { const lang = path.split('/')[1]; if (lang !== 'en' && lang !== 'ar') { - if ((path === '/') || (path === '/oauth/token')) { + if (path === '/' || path === '/oauth/token' || path === '/sign-s3') { return next(); } else { return res.render('error', { statusCode: 404, - errorMessage: 'Page not Found' + errorMessage: 'Page not Found', }); } } diff --git a/src/text.js b/src/text.js index 3fb6460..0b15287 100644 --- a/src/text.js +++ b/src/text.js @@ -19,7 +19,8 @@ module.exports = { noLocation: 'No Location available', accessibilityOptions: 'Accessibility Options', categories: 'Categories', - formInfo: '*indicates a required field in at least one of English or Arabic', + formInfo: + '*indicates a required field in at least one of English or Arabic', required: 'Required field', selectPlace: 'Select place', // places @@ -42,7 +43,8 @@ module.exports = { eventStart: 'Start time', eventEnd: 'End time', formOwnerId: 'Owner ID', - formImageUrl: 'Image URL', + formImage: 'Image', + formImageUrl: 'Image-URL', formSubmit: 'Submit', // Accessibility options audioRecordings: 'Audio recordings', @@ -77,7 +79,7 @@ module.exports = { miscellaneous: 'Miscellaneous', party: 'Party', sportEvent: 'Sport', - wedding: 'Wedding' + wedding: 'Wedding', }, ar: { pageTitle: 'إدخال البيانات', @@ -99,7 +101,8 @@ module.exports = { noLocation: 'لا يوجد مكان', accessibilityOptions: 'امكانيات سهولة الوصول', categories: 'فئات', - formInfo: '* مطلوب التعبئة على الاقل في واحدة من اللغتين العربية او الانجليزية', + formInfo: + '* مطلوب التعبئة على الاقل في واحدة من اللغتين العربية او الانجليزية', required: ' مطلوب التعبئة', selectPlace: 'اختر مكان الحدث', // places @@ -130,7 +133,8 @@ module.exports = { eventStart: 'وقت البدء', eventEnd: 'وقت النهاية', formOwnerId: 'هوية صاحب المكان', - formImageUrl: 'صورة', + formImage: 'صورة', + formImageUrl: 'رابط الصورة', formSubmit: 'إعرض', // Accessibility options audioRecordings: 'خدمات صوتية', @@ -165,7 +169,6 @@ module.exports = { miscellaneous: 'متنوع', party: 'حفل', sportEvent: 'رياضة', - wedding: 'عرس' - - } + wedding: 'عرس', + }, }; diff --git a/views/event-form.hbs b/views/event-form.hbs index 554a789..f918ee4 100644 --- a/views/event-form.hbs +++ b/views/event-form.hbs @@ -139,7 +139,7 @@