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..cd50550 --- /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..05c2fc1 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ +build: + docker build -t foo-comments-server . + +up: + docker-compose up -d + + +down: + docker-compose down + +dev: + docker-compose -f docker-compose.dev.yml up 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.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..9305aad --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.8' + +services: + mongo: + image: mongo + restart: always + volumes: + - './mongo-data:/data/db' + + foo-comment-server: + image: foo-comments-server + restart: always + volumes: + - ./.env.docker:/app/.env:ro + ports: + - 9547:9547 + depends_on: + - mongo 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..6e83147 --- /dev/null +++ b/src/api/reactions/controller.js @@ -0,0 +1,40 @@ +import Reaction from './model'; +import { getPageData } from './helper'; +import { asyncRoute } from '../../services/express'; + +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.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); + + if (!recordInDB) { + 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 new file mode 100644 index 0000000..6f08586 --- /dev/null +++ b/src/api/reactions/helper.js @@ -0,0 +1,24 @@ +import Reaction from './model'; + +export const getPageData = async ({ fingerprint, pageId }) => { + const userReactionPromise = Reaction.findOne({ + fingerprint, + pageId + }).select('reaction -_id'); + + const aggregationPromise = Reaction.aggregate() + .match({ + pageId + }) + .group({ + _id: '$reaction', + count: { $count: {} } + }); + + const [aggregation, userReaction] = await Promise.all([ + aggregationPromise, + userReactionPromise + ]); + + return { aggregation, userReaction }; +}; diff --git a/src/api/reactions/index.js b/src/api/reactions/index.js new file mode 100644 index 0000000..0f955af --- /dev/null +++ b/src/api/reactions/index.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { list, add } from './controller'; + +const router = new Router(); + +router.post('/', add); +router.get('/', list); + +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);