From 23b71d7aaa346dd804f63feb9f9a5ba3af17facb Mon Sep 17 00:00:00 2001 From: Ruben Shekspir Date: Sat, 22 Jul 2023 21:13:09 +0400 Subject: [PATCH 1/4] Added docker updated mongoose added reactions with page view functoinality --- .env.docker | 7 +++++++ .gitignore | 2 ++ Dockerfile | 11 +++++++++++ Makefile | 12 ++++++++++++ docker-compose.dev.yml | 11 +++++++++++ docker-compose.yml | 15 +++++++++++++++ package.json | 2 +- src/api/index.js | 2 ++ src/api/reactions/controller.js | 32 ++++++++++++++++++++++++++++++++ src/api/reactions/helper.js | 24 ++++++++++++++++++++++++ src/api/reactions/index.js | 9 +++++++++ src/api/reactions/model.js | 30 ++++++++++++++++++++++++++++++ src/services/mailer/index.js | 7 +++++++ src/services/mongoose/index.js | 4 ---- 14 files changed, 163 insertions(+), 5 deletions(-) create mode 100644 .env.docker create mode 100644 Dockerfile create mode 100644 Makefile create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 src/api/reactions/controller.js create mode 100644 src/api/reactions/helper.js create mode 100644 src/api/reactions/index.js create mode 100644 src/api/reactions/model.js diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..aace24f --- /dev/null +++ b/.env.docker @@ -0,0 +1,7 @@ +MONGODB_URI=mongodb://mongo:27017/foo-comments +API_URL=http://localhost:9547/api +PORT=9547 + +ADMIN_EMAIL_ADDR= +BOT_EMAIL_ADDR= +BOT_EMAIL_PASS= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 52d3056..2053b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ jsconfig.json yarn.lock .DS_Store *package-lock.json + +mongo-data diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7a550f3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY ["package.json", "package-lock.json*", "./"] + +RUN npm install --production + +COPY . . + +CMD ["node", "index .js"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5775965 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +build: + docker build -t foo-comments-server . + +up: build + docker-compose up -d + + +down: + docker-compose down + +dev: + docker-compose -f docker-compose.dev.yml up diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..b386dcb --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,11 @@ +version: '3.8' + +services: + + mongo: + image: mongo + restart: always + volumes: + - './mongo-data:/data/db' + ports: + - 27017:27017 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..edf1a3f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + + mongo: + image: mongo + restart: always + volumes: + - './mongo-data:/data/db' + + foo-comment: + image: foo-comments-server + restart: always + volumes: + - ./.env.docker:/app/.env:ro \ No newline at end of file diff --git a/package.json b/package.json index 092dfdd..43e5b42 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "handlebars": "^4.7.6", "handlebars-dateformat": "^1.1.1", "moment": "^2.29.1", - "mongoose": "^5.10.11", + "mongoose": "^7.4.0", "nodemailer": "^6.4.15", "nodemon": "^2.0.6", "pm2": "^4.5.0" diff --git a/src/api/index.js b/src/api/index.js index c0fe00c..e85dd02 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,8 +1,10 @@ import { Router } from 'express'; import comments from './comments'; +import reactions from './reactions'; const router = new Router(); router.use('/comments', comments); +router.use('/reactions', reactions); export default router; diff --git a/src/api/reactions/controller.js b/src/api/reactions/controller.js new file mode 100644 index 0000000..2506da0 --- /dev/null +++ b/src/api/reactions/controller.js @@ -0,0 +1,32 @@ +import Reaction from './model'; +import { getReactions } from './helper'; + +import { asyncRoute } from '../../services/express'; +const VIEW = 'view'; + +export const reactOrView = asyncRoute(async (req, res) => { + const fingerprint = req.headers.fingerprint; + const pageId = req.body.pageId || req.query.pageId; + const reaction = (req.body.reaction || VIEW).slice(0, 20); // Just in case, limit characters by 20 + + if (!fingerprint) { + return res.status(500).send('Fingerprint required for reacting.'); + } + + const searchCondition = { pageId, fingerprint }; + const recordInDB = await Reaction.findOne(searchCondition); + + const newReaction = recordInDB && recordInDB.reaction === reaction ? VIEW : reaction; + if (recordInDB && reaction !== VIEW) { + recordInDB.reaction = newReaction; + await recordInDB.save(); + } + + if (!recordInDB) { + await new Reaction({ ...searchCondition, reaction }).save(); + } + + const reactions = await getReactions(searchCondition); + + return res.status(200).json({ reactions }); +}); diff --git a/src/api/reactions/helper.js b/src/api/reactions/helper.js new file mode 100644 index 0000000..4c5bed3 --- /dev/null +++ b/src/api/reactions/helper.js @@ -0,0 +1,24 @@ +import Reaction from './model'; + +export const getReactions = async ({ fingerprint, pageId }) => { + + const userReactionPromise = Reaction.findOne({ fingerprint, pageId }); + const pageViewsPromise = Reaction.find({ pageId }).count(); + const aggregationPromise = Reaction.aggregate() + .match({ + fingerprint, + pageId + }) + .group({ + _id: '$reaction', + count: { $count: {} } + }); + + const [aggregation, reaction, pageViews] = await Promise.all([ + aggregationPromise, + userReactionPromise, + pageViewsPromise + ]); + + return { aggregation, reaction, pageViews }; +}; diff --git a/src/api/reactions/index.js b/src/api/reactions/index.js new file mode 100644 index 0000000..fd17637 --- /dev/null +++ b/src/api/reactions/index.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { reactOrView } from './controller'; + +const router = new Router(); + +router.post('/', reactOrView); +router.get('/', reactOrView); + +export default router; diff --git a/src/api/reactions/model.js b/src/api/reactions/model.js new file mode 100644 index 0000000..ad572b4 --- /dev/null +++ b/src/api/reactions/model.js @@ -0,0 +1,30 @@ +import mongoose, { Schema } from 'mongoose'; + +const schema = new Schema( + { + owner: { + ip: { + type: String, + required: false + } + }, + fingerprint: { + type: String, + required: true + }, + pageId: { + type: String, + required: true + }, + reaction: { + type: String, + } + }, + { + timestamps: true + } +); + +const model = mongoose.model('Reaction', schema); + +export default model; diff --git a/src/services/mailer/index.js b/src/services/mailer/index.js index eda6adf..b8810a3 100644 --- a/src/services/mailer/index.js +++ b/src/services/mailer/index.js @@ -1,6 +1,10 @@ import nodemailer from 'nodemailer'; import moment from 'moment'; +if (!process.env.ADMIN_EMAIL_ADDR) { + console.warn('WARNIGN: ADMIN_EMAIL_ADDR Admin email is not present, you will not be notified about new emails.'); +} + const transporter = nodemailer.createTransport({ port: 465, secure: true, @@ -12,6 +16,9 @@ const transporter = nodemailer.createTransport({ }); export async function newCommentNotification(comment) { + if (!process.env.ADMIN_EMAIL_ADDR) { + return; + } const date = moment(comment.createdAt).format('DD.MMM.YYYY - HH:mm'); await transporter.sendMail({ from: `"FooCommmets" <${process.env.BOT_EMAIL_ADDR}>`, diff --git a/src/services/mongoose/index.js b/src/services/mongoose/index.js index 08fda45..bc24413 100644 --- a/src/services/mongoose/index.js +++ b/src/services/mongoose/index.js @@ -1,10 +1,6 @@ import mongoose from 'mongoose'; mongoose.Promise = Promise; -mongoose.set('useNewUrlParser', true); -mongoose.set('useFindAndModify', false); -mongoose.set('useUnifiedTopology', true); -mongoose.set('useCreateIndex', true); mongoose.connection.on('error', err => { console.error('MongoDB connection error: ' + err); From 08b7c4434b2ec4b3251f6ae5490394f0d5130b98 Mon Sep 17 00:00:00 2001 From: Ruben Shekspir Date: Sun, 23 Jul 2023 09:23:19 +0400 Subject: [PATCH 2/4] fixed bug with reactions, removed redundant fields in response --- src/api/reactions/controller.js | 6 +++--- src/api/reactions/helper.js | 9 ++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/api/reactions/controller.js b/src/api/reactions/controller.js index 2506da0..d11dcbe 100644 --- a/src/api/reactions/controller.js +++ b/src/api/reactions/controller.js @@ -1,5 +1,5 @@ import Reaction from './model'; -import { getReactions } from './helper'; +import { getPageData } from './helper'; import { asyncRoute } from '../../services/express'; const VIEW = 'view'; @@ -26,7 +26,7 @@ export const reactOrView = asyncRoute(async (req, res) => { await new Reaction({ ...searchCondition, reaction }).save(); } - const reactions = await getReactions(searchCondition); + const reactions = await getPageData(searchCondition); - return res.status(200).json({ reactions }); + return res.status(200).json(reactions); }); diff --git a/src/api/reactions/helper.js b/src/api/reactions/helper.js index 4c5bed3..d6bbfd4 100644 --- a/src/api/reactions/helper.js +++ b/src/api/reactions/helper.js @@ -1,12 +1,11 @@ import Reaction from './model'; -export const getReactions = async ({ fingerprint, pageId }) => { +export const getPageData = async ({ fingerprint, pageId }) => { - const userReactionPromise = Reaction.findOne({ fingerprint, pageId }); + const userReactionPromise = Reaction.findOne({ fingerprint, pageId }).select('reaction -_id'); const pageViewsPromise = Reaction.find({ pageId }).count(); const aggregationPromise = Reaction.aggregate() .match({ - fingerprint, pageId }) .group({ @@ -14,11 +13,11 @@ export const getReactions = async ({ fingerprint, pageId }) => { count: { $count: {} } }); - const [aggregation, reaction, pageViews] = await Promise.all([ + const [aggregation, userReaction, pageViews] = await Promise.all([ aggregationPromise, userReactionPromise, pageViewsPromise ]); - return { aggregation, reaction, pageViews }; + return { aggregation, userReaction, pageViews }; }; From a81b3053be6c53b530681140531c21af2c8ffa4d Mon Sep 17 00:00:00 2001 From: Ruben Shekspir Date: Sun, 23 Jul 2023 10:09:24 +0400 Subject: [PATCH 3/4] Running with docker fixed and documented --- Dockerfile | 2 +- Makefile | 2 +- README.md | 24 ++++++++++++++++++++++++ docker-compose.yml | 13 ++++++++----- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7a550f3..cd50550 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,4 +8,4 @@ RUN npm install --production COPY . . -CMD ["node", "index .js"] +CMD ["node", "index.js"] diff --git a/Makefile b/Makefile index 5775965..05c2fc1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ build: docker build -t foo-comments-server . -up: build +up: docker-compose up -d diff --git a/README.md b/README.md index a482f6f..f59be8a 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,27 @@ $ npm run kill ``` +## Docker + +modify `.env.docker` to run with docker. + +### run mongo with docker + +``` +make dev +``` +### build the docker image +``` +make build +``` + +### start/stop system +To start +``` +make up +``` + +To stop +``` +make down +``` diff --git a/docker-compose.yml b/docker-compose.yml index edf1a3f..9305aad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,18 @@ version: '3.8' services: - mongo: image: mongo - restart: always + restart: always volumes: - './mongo-data:/data/db' - - foo-comment: + + foo-comment-server: image: foo-comments-server restart: always volumes: - - ./.env.docker:/app/.env:ro \ No newline at end of file + - ./.env.docker:/app/.env:ro + ports: + - 9547:9547 + depends_on: + - mongo From 7e7973ca8095b039185b24d5272765c1fb983322 Mon Sep 17 00:00:00 2001 From: tigran Date: Sun, 17 Dec 2023 20:28:25 +0400 Subject: [PATCH 4/4] added reactions --- src/api/reactions/controller.js | 30 +++++++++++++++++++----------- src/api/reactions/helper.js | 13 +++++++------ src/api/reactions/index.js | 6 +++--- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/api/reactions/controller.js b/src/api/reactions/controller.js index d11dcbe..6e83147 100644 --- a/src/api/reactions/controller.js +++ b/src/api/reactions/controller.js @@ -1,13 +1,11 @@ import Reaction from './model'; import { getPageData } from './helper'; - import { asyncRoute } from '../../services/express'; -const VIEW = 'view'; -export const reactOrView = asyncRoute(async (req, res) => { +export const add = asyncRoute(async (req, res) => { const fingerprint = req.headers.fingerprint; const pageId = req.body.pageId || req.query.pageId; - const reaction = (req.body.reaction || VIEW).slice(0, 20); // Just in case, limit characters by 20 + const reaction = req.body.reaction.slice(0, 20); // Just in case, limit characters by 20 if (!fingerprint) { return res.status(500).send('Fingerprint required for reacting.'); @@ -16,17 +14,27 @@ export const reactOrView = asyncRoute(async (req, res) => { const searchCondition = { pageId, fingerprint }; const recordInDB = await Reaction.findOne(searchCondition); - const newReaction = recordInDB && recordInDB.reaction === reaction ? VIEW : reaction; - if (recordInDB && reaction !== VIEW) { - recordInDB.reaction = newReaction; - await recordInDB.save(); - } - if (!recordInDB) { - await new Reaction({ ...searchCondition, reaction }).save(); + await Reaction.create({ ...searchCondition, reaction }); + } else { + if (recordInDB.reaction === reaction) { + await Reaction.deleteOne(recordInDB); + } else { + recordInDB.reaction = reaction; + await recordInDB.save(); + } } const reactions = await getPageData(searchCondition); return res.status(200).json(reactions); }); + +export const list = asyncRoute(async (req, res) => { + const fingerprint = req.headers.fingerprint; + const pageId = req.body.pageId || req.query.pageId; + + const reactions = await getPageData({ pageId, fingerprint }); + + return res.status(200).json(reactions); +}); diff --git a/src/api/reactions/helper.js b/src/api/reactions/helper.js index d6bbfd4..6f08586 100644 --- a/src/api/reactions/helper.js +++ b/src/api/reactions/helper.js @@ -1,9 +1,11 @@ import Reaction from './model'; export const getPageData = async ({ fingerprint, pageId }) => { + const userReactionPromise = Reaction.findOne({ + fingerprint, + pageId + }).select('reaction -_id'); - const userReactionPromise = Reaction.findOne({ fingerprint, pageId }).select('reaction -_id'); - const pageViewsPromise = Reaction.find({ pageId }).count(); const aggregationPromise = Reaction.aggregate() .match({ pageId @@ -13,11 +15,10 @@ export const getPageData = async ({ fingerprint, pageId }) => { count: { $count: {} } }); - const [aggregation, userReaction, pageViews] = await Promise.all([ + const [aggregation, userReaction] = await Promise.all([ aggregationPromise, - userReactionPromise, - pageViewsPromise + userReactionPromise ]); - return { aggregation, userReaction, pageViews }; + return { aggregation, userReaction }; }; diff --git a/src/api/reactions/index.js b/src/api/reactions/index.js index fd17637..0f955af 100644 --- a/src/api/reactions/index.js +++ b/src/api/reactions/index.js @@ -1,9 +1,9 @@ import { Router } from 'express'; -import { reactOrView } from './controller'; +import { list, add } from './controller'; const router = new Router(); -router.post('/', reactOrView); -router.get('/', reactOrView); +router.post('/', add); +router.get('/', list); export default router;