From 4888e4dc3deda77f3f6bf0b967ce3e38133a2550 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Thu, 2 Apr 2020 17:08:17 +0300 Subject: [PATCH 01/15] Gallery - `GALLERY_NEW_PHOTOS_FIRST` option added --- CHANGELOG.md | 3 + dashboard/.env | 3 +- dashboard/src/components/Gallery/Index.vue | 82 ++++++++++++---------- dashboard/src/plugins/toast/index.js | 2 +- dashboard/src/store/modules/gallery.js | 28 ++++++-- facade/.env.defaults | 1 + facade/server/config.js | 3 +- facade/server/routes/facade.js | 11 ++- 8 files changed, 83 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbae3ae..a25acb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## **2.0.2** - *2020-xx-xx* +* Gallery - `GALLERY_NEW_PHOTOS_FIRST` option added; + ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; * consider `FACADE_PORT`, `DASHBOARD_PORT`, `API_PORT` env vars; diff --git a/dashboard/.env b/dashboard/.env index 1bb9bb7..dc267da 100644 --- a/dashboard/.env +++ b/dashboard/.env @@ -2,4 +2,5 @@ PORT=8083 BASE_URL=/dashboard/ COMMON_STATIC_PATH=../public VUE_APP_API_BASE_URL=http://localhost:8080/api -VUE_APP_GALLERY_DASHBOARD_THUMBNAIL_SUFFIX=tm \ No newline at end of file +VUE_APP_GALLERY_DASHBOARD_THUMBNAIL_SUFFIX=tm +VUE_APP_GALLERY_NEW_PHOTOS_FIRST=true \ No newline at end of file diff --git a/dashboard/src/components/Gallery/Index.vue b/dashboard/src/components/Gallery/Index.vue index a60b64d..b630393 100644 --- a/dashboard/src/components/Gallery/Index.vue +++ b/dashboard/src/components/Gallery/Index.vue @@ -38,43 +38,45 @@ :imageMimeTypes="imageMimeTypes" class="saved-photo" /> - - - - mdi-plus - + + + + mdi-plus + + + + @@ -109,14 +111,15 @@ export default { "photoSets", "activePhotoSet", "allPhotos", - "unclassifiedPhotos" + "unclassifiedPhotos", + "newPhotosFirst" ]), photos: { get() { return this.$store.getters["gallery/photos"]; }, async set(value) { - let ids = value.map(x => x._id); + let ids = value.filter(x => x).map(x => x._id); await this.$store.dispatch("gallery/reorderPhotos", { photoSet: this.activePhotoSet, photos: ids @@ -164,7 +167,8 @@ export default { if (xmpJson) { result.alt = xmpJson.title || ""; result.title = xmpJson.title || ""; - result.description = xmpJson.description || ""; + result.description = + xmpJson.description || ""; } } catch (err) { console.error(err); diff --git a/dashboard/src/plugins/toast/index.js b/dashboard/src/plugins/toast/index.js index 2770963..f69f742 100644 --- a/dashboard/src/plugins/toast/index.js +++ b/dashboard/src/plugins/toast/index.js @@ -11,7 +11,7 @@ class ToastPlugin { this.options = { container: ".v-application", // default container to place toasts in property: "$toast", // Vue instance property name - queue: true // if queue is false then next toast will immediately close previous + queue: false // if queue is false then next toast will immediately close previous }; } diff --git a/dashboard/src/store/modules/gallery.js b/dashboard/src/store/modules/gallery.js index 102c96c..a492a0b 100644 --- a/dashboard/src/store/modules/gallery.js +++ b/dashboard/src/store/modules/gallery.js @@ -4,7 +4,8 @@ import _ from "lodash"; const state = { photoSets: [], photos: [], - activePhotoSet: "all" + activePhotoSet: "all", + newPhotosFirst: process.env.VUE_APP_GALLERY_NEW_PHOTOS_FIRST === "true" }; const getters = { @@ -36,13 +37,17 @@ const getters = { }, activePhotoSet: state => state.activePhotoSet, photos: (state, getters) => { + let result; switch (state.activePhotoSet) { case "all": - return getters.allPhotos; + result = getters.allPhotos; + break; case "unclassified": - return getters.unclassifiedPhotos; + result = getters.unclassifiedPhotos; + break; case "trash": - return getters.deletedPhotos; + result = getters.deletedPhotos; + break; default: { let photoSet = state.photoSets.find( photoSet => photoSet.code === getters.activePhotoSet @@ -50,7 +55,7 @@ const getters = { if (!photoSet) { return []; } - return photoSet.photos + result = photoSet.photos .map(id => { let photo = state.photos.find(x => x._id === id); if (photo && !photo.deleted) { @@ -58,8 +63,13 @@ const getters = { } }) .filter(x => !!x); + break; } } + if (state.newPhotosFirst) { + result.reverse(); + } + return result; }, allPhotos: state => state.photos.filter(photo => !photo.deleted), unclassifiedPhotos: state => @@ -68,7 +78,8 @@ const getters = { !photo.deleted && (!Array.isArray(photo.sets) || !photo.sets.length) ), - deletedPhotos: state => state.photos.filter(photo => photo.deleted) + deletedPhotos: state => state.photos.filter(photo => photo.deleted), + newPhotosFirst: state => state.newPhotosFirst }; const mutations = { @@ -258,7 +269,10 @@ const actions = { commit("setActivePhotoSet", "all"); } }, - async reorderPhotos({ commit }, payload) { + async reorderPhotos({ commit, getters }, payload) { + if (getters.newPhotosFirst) { + payload.photos.reverse(); + } await Vue.axios.post("/gallery/photos/reorder", payload); commit("reorderPhotos", payload); } diff --git a/facade/.env.defaults b/facade/.env.defaults index deb2c89..3f1ad72 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -55,3 +55,4 @@ GALLERY_IMAGE_PROCESSING_MODULE=sharp GALLERY_JPG_QUALITY=90 # comma-separated image sizes: ":x" GALLERY_IMAGE_SIZES=ts:192x192,tm:256x256,tl:512x512,s:900x600,m:1200x800,l:1860x1020 +GALLERY_NEW_PHOTOS_FIRST=true diff --git a/facade/server/config.js b/facade/server/config.js index b24c763..1077d90 100644 --- a/facade/server/config.js +++ b/facade/server/config.js @@ -78,6 +78,7 @@ module.exports = { jpgQuality: parseInt(process.env.GALLERY_JPG_QUALITY), // comma-separated image sizes: ":x" imageSizes: process.env.GALLERY_IMAGE_SIZES, - dashboardThumbnailSuffix: process.env.GALLERY_DASHBOARD_THUMBNAIL_SUFFIX + dashboardThumbnailSuffix: process.env.GALLERY_DASHBOARD_THUMBNAIL_SUFFIX, + newPhotosFirst: process.env.GALLERY_NEW_PHOTOS_FIRST === "true" } }; \ No newline at end of file diff --git a/facade/server/routes/facade.js b/facade/server/routes/facade.js index 4d58885..c8836f8 100644 --- a/facade/server/routes/facade.js +++ b/facade/server/routes/facade.js @@ -23,6 +23,7 @@ router.get("/gallery", async function (req, res) { } // if there are still photos to display (unclassified) - then redirect to /gallery/all + // TODO: implement api.photos.count() method let allPhotos = await api.photos.all(); if (allPhotos.length > 0) { return res.redirect("/gallery/all"); @@ -44,10 +45,14 @@ router.get("/gallery/:photoSet", async function (req, res) { }; } - if (!photoSet|| photoSet.photos.length === 0) { + if (!photoSet|| !photoSet.photos || photoSet.photos.length === 0) { return res.error(404); } + if (config.gallery.newPhotosFirst) { + photoSet.photos.reverse(); + } + res.locals.model = { photoSet }; @@ -69,6 +74,10 @@ router.get("/gallery/:photoSet/:photoId(\\d+)", async function (req, res) { return res.error(404); } + if (config.gallery.newPhotosFirst && photoSet.photos) { + photoSet.photos.reverse(); + } + let nextPhotoId, prevPhotoId; if (photoSet.code !== "all") { From 96ea2fef2cc74ee223f8d2c4281e67b0cac6f17f Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Thu, 2 Apr 2020 21:22:32 +0300 Subject: [PATCH 02/15] API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`) --- CHANGELOG.md | 1 + api/.env.defaults | 3 +++ api/config.js | 2 ++ api/facade.http | 7 ++++++- api/routes/facade.js | 23 +++++++++++++++++++---- api/store/store.js | 6 ++++++ facade/server/index.js | 1 + facade/server/lib/api.js | 3 ++- facade/server/lib/auth.js | 4 ++++ facade/server/routes/facade.js | 23 ++++++++++++++++++++--- facade/server/views/_header.html | 2 ++ 11 files changed, 66 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a25acb4..11620f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## **2.0.2** - *2020-xx-xx* * Gallery - `GALLERY_NEW_PHOTOS_FIRST` option added; +* API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; diff --git a/api/.env.defaults b/api/.env.defaults index ad0180f..e7abb97 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -11,6 +11,9 @@ LOG_CONSOLE_PREFIX=api LOG_CONSOLE_FG_COLOR=azure LOG_CONSOLE_BG_COLOR=turquoise +// `open` or `invite` +REGISTRATION_MODE=open + # password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) PASSWORD_HASH_ALGORITHM=md5 diff --git a/api/config.js b/api/config.js index 917795e..a3bcb96 100644 --- a/api/config.js +++ b/api/config.js @@ -20,6 +20,8 @@ const config = { facadeToken: process.env.FACADE_TOKEN, + registrationMode: process.env.REGISTRATION_MODE, + //password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) passwordHashAlgorithm: process.env.PASSWORD_HASH_ALGORITHM, diff --git a/api/facade.http b/api/facade.http index d733834..a59d8b5 100644 --- a/api/facade.http +++ b/api/facade.http @@ -2,8 +2,13 @@ @auth = Authorization: Bearer {{$dotenv FACADE_TOKEN}} @ajax = X-Requested-With: XMLHttpRequest +### Can register +GET {{baseUrl}}/users/can-register +{{auth}} +{{ajax}} + ### Get user -GET {{baseUrl}}/users/mail@ysemeniuk.com +GET {{baseUrl}}/user/mail@ysemeniuk.com {{auth}} {{ajax}} diff --git a/api/routes/facade.js b/api/routes/facade.js index d58f761..de0e4c7 100644 --- a/api/routes/facade.js +++ b/api/routes/facade.js @@ -1,7 +1,15 @@ const express = require("express"), router = express.Router(), + config = require("@config"), store = require("@store"); +router.get("/users/can-register", async function (req, res) { + if (config.registrationMode === "open" || await store.users.count() === 0) { + return res.send(true).end(); + } + return res.send(false).end(); +}); + router.get("/user/:usernameOrEmail", async function (req, res) { let usernameOrEmail = req.params.usernameOrEmail, user; @@ -19,10 +27,17 @@ router.get("/user/:usernameOrEmail", async function (req, res) { return res.sendStatus(404); }); -router.post("/user", async function (req, res) { - let user = req.body; - await store.users.insert(user); - return res.end(); +router.post("/user", async function (req, res) { + let usersCount = await store.users.count(); + if (config.registrationMode === "open" || usersCount === 0) { + let user = req.body; + if (usersCount === 0) { + user.role = "owner"; + } + await store.users.insert(user); + return res.end(); + } + return res.sendStatus(404); }); diff --git a/api/store/store.js b/api/store/store.js index ca24e4e..ec042b8 100644 --- a/api/store/store.js +++ b/api/store/store.js @@ -23,6 +23,12 @@ class Store { } return result.toArray(); } + async count(query, options) { + if (query === undefined && options === undefined) { + return this.collection.estimatedDocumentCount(query, options); + } + return this.collection.countDocuments(query, options); + } async all(sort) { return this.find({}, undefined, sort); } diff --git a/facade/server/index.js b/facade/server/index.js index 6a6a81a..d425515 100644 --- a/facade/server/index.js +++ b/facade/server/index.js @@ -91,6 +91,7 @@ express.use(function (req, res, next) { lang: require("@server/langs").en, user: req.user, isAuthenticated: req.isAuthenticated(), + registrationEnabled: req.app.get("registration-enabled"), production: config.prod, development: config.dev, cacheHash: "production" === config.env ? config.commit : devCacheHash diff --git a/facade/server/lib/api.js b/facade/server/lib/api.js index f45ad58..1dcbe3f 100644 --- a/facade/server/lib/api.js +++ b/facade/server/lib/api.js @@ -69,7 +69,8 @@ const call = new Proxy(axios, { module.exports.users = { get: async usernameOrEmail => call.get("user/" + usernameOrEmail), - add: async user => call.post("user", user) + add: async user => call.post("user", user), + canRegister: async () => call.get("users/can-register") }; module.exports.photos = { diff --git a/facade/server/lib/auth.js b/facade/server/lib/auth.js index 7d72aec..a89d021 100644 --- a/facade/server/lib/auth.js +++ b/facade/server/lib/auth.js @@ -229,6 +229,10 @@ var security = require("./security"), module.exports = function (express) { logger.info("Init Authentication"); + api.users.canRegister().then(canRegister => { + express.set("registration-enabled", canRegister); + }); + // used to serialize the user for the session passport.serializeUser(function (user, done) { logger.debug("serializeUser " + user.email); diff --git a/facade/server/routes/facade.js b/facade/server/routes/facade.js index c8836f8..72c2547 100644 --- a/facade/server/routes/facade.js +++ b/facade/server/routes/facade.js @@ -13,6 +13,12 @@ function signin(req, res) { }); } +function refreshRegistrationEnabled(app) { + api.users.canRegister().then(canRegister => { + app.set("registration-enabled", canRegister); + }); +} + router.get("/", function (req, res) { return res.render("index"); }); @@ -115,15 +121,20 @@ router.get("/signin", function (req, res) { } }); -router.get("/register", function (req, res) { +router.get("/register", async function (req, res) { if (req.isAuthenticated()) { return res.redirect("/dashboard"); } + if (req.xhr) { return res.error(404); - } else { - return res.render("register"); } + + if (!req.app.get("registration-enabled")) { + return res.error(404); + } + + return res.render("register"); }); router.get("/check-email", async function (req, res) { @@ -143,6 +154,10 @@ router.post("/register", async function (req, res) { return res.redirect("/dashboard"); } + if (!req.app.get("registration-enabled")) { + return res.error(404); + } + let name = req.body.name, email = req.body.email, password = req.body.password, @@ -169,12 +184,14 @@ router.post("/register", async function (req, res) { case "md5": user.password = security.md5(password); await api.users.add(user); + refreshRegistrationEnabled(req.app); signin(req, res); break; case "bcrypt": security.bcryptHash(password, async function (err, passwordHash) { user.password = passwordHash; await api.users.add(user); + refreshRegistrationEnabled(req.app); signin(req, res); }); break; diff --git a/facade/server/views/_header.html b/facade/server/views/_header.html index 198c0f3..fb01dfb 100644 --- a/facade/server/views/_header.html +++ b/facade/server/views/_header.html @@ -49,10 +49,12 @@ + {{#if registrationEnabled}} {{/if}} + {{/if}} From cf8a7e372f1f12fa3779b57ebafc3c837933319b Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Fri, 3 Apr 2020 16:06:27 +0300 Subject: [PATCH 03/15] Gallery - resize photoset cover --- .gitignore | 1 + CHANGELOG.md | 4 ++- api/.env.defaults | 5 +++- api/config.js | 16 ++++++++++- api/routes/gallery.js | 65 ++++++++++++++++++++++++++++++++----------- facade/.env.defaults | 2 +- 6 files changed, 73 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index c075249..2ba7b32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode node_modules *.log /facade/public/service-worker.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 11620f0..d6f2f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog ## **2.0.2** - *2020-xx-xx* -* Gallery - `GALLERY_NEW_PHOTOS_FIRST` option added; +* Gallery: + * `GALLERY_NEW_PHOTOS_FIRST` option added; + * resize photoset cover; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/api/.env.defaults b/api/.env.defaults index e7abb97..3f8aa3d 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -59,5 +59,8 @@ GALLERY_TRASH_PATH=gallery/trash # image processing module (sharp or jimp, must be installed) GALLERY_IMAGE_PROCESSING_MODULE=sharp GALLERY_JPG_QUALITY=90 -# comma-separated image sizes: ":x" +# comma-separated image sizes to fit in. Format of each size: ":x" GALLERY_IMAGE_SIZES=ts:192x192,tm:256x256,tl:512x512,s:900x600,m:1200x800,l:1860x1020 +# comma-separated photo set cover sizes. Format of each size: ":x" +GALLERY_PHOTOSET_COVER_SIZES=:375x250,2x:750x500 + diff --git a/api/config.js b/api/config.js index a3bcb96..a50bf28 100644 --- a/api/config.js +++ b/api/config.js @@ -1,6 +1,18 @@ const path = require("path"), packageJson = require(process.cwd() + "/package.json"); +function parseImageSizes(value) { + return value.split(",").map(x => { + let suffix = x.split(":")[0], + size = x.split(":")[1]; + return { + width: Number(size.split("x")[0]), + height: Number(size.split("x")[1]), + suffix + }; + }); +} + const config = { // environment env: process.env.NODE_ENV || "development", @@ -81,7 +93,9 @@ const config = { imageProcessingModule: process.env.GALLERY_IMAGE_PROCESSING_MODULE, jpgQuality: parseInt(process.env.GALLERY_JPG_QUALITY), // comma-separated image sizes: ":x" - imageSizes: process.env.GALLERY_IMAGE_SIZES + imageSizes: parseImageSizes(process.env.GALLERY_IMAGE_SIZES), + // comma-separated photoset cover sizes: ":x" + photoSetCoverSizes: parseImageSizes(process.env.GALLERY_PHOTOSET_COVER_SIZES) } }; module.exports = config; \ No newline at end of file diff --git a/api/routes/gallery.js b/api/routes/gallery.js index 915fd01..e7971b4 100644 --- a/api/routes/gallery.js +++ b/api/routes/gallery.js @@ -17,16 +17,6 @@ const express = require("express"), trashPath = path.resolve(rootPath, config.gallery.trashPath), upload = multer({ dest: uploadPath }); -const IMAGE_SIZES = config.gallery.imageSizes.split(",").map(x => { - let suffix = x.split(":")[0], - size = x.split(":")[1]; - return { - width: Number(size.split("x")[0]), - height: Number(size.split("x")[1]), - suffix - }; -}); - // creates thumbnails and other image sizes async function resizeImage(sourcePath, outPath) { let extension = path.extname(outPath), @@ -38,25 +28,62 @@ async function resizeImage(sourcePath, outPath) { let file = sharp(sourcePath).jpeg({ quality: config.gallery.jpgQuality, }); - for (let size of IMAGE_SIZES) { + for (let size of config.gallery.imageSizes) { await file .clone() .resize(size.width, size.height, { fit: "inside", }) - .toFile(`${filename}_${size.suffix}${extension}`); + .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); } break; } case "jimp": { const jimp = require("jimp"); let file = await jimp.read(sourcePath); - for (let size of IMAGE_SIZES) { + for (let size of config.gallery.imageSizes) { await file .clone() .scaleToFit(size.width, size.height) .quality(config.gallery.jpgQuality) - .writeAsync(`${filename}_${size.suffix}${extension}`); + .writeAsync(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); + } + break; + } + } +} + +// resizes photo set cover +// eslint-disable-next-line no-unused-vars +async function resizePhotoSetCover(sourcePath, outPath) { + let extension = path.extname(outPath), + filename = outPath.slice(0, -extension.length); + + switch (config.gallery.imageProcessingModule) { + case "sharp": { + const sharp = require("sharp"); + let file = sharp(sourcePath).jpeg({ + quality: config.gallery.jpgQuality, + }); + for (let size of config.gallery.photoSetCoverSizes) { + await file + .clone() + .resize(size.width, size.height, { + fit: "outside", + }) + .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); + } + break; + } + case "jimp": { + const jimp = require("jimp"); + let file = await jimp.read(sourcePath); + for (let size of config.gallery.photoSetCoverSizes) { + await file + .clone() + .cover(size.width, size.height) + .quality(config.gallery.jpgQuality) + .writeAsync(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); } break; } @@ -184,7 +211,13 @@ router.post("/photoset", upload.single("file"), async (req, res) => { } photoSet.cover = `/${config.gallery.photoSetsPath}/${photoSet._id}${extension}`; - await rename(req.file.path, `${coversPath}/${photoSet._id}${extension}`); + + let filename = `${coversPath}/${photoSet._id}${extension}`; + + await resizePhotoSetCover(req.file.path, filename); + + // remove original cover file + await unlink(req.file.path); } if (isNew) { photoSet.photos = []; @@ -220,7 +253,7 @@ router.delete("/photo/:id", async (req, res) => { let src = path.resolve(config.gallery.rootPath, photo.src.slice(1)); // remove thumbnails and other image sizes permanently - for (let { suffix } of IMAGE_SIZES) { + for (let { suffix } of config.gallery.imageSizes) { try { let extension = path.extname(src); await unlink(`${src.slice(0, -extension.length)}_${suffix}${extension}`); diff --git a/facade/.env.defaults b/facade/.env.defaults index 3f1ad72..f02e185 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -1,7 +1,7 @@ # Express.js Facade backend port PORT=8082 -API_BASE_URL=http://localhost:8080/api/facade +API_BASE_URL=http://localhost:8081/facade API_TOKEN=CHANGE_ME # logger settings From cf9800183f6648283437636a3c294b712ffe2410 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Fri, 3 Apr 2020 16:56:31 +0300 Subject: [PATCH 04/15] Gallery - `jimp` support dropped --- CHANGELOG.md | 1 + api/.env.defaults | 8 ++--- api/config.js | 11 +++--- api/routes/gallery.js | 79 ++++++------------------------------------- 4 files changed, 22 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f2f64..07a8039 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Gallery: * `GALLERY_NEW_PHOTOS_FIRST` option added; * resize photoset cover; + * `jimp` support dropped; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/api/.env.defaults b/api/.env.defaults index 3f8aa3d..fb095bb 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -59,8 +59,8 @@ GALLERY_TRASH_PATH=gallery/trash # image processing module (sharp or jimp, must be installed) GALLERY_IMAGE_PROCESSING_MODULE=sharp GALLERY_JPG_QUALITY=90 -# comma-separated image sizes to fit in. Format of each size: ":x" -GALLERY_IMAGE_SIZES=ts:192x192,tm:256x256,tl:512x512,s:900x600,m:1200x800,l:1860x1020 -# comma-separated photo set cover sizes. Format of each size: ":x" -GALLERY_PHOTOSET_COVER_SIZES=:375x250,2x:750x500 +# comma-separated image sizes. Format of each size: "::x" +# default fit is `cover`. See https://sharp.pixelplumbing.com/api-resize +GALLERY_IMAGE_SIZES=ts:inside:192x192,tm:inside:256x256,tl:inside:512x512,s:inside:900x600,m:inside:1200x800,l:inside:1860x1020 +GALLERY_PHOTOSET_COVER_SIZES=::375x250,2x::750x500 diff --git a/api/config.js b/api/config.js index a50bf28..47aa48b 100644 --- a/api/config.js +++ b/api/config.js @@ -3,12 +3,15 @@ const path = require("path"), function parseImageSizes(value) { return value.split(",").map(x => { - let suffix = x.split(":")[0], - size = x.split(":")[1]; + let splitted = x.split(":"), + suffix = splitted[0], + fit = splitted[1] || "cover", + size = splitted[2]; return { + suffix, + fit, width: Number(size.split("x")[0]), - height: Number(size.split("x")[1]), - suffix + height: Number(size.split("x")[1]) }; }); } diff --git a/api/routes/gallery.js b/api/routes/gallery.js index e7971b4..8b80572 100644 --- a/api/routes/gallery.js +++ b/api/routes/gallery.js @@ -18,75 +18,16 @@ const express = require("express"), upload = multer({ dest: uploadPath }); // creates thumbnails and other image sizes -async function resizeImage(sourcePath, outPath) { +async function resizeImage(sourcePath, outPath, sizes, quality) { let extension = path.extname(outPath), filename = outPath.slice(0, -extension.length); - - switch (config.gallery.imageProcessingModule) { - case "sharp": { - const sharp = require("sharp"); - let file = sharp(sourcePath).jpeg({ - quality: config.gallery.jpgQuality, - }); - for (let size of config.gallery.imageSizes) { - await file - .clone() - .resize(size.width, size.height, { - fit: "inside", - }) - .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); - } - break; - } - case "jimp": { - const jimp = require("jimp"); - let file = await jimp.read(sourcePath); - for (let size of config.gallery.imageSizes) { - await file - .clone() - .scaleToFit(size.width, size.height) - .quality(config.gallery.jpgQuality) - .writeAsync(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); - } - break; - } - } -} - -// resizes photo set cover -// eslint-disable-next-line no-unused-vars -async function resizePhotoSetCover(sourcePath, outPath) { - let extension = path.extname(outPath), - filename = outPath.slice(0, -extension.length); - - switch (config.gallery.imageProcessingModule) { - case "sharp": { - const sharp = require("sharp"); - let file = sharp(sourcePath).jpeg({ - quality: config.gallery.jpgQuality, - }); - for (let size of config.gallery.photoSetCoverSizes) { - await file - .clone() - .resize(size.width, size.height, { - fit: "outside", - }) - .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); - } - break; - } - case "jimp": { - const jimp = require("jimp"); - let file = await jimp.read(sourcePath); - for (let size of config.gallery.photoSetCoverSizes) { - await file - .clone() - .cover(size.width, size.height) - .quality(config.gallery.jpgQuality) - .writeAsync(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); - } - break; - } + const sharp = require("sharp"); + let file = sharp(sourcePath).jpeg({quality}); + for (let size of sizes) { + await file + .clone() + .resize(size.width, size.height, { fit: size.fit }) + .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); } } @@ -180,7 +121,7 @@ router.post("/photo", upload.single("file"), async (req, res) => { let filename = `${photosPath}/${photo._id}${extension}`; - await resizeImage(req.file.path, filename); + await resizeImage(req.file.path, filename, config.gallery.imageSizes, config.gallery.jpgQuality); // move original photo file await rename(req.file.path, filename); @@ -214,7 +155,7 @@ router.post("/photoset", upload.single("file"), async (req, res) => { let filename = `${coversPath}/${photoSet._id}${extension}`; - await resizePhotoSetCover(req.file.path, filename); + await resizeImage(req.file.path, filename, config.gallery.photoSetCoverSizes, config.gallery.jpgQuality); // remove original cover file await unlink(req.file.path); From 3380f35786b82c5f263cc2f1f8a29b5cebe4dfd3 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Fri, 3 Apr 2020 16:57:21 +0300 Subject: [PATCH 05/15] refresh photoset's cover after changing it --- CHANGELOG.md | 1 + dashboard/src/components/Gallery/PhotoSetChip.vue | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a8039..2f53222 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * `GALLERY_NEW_PHOTOS_FIRST` option added; * resize photoset cover; * `jimp` support dropped; + * refresh photoset's cover after changing it; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/dashboard/src/components/Gallery/PhotoSetChip.vue b/dashboard/src/components/Gallery/PhotoSetChip.vue index 35ec2ff..0fdb944 100644 --- a/dashboard/src/components/Gallery/PhotoSetChip.vue +++ b/dashboard/src/components/Gallery/PhotoSetChip.vue @@ -136,6 +136,7 @@ export default { title: this.newTitle, code: this.newCode }); + this.photoSet.cover = this.newCoverSrc; this.$toast.success(`Photo set ${this.blank ? "added" : "saved"}`); // close menu this.menu = false; From 48f441e9a107179274e87074d1350a158c19e7de Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Fri, 3 Apr 2020 17:08:42 +0300 Subject: [PATCH 06/15] view photo set - load the smallest thumbnails by default --- CHANGELOG.md | 1 + facade/.env.defaults | 18 ++-------------- facade/package.json | 1 + facade/server/config.js | 22 +++----------------- facade/server/lib/utils.js | 10 ++++++++- facade/server/router.js | 2 +- facade/server/routes/{facade.js => index.js} | 10 ++++++++- 7 files changed, 26 insertions(+), 38 deletions(-) rename facade/server/routes/{facade.js => index.js} (95%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f53222..b2e71c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * resize photoset cover; * `jimp` support dropped; * refresh photoset's cover after changing it; + * view photo set - load the smallest thumbnails by default; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/facade/.env.defaults b/facade/.env.defaults index f02e185..9ccdaa8 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -37,22 +37,8 @@ MONGODB_URL=mongodb://express-template-user:express-template-password@localhost: TELEGRAM_BOT_TOKEN=CHANGE_ME TELEGRAM_CHAT_ID=CHANGE_ME -# FileBrowser -FILEBROWSER_UPLOAD_PATH=../public/uploads -FILEBROWSER_ROOT_PATH=../public - # Gallery # currently only `local` gallery storage is supported -GALLERY_STORAGE=local -GALLERY_UPLOAD_PATH=../public/uploads -GALLERY_ROOT_PATH=../public -# relative to GALLERY_ROOT_PATH -GALLERY_PHOTOS_PATH=gallery/photos -GALLERY_PHOTOSETS_PATH=gallery/photosets -GALLERY_TRASH_PATH=gallery/trash -# image processing module (sharp or jimp, must be installed) -GALLERY_IMAGE_PROCESSING_MODULE=sharp -GALLERY_JPG_QUALITY=90 -# comma-separated image sizes: ":x" -GALLERY_IMAGE_SIZES=ts:192x192,tm:256x256,tl:512x512,s:900x600,m:1200x800,l:1860x1020 GALLERY_NEW_PHOTOS_FIRST=true +GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX=ts +GALLERY_DEFAULT_PHOTO_SUFFIX=l diff --git a/facade/package.json b/facade/package.json index b3f08d1..8be2ab9 100644 --- a/facade/package.json +++ b/facade/package.json @@ -22,6 +22,7 @@ "@server": "server", "@lib": "server/lib", "@api": "server/lib/api", + "@utils": "server/lib/utils", "@config": "server/config.js", "@logger": "server/lib/logger.js" }, diff --git a/facade/server/config.js b/facade/server/config.js index 1077d90..ac4b3a2 100644 --- a/facade/server/config.js +++ b/facade/server/config.js @@ -60,25 +60,9 @@ module.exports = { chatId: process.env.TELEGRAM_CHAT_ID }, - fileBrowser: { - uploadPath: path.resolve(process.env.FILEBROWSER_UPLOAD_PATH), - rootPath: path.resolve(process.env.FILEBROWSER_ROOT_PATH) - }, - gallery: { - // currently only `local` gallery storage is supported - storage: process.env.GALLERY_STORAGE, - uploadPath: path.resolve(process.env.GALLERY_UPLOAD_PATH), - rootPath: path.resolve(process.env.GALLERY_ROOT_PATH), - photosPath: process.env.GALLERY_PHOTOS_PATH, - photoSetsPath: process.env.GALLERY_PHOTOSETS_PATH, - trashPath: process.env.GALLERY_TRASH_PATH, - // image processing module (sharp or jimp, must be installed) - imageProcessingModule: process.env.GALLERY_IMAGE_PROCESSING_MODULE, - jpgQuality: parseInt(process.env.GALLERY_JPG_QUALITY), - // comma-separated image sizes: ":x" - imageSizes: process.env.GALLERY_IMAGE_SIZES, - dashboardThumbnailSuffix: process.env.GALLERY_DASHBOARD_THUMBNAIL_SUFFIX, - newPhotosFirst: process.env.GALLERY_NEW_PHOTOS_FIRST === "true" + newPhotosFirst: process.env.GALLERY_NEW_PHOTOS_FIRST === "true", + defaultPhotoThumbnailSuffix: process.env.GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX, + defaultPhotoSuffix: process.env.GALLERY_DEFAULT_PHOTO_SUFFIX, } }; \ No newline at end of file diff --git a/facade/server/lib/utils.js b/facade/server/lib/utils.js index 2a1b03e..de6bf05 100644 --- a/facade/server/lib/utils.js +++ b/facade/server/lib/utils.js @@ -10,6 +10,14 @@ function partial(fn) { }; } +function addFilenameSuffix(filename, suffix) { + const path = require("path"), + extension = path.extname(filename); + filename = filename.slice(0, -extension.length); + return filename + "_" + suffix + extension; +} + module.exports = { - partial: partial + partial, + addFilenameSuffix }; \ No newline at end of file diff --git a/facade/server/router.js b/facade/server/router.js index cb4beac..4ecfd12 100644 --- a/facade/server/router.js +++ b/facade/server/router.js @@ -33,7 +33,7 @@ module.exports = function (express) { next(); }; - express.use("/", require("./routes/facade")(express)); + express.use("/", require("./routes")(express)); // handle 404 express.use(function (req, res) { diff --git a/facade/server/routes/facade.js b/facade/server/routes/index.js similarity index 95% rename from facade/server/routes/facade.js rename to facade/server/routes/index.js index 72c2547..b9a9556 100644 --- a/facade/server/routes/facade.js +++ b/facade/server/routes/index.js @@ -1,7 +1,8 @@ const router = require("express").Router(), config = require("@config"), logger = require("@logger"), - api = require("@api"); + api = require("@api"), + { addFilenameSuffix } = require("@utils"); function signin(req, res) { req.signin(function (err, user, info) { @@ -59,6 +60,11 @@ router.get("/gallery/:photoSet", async function (req, res) { photoSet.photos.reverse(); } + // load the smallest thumbnail by default + for (let photo of photoSet.photos) { + photo.src = addFilenameSuffix(photo.src, config.gallery.defaultPhotoThumbnailSuffix); + } + res.locals.model = { photoSet }; @@ -99,6 +105,8 @@ router.get("/gallery/:photoSet/:photoId(\\d+)", async function (req, res) { : photos[photos.length - 1]; } + photo.src = addFilenameSuffix(photo.src, config.gallery.defaultPhotoSuffix); + res.locals.model = { photoSet, photo, From 492be2aee5cc8b696bebcac709c12760fbf611e0 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Fri, 3 Apr 2020 18:59:13 +0300 Subject: [PATCH 07/15] Facade - consider `GALLERY_IMAGE_SIZES` and `GALLERY_PHOTOSET_COVER_SIZES` options in `justifiedGallery` --- CHANGELOG.md | 1 + api/.env.defaults | 4 ---- api/config.js | 5 ----- facade/.env.defaults | 4 ++++ facade/gulpfile.js | 4 +++- facade/js/pages/gallery.js | 22 +++++++++++++--------- facade/server/config.js | 3 +-- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2e71c8..d5f5d52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * `jimp` support dropped; * refresh photoset's cover after changing it; * view photo set - load the smallest thumbnails by default; + * Facade - consider `GALLERY_IMAGE_SIZES` and `GALLERY_PHOTOSET_COVER_SIZES` options in `justifiedGallery`; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/api/.env.defaults b/api/.env.defaults index fb095bb..41e22f2 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -39,10 +39,6 @@ AWS_S3_BUCKET=CHANGE_ME AWS_SES_FROM=CHANGE_ME AWS_SES_SEND_RATE=1 -# Telegram -TELEGRAM_BOT_TOKEN=CHANGE_ME -TELEGRAM_CHAT_ID=CHANGE_ME - # FileBrowser FILEBROWSER_UPLOAD_PATH=../public/uploads FILEBROWSER_ROOT_PATH=../public diff --git a/api/config.js b/api/config.js index 47aa48b..56bc61e 100644 --- a/api/config.js +++ b/api/config.js @@ -74,11 +74,6 @@ const config = { sesSendRate: parseInt(process.env.AWS_SES_SEND_RATE) }, - telegram: { - botToken: process.env.TELEGRAM_BOT_TOKEN, - chatId: process.env.TELEGRAM_CHAT_ID - }, - fileBrowser: { uploadPath: path.resolve(process.env.FILEBROWSER_UPLOAD_PATH), rootPath: path.resolve(process.env.FILEBROWSER_ROOT_PATH) diff --git a/facade/.env.defaults b/facade/.env.defaults index 9ccdaa8..8c5fa29 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -39,6 +39,10 @@ TELEGRAM_CHAT_ID=CHANGE_ME # Gallery # currently only `local` gallery storage is supported +# comma-separated image sizes. Format of each size: "::x" +# default fit is `cover`. See https://sharp.pixelplumbing.com/api-resize +GALLERY_IMAGE_SIZES=ts:inside:192x192,tm:inside:256x256,tl:inside:512x512,s:inside:900x600,m:inside:1200x800,l:inside:1860x1020 +GALLERY_PHOTOSET_COVER_SIZES=::375x250,2x::750x500 GALLERY_NEW_PHOTOS_FIRST=true GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX=ts GALLERY_DEFAULT_PHOTO_SUFFIX=l diff --git a/facade/gulpfile.js b/facade/gulpfile.js index c424b89..dcc12c7 100644 --- a/facade/gulpfile.js +++ b/facade/gulpfile.js @@ -109,7 +109,9 @@ async function rollup(input, output, watch, callback) { "exclude": ["node_modules/**", "js/vendor/**"] }), replace({ - "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), + "process.env.GALLERY_PHOTOSET_COVER_SIZES": JSON.stringify(process.env.GALLERY_PHOTOSET_COVER_SIZES), + "process.env.GALLERY_IMAGE_SIZES": JSON.stringify(process.env.GALLERY_IMAGE_SIZES) }), alias({ entries: { diff --git a/facade/js/pages/gallery.js b/facade/js/pages/gallery.js index fb8e4ed..09b4aff 100644 --- a/facade/js/pages/gallery.js +++ b/facade/js/pages/gallery.js @@ -5,12 +5,23 @@ import justifiedGallery from "justifiedGallery"; // register justifiedGallery as jQuery plugin justifiedGallery(); +function parseImageSizes(value) { + return value.split(",").reduce((acc, x) => { + let splitted = x.split(":"), + suffix = splitted[0], + size = splitted[2]; + acc[Number(size.split("x")[1])] = suffix ? "_" + suffix : ""; + return acc; + }, {}); +} + export function gallery() { log("Gallery"); $(".photosets").justifiedGallery({ rowHeight : 250, lastRow : "center", - margins : 3 + margins : 3, + sizeRangeSuffixes: parseImageSizes(process.env.GALLERY_PHOTOSET_COVER_SIZES), }); } @@ -20,14 +31,7 @@ export function photoSet() { rowHeight : 250, lastRow : "center", margins : 3, - sizeRangeSuffixes: { - 192: "_ts", - 256: "_tm", - 512: "_tl", - 600: "_s", - 800: "_m", - 1020: "_l", - }, + sizeRangeSuffixes: parseImageSizes(process.env.GALLERY_IMAGE_SIZES), captionSettings: { animationDuration: 500, visibleOpacity: 1.0, diff --git a/facade/server/config.js b/facade/server/config.js index ac4b3a2..fcc8459 100644 --- a/facade/server/config.js +++ b/facade/server/config.js @@ -1,5 +1,4 @@ -const path = require("path"), - packageJson = require(process.cwd() + "/package.json"); +const packageJson = require(process.cwd() + "/package.json"); module.exports = { // environment From 9b13a6a6f570f88b67bbbf732c26e3354473b229 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sat, 4 Apr 2020 16:58:23 +0300 Subject: [PATCH 08/15] image loading adjusted (show blurred thumbnail while loading larger image) --- CHANGELOG.md | 1 + api/routes/gallery.js | 13 ++- facade/.env.defaults | 2 +- facade/js/pages/gallery.js | 39 +++++++++ facade/scss/base/_animations.scss | 4 + facade/scss/base/_general.scss | 2 +- facade/scss/pages/_gallery.scss | 117 +++++++++++++++++-------- facade/server/views/gallery/photo.html | 7 +- 8 files changed, 140 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5f5d52..2c11221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * refresh photoset's cover after changing it; * view photo set - load the smallest thumbnails by default; * Facade - consider `GALLERY_IMAGE_SIZES` and `GALLERY_PHOTOSET_COVER_SIZES` options in `justifiedGallery`; + * image loading adjusted (show blurred thumbnail while loading larger image); * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* diff --git a/api/routes/gallery.js b/api/routes/gallery.js index 8b80572..0cf3fa0 100644 --- a/api/routes/gallery.js +++ b/api/routes/gallery.js @@ -17,8 +17,8 @@ const express = require("express"), trashPath = path.resolve(rootPath, config.gallery.trashPath), upload = multer({ dest: uploadPath }); -// creates thumbnails and other image sizes -async function resizeImage(sourcePath, outPath, sizes, quality) { +// creates thumbnails and other image sizes, returns image properties +async function processImage(sourcePath, outPath, sizes, quality) { let extension = path.extname(outPath), filename = outPath.slice(0, -extension.length); const sharp = require("sharp"); @@ -29,6 +29,7 @@ async function resizeImage(sourcePath, outPath, sizes, quality) { .resize(size.width, size.height, { fit: size.fit }) .toFile(`${filename}${size.suffix ? "_" + size.suffix : ""}${extension}`); } + return file.metadata(); } async function makeDirs() { @@ -121,7 +122,11 @@ router.post("/photo", upload.single("file"), async (req, res) => { let filename = `${photosPath}/${photo._id}${extension}`; - await resizeImage(req.file.path, filename, config.gallery.imageSizes, config.gallery.jpgQuality); + let metadata = await processImage(req.file.path, filename, config.gallery.imageSizes, config.gallery.jpgQuality); + if (metadata && metadata.width && metadata.height) { + photo.width = metadata.width; + photo.height = metadata.height; + } // move original photo file await rename(req.file.path, filename); @@ -155,7 +160,7 @@ router.post("/photoset", upload.single("file"), async (req, res) => { let filename = `${coversPath}/${photoSet._id}${extension}`; - await resizeImage(req.file.path, filename, config.gallery.photoSetCoverSizes, config.gallery.jpgQuality); + await processImage(req.file.path, filename, config.gallery.photoSetCoverSizes, config.gallery.jpgQuality); // remove original cover file await unlink(req.file.path); diff --git a/facade/.env.defaults b/facade/.env.defaults index 8c5fa29..2753eb8 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -45,4 +45,4 @@ GALLERY_IMAGE_SIZES=ts:inside:192x192,tm:inside:256x256,tl:inside:512x512,s:insi GALLERY_PHOTOSET_COVER_SIZES=::375x250,2x::750x500 GALLERY_NEW_PHOTOS_FIRST=true GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX=ts -GALLERY_DEFAULT_PHOTO_SUFFIX=l +GALLERY_DEFAULT_PHOTO_SUFFIX=ts diff --git a/facade/js/pages/gallery.js b/facade/js/pages/gallery.js index 09b4aff..a32b21f 100644 --- a/facade/js/pages/gallery.js +++ b/facade/js/pages/gallery.js @@ -42,6 +42,45 @@ export function photoSet() { export function photo() { log("Gallery Photo"); + + let photoContainer = document.querySelector(".photo-container"), + photoWrapper = photoContainer.querySelector(".photo-wrapper"), + photo = photoWrapper.querySelector("img[data-src]"), + photoPreview = photoWrapper.querySelector("img.preview"), + src = photo.getAttribute("data-src"), + width = photo.getAttribute("width"), + height = photo.getAttribute("height"), + aspectRatio = width / height, + suffixStart = src.lastIndexOf("_"), + suffixEnd = src.lastIndexOf("."); + + photo.onload = () => { + photoWrapper.classList.remove("loading"); + photo.removeAttribute("width"); + photo.removeAttribute("height"); + photoPreview.setAttribute("width", photo.offsetWidth); + photoPreview.setAttribute("height", photo.offsetHeight); + }; + + // TODO: load appropriate image size + photo.src = src.slice(0, suffixStart) + "_l" + src.slice(suffixEnd); + + function fitHeight() { + // get viewport dimensions + const vh = document.documentElement.clientHeight; + + if (vh < photo.offsetHeight) { + // fit height + let newHeight = vh - 20, + newWidth = newHeight * aspectRatio; + + photo.style.width = photoPreview.style.width = newWidth + "px"; + photo.style.height = photoPreview.style.height = newHeight + "px"; + } + } + + window.onresize = fitHeight; + fitHeight(); } export default { diff --git a/facade/scss/base/_animations.scss b/facade/scss/base/_animations.scss index 94a6a39..c952b86 100644 --- a/facade/scss/base/_animations.scss +++ b/facade/scss/base/_animations.scss @@ -20,4 +20,8 @@ transform-origin: 50% 0%; transform: perspective(600px) rotateX(0deg); } +} + +@keyframes loading { + 100% { transform: translateX(100%); } } \ No newline at end of file diff --git a/facade/scss/base/_general.scss b/facade/scss/base/_general.scss index 9a46b6e..9a4a579 100644 --- a/facade/scss/base/_general.scss +++ b/facade/scss/base/_general.scss @@ -102,7 +102,7 @@ a { height: 0; opacity: 0; position: relative; - top: -100px; + top: -10px; } .container, diff --git a/facade/scss/pages/_gallery.scss b/facade/scss/pages/_gallery.scss index 93215a8..2738391 100644 --- a/facade/scss/pages/_gallery.scss +++ b/facade/scss/pages/_gallery.scss @@ -42,55 +42,98 @@ text-align: center; } - .photo-wrapper { - position: relative; - display: inline-block; - - .photo-nav { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - - .prev, - .next { - position: absolute; - top: 0; - bottom: 0; - - i { - color: #fff; + .photo-container { + text-align: center; + padding: 20px; + + .photo-wrapper { + position: relative; + display: inline-block; + overflow: hidden; + + &.loading { + &::after { + content: ""; + display: block; position: absolute; - top: 50%; - transform: translateY(-50%); - font-size: 3rem; - text-shadow: 0 0 5px rgba($color: #000, $alpha: 0.33); - opacity: 0; - transition: 0.5s ease opacity; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: linear-gradient( + 90deg, transparent, #fff, #fff, transparent); + opacity: 0.25; + animation: loading 1.5s infinite; + transform: translateX(-100%); } + } - &:hover i, - &.focus i { - opacity: 0.7; - } + img { + //max-width: none; + transition: opacity 0.5s linear; } - .prev { + img:not(.preview) { + position:absolute; + top: 0; left: 0; - right: 75%; - i { - left: 10px; + .loading & { + opacity: 0; } } - .next { + img.preview { + filter: blur(5px); + //transform: scale(1.07); + } + + .photo-nav { + position: absolute; + top: 0; + left: 0; right: 0; - left: 25%; + bottom: 0; + + .prev, + .next { + position: absolute; + top: 0; + bottom: 0; + + i { + color: #fff; + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 3rem; + text-shadow: 0 0 5px rgba($color: #000, $alpha: 0.33); + opacity: 0; + transition: 0.5s ease opacity; + } + + &:hover i, + &.focus i { + opacity: 0.7; + } + } + + .prev { + left: 0; + right: 75%; + + i { + left: 10px; + } + } + + .next { + right: 0; + left: 25%; - i { - right: 10px; + i { + right: 10px; + } } } } diff --git a/facade/server/views/gallery/photo.html b/facade/server/views/gallery/photo.html index d9cfa05..8ebe3ab 100644 --- a/facade/server/views/gallery/photo.html +++ b/facade/server/views/gallery/photo.html @@ -6,9 +6,12 @@ {{#with model}}

Gallery / {{photoSet.title}}

-
+
+
+
- {{photo.alt}} + {{photo.alt}} + {{photo.alt}}
{{#if prevPhotoId}} From df9912859032c9c398ca46c9c2e5da193d10ee6e Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sat, 4 Apr 2020 17:27:13 +0300 Subject: [PATCH 09/15] Facade - show Slick header slideshow only on homepage --- CHANGELOG.md | 3 ++- api/index.js | 18 ------------------ facade/js/index.js | 5 ----- facade/js/pages/homepage.js | 5 +++++ facade/scss/layout/_header.scss | 17 ++++++++++------- facade/server/index.js | 3 +-- facade/server/views/_header.html | 2 ++ 7 files changed, 20 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c11221..21fd137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## **2.0.2** - *2020-xx-xx* +* Facade - show Slick header slideshow only on homepage; +* API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); * Gallery: * `GALLERY_NEW_PHOTOS_FIRST` option added; * resize photoset cover; @@ -9,7 +11,6 @@ * view photo set - load the smallest thumbnails by default; * Facade - consider `GALLERY_IMAGE_SIZES` and `GALLERY_PHOTOSET_COVER_SIZES` options in `justifiedGallery`; * image loading adjusted (show blurred thumbnail while loading larger image); -* API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; diff --git a/api/index.js b/api/index.js index eb7ecc5..e837409 100644 --- a/api/index.js +++ b/api/index.js @@ -94,24 +94,6 @@ require("@store/client").connect((err, client) => { // init authentication module require("@lib/auth")(express); - // define global response locals - var devCacheHash = require("crypto").randomBytes(20).toString("hex").slice(0, 7); - express.use(function (req, res, next) { - if (req.user && req.user.config) { - //req.user.config.foo = "bar"; - } - - res.locals = { - config: config, - lang: require("./langs").en, - user: req.user, - isAuthenticated: req.isAuthenticated(), - production: "production" === config.env, - cacheHash: "production" === config.env ? config.commit : devCacheHash - }; - next(); - }); - // init Express' routes require("./router")(express); diff --git a/facade/js/index.js b/facade/js/index.js index f4b3992..19193ab 100644 --- a/facade/js/index.js +++ b/facade/js/index.js @@ -5,8 +5,6 @@ import pages from "./pages"; import $ from "jquery"; import "bootstrap"; import AOS from "aos"; -import slideshow from "./components/slideshow"; - log("Welcome to NordicSoft Express 4 Template! Environment: " + options.env); page("/", pages.homepage); @@ -33,6 +31,3 @@ $.ajaxSetup({ // setup AOS AOS.init({ once: true }); - -// setup Slick -slideshow($("header .slideshow")); \ No newline at end of file diff --git a/facade/js/pages/homepage.js b/facade/js/pages/homepage.js index bcf2668..659554e 100644 --- a/facade/js/pages/homepage.js +++ b/facade/js/pages/homepage.js @@ -1,5 +1,10 @@ import { log } from "core"; +import $ from "jquery"; +import slideshow from "./../components/slideshow"; export default function () { log("Homepage"); + + // setup Slick + slideshow($("header .slideshow")); } \ No newline at end of file diff --git a/facade/scss/layout/_header.scss b/facade/scss/layout/_header.scss index 8e99986..804bd47 100644 --- a/facade/scss/layout/_header.scss +++ b/facade/scss/layout/_header.scss @@ -3,16 +3,19 @@ /*************************************************************/ header { - min-height: 600px; - position: relative; - box-shadow: 0 0 30px rgba(0, 0, 0, 0.3); - border-bottom: 1px solid $white; + .homepage & { + min-height: 601px; + position: relative; - @include media-breakpoint-down(sm) { - min-height: 400px; + @include media-breakpoint-down(sm) { + min-height: 401px; + } } + box-shadow: 0 0 30px rgba(0, 0, 0, 0.3), 0 1px 6px rgba(0, 0, 0, 0.15); + border-bottom: 1px solid $white; + nav { box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15); background: linear-gradient(to bottom, rgba(50, 81, 153, 0.95), rgba(75, 107, 183, 0.95)); @@ -149,7 +152,7 @@ header { .slideshow { position: absolute; top: 0; - bottom: 0; + bottom: 1px; left: 0; right: 0; text-align: center; diff --git a/facade/server/index.js b/facade/server/index.js index d425515..fd876df 100644 --- a/facade/server/index.js +++ b/facade/server/index.js @@ -80,7 +80,6 @@ require("@server/io").init(server, session); require("@lib/auth")(express); // define global response locals -var devCacheHash = require("crypto").randomBytes(20).toString("hex").slice(0, 7); express.use(function (req, res, next) { if (req.user && req.user.config) { //req.user.config.foo = "bar"; @@ -94,7 +93,7 @@ express.use(function (req, res, next) { registrationEnabled: req.app.get("registration-enabled"), production: config.prod, development: config.dev, - cacheHash: "production" === config.env ? config.commit : devCacheHash + page: req.url.slice(1) || "homepage" }; next(); }); diff --git a/facade/server/views/_header.html b/facade/server/views/_header.html index fb01dfb..6cac04d 100644 --- a/facade/server/views/_header.html +++ b/facade/server/views/_header.html @@ -59,6 +59,7 @@
+ {{#if (eq page "homepage")}}
@@ -102,4 +103,5 @@
{{> _social-links}} + {{/if}} \ No newline at end of file From 0fa4d64418b1a22a9398d2c2f06c24802cc0797c Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sat, 4 Apr 2020 20:40:20 +0300 Subject: [PATCH 10/15] show last `GALLERY_LAST_PHOTOS_COUNT` photos below photo sets; navigation in `All Photos` photo set --- CHANGELOG.md | 2 + api/routes/facade.js | 26 ++++++++++--- api/store/modules/photos.js | 6 +-- api/store/store.js | 4 +- facade/.env.defaults | 1 + facade/js/pages/gallery.js | 12 ++++++ facade/scss/layout/_main.scss | 3 +- facade/scss/pages/_gallery.scss | 19 +++++++-- facade/server/config.js | 1 + facade/server/lib/api.js | 4 +- facade/server/routes/index.js | 54 +++++++++++++------------- facade/server/views/gallery/index.html | 17 ++++++++ 12 files changed, 107 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21fd137..ebf94b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * view photo set - load the smallest thumbnails by default; * Facade - consider `GALLERY_IMAGE_SIZES` and `GALLERY_PHOTOSET_COVER_SIZES` options in `justifiedGallery`; * image loading adjusted (show blurred thumbnail while loading larger image); + * show last `GALLERY_LAST_PHOTOS_COUNT` photos below photo sets; + * navigation in `All Photos` photo set; ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; diff --git a/api/routes/facade.js b/api/routes/facade.js index de0e4c7..421c749 100644 --- a/api/routes/facade.js +++ b/api/routes/facade.js @@ -47,18 +47,34 @@ router.get("/gallery/photosets/not-empty", async function (req, res) { }); router.get("/gallery/photoset/:code", async function (req, res) { - let photoSet; - if (req.query.photos === "true") { - photoSet = await store.photoSets.getByCodeWithPhotos(req.params.code); + let photoSet, + code = req.params.code; + + if (code === "all") { + photoSet = { title: "All photos", code: "all" }; + if (req.query.photos === "true") { + photoSet.photos = await store.photos.all(undefined, false); + } else { + let photos = await store.photos.all(undefined, false, { projection: { _id: 1 } }); + photoSet.photos = photos.map(x => x._id); + } } else { - photoSet = await store.photoSets.getByCode(req.params.code); + photoSet = req.query.photos === "true" + ? await store.photoSets.getByCodeWithPhotos(code) + : await store.photoSets.getByCode(code); } + return res.json(photoSet); }); router.get("/gallery/photos", async function (req, res) { - let photos = await store.photos.all(undefined, false); + let sortStr = req.query.sort, + sort = sortStr ? [sortStr.split(":")[0], parseInt(sortStr.split(":")[1])] : undefined, + skip = req.query.skip ? parseInt(req.query.skip) : undefined, + limit = req.query.limit ? parseInt(req.query.limit) : undefined; + + let photos = await store.photos.all(sort, false, { skip, limit }); return res.json(photos); }); diff --git a/api/store/modules/photos.js b/api/store/modules/photos.js index bd8b155..b95caa9 100644 --- a/api/store/modules/photos.js +++ b/api/store/modules/photos.js @@ -24,11 +24,11 @@ class PhotosStore extends Store { } return super.insert(docs, options); } - async all(sort, includeDeleted = true) { + async all(sort, includeDeleted = true, options) { if (includeDeleted) { - return super.all(sort || { created: 1 }); + return super.all(sort || { created: 1 }, options); } - return this.find({ deleted: { $exists: false } }, undefined, sort || { created: 1 }); + return this.find({ deleted: { $exists: false } }, options, sort || { created: 1 }); } } diff --git a/api/store/store.js b/api/store/store.js index ec042b8..1c61771 100644 --- a/api/store/store.js +++ b/api/store/store.js @@ -29,8 +29,8 @@ class Store { } return this.collection.countDocuments(query, options); } - async all(sort) { - return this.find({}, undefined, sort); + async all(sort, options) { + return this.find({}, options, sort); } async insert(docs, options) { if (!Array.isArray(docs)) { diff --git a/facade/.env.defaults b/facade/.env.defaults index 2753eb8..350bc8a 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -46,3 +46,4 @@ GALLERY_PHOTOSET_COVER_SIZES=::375x250,2x::750x500 GALLERY_NEW_PHOTOS_FIRST=true GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX=ts GALLERY_DEFAULT_PHOTO_SUFFIX=ts +GALLERY_LAST_PHOTOS_COUNT=15 diff --git a/facade/js/pages/gallery.js b/facade/js/pages/gallery.js index a32b21f..b976ec0 100644 --- a/facade/js/pages/gallery.js +++ b/facade/js/pages/gallery.js @@ -23,6 +23,18 @@ export function gallery() { margins : 3, sizeRangeSuffixes: parseImageSizes(process.env.GALLERY_PHOTOSET_COVER_SIZES), }); + + $(".photos").justifiedGallery({ + rowHeight : 250, + lastRow : "center", + margins : 3, + sizeRangeSuffixes: parseImageSizes(process.env.GALLERY_IMAGE_SIZES), + captionSettings: { + animationDuration: 500, + visibleOpacity: 1.0, + nonVisibleOpacity: 0.0 + } + }); } export function photoSet() { diff --git a/facade/scss/layout/_main.scss b/facade/scss/layout/_main.scss index 79964ce..2b17e39 100644 --- a/facade/scss/layout/_main.scss +++ b/facade/scss/layout/_main.scss @@ -6,7 +6,7 @@ main { flex: 1 0 auto; padding: 20px 0; - h1, h2 { + h1 { text-align: center; font-size: 2.5rem; padding: 0.3rem 0 1.5rem; @@ -27,6 +27,7 @@ main { } h2 { + text-align: center; font-size: 2rem; padding: 0.3rem 0 1rem; } diff --git a/facade/scss/pages/_gallery.scss b/facade/scss/pages/_gallery.scss index 2738391..bb0a1b7 100644 --- a/facade/scss/pages/_gallery.scss +++ b/facade/scss/pages/_gallery.scss @@ -4,6 +4,7 @@ .gallery { main { + .caption { text-align: center; opacity: 1 !important; @@ -26,9 +27,21 @@ } } - &.index main .caption { - display: block; - font-size: 1.5rem; + &.index main { + .photosets { + margin-bottom: 35px; + + .caption { + display: block; + font-size: 1.5rem; + } + } + + .btn-all-photos { + display: block; + width: 220px; + margin: 20px auto; + } } &.photoset { diff --git a/facade/server/config.js b/facade/server/config.js index fcc8459..b7d0e2c 100644 --- a/facade/server/config.js +++ b/facade/server/config.js @@ -63,5 +63,6 @@ module.exports = { newPhotosFirst: process.env.GALLERY_NEW_PHOTOS_FIRST === "true", defaultPhotoThumbnailSuffix: process.env.GALLERY_DEFAULT_PHOTO_THUMBNAIL_SUFFIX, defaultPhotoSuffix: process.env.GALLERY_DEFAULT_PHOTO_SUFFIX, + lastPhotosCount: parseInt(process.env.GALLERY_LAST_PHOTOS_COUNT), } }; \ No newline at end of file diff --git a/facade/server/lib/api.js b/facade/server/lib/api.js index 1dcbe3f..8e6f63f 100644 --- a/facade/server/lib/api.js +++ b/facade/server/lib/api.js @@ -74,12 +74,12 @@ module.exports.users = { }; module.exports.photos = { - all: () => call.get("/gallery/photos"), + all: (sort, skip, limit) => call.get("/gallery/photos", { params: { sort, skip, limit } }), get: id => call.get("/gallery/photo/" + id) }; module.exports.photoSets = { notEmpty: () => call.get("/gallery/photosets/not-empty"), get: code => call.get("/gallery/photoset/" + code), - getWithPhotos: code => call.get(`/gallery/photoset/${code}?photos=true`) + getWithPhotos: code => call.get(`/gallery/photoset/${code}`, { params: { photos: true } }) }; \ No newline at end of file diff --git a/facade/server/routes/index.js b/facade/server/routes/index.js index b9a9556..a4319cd 100644 --- a/facade/server/routes/index.js +++ b/facade/server/routes/index.js @@ -26,6 +26,19 @@ router.get("/", function (req, res) { router.get("/gallery", async function (req, res) { if (res.locals.photoSets && res.locals.photoSets.length > 0) { + let lastPhotos = await api.photos.all( + `created:${config.gallery.newPhotosFirst ? -1 : 1}`, + 0, + config.gallery.lastPhotosCount); + + // load the smallest thumbnail by default + for (let photo of lastPhotos) { + photo.src = addFilenameSuffix(photo.src, config.gallery.defaultPhotoThumbnailSuffix); + } + + res.locals.model = { + lastPhotos + }; return res.render("gallery/index"); } @@ -44,11 +57,15 @@ router.get("/gallery/:photoSet", async function (req, res) { if (req.params.photoSet !== "all") { photoSet = await api.photoSets.getWithPhotos(req.params.photoSet); + + if (photoSet && photoSet.photos && config.gallery.newPhotosFirst) { + photoSet.photos.reverse(); + } } else { photoSet = { title: "All photos", code: "all", - photos: await api.photos.all() + photos: await api.photos.all(`created:${config.gallery.newPhotosFirst ? -1 : 1}`) }; } @@ -56,10 +73,6 @@ router.get("/gallery/:photoSet", async function (req, res) { return res.error(404); } - if (config.gallery.newPhotosFirst) { - photoSet.photos.reverse(); - } - // load the smallest thumbnail by default for (let photo of photoSet.photos) { photo.src = addFilenameSuffix(photo.src, config.gallery.defaultPhotoThumbnailSuffix); @@ -72,15 +85,8 @@ router.get("/gallery/:photoSet", async function (req, res) { }); router.get("/gallery/:photoSet/:photoId(\\d+)", async function (req, res) { - let photoSet, photo; - - if (req.params.photoSet !== "all") { - photoSet = await api.photoSets.get(req.params.photoSet); - } else { - photoSet = { title: "All photos", code: "all" }; - } - - photo = await api.photos.get(req.params.photoId); + let photoSet = await api.photoSets.get(req.params.photoSet), + photo = await api.photos.get(req.params.photoId); if (!photoSet || !photo) { return res.error(404); @@ -90,20 +96,16 @@ router.get("/gallery/:photoSet/:photoId(\\d+)", async function (req, res) { photoSet.photos.reverse(); } - let nextPhotoId, prevPhotoId; - - if (photoSet.code !== "all") { - let photos = photoSet.photos, - currentPhotoIndex = photos.indexOf(photo._id); + let photos = photoSet.photos, + currentPhotoIndex = photos.indexOf(photo._id); - nextPhotoId = currentPhotoIndex < photos.length - 1 ? - photos[currentPhotoIndex + 1] - : photos[0]; + let nextPhotoId = currentPhotoIndex < photos.length - 1 ? + photos[currentPhotoIndex + 1] + : photos[0]; - prevPhotoId = currentPhotoIndex > 0 ? - photos[currentPhotoIndex - 1] - : photos[photos.length - 1]; - } + let prevPhotoId = currentPhotoIndex > 0 ? + photos[currentPhotoIndex - 1] + : photos[photos.length - 1]; photo.src = addFilenameSuffix(photo.src, config.gallery.defaultPhotoSuffix); diff --git a/facade/server/views/gallery/index.html b/facade/server/views/gallery/index.html index b90e346..65797e6 100644 --- a/facade/server/views/gallery/index.html +++ b/facade/server/views/gallery/index.html @@ -6,6 +6,7 @@

Gallery / Photo Sets

+

Photo Sets

{{#photoSets}} @@ -18,4 +19,20 @@

Gallery / Photo Sets

{{/photoSets}}
+ +

Latest Photos

+ + All Photos
\ No newline at end of file From d3df02398812bb81d862b3659bbf833c2d3b3d7d Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sun, 5 Apr 2020 13:46:42 +0300 Subject: [PATCH 11/15] dev proxy - copy dev URL to clipboard --- CHANGELOG.md | 1 + index.js | 4 +++- package.json | 4 +++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf94b9..287af43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * image loading adjusted (show blurred thumbnail while loading larger image); * show last `GALLERY_LAST_PHOTOS_COUNT` photos below photo sets; * navigation in `All Photos` photo set; +* dev proxy - copy dev URL to clipboard; ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; diff --git a/index.js b/index.js index e55e84d..0bd67dc 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,9 @@ app.use("/", proxy({ })); app.listen(port, () => { - console.log(`${logPrefix} Development proxy running at ${chalk.blue("http://localhost:" + port)}`); + const clipboardy = require('clipboardy'); + clipboardy.writeSync("http://localhost:" + port); + console.log(`${logPrefix} Development proxy running at ${chalk.blue("http://localhost:" + port)} (copied to clipboard)`); }); // handle errors diff --git a/package.json b/package.json index 38a48e5..7c5d98f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "http-proxy": "^1.18.0", "http-proxy-middleware": "^1.0.3" }, - "devDependencies": {}, + "devDependencies": { + "clipboardy": "^2.3.0" + }, "nodemonConfig": { "watch": [ "index.js" From 004c0e5eb22b9c2f3f73d7ee546ebbde67df0691 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sun, 5 Apr 2020 13:59:15 +0300 Subject: [PATCH 12/15] photo sets order - init; minor changes --- CHANGELOG.md | 2 ++ api/routes/facade.js | 18 +++++++++++++----- api/store/modules/photosets.js | 6 +++++- .../src/components/Gallery/PhotoSetChip.vue | 17 ++++++++++++++++- facade/server/lib/api.js | 2 +- facade/server/routes/index.js | 3 ++- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 287af43..ed04eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,9 @@ * image loading adjusted (show blurred thumbnail while loading larger image); * show last `GALLERY_LAST_PHOTOS_COUNT` photos below photo sets; * navigation in `All Photos` photo set; + * photo sets order - init; * dev proxy - copy dev URL to clipboard; +* minor changes; ## **2.0.1** - *2020-04-01* * ability to use MongoDB as session storage added; diff --git a/api/routes/facade.js b/api/routes/facade.js index 421c749..82a004e 100644 --- a/api/routes/facade.js +++ b/api/routes/facade.js @@ -41,8 +41,16 @@ router.post("/user", async function (req, res) { }); -router.get("/gallery/photosets/not-empty", async function (req, res) { - let photoSets = await store.photoSets.getNotEmpty(); +router.get("/gallery/photosets", async function (req, res) { + let sortStr = req.query.sort, + sort; + + if (sortStr) { + sort = {}; + sort[sortStr.split(":")[0]] = parseInt(sortStr.split(":")[1] || 1); + } + + let photoSets = await store.photoSets.getNotEmpty(sort); return res.json(photoSets); }); @@ -53,9 +61,9 @@ router.get("/gallery/photoset/:code", async function (req, res) { if (code === "all") { photoSet = { title: "All photos", code: "all" }; if (req.query.photos === "true") { - photoSet.photos = await store.photos.all(undefined, false); + photoSet.photos = await store.photos.all({ created: 1 }, false); } else { - let photos = await store.photos.all(undefined, false, { projection: { _id: 1 } }); + let photos = await store.photos.all({ created: 1 }, false, { projection: { _id: 1 } }); photoSet.photos = photos.map(x => x._id); } } else { @@ -70,7 +78,7 @@ router.get("/gallery/photoset/:code", async function (req, res) { router.get("/gallery/photos", async function (req, res) { let sortStr = req.query.sort, - sort = sortStr ? [sortStr.split(":")[0], parseInt(sortStr.split(":")[1])] : undefined, + sort = sortStr ? [sortStr.split(":")[0], parseInt(sortStr.split(":")[1] || 1)] : undefined, skip = req.query.skip ? parseInt(req.query.skip) : undefined, limit = req.query.limit ? parseInt(req.query.limit) : undefined; diff --git a/api/store/modules/photosets.js b/api/store/modules/photosets.js index 9111c38..023c8c8 100644 --- a/api/store/modules/photosets.js +++ b/api/store/modules/photosets.js @@ -51,7 +51,7 @@ class PhotoSetsStore extends Store { return (await cursor.toArray())[0]; } - async getNotEmpty() { + async getNotEmpty(sort) { let cursor = this.getCollection().aggregate([ { $lookup: { @@ -78,6 +78,10 @@ class PhotoSetsStore extends Store { } ]); + if (sort) { + cursor.sort(sort); + } + return cursor.toArray(); } } diff --git a/dashboard/src/components/Gallery/PhotoSetChip.vue b/dashboard/src/components/Gallery/PhotoSetChip.vue index 0fdb944..02d1e74 100644 --- a/dashboard/src/components/Gallery/PhotoSetChip.vue +++ b/dashboard/src/components/Gallery/PhotoSetChip.vue @@ -136,7 +136,16 @@ export default { title: this.newTitle, code: this.newCode }); - this.photoSet.cover = this.newCoverSrc; + + // refresh cover + if (this.photoSet && this.newCoverSrc) { + this.photoSet.cover = this.newCoverSrc; + } + + if (this.blank) { + this.reset(); + } + this.$toast.success(`Photo set ${this.blank ? "added" : "saved"}`); // close menu this.menu = false; @@ -168,6 +177,12 @@ export default { this.loading = false; }; reader.readAsDataURL(this.newCoverFile); + }, + reset() { + this.newCoverSrc = ""; + this.newCoverFile = null; + this.newTitle = ""; + this.newCode = ""; } } }; diff --git a/facade/server/lib/api.js b/facade/server/lib/api.js index 8e6f63f..cccdabf 100644 --- a/facade/server/lib/api.js +++ b/facade/server/lib/api.js @@ -79,7 +79,7 @@ module.exports.photos = { }; module.exports.photoSets = { - notEmpty: () => call.get("/gallery/photosets/not-empty"), + all: (sort) => call.get("/gallery/photosets", { params: { sort } }), get: code => call.get("/gallery/photoset/" + code), getWithPhotos: code => call.get(`/gallery/photoset/${code}`, { params: { photos: true } }) }; \ No newline at end of file diff --git a/facade/server/routes/index.js b/facade/server/routes/index.js index a4319cd..0f2e310 100644 --- a/facade/server/routes/index.js +++ b/facade/server/routes/index.js @@ -247,10 +247,11 @@ router.get("/*", function (req, res) { module.exports = function (express) { express.use(async function (req, res, next) { - let photoSets = await api.photoSets.notEmpty(), + let photoSets = await api.photoSets.all("order"), isGalleryVisible = photoSets.length > 0; if (!isGalleryVisible) { + // TODO: implement api.photos.count() method let allPhotos = await api.photos.all(); isGalleryVisible = allPhotos.length > 0; } From 2977bda4ac983c1c667b9abb1acdb93bc05a4ad7 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sun, 5 Apr 2020 14:29:11 +0300 Subject: [PATCH 13/15] Facade - `api.photos.count` method implemented --- api/facade.http | 5 +++++ api/routes/facade.js | 8 ++++++-- api/store/store.js | 2 +- facade/server/lib/api.js | 1 + facade/server/routes/index.js | 10 ++++------ 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/api/facade.http b/api/facade.http index a59d8b5..7eadcb4 100644 --- a/api/facade.http +++ b/api/facade.http @@ -17,6 +17,11 @@ GET {{baseUrl}}/gallery/photosets/not-empty {{auth}} {{ajax}} +### Get all photos count +GET {{baseUrl}}/gallery/photos/count +{{auth}} +{{ajax}} + ### Get all photos GET {{baseUrl}}/gallery/photos {{auth}} diff --git a/api/routes/facade.js b/api/routes/facade.js index 82a004e..abce81a 100644 --- a/api/routes/facade.js +++ b/api/routes/facade.js @@ -5,9 +5,9 @@ const express = require("express"), router.get("/users/can-register", async function (req, res) { if (config.registrationMode === "open" || await store.users.count() === 0) { - return res.send(true).end(); + return res.json(true); } - return res.send(false).end(); + return res.json(false); }); router.get("/user/:usernameOrEmail", async function (req, res) { @@ -75,6 +75,10 @@ router.get("/gallery/photoset/:code", async function (req, res) { return res.json(photoSet); }); +router.get("/gallery/photos/count", async function (req, res) { + let result = await store.photos.count({ deleted: { $exists: false } }); + return res.json(result); +}); router.get("/gallery/photos", async function (req, res) { let sortStr = req.query.sort, diff --git a/api/store/store.js b/api/store/store.js index 1c61771..24515c1 100644 --- a/api/store/store.js +++ b/api/store/store.js @@ -25,7 +25,7 @@ class Store { } async count(query, options) { if (query === undefined && options === undefined) { - return this.collection.estimatedDocumentCount(query, options); + return this.collection.estimatedDocumentCount(); } return this.collection.countDocuments(query, options); } diff --git a/facade/server/lib/api.js b/facade/server/lib/api.js index cccdabf..08e6b9c 100644 --- a/facade/server/lib/api.js +++ b/facade/server/lib/api.js @@ -75,6 +75,7 @@ module.exports.users = { module.exports.photos = { all: (sort, skip, limit) => call.get("/gallery/photos", { params: { sort, skip, limit } }), + count: () => call.get("/gallery/photos/count"), get: id => call.get("/gallery/photo/" + id) }; diff --git a/facade/server/routes/index.js b/facade/server/routes/index.js index 0f2e310..25ac950 100644 --- a/facade/server/routes/index.js +++ b/facade/server/routes/index.js @@ -43,9 +43,8 @@ router.get("/gallery", async function (req, res) { } // if there are still photos to display (unclassified) - then redirect to /gallery/all - // TODO: implement api.photos.count() method - let allPhotos = await api.photos.all(); - if (allPhotos.length > 0) { + let allPhotosCount = await api.photos.count(); + if (allPhotosCount > 0) { return res.redirect("/gallery/all"); } @@ -251,9 +250,8 @@ module.exports = function (express) { isGalleryVisible = photoSets.length > 0; if (!isGalleryVisible) { - // TODO: implement api.photos.count() method - let allPhotos = await api.photos.all(); - isGalleryVisible = allPhotos.length > 0; + let allPhotosCount = await api.photos.count(); + isGalleryVisible = allPhotosCount > 0; } res.locals = res.locals || {}; From eb0e64ddcf8fea51331d672ff6fe86c998238adb Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Sun, 5 Apr 2020 18:12:59 +0300 Subject: [PATCH 14/15] Dashboard - check user authentication before rendering page --- CHANGELOG.md | 3 ++- api/routes/index.js | 6 ++++- dashboard/.env | 1 + dashboard/src/error-handler.js | 8 +++--- dashboard/src/main.js | 45 +++++++++++++++++++++++----------- 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed04eb5..f312347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Changelog ## **2.0.2** - *2020-xx-xx* -* Facade - show Slick header slideshow only on homepage; * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); +* Facade - show Slick header slideshow only on homepage; +* Dashboard - check user authentication before rendering page; * Gallery: * `GALLERY_NEW_PHOTOS_FIRST` option added; * resize photoset cover; diff --git a/api/routes/index.js b/api/routes/index.js index e07f002..fbdeb27 100644 --- a/api/routes/index.js +++ b/api/routes/index.js @@ -3,7 +3,11 @@ const express = require("express"), config = require("@config"), vuetifyFileBrowserSDK = require("vuetify-file-browser-server/sdk"); -router.post("/send-email", async function (req, res) { +router.get("/auth", (req, res) => { + res.json(true); +}); + +router.post("/send-email", async (req, res) => { let subject = req.body.subject, message = req.body.message; diff --git a/dashboard/.env b/dashboard/.env index dc267da..9c66bda 100644 --- a/dashboard/.env +++ b/dashboard/.env @@ -2,5 +2,6 @@ PORT=8083 BASE_URL=/dashboard/ COMMON_STATIC_PATH=../public VUE_APP_API_BASE_URL=http://localhost:8080/api +VUE_APP_SIGNIN_URL=/signin?return= VUE_APP_GALLERY_DASHBOARD_THUMBNAIL_SUFFIX=tm VUE_APP_GALLERY_NEW_PHOTOS_FIRST=true \ No newline at end of file diff --git a/dashboard/src/error-handler.js b/dashboard/src/error-handler.js index c65b684..9e5f8c5 100644 --- a/dashboard/src/error-handler.js +++ b/dashboard/src/error-handler.js @@ -13,7 +13,9 @@ export default function(vueApp) { case 401: window.location = - "/signin?return=" + location.pathname + location.search; + process.env.VUE_APP_SIGNIN_URL + + location.pathname + + location.search; return; default: @@ -44,7 +46,7 @@ export default function(vueApp) { console.log(url); console.log(line); console.log(col); - console.log(error); + console.error(error); } }; @@ -53,7 +55,7 @@ export default function(vueApp) { { //if (options.env === "development") { console.log("unhandledrejection"); - console.log(event); + console.error(event); } }); } diff --git a/dashboard/src/main.js b/dashboard/src/main.js index e0414a8..82ae3c6 100644 --- a/dashboard/src/main.js +++ b/dashboard/src/main.js @@ -22,17 +22,34 @@ Vue.axios = Vue.prototype.$http = axios.create({ } }); -// register Toast plugin -Vue.use(Toast); -let app = new Vue({ - router, - store, - vuetify, - render: h => h(App) -}).$mount("#app"); - -errorHandler(app); - -console.log( - `Welcome to NordicSoft Express 4 Template! Environment: ${process.env.NODE_ENV}` -); +Vue.axios("/auth") + .then(() => { + // register Toast plugin + Vue.use(Toast); + + // start Dashboard app + let app = new Vue({ + router, + store, + vuetify, + render: h => h(App) + }).$mount("#app"); + + // catch all errors + errorHandler(app); + + console.log( + `Welcome to NordicSoft Express 4 Template! Environment: ${process.env.NODE_ENV}` + ); + }) + .catch(err => { + if (err.response && err.response.status === 401) { + window.location = + process.env.VUE_APP_SIGNIN_URL + + location.pathname + + location.search; + return; + } + + console.error(err); + }); From 29657ac5e99662f8e58da1d198a1627f59d5eed0 Mon Sep 17 00:00:00 2001 From: Yurii Semeniuk Date: Tue, 7 Apr 2020 15:42:12 +0300 Subject: [PATCH 15/15] user authentication & registration implemented inside Dashboard --- CHANGELOG.md | 6 +- api/lib/auth.js | 22 +++-- api/package.json | 2 +- api/router.js | 1 + api/routes/auth.js | 114 ++++++++++++++++++++++++ api/routes/facade.js | 5 +- api/routes/index.js | 4 - dashboard/.env | 3 +- dashboard/.eslintrc.js | 9 +- dashboard/package.json | 2 +- dashboard/public/error.html | 41 +++++++++ dashboard/public/img/error.svg | 1 + dashboard/server.js | 18 +++- dashboard/src/auth/App.vue | 51 +++++++++++ dashboard/src/auth/Register.vue | 131 ++++++++++++++++++++++++++++ dashboard/src/auth/SignIn.vue | 73 ++++++++++++++++ dashboard/src/auth/main.js | 52 +++++++++++ dashboard/src/auth/router.js | 23 +++++ dashboard/src/components/Navbar.vue | 15 +++- dashboard/src/error-handler.js | 20 +++-- dashboard/src/main.js | 21 +++-- dashboard/src/store/index.js | 3 + dashboard/vue.config.js | 82 ++++++++++++++++- facade/.env.defaults | 3 + facade/package.json | 2 +- facade/server/config.js | 5 ++ facade/server/views/_header.html | 4 +- package.json | 2 +- 28 files changed, 666 insertions(+), 49 deletions(-) create mode 100644 api/routes/auth.js create mode 100644 dashboard/public/error.html create mode 100644 dashboard/public/img/error.svg create mode 100644 dashboard/src/auth/App.vue create mode 100644 dashboard/src/auth/Register.vue create mode 100644 dashboard/src/auth/SignIn.vue create mode 100644 dashboard/src/auth/main.js create mode 100644 dashboard/src/auth/router.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f312347..cad2093 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Changelog -## **2.0.2** - *2020-xx-xx* +## **2.1.0** - *2020-04-07* * API - `REGISTRATION_MODE` option added (possible values are `open` and `invite`); * Facade - show Slick header slideshow only on homepage; -* Dashboard - check user authentication before rendering page; +* Dashboard: + * check user authentication before rendering page; + * user authentication & registration implemented inside Dashboard; * Gallery: * `GALLERY_NEW_PHOTOS_FIRST` option added; * resize photoset cover; diff --git a/api/lib/auth.js b/api/lib/auth.js index f17e3ac..85dadc3 100644 --- a/api/lib/auth.js +++ b/api/lib/auth.js @@ -19,7 +19,7 @@ var security = require("./security"), strategies = { local: new LocalStrategy({ // by default, local strategy uses username and password, we will override with email - usernameField: "email", + usernameField: "username", passwordField: "password", passReqToCallback: true // allows us to pass back the entire request to the callback }, @@ -31,23 +31,23 @@ var security = require("./security"), // find a user let user; try { - user = await store.users.getByEmail(username); + user = await store.users[username.includes("@") ? "getByEmail": "getByUsername"](username); logger.dir(user); } catch (err) { logger.error(err); - return done(null, false, "Unknown error"); + return done(null, false, { message: "Unknown error" }); } // if no user is found or password is wrong return error if (!user) { logger.info("User not found"); - return done(null, false, "User was not found or password is incorrect"); + return done(null, false, { message: "User was not found or password is incorrect" }); } switch (config.passwordHashAlgorithm) { case "md5": if (md5(password) !== user.password) { - return done(null, false, "User was not found or password is incorrect"); + return done(null, false, { message: "User was not found or password is incorrect" }); } // all is well, return successful user logger.info("Signin successful"); @@ -56,11 +56,11 @@ var security = require("./security"), security.bcryptCheck(password, user.password, function (err, result) { if (err) { logger.error("password check failed", err); - return done(null, false, "Unknown error"); + return done(null, false, { message: "Unknown error" }); } if (!result) { logger.info(!user ? "User not found" : "Password is incorrect"); - return done(null, false, "User was not found or password is incorrect"); + return done(null, false, { message: "User was not found or password is incorrect" }); } // all is well, return successful user @@ -231,6 +231,14 @@ var security = require("./security"), module.exports = function (express) { logger.info("Init Authentication"); + if (config.registrationMode === "open") { + express.set("registration-enabled", true); + } else { + store.users.count().then(count => { + express.set("registration-enabled", count === 0); + }); + } + // used to serialize the user for the session passport.serializeUser(function (user, done) { logger.debug("serializeUser " + user.email); diff --git a/api/package.json b/api/package.json index 3abfcc9..5cbea0a 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "express-template-api", - "version": "2.0.1", + "version": "2.1.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT", diff --git a/api/router.js b/api/router.js index 494d108..bb1200f 100644 --- a/api/router.js +++ b/api/router.js @@ -45,6 +45,7 @@ module.exports = function (express) { express.use("/profile", signinRequired, require("./routes/profile")); express.use("/gallery", signinRequired, require("./routes/gallery")); express.use("/facade", facadeOnly, require("./routes/facade")); + express.use("/auth", require("./routes/auth")); express.use("/", signinRequired, require("./routes")); // handle 404 diff --git a/api/routes/auth.js b/api/routes/auth.js new file mode 100644 index 0000000..9cc632b --- /dev/null +++ b/api/routes/auth.js @@ -0,0 +1,114 @@ +const express = require("express"), + router = express.Router(), + logger = require("@logger"), + config = require("@config"), + store = require("@store"); + +function signin(req, res) { + req.signin(function (err, user, info) { + if (user) { + return res.json({}); + } + return res.status(400).json(info); + }); +} + +async function refreshRegistrationEnabled(app) { + let registrationEnabled = config.registrationMode === "open" || await store.users.count() === 0; + app.set("registration-enabled", registrationEnabled); +} + +router.get("/", function (req, res) { + return res.json({ + isAuthenticated: req.isAuthenticated(), + registrationEnabled: req.app.get("registration-enabled") + }); +}); + +router.get("/check-email/:email", async function (req, res) { + var email = req.params.email, + user = await store.users.getByEmail(email); + return res.json(!user); +}); + +router.get("/check-username/:username", async function (req, res) { + var username = req.params.username, + user = await store.users.getByUsername(username); + return res.json(!user); +}); + +router.post("/signin", signin); + +router.post("/register", async function (req, res) { + if (req.isAuthenticated()) { + return res.status(400).json({ message: "User is authenticated"}); + } + + if (!req.app.get("registration-enabled")) { + return res.sendStatus(404); + } + + let name = req.body.name, + email = req.body.email, + username = req.body.username, + password = req.body.password, + passwordConfirm = req.body.passwordConfirm; + + logger.info(`Register new user ${name} (${email})`); + + if (password !== passwordConfirm) { + return res.status(400).json({ + message: "Password and confirm password does not match", + }); + } + + // check username + if (await store.users.getByUsername(username)) { + return res.status(400).json({ message: "This username is already taken" }); + } + + // check email + if (await store.users.getByEmail(email)) { + return res.status(400).json({ message: "This email is already taken" }); + } + + let security = require("@lib/security"); + let user = { + name, + email, + username + }; + + let usersCount = await store.users.count(); + if (usersCount === 0) { + user.roles = ["owner"]; + } + + switch (config.passwordHashAlgorithm) { + case "md5": + user.password = security.md5(password); + await store.users.insert(user); + refreshRegistrationEnabled(req.app); + signin(req, res); + break; + case "bcrypt": + security.bcryptHash(password, async function (err, passwordHash) { + user.password = passwordHash; + await store.users.insert(user); + refreshRegistrationEnabled(req.app); + signin(req, res); + }); + break; + default: + logger.error("Incorrect passwordHashAlgorithm specified in config.json"); + break; + } +}); + +router.get("/signout", function (req, res) { + req.signout(); + req.session.destroy(); + return res.json(true); +}); + +module.exports = router; diff --git a/api/routes/facade.js b/api/routes/facade.js index abce81a..bcfc262 100644 --- a/api/routes/facade.js +++ b/api/routes/facade.js @@ -4,10 +4,7 @@ const express = require("express"), store = require("@store"); router.get("/users/can-register", async function (req, res) { - if (config.registrationMode === "open" || await store.users.count() === 0) { - return res.json(true); - } - return res.json(false); + return res.json(req.app.get("registration-enabled")); }); router.get("/user/:usernameOrEmail", async function (req, res) { diff --git a/api/routes/index.js b/api/routes/index.js index fbdeb27..e5bf8e9 100644 --- a/api/routes/index.js +++ b/api/routes/index.js @@ -3,10 +3,6 @@ const express = require("express"), config = require("@config"), vuetifyFileBrowserSDK = require("vuetify-file-browser-server/sdk"); -router.get("/auth", (req, res) => { - res.json(true); -}); - router.post("/send-email", async (req, res) => { let subject = req.body.subject, message = req.body.message; diff --git a/dashboard/.env b/dashboard/.env index 9c66bda..e629ab6 100644 --- a/dashboard/.env +++ b/dashboard/.env @@ -2,6 +2,7 @@ PORT=8083 BASE_URL=/dashboard/ COMMON_STATIC_PATH=../public VUE_APP_API_BASE_URL=http://localhost:8080/api -VUE_APP_SIGNIN_URL=/signin?return= +VUE_APP_FACADE_URL=/ +VUE_APP_SIGNIN_URL=/dashboard/auth?return= VUE_APP_GALLERY_DASHBOARD_THUMBNAIL_SUFFIX=tm VUE_APP_GALLERY_NEW_PHOTOS_FIRST=true \ No newline at end of file diff --git a/dashboard/.eslintrc.js b/dashboard/.eslintrc.js index 31ea0b1..55e5b30 100644 --- a/dashboard/.eslintrc.js +++ b/dashboard/.eslintrc.js @@ -9,7 +9,14 @@ module.exports = { }, rules: { //"no-console": process.env.NODE_ENV === "production" ? "error" : "off", - "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off" + "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", + indent: ["error", 4, { SwitchCase: 1 }], + "linebreak-style": [ + process.env.NODE_ENV === "production" ? "error" : "off", + "windows" + ], + quotes: ["error", "double"], + semi: ["error", "always"] }, overrides: [ { diff --git a/dashboard/package.json b/dashboard/package.json index f773284..d75308b 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "express-template-dashboard", - "version": "2.0.1", + "version": "2.1.0", "private": true, "main": "server.js", "scripts": { diff --git a/dashboard/public/error.html b/dashboard/public/error.html new file mode 100644 index 0000000..58429f0 --- /dev/null +++ b/dashboard/public/error.html @@ -0,0 +1,41 @@ + + + + + + + + Unknown Error + + + + +

Unknown error occurred

+
+ + + diff --git a/dashboard/public/img/error.svg b/dashboard/public/img/error.svg new file mode 100644 index 0000000..a634e57 --- /dev/null +++ b/dashboard/public/img/error.svg @@ -0,0 +1 @@ +bug fixing \ No newline at end of file diff --git a/dashboard/server.js b/dashboard/server.js index 01f671b..5ce5a64 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -7,6 +7,7 @@ require("dotenv-defaults").config(); const express = require("express"), path = require("path"), history = require("connect-history-api-fallback"), + publicPath = process.env.BASE_URL, port = parseInt(process.env.DASHBOARD_PORT || process.env.PORT || 8083); const app = express(); @@ -18,18 +19,29 @@ const publicMiddleware = express.static( // https://github.com/bripkens/connect-history-api-fallback/blob/master/examples/static-files-and-index-rewrite/README.md -app.use(process.env.BASE_URL, distMiddleware); +app.use(publicPath, distMiddleware); app.use(publicMiddleware); app.use( history({ disableDotRule: true, verbose: true, - index: path.join(process.env.BASE_URL, "index.html") + index: path.join(publicPath, "index.html"), + rewrites: [ + { + from: new RegExp(`^${publicPath}auth.*$`), + to: `${publicPath}auth.html` + }, + { + from: new RegExp(`^${publicPath}error/?$`), + to: `${publicPath}error.html` + }, + { from: /./, to: publicPath + "index.html" } + ] }) ); -app.use(process.env.BASE_URL, distMiddleware); +app.use(publicPath, distMiddleware); app.use(publicMiddleware); app.get("/", function(req, res) { diff --git a/dashboard/src/auth/App.vue b/dashboard/src/auth/App.vue new file mode 100644 index 0000000..bae72ac --- /dev/null +++ b/dashboard/src/auth/App.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/dashboard/src/auth/Register.vue b/dashboard/src/auth/Register.vue new file mode 100644 index 0000000..1d1f767 --- /dev/null +++ b/dashboard/src/auth/Register.vue @@ -0,0 +1,131 @@ + + + diff --git a/dashboard/src/auth/SignIn.vue b/dashboard/src/auth/SignIn.vue new file mode 100644 index 0000000..0102b59 --- /dev/null +++ b/dashboard/src/auth/SignIn.vue @@ -0,0 +1,73 @@ + + + diff --git a/dashboard/src/auth/main.js b/dashboard/src/auth/main.js new file mode 100644 index 0000000..004895e --- /dev/null +++ b/dashboard/src/auth/main.js @@ -0,0 +1,52 @@ +import Vue from "vue"; +import App from "./App.vue"; +import router from "./router"; +import vuetify from "@/plugins/vuetify"; +import Toast from "@/plugins/toast"; +import axios from "axios"; + +import errorHandler from "@/error-handler"; +import "roboto-fontface/css/roboto/roboto-fontface.css"; +import "@mdi/font/css/materialdesignicons.css"; + +Vue.config.productionTip = false; + +// setup axios +Vue.axios = Vue.prototype.$http = axios.create({ + baseURL: process.env.VUE_APP_API_BASE_URL, + withCredentials: true, + headers: { + "X-Requested-With": "XMLHttpRequest" + } +}); + +Vue.axios("/auth") + .then(response => { + let { isAuthenticated } = response.data; + + if (isAuthenticated) { + window.location = process.env.BASE_URL.slice(0, -1); + return; + } + + // register Toast plugin + Vue.use(Toast); + + // start Dashboard app + let app = new Vue({ + router, + vuetify, + render: h => h(App) + }).$mount("#app"); + + // catch all errors + errorHandler(app); + + console.log( + `Sign In to NordicSoft Express 4 Template! Environment: ${process.env.NODE_ENV}` + ); + }) + .catch(err => { + // TODO: redirect to `error` page + console.error(err); + }); diff --git a/dashboard/src/auth/router.js b/dashboard/src/auth/router.js new file mode 100644 index 0000000..10756c8 --- /dev/null +++ b/dashboard/src/auth/router.js @@ -0,0 +1,23 @@ +import Vue from "vue"; +import Router from "vue-router"; +import SignIn from "./SignIn"; +import Register from "./Register"; + +Vue.use(Router); + +export default new Router({ + mode: "history", + base: process.env.BASE_URL + "auth/", + routes: [ + { + path: "", + name: "signin", + component: SignIn + }, + { + path: "/register", + name: "register", + component: Register + } + ] +}); diff --git a/dashboard/src/components/Navbar.vue b/dashboard/src/components/Navbar.vue index cee2713..4992c0a 100644 --- a/dashboard/src/components/Navbar.vue +++ b/dashboard/src/components/Navbar.vue @@ -20,7 +20,7 @@ - + mdi-feature-search-outline Facade @@ -28,7 +28,7 @@ mdi-account-box-outline Profile - + mdi-exit-to-app Sign Out @@ -103,8 +103,15 @@ diff --git a/dashboard/src/error-handler.js b/dashboard/src/error-handler.js index 9e5f8c5..42ef12a 100644 --- a/dashboard/src/error-handler.js +++ b/dashboard/src/error-handler.js @@ -4,12 +4,22 @@ import Vue from "vue"; export default function(vueApp) { // handle errors Vue.config.errorHandler = function(error, vm, info) { - if (error.isAxiosError) { + if (error.isAxiosError && error.response) { let response = error.response; switch (response.status) { case 400: - vueApp.$toast.error(response.data); - break; + if (typeof response.data === "string") { + vueApp.$toast.warning(response.data); + } else if ( + typeof response.data === "object" && + response.data.message + ) { + vueApp.$toast.warning(response.data.message); + } else { + vueApp.$toast.error("Unknown error"); + console.error(error); + } + return; case 401: window.location = @@ -20,10 +30,8 @@ export default function(vueApp) { default: vueApp.$toast.error("Request completed with error"); - break; + return; } - - return; } vueApp.$toast.error("Unknown error occurred"); diff --git a/dashboard/src/main.js b/dashboard/src/main.js index 82ae3c6..3072d9b 100644 --- a/dashboard/src/main.js +++ b/dashboard/src/main.js @@ -23,7 +23,17 @@ Vue.axios = Vue.prototype.$http = axios.create({ }); Vue.axios("/auth") - .then(() => { + .then(response => { + let { isAuthenticated } = response.data; + + if (!isAuthenticated) { + window.location = + process.env.VUE_APP_SIGNIN_URL + + location.pathname + + location.search; + return; + } + // register Toast plugin Vue.use(Toast); @@ -43,13 +53,6 @@ Vue.axios("/auth") ); }) .catch(err => { - if (err.response && err.response.status === 401) { - window.location = - process.env.VUE_APP_SIGNIN_URL + - location.pathname + - location.search; - return; - } - + // TODO: redirect to `error` page console.error(err); }); diff --git a/dashboard/src/store/index.js b/dashboard/src/store/index.js index 08cad19..5337a30 100644 --- a/dashboard/src/store/index.js +++ b/dashboard/src/store/index.js @@ -19,6 +19,9 @@ const mutations = { const actions = { loading({ commit }, payload) { commit("loading", payload); + }, + async signOut() { + await Vue.axios.get("/auth/signout"); } }; diff --git a/dashboard/vue.config.js b/dashboard/vue.config.js index fea7acc..18cc14c 100644 --- a/dashboard/vue.config.js +++ b/dashboard/vue.config.js @@ -1,8 +1,54 @@ -const path = require("path"); +const path = require("path"), + publicPath = process.env.BASE_URL; + +// https://cli.vuejs.org/config/#pages +const pages = { + index: { + entry: "src/main.js", + template: "public/index.html", + filename: "index.html", + title: "Express Template Dashboard", + chunks: [ + "chunk-vendors", + "chunk-index-vendors", + "chunk-common", + "index" + ] + }, + auth: { + entry: "src/auth/main.js", + template: "public/index.html", + filename: "auth.html", + title: "Authenticate", + chunks: ["chunk-vendors", "chunk-auth-vendors", "chunk-common", "auth"] + } +}; + module.exports = { + publicPath, + pages, + transpileDependencies: ["vuetify"], - publicPath: process.env.BASE_URL, + + lintOnSave: process.env.NODE_ENV !== 'production', + devServer: { + historyApiFallback: { + verbose: true, + disableDotRule: false, + rewrites: [ + { + from: new RegExp(`^${publicPath}auth.*$`), + to: `${publicPath}auth.html` + }, + { + from: new RegExp(`^${publicPath}error/?$`), + to: `${publicPath}error.html` + }, + { from: /./, to: publicPath + "index.html" } + //{ from: /./, to: context => { console.log(context); return context.parsedUrl.pathname; } } + ] + }, contentBase: [ path.resolve(__dirname, "dist"), path.resolve(__dirname, process.env.COMMON_STATIC_PATH) @@ -10,5 +56,37 @@ module.exports = { watchContentBase: false, port: process.env.PORT || 8083, progress: false + }, + + chainWebpack: config => { + // https://github.com/vuejs/vue-cli/issues/2381#issuecomment-425038367 + const IS_VENDOR = /[\\/]node_modules[\\/]/; + config.optimization.splitChunks({ + cacheGroups: { + vendors: { + name: "chunk-vendors", + priority: -10, + chunks: "initial", + minChunks: 2, + test: IS_VENDOR, + enforce: true + }, + ...Object.keys(pages).map(key => ({ + name: `chunk-${key}-vendors`, + priority: -11, + chunks: chunk => chunk.name === key, + test: IS_VENDOR, + enforce: true + })), + common: { + name: "chunk-common", + priority: -20, + chunks: "initial", + minChunks: 2, + reuseExistingChunk: true, + enforce: true + } + } + }); } }; diff --git a/facade/.env.defaults b/facade/.env.defaults index 350bc8a..b0ddd20 100644 --- a/facade/.env.defaults +++ b/facade/.env.defaults @@ -16,6 +16,9 @@ LOG_CONSOLE_BG_COLOR=royalblue STATIC_PATH=public COMMON_STATIC_PATH=../public +AUTH_SIGNIN_URL=/dashboard/auth +AUTH_REGISTER_URL=/dashboard/auth/register + # password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) PASSWORD_HASH_ALGORITHM=md5 diff --git a/facade/package.json b/facade/package.json index 8be2ab9..64955dc 100644 --- a/facade/package.json +++ b/facade/package.json @@ -1,6 +1,6 @@ { "name": "express-template-facade", - "version": "2.0.1", + "version": "2.1.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT", diff --git a/facade/server/config.js b/facade/server/config.js index b7d0e2c..93a7a0b 100644 --- a/facade/server/config.js +++ b/facade/server/config.js @@ -26,6 +26,11 @@ module.exports = { token: process.env.API_TOKEN, }, + auth: { + signInUrl: process.env.AUTH_SIGNIN_URL, + registerUrl: process.env.AUTH_REGISTER_URL + }, + //password hashing algorithm (md5 or bcrypt; for bcrypt install https://www.npmjs.com/package/bcrypt) passwordHashAlgorithm: process.env.PASSWORD_HASH_ALGORITHM, diff --git a/facade/server/views/_header.html b/facade/server/views/_header.html index 6cac04d..17420e8 100644 --- a/facade/server/views/_header.html +++ b/facade/server/views/_header.html @@ -47,11 +47,11 @@ {{else}} {{#if registrationEnabled}} {{/if}} {{/if}} diff --git a/package.json b/package.json index 7c5d98f..52a92bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-template", - "version": "2.0.1", + "version": "2.1.0", "description": "Website template (skeleton) based on Express.js 4, Vue.js and Vuetify 2", "author": "NordicSoft", "license": "MIT",