diff --git a/README.md b/README.md
new file mode 100644
index 0000000..ad9d21c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,394 @@
+
+Authors ahmedkhaled4d - A Social platform Using nodeJS .
+=======
+
+
+## Vision
+Building next genration of Social platform Using nodeJS a community of like minded authors to foster inspiration and innovation
+by leveraging the modern web (Javascript stack).
+
+---
+
+## API Spec
+The preferred JSON object to be returned by the API should be structured as follows:
+
+### Users (for authentication)
+
+```source-json
+{
+ "user": {
+ "email": "ahmed@mail.com",
+ "token": "jwt.token.here",
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": null
+ }
+}
+```
+### Profile
+```source-json
+{
+ "profile": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "image-link",
+ "following": false
+ }
+}
+```
+### Single Article
+```source-json
+{
+ "article": {
+ "slug": "how-to-train-your-dragon",
+ "title": "How to train your dragon",
+ "description": "Ever wonder how?",
+ "body": "It takes a Jacobian",
+ "tagList": ["dragons", "training"],
+ "createdAt": "2016-02-18T03:22:56.637Z",
+ "updatedAt": "2016-02-18T03:48:35.824Z",
+ "favorited": false,
+ "favoritesCount": 0,
+ "author": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg",
+ "following": false
+ }
+ }
+}
+```
+### Multiple Articles
+```source-json
+{
+ "articles":[{
+ "slug": "how-to-train-your-dragon",
+ "title": "How to train your dragon",
+ "description": "Ever wonder how?",
+ "body": "It takes a Jacobian",
+ "tagList": ["dragons", "training"],
+ "createdAt": "2016-02-18T03:22:56.637Z",
+ "updatedAt": "2016-02-18T03:48:35.824Z",
+ "favorited": false,
+ "favoritesCount": 0,
+ "author": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg",
+ "following": false
+ }
+ }, {
+
+ "slug": "how-to-train-your-dragon-2",
+ "title": "How to train your dragon 2",
+ "description": "So toothless",
+ "body": "It a dragon",
+ "tagList": ["dragons", "training"],
+ "createdAt": "2016-02-18T03:22:56.637Z",
+ "updatedAt": "2016-02-18T03:48:35.824Z",
+ "favorited": false,
+ "favoritesCount": 0,
+ "author": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg",
+ "following": false
+ }
+ }],
+ "articlesCount": 2
+}
+```
+### Single Comment
+```source-json
+{
+ "comment": {
+ "id": 1,
+ "createdAt": "2016-02-18T03:22:56.637Z",
+ "updatedAt": "2016-02-18T03:22:56.637Z",
+ "body": "It takes a Jacobian",
+ "author": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg",
+ "following": false
+ }
+ }
+}
+```
+### Multiple Comments
+```source-json
+{
+ "comments": [{
+ "id": 1,
+ "createdAt": "2016-02-18T03:22:56.637Z",
+ "updatedAt": "2016-02-18T03:22:56.637Z",
+ "body": "It takes a Jacobian",
+ "author": {
+ "username": "ahmed",
+ "bio": "I work at statefarm",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg",
+ "following": false
+ }
+ }],
+ "commentsCount": 1
+}
+```
+### List of Tags
+```source-json
+{
+ "tags": [
+ "reactjs",
+ "angularjs"
+ ]
+}
+```
+### Errors and Status Codes
+If a request fails any validations, expect errors in the following format:
+
+```source-json
+{
+ "errors":{
+ "body": [
+ "can't be empty"
+ ]
+ }
+}
+```
+### Other status codes:
+401 for Unauthorized requests, when a request requires authentication but it isn't provided
+
+403 for Forbidden requests, when a request may be valid but the user doesn't have permissions to perform the action
+
+404 for Not found requests, when a resource can't be found to fulfill the request
+
+
+Endpoints:
+----------
+
+### Authentication:
+
+`POST /api/users/login`
+
+Example request body:
+
+```source-json
+{
+ "user":{
+ "email": "ahmed@mail.com",
+ "password": "ahmedahmed"
+ }
+}
+```
+
+No authentication required, returns a User
+
+Required fields: `email`, `password`
+
+### Registration:
+
+`POST /api/users`
+
+Example request body:
+
+```source-json
+{
+ "user":{
+ "username": "Jacob",
+ "email": "ahmed@mail.com",
+ "password": "ahmedahmed"
+ }
+}
+```
+
+No authentication required, returns a User
+
+Required fields: `email`, `username`, `password`
+
+### Get Current User
+
+`GET /api/user`
+
+Authentication required, returns a User that's the current user
+
+### Update User
+
+`PUT /api/user`
+
+Example request body:
+
+```source-json
+{
+ "user":{
+ "email": "ahmed@mail.com",
+ "bio": "I like to skateboard",
+ "image": "https://i.stack.imgur.com/xHWG8.jpg"
+ }
+}
+```
+
+Authentication required, returns the User
+
+Accepted fields: `email`, `username`, `password`, `image`, `bio`
+
+### Get Profile
+
+`GET /api/profiles/:username`
+
+Authentication optional, returns a Profile
+
+### Follow user
+
+`POST /api/profiles/:username/follow`
+
+Authentication required, returns a Profile
+
+No additional parameters required
+
+### Unfollow user
+
+`DELETE /api/profiles/:username/follow`
+
+Authentication required, returns a Profile
+
+No additional parameters required
+
+### List Articles
+
+`GET /api/articles`
+
+Returns most recent articles globally by default, provide `tag`, `author` or `favorited` query parameter to filter results
+
+Query Parameters:
+
+Filter by tag:
+
+`?tag=AngularJS`
+
+Filter by author:
+
+`?author=ahmed`
+
+Favorited by user:
+
+`?favorited=ahmed`
+
+Limit number of articles (default is 20):
+
+`?limit=20`
+
+Offset/skip number of articles (default is 0):
+
+`?offset=0`
+
+Authentication optional, will return multiple articles, ordered by most recent first
+
+### Feed Articles
+
+`GET /api/articles/feed`
+
+Can also take `limit` and `offset` query parameters like List Articles
+
+Authentication required, will return multiple articles created by followed users, ordered by most recent first.
+
+### Get Article
+
+`GET /api/articles/:slug`
+
+No authentication required, will return single article
+
+### Create Article
+
+`POST /api/articles`
+
+Example request body:
+
+```source-json
+{
+ "article": {
+ "title": "How to train your dragon",
+ "description": "Ever wonder how?",
+ "body": "You have to believe",
+ "tagList": ["reactjs", "angularjs", "dragons"]
+ }
+}
+```
+
+Authentication required, will return an Article
+
+Required fields: `title`, `description`, `body`
+
+Optional fields: `tagList` as an array of Strings
+
+### Update Article
+
+`PUT /api/articles/:slug`
+
+Example request body:
+
+```source-json
+{
+ "article": {
+ "title": "Did you train your dragon?"
+ }
+}
+```
+
+Authentication required, returns the updated Article
+
+Optional fields: `title`, `description`, `body`
+
+The `slug` also gets updated when the `title` is changed
+
+### Delete Article
+
+`DELETE /api/articles/:slug`
+
+Authentication required
+
+### Add Comments to an Article
+
+`POST /api/articles/:slug/comments`
+
+Example request body:
+
+```source-json
+{
+ "comment": {
+ "body": "His name was my name too."
+ }
+}
+```
+
+Authentication required, returns the created Comment
+Required field: `body`
+
+### Get Comments from an Article
+
+`GET /api/articles/:slug/comments`
+
+Authentication optional, returns multiple comments
+
+### Delete Comment
+
+`DELETE /api/articles/:slug/comments/:id`
+
+Authentication required
+
+### Favorite Article
+
+`POST /api/articles/:slug/favorite`
+
+Authentication required, returns the Article
+No additional parameters required
+
+### Unfavorite Article
+
+`DELETE /api/articles/:slug/favorite`
+
+Authentication required, returns the Article
+
+No additional parameters required
+
+### Get Tags
+
+`GET /api/tags`
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f79961c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,93 @@
+{
+ "name": "social-platform",
+ "version": "1.0.0",
+ "description": "A Social platform - very simple ",
+ "main": "index.js",
+ "scripts": {
+ "dev": "nodemon --exec babel-node src/index.js",
+ "build": "babel src --out-dir dist",
+ "start": "node dist/index.js",
+ "test": "nyc --reporter=html --reporter=text mocha ./test/*.js --exit --require @babel/register --require regenerator-runtime",
+ "devtest": "NODE_ENV=test & yarn destroy & yarn run seed & yarn run test",
+ "test:reset": "npm run migrate:reset && nyc --reporter=html --reporter=text mocha ./test/*.js --timeout 80000 --exit --require @babel/register --require regenerator-runtime",
+ "coverage": "nyc report --reporter=text-lcov | coveralls",
+ "test:local": "yarn undo && yarn migrate && yarn seed && yarn test",
+ "migrate:reset": "sequelize db:drop && sequelize db:create && sequelize db:migrate && sequelize db:seed:all",
+ "undo": "sequelize db:migrate:undo",
+ "undo:all": "sequelize db:migrate:undo",
+ "seed": "sequelize db:seed:all",
+ "destroy": "babel-node src/helpers/destroyTables"
+ },
+ "author": "ahmedkhaled4d",
+ "license": "MIT",
+ "dependencies": {
+ "@hapi/joi": "^15.0.3",
+ "async": "^2.6.3",
+ "bcrypt": "^3.0.6",
+ "body-parser": "^1.19.0",
+ "cloudinary": "^1.14.0",
+ "cors": "^2.8.4",
+ "dotenv": "^8.0.0",
+ "ejs": "^2.6.1",
+ "errorhandler": "^1.5.0",
+ "express": "^4.16.3",
+ "express-jwt": "^5.3.1",
+ "express-session": "^1.16.2",
+ "handlebars": "^4.1.2",
+ "joi": "^14.3.1",
+ "jsonwebtoken": "^8.5.1",
+ "lodash": "^4.17.11",
+ "method-override": "^2.3.10",
+ "methods": "^1.1.2",
+ "morgan": "^1.9.1",
+ "multer": "^1.4.1",
+ "node-cron": "^2.0.3",
+ "nodemailer": "^6.2.1",
+ "open": "^6.3.0",
+ "passport": "^0.4.0",
+ "passport-facebook": "^3.0.0",
+ "passport-google-oauth20": "^2.0.0",
+ "passport-local": "^1.0.0",
+ "passport-twitter": "^1.0.4",
+ "pg": "^7.11.0",
+ "pg-hstore": "^2.3.3",
+ "prettier": "^1.18.2",
+ "ratingpercentage": "^1.0.7",
+ "regenerator-runtime": "^0.13.2",
+ "request": "^2.88.0",
+ "sequelize": "^5.8.7",
+ "slug": "^1.1.0",
+ "social-share": "^0.1.0",
+ "socket.io": "^2.2.0",
+ "swagger-ui-express": "^4.0.6",
+ "underscore": "^1.9.1",
+ "uniqid": "^5.0.3",
+ "worker-farm": "^1.7.0"
+ },
+ "devDependencies": {
+ "@babel/cli": "^7.4.4",
+ "@babel/core": "^7.4.5",
+ "@babel/node": "^7.4.5",
+ "@babel/preset-env": "^7.4.5",
+ "@babel/register": "^7.4.4",
+ "chai": "^4.2.0",
+ "chai-http": "^4.3.0",
+ "coveralls": "^3.0.4",
+ "eslint": "^5.16.0",
+ "eslint-config-airbnb-base": "^13.1.0",
+ "eslint-plugin-import": "^2.17.3",
+ "mocha": "^6.1.4",
+ "nodemon": "^1.19.1",
+ "nyc": "^14.1.1"
+ },
+ "repository": "https://github.com/ahmedkhaled4d/social-platform.git",
+ "nyc": {
+ "exclude": [
+ "src/config/passportSetup.js",
+ "src/sequelize/**/*.js",
+ "test/**/*.js",
+ "src/helpers/notifications/*.js",
+ "src/helpers/SocketIO.js"
+ ]
+ }
+}
diff --git a/src/api/controllers/articlesController.js b/src/api/controllers/articlesController.js
new file mode 100644
index 0000000..feb5b01
--- /dev/null
+++ b/src/api/controllers/articlesController.js
@@ -0,0 +1,550 @@
+/* eslint-disable import/no-cycle */
+import articles from '../../helpers/articlesHelper';
+import models from '../../sequelize/models';
+import readTime from '../../helpers/ReadTime.helper';
+import eventEmitter from '../../helpers/notifications/EventEmitter';
+import findUser from '../../helpers/FindUser';
+import AuthorNotifier from '../../helpers/NotifyAuthorOnArticleBlock';
+import workers from '../../workers';
+
+const {
+ notifyAuthorblock, notifyAuthorUnblock
+} = AuthorNotifier;
+const { uploadImageWorker } = workers;
+
+const {
+ User,
+ Article,
+ LikeDislike,
+ ReportedArticles,
+ BlockedArticles,
+ Share
+} = models;
+// eslint-disable-next-line no-array-constructor
+const days = new Array(
+ 'Monday',
+ 'Tuesday',
+ 'Wednesday',
+ 'Thursday',
+ 'Friday',
+ 'Saturday',
+ 'Sunday'
+);
+
+/**
+ * @Author - Audace Uhiriwe
+ */
+class articlesController {
+ /**
+ * creating a new article
+ * @param {object} req - Request.
+ * @param {object} res - Response.
+ * @returns {object} - returns created article
+ */
+ static async createArticle(req, res) {
+ const { id } = req.user;
+
+ // @check if that user is verified
+ const user = await User.findOne({ where: { id } });
+ if (user.dataValues.verified === false) {
+ return res
+ .status(403)
+ .send({ error: 'Please Verify your account, first!' });
+ }
+
+ const dataValues = await articles.createNewArticle(req);
+
+ const {
+ slug,
+ title,
+ description,
+ body,
+ tagList,
+ author,
+ updatedAt,
+ createdAt,
+ readtime,
+ views
+ } = dataValues;
+ const userInfo = await findUser(author.username);
+ eventEmitter.emit('publishArticle', userInfo.id, slug);
+ const result = {
+ // eslint-disable-next-line max-len
+ slug,
+ title,
+ description,
+ body,
+ tagList,
+ updatedAt,
+ createdAt,
+ author,
+ readtime,
+ views
+ };
+
+ res.status(201).send({
+ article: result
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns array of articles
+ */
+ static async getAllArticle(req, res) {
+ const allArticle = await articles.getAllArticle();
+
+ if (!allArticle[0]) {
+ return res.status(404).send({ error: 'Whoops! No Articles found!' });
+ }
+ res.status(200).send({
+ articles: allArticle
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns article object
+ */
+ static async getOneArticle(req, res) {
+ const { slug } = req.params;
+
+ // @check if the article's slug exist
+ const result = await Article.findOne({ where: { slug } });
+ if (result === null) { return res.status(404).send({ error: 'This Slug Not found!' }); }
+ const oneArticle = await articles.getOneSlug(slug);
+ await Article.update(
+ {
+ views: oneArticle.dataValues.views += 1,
+ },
+ { where: { slug } }
+ );
+ res.status(200).send({
+ status: 200,
+ article: oneArticle
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status and updated article object
+ */
+ static async updateArticle(req, res) {
+ const { slug } = req.params;
+ const {
+ title, body, description, tagList
+ } = req.body;
+
+ let updatedSlug;
+ if (tagList !== undefined) {
+ updatedSlug = tagList.split(',');
+ } else {
+ updatedSlug = req.foundArticle.tagList;
+ }
+
+ // @Object containing the Updated Data
+ const updateSlug = {
+ title: title || req.foundArticle.title,
+ body: body || req.foundArticle.body,
+ description: description || req.foundArticle.description,
+ tagList: updatedSlug
+ };
+
+ // @generate an updated new slug
+ const newSlug = await articles.createSlug(updateSlug.title);
+ const newReadTime = readTime(updateSlug.body);
+
+ // @Updating the article's data in Database
+ const updatedArticle = await Article.update(
+ {
+ slug: newSlug,
+ title: updateSlug.title,
+ body: updateSlug.body,
+ description: updateSlug.description,
+ tagList: updateSlug.tagList,
+ readtime: newReadTime
+ },
+ { where: { slug }, returning: true }
+ );
+
+
+ // Uplooad article image
+ if (req.files) {
+ uploadImageWorker(req.files, updatedArticle[1][0].dataValues.id, 'article', null);
+ }
+
+ // @returning the response
+ res.status(200).send({
+ message: 'Article updated successfully',
+ article: updateSlug
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async deleteArticle(req, res) {
+ const { slug } = req.params;
+
+ // @delete an article in Database
+ await Article.destroy({ where: { slug } });
+
+ // @returning the response
+ res.status(200).send({ message: 'Article deleted successfully!' });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async reportArticle(req, res) {
+ const { username } = req.user;
+ const { comment } = req.body;
+ const { slug } = req.params;
+ ReportedArticles.create({
+ slug,
+ comment,
+ username,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }).then((out) => {
+ res.status(201).send({
+ status: 201,
+ data: out
+ });
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async likeArticle(req, res) {
+ const { id: currentUser } = req.user;
+ const { slug } = req.params;
+
+ try {
+ // Find the article
+ const query = await Article.findAll({ where: { slug } });
+
+ if (!query[0]) {
+ return res
+ .status(404)
+ .json({ message: `Article with slug: ${slug} not found` });
+ }
+
+ const { dataValues: foundArticle } = query[0];
+
+ // Check the current user has not liked or disliked this article before
+ const hasLiked = await LikeDislike.findAll({
+ where: {
+ articleId: foundArticle.id,
+ userId: currentUser,
+ likes: 1
+ }
+ });
+ const hasDisliked = await LikeDislike.findAll({
+ where: {
+ articleId: foundArticle.id,
+ userId: currentUser,
+ dislikes: 1
+ }
+ });
+
+ // If the user has already liked send a response
+ if (hasLiked[0]) {
+ return res.status(403).json({
+ message: `User ${currentUser} has already liked article: ${slug}`
+ });
+ }
+
+ // If user has disliked before, remove dislike, add like.
+ if (hasDisliked[0]) {
+ await LikeDislike.update(
+ { dislikes: 0, likes: 1 },
+ { where: { id: hasDisliked[0].id } }
+ );
+ return res
+ .status(200)
+ .json({ message: `User ${currentUser} has liked article ${slug}` });
+ }
+
+ // the user hasn't liked or disliked before, create new like
+ await LikeDislike.create({
+ userId: currentUser,
+ articleId: foundArticle.id,
+ dislikes: 0,
+ likes: 1
+ });
+
+ return res
+ .status(200)
+ .json({ message: `User ${currentUser} has liked article ${slug}` });
+ } catch (error) {
+ return res.status(500).json({ error: `${error}` });
+ }
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async dislikeArticle(req, res) {
+ const { id: currentUser } = req.user;
+ const { slug } = req.params;
+
+ try {
+ // Find the article
+ const query = await Article.findAll({ where: { slug } });
+
+ if (!query[0]) {
+ return res
+ .status(404)
+ .json({ message: `Article with slug: ${slug} not found` });
+ }
+
+ const { dataValues: foundArticle } = query[0];
+
+ // Check the current user has not liked or disliked this article before
+ const hasLiked = await LikeDislike.findAll({
+ where: {
+ articleId: foundArticle.id,
+ userId: currentUser,
+ likes: 1
+ }
+ });
+ const hasDisliked = await LikeDislike.findAll({
+ where: {
+ articleId: foundArticle.id,
+ userId: currentUser,
+ dislikes: 1
+ }
+ });
+
+ // If the user has already disliked send a response
+ if (hasDisliked[0]) {
+ return res.status(403).json({
+ message: `User ${currentUser} has already disliked article: ${slug}`
+ });
+ }
+
+ // If user has liked before, remove like, add dislike.
+ if (hasLiked[0]) {
+ await LikeDislike.update(
+ { dislikes: 1, likes: 0 },
+ { where: { id: hasLiked[0].id } }
+ );
+ return res.status(200).json({
+ message: `User ${currentUser} has disliked article ${slug}`
+ });
+ }
+
+ // the user hasn't disliked before, create new dislike
+ await LikeDislike.create({
+ userId: currentUser,
+ articleId: foundArticle.id,
+ dislikes: 1,
+ likes: 0
+ });
+
+ return res
+ .status(200)
+ .json({ message: `User ${currentUser} has disliked article ${slug}` });
+ } catch (error) {
+ return res.status(500).json({ error: `${error}` });
+ }
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async getLikes(req, res) {
+ const { slug } = req.params;
+
+ // Find the article
+ const query = await Article.findAll({ where: { slug } });
+
+ if (!query[0]) {
+ return res
+ .status(404)
+ .json({ message: `Article with slug: ${slug} not found` });
+ }
+
+ const { dataValues: foundArticle } = query[0];
+
+ // Get likes
+ const likeCount = await LikeDislike.count({
+ where: { articleId: foundArticle.id, likes: 1 }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ articleSlug: slug,
+ numberOfLikes: likeCount
+ }
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async getDislikes(req, res) {
+ const { slug } = req.params;
+
+ // Find the article
+ const query = await Article.findAll({ where: { slug } });
+
+ if (!query[0]) {
+ return res
+ .status(404)
+ .json({ message: `Article with slug: ${slug} not found` });
+ }
+
+ const { dataValues: foundArticle } = query[0];
+
+ // Get likes
+ const likeCount = await LikeDislike.count({
+ where: {
+ articleId: foundArticle.id,
+ dislikes: 1
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ articleSlug: slug,
+ numberOfDislikes: likeCount
+ }
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async getBlockedArticles(req, res) {
+ const blockedArticles = await BlockedArticles.findAll({});
+ return res.status(200).json({ status: 200, data: blockedArticles });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async getReportedArticles(req, res) {
+ const reportedArticles = await ReportedArticles.findAll({});
+ return res.status(200).json({ status: 200, data: reportedArticles });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async blockArticle(req, res) {
+ const { slug } = req.params;
+ const { user } = req;
+ const { description } = req.body;
+ const article = await Article.findOne({ where: { slug } });
+ const reporterUsername = await ReportedArticles.findOne({
+ where: { slug }
+ });
+ const { dataValues: { email, lastName } } = await User.findOne({
+ where: { id: article.authorId }
+ });
+ const username = !reporterUsername
+ ? null
+ : reporterUsername.dataValues.username;
+ const repoterId = username === null ? null : await User.findOne({ where: { username } });
+ const id = repoterId === null ? null : repoterId.dataValues.id;
+ const object = {
+ reporterId: id,
+ articleId: article.id,
+ authorId: article.authorId,
+ moderatorId: user.id,
+ blockedDay: days[new Date().getDay() - 1] || 'Sunday',
+ description
+ };
+
+ BlockedArticles.create(object).then(async (responce) => {
+ await Article.update(
+ { blocked: true },
+ { where: { id: responce.articleId } }
+ );
+
+ await notifyAuthorblock({ email, lastName });
+
+ res.status(201).send({
+ status: 201,
+ data: {
+ message: 'Article blocked successfully',
+ responce
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a message with operation status
+ */
+ static async unBlockArticle(req, res) {
+ const { slug } = req.params;
+ const { id, authorId } = await Article.findOne({ where: { slug } });
+
+ const { dataValues: { email, lastName } } = await User.findOne({
+ where: { id: authorId }
+ });
+
+ BlockedArticles.destroy({ where: { articleId: id } }).then(async () => {
+ await Article.update({ blocked: false }, { where: { slug } });
+ await notifyAuthorUnblock({ email, lastName, slug });
+ res.status(200).send({
+ status: 200,
+ data: {
+ message: 'Article unblocked successfully'
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @returns {object} Object representing the response returned
+ */
+
+ // eslint-disable-next-line require-jsdoc
+ static async share(req, res) {
+ const { slug, provider } = req.share;
+ const { id } = req.user;
+ await Share.create({
+ userId: id,
+ slug,
+ provider
+ });
+ return res.status(200).json({
+ message: 'Thanks for sharing!',
+ article: req.article
+ });
+ }
+}
+
+export default articlesController;
diff --git a/src/api/controllers/auth.js b/src/api/controllers/auth.js
new file mode 100644
index 0000000..15664a4
--- /dev/null
+++ b/src/api/controllers/auth.js
@@ -0,0 +1,307 @@
+import jwt from 'jsonwebtoken';
+import dotenv from 'dotenv';
+import { omit } from 'lodash';
+import sequelize from 'sequelize';
+import tokenHelper from '../../helpers/Token.helper';
+import HashHelper from '../../helpers/hashHelper';
+import db from '../../sequelize/models/index';
+import verifyTemplate from '../../helpers/emailVerifyTemplate';
+import template from '../../helpers/emailTemplate';
+import workers from '../../workers';
+
+const { generateToken, decodeToken } = tokenHelper;
+const { User, Blacklist, Opt } = db;
+const { queueEmailWorker } = workers;
+const { Op } = sequelize;
+
+dotenv.config();
+
+/**
+ * @author Elie Mugenzi
+ * @class AuthController
+ * @description this class performs the whole authentication
+ */
+class AuthController {
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async register(req, res) {
+ let { body } = req;
+
+ if (Object.keys(req.body).length === 0) {
+ return res.status(400).json({
+ status: 400,
+ error: 'No data sent'
+ });
+ }
+
+ body = await omit(body, ['roles']);
+
+ const userObject = {
+ ...body,
+ password: HashHelper.hashPassword(body.password),
+ verified: false
+ };
+
+ const newUser = await User.create(userObject);
+
+ if (newUser) {
+ const token = await generateToken({
+ ...newUser.dataValues,
+ password: null,
+ });
+
+ await Opt.create({
+ userId: newUser.id,
+ type: 'email'
+ });
+
+ await Opt.create({
+ userId: newUser.id,
+ type: 'inapp'
+ });
+
+ const htmlToSend = verifyTemplate.sendVerification(`${newUser.firstName} ${newUser.lastName}`, newUser.email, token);
+
+ queueEmailWorker({ email: newUser.email }, htmlToSend, 'Welcome to Authorshaven', null);
+
+ res.status(201).send({
+ status: 201,
+ data: {
+ message: 'You will reveive an account verification email shortly',
+ email: `${newUser.email}`,
+ token
+ },
+ });
+ }
+ }
+
+
+ /**
+ * Verifies account
+ * @param {Object} req - Request
+ * @param {*} res - Response
+ * @returns {Object} - Response
+ */
+ static async verifyAccount(req, res) {
+ const { token } = req.query;
+ try {
+ const user = await decodeToken(token);
+ await User.update(
+ {
+ verified: true
+ },
+ {
+ where: {
+ email: user.email
+ }
+ }
+ );
+ res.status(202).json({
+ status: 202,
+ message: 'Account is now verified!'
+ });
+ } catch (err) {
+ res.status(400).json({
+ status: 400,
+ error: `Invalid Request ${err}`
+ });
+ }
+ }
+
+ /**
+ * User should be able to sign out
+ * @param {Object} req - Request object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async SignOut(req, res) {
+ const {
+ user: { id },
+ token
+ } = req;
+
+ const { exp } = await decodeToken(token);
+
+ await Blacklist.create({
+ userId: id,
+ token,
+ expiresAt: exp * 1000,
+ });
+
+ res.json({
+ status: 200,
+ message: 'You are now signed out!'
+ });
+ }
+
+ /**
+ * signup controller
+ * @param {Object} req - Response
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async login(req, res) {
+ const { email } = req.body;
+ const users = await User.findOne({
+ where: {
+ [Op.or]: [{ email }, { username: email }]
+ }
+ });
+ if (users) {
+ if (
+ !HashHelper.comparePassword(req.body.password, users.dataValues.password)
+ ) {
+ res.status(400).send({
+ status: 400,
+ error: {
+ message: 'Incorrect password'
+ }
+ });
+ } else {
+ generateToken({
+ ...users.dataValues,
+ password: null,
+ }).then((token) => {
+ res.status(200).send({
+ status: 200,
+ data: {
+ message: 'User logged in successful',
+ token
+ }
+ });
+ });
+ }
+ } else {
+ res.status(404).send({
+ status: 404,
+ error: {
+ message: 'User with that email does not exist.'
+ }
+ });
+ }
+ }
+
+ /**
+ * RequestPasswordReset controller
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async RequestPasswordReset(req, res) {
+ User.findAll({
+ where: {
+ email: req.body.email
+ }
+ }).then(async (response) => {
+ if (response[0]) {
+ const token = await generateToken({
+ userId: response[0].id,
+ userName: response[0].username,
+ userEmail: response[0].email
+ });
+
+ const user = response[0];
+ const { firstName, lastName, email } = user.dataValues;
+ const link = `${process.env.BASE_URL}/api/auth/reset/${token}`;
+ const mail = {
+ firstName, lastName, link, email
+ };
+ const htmlToSend = template.getPasswordResetTemplete(
+ mail.firstName,
+ mail.lastName,
+ mail.link
+ );
+
+ queueEmailWorker(mail, htmlToSend, 'Password Reset', null);
+
+ return res.status(201).send({
+ status: 201,
+ data: {
+ message: `A reset link will be sent to <${mail.email}> shortly.`,
+ email: `${mail.email}`,
+ token
+ }
+ });
+ }
+
+ return res.status(404).send({
+ status: 404,
+ data: { message: 'User with that email in not exist' }
+ });
+ });
+ }
+
+ /**
+ * ConfirmPasswordReset controller
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async ConfirmPasswordReset(req, res) {
+ try {
+ const { token } = req.params;
+ const user = await jwt.verify(token, process.env.SECRET_KEY);
+ const aprvToken = await jwt.sign(
+ {
+ userId: user.userId,
+ userName: user.userName
+ },
+ process.env.SECRET_KEY,
+ { expiresIn: 60 * 10 }
+ );
+ const link = `${process.env.BASE_URL}/api/auth/reset/${aprvToken}`;
+ res.status(200).send({
+ status: 200,
+ data: {
+ message: 'Below is your reset link',
+ link,
+ token: aprvToken
+ }
+ });
+ } catch (error) {
+ res.status(error.status || 500).send({
+ status: error.status || 500,
+ error: {
+ message: 'Token is invalid or expired, Please request another one'
+ }
+ });
+ }
+ }
+
+ /**
+ * ApplyPasswordReset cotroller
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async ApplyPasswordReset(req, res) {
+ const { aprvToken } = req.params;
+ const token = await jwt.verify(aprvToken, process.env.SECRET_KEY);
+ const password = HashHelper.hashPassword(req.body.newpassword);
+ User.update(
+ {
+ password
+ },
+ {
+ where: {
+ id: token.userId
+ }
+ }
+ )
+ .then(() => {
+ res.status(201).send({
+ status: 201,
+ data: { message: 'Password changed successful' }
+ });
+ });
+ }
+}
+export default AuthController;
diff --git a/src/api/controllers/bookmark.js b/src/api/controllers/bookmark.js
new file mode 100644
index 0000000..67e41ed
--- /dev/null
+++ b/src/api/controllers/bookmark.js
@@ -0,0 +1,67 @@
+import db from '../../sequelize/models';
+
+const { Bookmarks } = db;
+
+/**
+ * @author Diane Mahoro
+ * @class Bookmark
+ * @description this class performs the whole of rating
+ */
+class Bookmark {
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async bookmark(req, res) {
+ const { id } = req.user;
+ const { slug } = req.params;
+ const data = {
+ slug,
+ userId: id
+ };
+
+ const response = await Bookmarks.findAll({
+ where: {
+ slug,
+ userId: id
+ }
+
+ });
+ if (!response[0]) {
+ const newBookmark = await Bookmarks.create({
+ slug: data.slug,
+ userId: data.userId
+ });
+ return res.status(201).json({
+ data: newBookmark,
+ message: 'Bookmark created'
+ });
+ }
+ await Bookmarks.destroy({ where: { slug, userId: id }, logging: false });
+ res.status(200).json({
+ message: 'Bookmark deleted'
+ });
+ }
+
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async getOwnerBookmarks(req, res) {
+ const { id } = req.user;
+ const yourBookmarks = await Bookmarks.findAll({
+ where: {
+ userId: id
+ }
+ });
+ res.status(200).json({
+ data: yourBookmarks
+ });
+ }
+}
+
+export default Bookmark;
diff --git a/src/api/controllers/chatController.js b/src/api/controllers/chatController.js
new file mode 100644
index 0000000..9d2cab4
--- /dev/null
+++ b/src/api/controllers/chatController.js
@@ -0,0 +1,107 @@
+import sequelize from 'sequelize';
+import db from '../../sequelize/models';
+import Tokenizer from '../../helpers/Token.helper';
+
+const { Chat, User, follows } = db;
+const { Op } = sequelize;
+/**
+ * @author Rukundo Eric
+ * @class chatController
+ * @description this class performs chat
+ */
+class chatController {
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @return {Object} - Response object
+ */
+ static async getUsers(req, res) {
+ const { id } = req.user;
+ follows
+ .findAll({
+ where: {
+ [Op.or]: [{ userId: id }, { followerId: id }]
+ },
+ include: [
+ {
+ model: User,
+ as: 'follower',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ },
+ {
+ model: User,
+ as: 'followedUser',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ }
+ ]
+ })
+ .then((data) => {
+ if (!data[0]) {
+ return res.status(200).json({ message: 'you do not have any followers currently', followers: data, me: req.user });
+ }
+ return res.status(200).json({ followers: data, me: req.user });
+ });
+ }
+
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async getMessages(req, res) {
+ const myId = req.user.id;
+ const { username } = req.params;
+ const { dataValues: { id } } = await User.findOne({ where: { username } });
+ Chat.findAll({
+ where: {
+ [Op.or]: [{
+ senderId: myId,
+ recieverId: id
+ },
+ {
+ senderId: id,
+ recieverId: myId
+ }]
+ },
+ include: [
+ {
+ model: User,
+ as: 'sender',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ },
+ {
+ model: User,
+ as: 'receiver',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ }
+ ]
+ }).then((messages) => {
+ res.status(200).json({
+ messages
+ });
+ });
+ }
+
+ /**
+ * @description - Get current User
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async getCurrentUser(req, res) {
+ const { token } = req.query;
+ try {
+ const currentUser = await Tokenizer.decodeToken(token);
+ res.json({
+ user: currentUser
+ });
+ } catch (error) {
+ res.status(401).json({
+ error: 'Invalid token'
+ });
+ }
+ }
+}
+export default chatController;
diff --git a/src/api/controllers/comments.js b/src/api/controllers/comments.js
new file mode 100644
index 0000000..2277595
--- /dev/null
+++ b/src/api/controllers/comments.js
@@ -0,0 +1,384 @@
+/* eslint-disable arrow-body-style */
+import models from '../../sequelize/models';
+import eventEmitter from '../../helpers/notifications/EventEmitter';
+
+/**
+ * @class
+ */
+export default class comments {
+ /**
+ * @description - Users should create comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async createComment(req, res) {
+ const { comment } = req.body;
+ const { slug } = req.params;
+ const { id, firstName, lastName } = req.user;
+ const data = await models.Article.findAll({
+ where: {
+ slug
+ }
+ });
+ const commentAdded = await models.Comment.create({
+ comment,
+ userId: id,
+ articleId: data[0].dataValues.id,
+ slug
+ });
+ const Id = commentAdded.dataValues.id;
+ eventEmitter.emit('commentArticle', commentAdded.dataValues);
+ return res.status(201).json({
+ message: `Dear ${firstName}, Thank you for contributing to this article`,
+ data: {
+ Id,
+ firstName,
+ lastName,
+ comment
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to comment a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async commentAcomment(req, res) {
+ const { comment } = req.body;
+ const { slug, commentId } = req.params;
+ const { id, firstName, lastName } = req.user;
+ const data = await models.Article.findAll({
+ where: {
+ slug
+ }
+ });
+ const commentAdded = await models.Comment.create({
+ comment,
+ userId: id,
+ articleId: data[0].dataValues.id,
+ slug,
+ commentId
+ });
+ const Id = commentAdded.dataValues.id;
+ return res.status(201).json({
+ message: `Dear ${firstName}, Thank you for contributing to this comment`,
+ data: {
+ Id,
+ firstName,
+ lastName,
+ comment
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to edit a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async editComment(req, res) {
+ const { comment } = req.body;
+ const { commentId } = req.params;
+ const findComment = await models.Comment.findAll({
+ where: {
+ id: commentId
+ }
+ });
+ const { userId } = findComment[0].dataValues;
+ const { id, firstName } = req.user;
+ if (userId === id) {
+ const oldComment = findComment[0].dataValues.comment;
+ await models.Comment.update(
+ {
+ comment
+ },
+ { where: { id: commentId } }
+ ).then(async () => {
+ await models.CommentsHistory.create({
+ userId: id,
+ editedComment: oldComment,
+ commentId: findComment[0].dataValues.id
+ });
+ return res.status(200).json({
+ message: 'Your comment has been edited',
+ data: {
+ comment
+ }
+ });
+ });
+ } else {
+ return res.status(403).json({
+ message: `Dear ${firstName}, You do not have the right to edit this comment!`
+ });
+ }
+ }
+
+ /**
+ * @description - Users should be able to delete a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async deleteComment(req, res) {
+ const { id, firstName, roles } = req.user;
+ const { commentId } = req.params;
+ const findComment = await models.Comment.findAll({
+ where: {
+ id: commentId
+ }
+ });
+ const nestedComments = await models.Comment.findAll({
+ where: {
+ commentId
+ }
+ });
+ const { userId } = findComment[0].dataValues;
+ if (userId === id || roles.includes('moderator' || 'admin')) {
+ if (nestedComments[0]) {
+ await models.Comment.update(
+ {
+ comment:
+ 'This comment has been deleted!'
+ },
+ { where: { id: commentId } }
+ ).then(() => {
+ return res.status(200).json({
+ message: roles.includes('moderator' || 'admin') ? 'Comment deleted by moderator' : 'Comment deleted!'
+ });
+ });
+ } else {
+ await models.Comment.destroy({
+ where: {
+ id: commentId
+ }
+ }).then(() => {
+ return res.status(200).json({
+ message: 'Comment deleted!'
+ });
+ });
+ }
+ }
+ return res.status(403).json({
+ message: `Dear ${firstName}, You do not have the right to delete this comment!`
+ });
+ }
+
+ /**
+ * @description - Users should be able to get an article with its comments
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async getComment(req, res) {
+ const { slug } = req.params;
+ const findSlug = await models.Article.findAll({
+ attributes: ['id', 'views'],
+ where: {
+ slug
+ }
+ });
+ if (findSlug.length === 0) {
+ return res.status(404).json({
+ message: 'Not found!'
+ });
+ }
+ await models.Article.update(
+ {
+ views: findSlug[0].dataValues.views += 1,
+ },
+ { where: { slug } }
+ );
+ await models.Article.findAll({
+ attributes: [
+ 'title',
+ 'description',
+ 'body'
+ ],
+ where: {
+ slug
+ },
+ include: [
+ {
+ model: models.Comment,
+ attributes: ['comment'],
+ where: {
+ articleId: findSlug[0].dataValues.id,
+ commentId: null
+ },
+ include: [
+ {
+ model: models.Comment,
+ attributes: ['comment']
+ }
+ ]
+ }
+ ]
+ }).then((data) => {
+ if (data) {
+ return res.status(200).json({
+ data
+ });
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to like a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async likeComment(req, res) {
+ const { commentId } = req.params;
+ const { id, firstName } = req.user;
+ const hasDisliked = await models.LikeDislike.findAll({
+ where: {
+ commentId,
+ userId: id,
+ dislikes: 1
+ }
+ });
+ if (hasDisliked[0]) {
+ await models.LikeDislike.update(
+ { dislikes: 0, likes: 1 },
+ { where: { id: hasDisliked[0].id } }
+ );
+ return res.status(200).json({
+ message: `Dear ${firstName}, Thank you for liking this comment!`
+ });
+ }
+ await models.LikeDislike.create({
+ userId: id,
+ commentId,
+ dislikes: 0,
+ likes: 1
+ });
+
+ return res.status(201).json({
+ message: `Dear ${firstName}, Thank you for liking this comment!`
+ });
+ }
+
+ /**
+ * @description - Users should be able to dislike a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async dislikeComment(req, res) {
+ const { commentId } = req.params;
+ const { id, firstName } = req.user;
+ const hasLiked = await models.LikeDislike.findAll({
+ where: {
+ commentId,
+ userId: id,
+ likes: 1
+ }
+ });
+ if (hasLiked[0]) {
+ await models.LikeDislike.update(
+ { dislikes: 1, likes: 0 },
+ { where: { id: hasLiked[0].id } }
+ );
+ return res.status(200).json({
+ message: `Dear ${firstName}, Thank you for disliking this comment!`
+ });
+ }
+ await models.LikeDislike.create({
+ userId: id,
+ commentId,
+ dislikes: 1,
+ likes: 0
+ });
+
+ return res.status(201).json({
+ message: `Dear ${firstName}, Thank you for disliking this comment!`
+ });
+ }
+
+ /**
+ * @description - Users should be able to like a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async countLikes(req, res) {
+ const { commentId } = req.params;
+
+ // Get comment likes
+ const likeCount = await models.LikeDislike.count({
+ where: {
+ commentId,
+ likes: 1
+ }
+ });
+ return res.status(200).json({
+ status: 200,
+ data: {
+ commentId,
+ likes: likeCount
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to dislike a comment
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async countDislikes(req, res) {
+ const { commentId } = req.params;
+
+ // Get comment dislikes
+ const dislikeCount = await models.LikeDislike.count({
+ where: {
+ commentId,
+ dislikes: 1
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ commentId,
+ dislikes: dislikeCount
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to track edit history
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async commentHistory(req, res) {
+ const { commentId } = req.params;
+ const { id, roles } = req.user;
+ const findHistory = await models.CommentsHistory.findAll({
+ where: {
+ commentId
+ }
+ });
+ if (findHistory.length === 0) {
+ return res.status(404).json({
+ message: 'No edit history for this comment!'
+ });
+ }
+ if (findHistory[0].dataValues.userId === id || roles.includes('moderator' || 'admin')) {
+ return res.status(200).json({
+ data: {
+ findHistory
+ }
+ });
+ }
+ return res.status(403).json({
+ message: 'You do not have the right to view this history!'
+ });
+ }
+}
diff --git a/src/api/controllers/highlightController.js b/src/api/controllers/highlightController.js
new file mode 100644
index 0000000..26730ee
--- /dev/null
+++ b/src/api/controllers/highlightController.js
@@ -0,0 +1,35 @@
+import db from '../../sequelize/models';
+
+const { Highlights } = db;
+/**
+ * @author Diane Mahoro
+ * @class Highlight
+ * @description this class performs the whole of highlightings
+ */
+class Highlight {
+ /**
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async createHighlights(req, res) {
+ const { id } = req.user;
+ const {
+ highlightText, comment, occurencyNumber
+ } = req.body;
+ const { article } = req; // This contains the article
+ const newHighlight = await Highlights.create({
+ articleId: article.id,
+ userId: id,
+ highlightText,
+ comment,
+ occurencyNumber,
+ });
+ return res.status(201).json({
+ Message: `Thank you for highlighting this text ${newHighlight.highlightText}`,
+ data: newHighlight
+ });
+ }
+}
+
+export default Highlight;
diff --git a/src/api/controllers/optController.js b/src/api/controllers/optController.js
new file mode 100644
index 0000000..5e2d191
--- /dev/null
+++ b/src/api/controllers/optController.js
@@ -0,0 +1,135 @@
+import db from '../../sequelize/models';
+
+const { Opt } = db;
+
+/**
+ * @class
+ */
+class OptController {
+ /**
+ * @description User should be able to
+ * @param {Object} req Request object
+ * @param {Object} res Response object
+ * @returns {Object} Response object
+ */
+ static async OptInApp(req, res) {
+ const { id } = req.user;
+ const optedin = await Opt.findOne({
+ where: {
+ userId: id,
+ type: 'inapp'
+ }
+ });
+ if (optedin) {
+ return res.status(400).json({
+ message: 'You are already opted-in'
+ });
+ }
+ const newOpt = await Opt.create({
+ userId: id,
+ type: 'inapp'
+ });
+ if (newOpt) {
+ res.status(201).json({
+ message: 'You are now opted-in to in-app notifications',
+ data: newOpt
+ });
+ }
+ }
+
+ /**
+ * @description User should subscribe with email
+ * @param {Object} req Request object
+ * @param {Object} res Response object
+ * @returns {Object} Response object
+ */
+ static async OptInEmail(req, res) {
+ const { id } = req.user;
+ const optedin = await Opt.findOne({
+ where: {
+ userId: id,
+ type: 'email'
+ }
+ });
+ if (optedin) {
+ return res.status(400).json({
+ message: 'You are already opted in'
+ });
+ }
+
+ const newOpt = await Opt.create({
+ userId: id,
+ type: 'email'
+ });
+ if (newOpt) {
+ res.status(201).json({
+ message: 'You are now opted-in for receiving email notifications',
+ data: newOpt
+ });
+ }
+ }
+
+ /**
+ * @description User should be able to opt-out in-app notifications
+ * @param {Object} req Request Object
+ * @param {Object} res Response Object
+ * @returns {Object} Response object
+ */
+ static async optOutApp(req, res) {
+ const { id } = req.user;
+ const optedin = await Opt.findOne({
+ where: {
+ userId: id,
+ type: 'inapp'
+ }
+ });
+ if (optedin) {
+ await Opt.destroy({
+ where: {
+ userId: id,
+ type: 'inapp'
+ }
+ });
+ return res.json({
+ message: 'You are now opted-out!'
+ });
+ }
+ res.status(400).json({
+ message: 'You are not opted-in with in-app'
+ });
+ }
+
+ /**
+ * @description User should be able to opt-out email notifications
+ * @param {Object} req Request object
+ * @param {Object} res Response object
+ * @returns {Object} Response object
+ */
+ static async optOutEmail(req, res) {
+ const { id } = req.user;
+ const optedin = await Opt.findOne({
+ where: {
+ userId: id,
+ type: 'email'
+ }
+ });
+ if (optedin) {
+ await Opt.destroy({
+ where: {
+ userId: id,
+ type: 'email'
+ }
+ });
+
+ return res.json({
+ message: 'You are now opted-out!'
+ });
+ }
+
+ res.status(400).json({
+ message: 'You are not yet opted in with email'
+ });
+ }
+}
+
+export default OptController;
diff --git a/src/api/controllers/profiles.js b/src/api/controllers/profiles.js
new file mode 100644
index 0000000..ea0f421
--- /dev/null
+++ b/src/api/controllers/profiles.js
@@ -0,0 +1,220 @@
+import { omit } from 'lodash';
+import models from '../../sequelize/models';
+import workers from '../../workers';
+
+const { User, follows } = models;
+const { uploadImageWorker } = workers;
+
+
+/**
+ * This class contains user controllers
+ */
+export default class ProfilesController {
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns an object containing the user profile
+ */
+ static async getProfile(req, res) {
+ const { username } = req.params;
+
+ const queryResult = await User.findAll({ where: { username } });
+ if (!queryResult[0]) {
+ return res
+ .status(404)
+ .json({ message: `User ${username} does not exist` });
+ }
+ const profile = queryResult[0].dataValues;
+
+ return res.status(200).json({ profile });
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns an object containing the updated user profile
+ */
+ static async updateProfile(req, res) {
+ let { body } = req;
+ const { user, files, params } = req;
+
+ if (!files && Object.keys(body) < 1) return res.status(400).send({ message: 'Cannot update empty object' });
+
+ if (files && Object.keys(body) < 1) {
+ uploadImageWorker(files, params.id, 'user', null);
+ return res.status(200).json({ status: 200, message: 'Your image will be updated shortly' });
+ }
+
+ if (!user.roles.includes('admin')) {
+ body = await omit(body, ['roles']);
+ }
+
+ const updatedProfile = { ...body };
+
+ try {
+ const updatedUser = await User.update(
+ updatedProfile,
+ { where: { id: params.id } },
+ );
+
+ if (!updatedUser[0]) {
+ return res
+ .status(404)
+ .json({ message: `Could not find user with id: ${user.id}` });
+ }
+
+ uploadImageWorker(files, params.id, 'user', null);
+
+ return res.status(200).json({ user: { updatedUser } });
+ } catch (error) {
+ return res.status(500).json({ error: `${error}` });
+ }
+ }
+
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns a res status with message
+ */
+ static async deleteProfile(req, res) {
+ const { params } = req;
+ try {
+ await User.destroy({ where: { id: params.id } });
+ return res.status(200).json({ status: 200, message: `User ${params.id} deleted.` });
+ } catch (error) {
+ return res.status(500).json({ status: 500, error: `${error}` });
+ }
+ }
+
+ /**
+ * @Author - Audace Uhiriwe
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns an object containing the user's profiles
+ */
+ static async getAllProfile(req, res) {
+ const fetchedProfile = await User.findAll({ attributes: ['username', 'firstName', 'lastName', 'bio', 'image'] });
+ if (!fetchedProfile[0]) return res.status(200).send({ message: 'No Users Profiles found!', data: fetchedProfile });
+ return res.status(200).send({
+ profiles: fetchedProfile
+ });
+ }
+
+ /**
+ * @Author - Audace Uhiriwe
+ * @param {object} req
+ * @param {object} res
+ * @return {object} returns an object containing the user's profiles
+ */
+ static async follow(req, res) {
+ const { username } = req.params;
+ const registeredUser = await User.findOne({ where: { username } });
+ if (registeredUser.id === req.user.id) {
+ return res.status(400).json({ errors: 'You can not following yourself ' });
+ }
+
+ follows
+ .create({
+ userId: registeredUser.id,
+ followerId: req.user.id
+ })
+ // eslint-disable-next-line no-unused-vars
+ .then(response => res.status(200).json({
+ response,
+ message: `Congratulation, now you follow ${registeredUser.username} `
+ }))
+ .catch(error => (error.name === 'SequelizeUniqueConstraintError'
+ ? res.status(400).send({
+ error: ` ${username} is already your follower `
+ })
+ : res.status(500).json({ error: 'something went wrong' })));
+ }
+
+ /**
+ * Apply unfollow a user controller
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @returns {Object} The response object
+ */
+ static async unfollow(req, res) {
+ const [username, user] = [req.params.username, req.user];
+ const registeredUser = await User.findOne({ where: { username } });
+
+ const unfollowedUser = Object.keys(registeredUser).length
+ ? await follows.destroy({
+ where: {
+ userId: registeredUser.id,
+ followerId: user.id
+ }
+ })
+ : null;
+
+ if (unfollowedUser && unfollowedUser.errors) {
+ return res
+ .status(500)
+ .json({ errors: 'Internal server error' });
+ }
+
+ return unfollowedUser
+ ? res.status(200).json({
+ message: `you unfollowed ${username}`
+ })
+ : res
+ .status(400)
+ .json({ error: `you do not follow ${username}` });
+ }
+
+ /**
+ * Apply function to fetch user's followers
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @returns {Object} followers
+ */
+ static async followers(req, res) {
+ // eslint-disable-next-line no-unused-vars
+ const f = await follows.findAll();
+ follows
+ .findAll({
+ where: { userId: req.user.id },
+ include: [
+ {
+ model: User,
+ as: 'follower',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ }
+ ]
+ })
+ .then((data) => {
+ if (!data[0]) {
+ return res.status(200).json({ message: 'you do not have any followers currently', data });
+ }
+ return res.status(200).json({ followers: data });
+ });
+ }
+
+ /**
+ * Apply function to fetch all users you follow
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @returns {Object} followers
+ */
+ static async following(req, res) {
+ follows
+ .findAll({
+ where: { followerId: req.user.id },
+ include: [
+ {
+ model: User,
+ as: 'followedUser',
+ attributes: ['id', 'firstName', 'lastName', 'email', 'username']
+ }
+ ]
+ })
+ .then((data) => {
+ if (!data[0]) {
+ return res.status(200).json({ message: 'you do not following anyone currently', data });
+ }
+ return res.status(200).json({ following: data });
+ });
+ }
+}
diff --git a/src/api/controllers/ratingController.js b/src/api/controllers/ratingController.js
new file mode 100644
index 0000000..3febbf2
--- /dev/null
+++ b/src/api/controllers/ratingController.js
@@ -0,0 +1,142 @@
+import sequelize from 'sequelize';
+import calcRatings from 'ratingpercentage';
+import db from '../../sequelize/models/index';
+
+const { ArticleRatings } = db;
+
+/**
+ * @author Eric Rukundo
+ * @class ArticleRatings
+ * @description this class performs the whole authentication
+ */
+class Ratings {
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async createRatings(req, res) {
+ const { id } = req.user;
+ const { slug } = req.params;
+ const { rating } = req.body;
+
+
+ const data = {
+ slug,
+ userId: id,
+ rating: parseInt(rating, 10)
+ };
+ const response = await ArticleRatings.findAll({
+ where: {
+ slug,
+ userId: id
+ }
+ });
+ if (!response[0]) {
+ const NewRating = await ArticleRatings.create({
+ slug: data.slug,
+ userId: data.userId,
+ ratings: data.rating
+ });
+ res.status(201).json({
+ data: NewRating,
+ message: 'created'
+ });
+ }
+ return res.status(403).json({
+ Error: 'You are not allowed to rate this article more than once, but you can update your ratings.'
+ });
+ }
+
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async UpdateRatings(req, res) {
+ const { id } = req.user;
+ const { slug } = req.params;
+ const { rating } = req.body;
+
+ const data = {
+ rating: parseInt(rating, 10)
+ };
+
+ const response = await ArticleRatings.findAll({
+ where: {
+ slug,
+ userId: id
+ }
+ });
+ if (response) {
+ await ArticleRatings.update(
+ { ratings: data.rating },
+ { where: { slug, userId: id }, logging: false }
+ );
+
+ res.status(200).json({
+ NewRate: data.rating,
+ message: 'updated',
+ });
+ }
+ }
+
+ /**
+ *
+ * @param {Object} req - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async calculateArticleRatings(req, res) {
+ const { slug } = req.params;
+ ArticleRatings
+ .findAll({
+ where: {
+ slug
+ },
+ group: ['ratings'],
+ attributes: ['ratings', [sequelize.fn('COUNT', 'TagName'), 'count']]
+ })
+ .then((output) => {
+ if (!output[0]) {
+ res.status(404).send({
+ status: 404,
+ error: {
+ message: 'No any rating found for that article'
+ }
+ });
+ } else {
+ calcRatings
+ .setInput(output)
+ .getResult()
+ .then(({ report, percentage }) => {
+ res.status(200).send({
+ status: 200,
+ data: {
+ report: {
+ '1st': Number(report.oneStars),
+ '2st': Number(report.twoStars),
+ '3st': Number(report.threeStars),
+ '4st': Number(report.fourStars),
+ '5st': Number(report.fiveStars),
+ 'Number of User ': Number(report.totalCounts),
+ 'Total Ratings': Number(report.totalRatings),
+ Average: Number(report.average)
+ },
+ percentage: {
+ '1st': `${percentage.oneStars} %`,
+ '2st': `${percentage.twoStars} %`,
+ '3st': `${percentage.threeStars} %`,
+ '4st': `${percentage.fourStars} %`,
+ '5st': `${percentage.fiveStars} %`
+ }
+ }
+ });
+ });
+ }
+ });
+ }
+}
+export default Ratings;
diff --git a/src/api/controllers/shareHighlights.js b/src/api/controllers/shareHighlights.js
new file mode 100644
index 0000000..86c0e24
--- /dev/null
+++ b/src/api/controllers/shareHighlights.js
@@ -0,0 +1,19 @@
+/**
+ * @Author - Mireille Niwemuhuza
+ */
+class shareHighlightController {
+ /**
+ * @param {object} req
+ * @param {object} res
+ * @returns {object} Object representing the response returned
+ */
+
+ // eslint-disable-next-line require-jsdoc
+ static async shareHighlights(req, res) {
+ return res.status(200).json({
+ message: 'Highlight shared!',
+ });
+ }
+}
+
+export default shareHighlightController;
diff --git a/src/api/controllers/socialLogin.js b/src/api/controllers/socialLogin.js
new file mode 100644
index 0000000..f66b370
--- /dev/null
+++ b/src/api/controllers/socialLogin.js
@@ -0,0 +1,88 @@
+import models from '../../sequelize/models';
+import tokenGeneration from '../../helpers/Token.helper';
+
+const userInfo = {
+ async googleLogin(req, res) {
+ const { displayName } = req.user;
+ const newUser = await models.User.create({
+ firstName: req.user.name.givenName,
+ lastName: req.user.name.familyName,
+ email: req.user.emails[0].value,
+ image: req.user.photos[0].value,
+ provider: req.user.provider,
+ verified: req.user.emails[0].verified,
+ socialId: req.user.id,
+ });
+ if (newUser) {
+ const {
+ dataValues: {
+ id, firstName, lastName, email, provider
+ }
+ } = newUser;
+ const token = await tokenGeneration.generateToken(newUser.dataValues);
+ return res.status(200).json({
+ message: `Welcome to Authors Haven ${displayName} `,
+ data: {
+ token, id, firstName, lastName, email, provider
+ },
+ });
+ }
+ },
+ async facebookLogin(req, res) {
+ const { displayName } = req.user;
+ const names = displayName.split(' ');
+ const newUser = await models.User.create({
+ firstName: names[0],
+ lastName: names[1],
+ email: req.user.emails[0].value,
+ image: req.user.photos[0].value,
+ provider: req.user.provider,
+ verified: true,
+ socialId: req.user.id,
+ });
+ if (newUser) {
+ const {
+ dataValues: {
+ id, firstName, lastName, email, provider
+ }
+ } = newUser;
+ const token = await tokenGeneration.generateToken(newUser.dataValues);
+ return res.status(200).json({
+ message: `Welcome to Authors Haven ${displayName} `,
+ data: {
+ token, id, firstName, lastName, email, provider
+ },
+ });
+ }
+ },
+ async twitterLogin(req, res) {
+ const {
+ displayName
+ } = req.user;
+ const names = displayName.split(' ');
+ const newUser = await models.User.create({
+ firstName: names[0],
+ lastName: names[1],
+ username: req.user.username,
+ image: req.user.photos[0].value,
+ provider: req.user.provider,
+ verified: true,
+ socialId: req.user.id,
+ });
+ if (newUser) {
+ const {
+ dataValues: {
+ id, firstName, lastName, email, provider
+ }
+ } = newUser;
+ const token = await tokenGeneration.generateToken(newUser.dataValues);
+ return res.status(200).json({
+ message: `Welcome to Authors Haven ${displayName} `,
+ data: {
+ token, id, firstName, lastName, email, provider
+ },
+ });
+ }
+ },
+};
+export default userInfo;
diff --git a/src/api/controllers/stats.js b/src/api/controllers/stats.js
new file mode 100644
index 0000000..a3d830f
--- /dev/null
+++ b/src/api/controllers/stats.js
@@ -0,0 +1,162 @@
+import models from '../../sequelize/models';
+
+const {
+ Article,
+ Comment,
+ Share
+} = models;
+
+/**
+ * @Author - Mireille Niwemuhuza
+ */
+class statsController {
+ /**
+ * @description - Users should be able to view how many times an article has been viewed
+ * @param {object} req - Request object
+ * @param {object} res - Response object
+ * @return {object} - Response object
+ */
+ static async getViews(req, res) {
+ const { slug } = req.params;
+ const articleViews = await Article.findAll({
+ attributes: ['title', 'views'],
+ where: {
+ slug
+ }
+ });
+ const { title, views } = articleViews[0];
+ return res.status(200).json({
+ data: { title, views }
+
+ });
+ }
+
+ /**
+ * @description - Users should be able to view the number of comments on an article
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async commentNumber(req, res) {
+ const { slug } = req.params;
+
+ // Count comments
+ const countComment = await Comment.count({
+ where: {
+ slug
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ slug,
+ countComment
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to view the number of shares on facebook
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async facebookShares(req, res) {
+ const { slug } = req.params;
+
+ // Count facebook shares
+ const shares = await Share.count({
+ where: {
+ slug,
+ provider: 'facebook'
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ slug,
+ shares
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to view the number of shares on twitter
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async twitterShares(req, res) {
+ const { slug } = req.params;
+
+ // Count shares on twitter
+ const shares = await Share.count({
+ where: {
+ slug,
+ provider: 'twitter'
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ slug,
+ shares
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to view the number of shares on email
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async emailShares(req, res) {
+ const { slug } = req.params;
+
+ // Count shares on email
+ const shares = await Share.count({
+ where: {
+ slug,
+ provider: 'email'
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ slug,
+ shares
+ }
+ });
+ }
+
+ /**
+ * @description - Users should be able to view the number of shares on email
+ * @param {Object} req - Request Object
+ * @param {Object} res - Response Object
+ * @returns {Object} - Response object
+ */
+ static async shares(req, res) {
+ const { slug } = req.params;
+
+ // Count shares on email
+ const shares = await Share.count({
+ where: {
+ slug
+ }
+ });
+
+ return res.status(200).json({
+ status: 200,
+ data: {
+ slug,
+ shares
+ }
+ });
+ }
+}
+export default statsController;
diff --git a/src/api/controllers/termsAndConditions.js b/src/api/controllers/termsAndConditions.js
new file mode 100644
index 0000000..6f8150b
--- /dev/null
+++ b/src/api/controllers/termsAndConditions.js
@@ -0,0 +1,50 @@
+import models from '../../sequelize/models';
+
+const {
+ termsAndCondition
+} = models;
+
+/**
+ * @Author - Mireille Niwemuhuza
+ */
+export default class termsAndConditions {
+/**
+* @description - Users should be able to get terms and conditions before signing up
+* @param {object} req - Request object
+* @param {object} res - Response object
+* @return {object} - Response object
+*/
+ static async getTermsAndConditions(req, res) {
+ const { id } = req.params;
+ const terms = await termsAndCondition.findOne({
+ where: {
+ id
+ }
+ });
+ if (terms) {
+ return res.status(200).json({
+ data: terms
+ });
+ }
+ return res.status(404).json({
+ message: 'Terms and Conditions not found!'
+ });
+ }
+
+ /**
+ * @description - Admin should be able to update terms and conditions
+ * @param {object} req - Request object
+ * @param {object} res - Response object
+ * @return {object} - Response object
+ */
+ static async updateTermsAndConditions(req, res) {
+ const { id } = req.params;
+ const termsConditions = await termsAndCondition.update({
+ termsAndConditions: req.body.termsAndConditions
+ }, { where: { id }, returning: true });
+ return res.status(200).json({
+ message: 'Terms and Conditions Updated!',
+ data: termsConditions[1][0]
+ });
+ }
+}
diff --git a/src/api/routes/articlesRouter.js b/src/api/routes/articlesRouter.js
new file mode 100644
index 0000000..d180554
--- /dev/null
+++ b/src/api/routes/articlesRouter.js
@@ -0,0 +1,137 @@
+import { Router } from 'express';
+import articlesController from '../controllers/articlesController';
+import Auth from '../../middleware/auth';
+import check from '../../middleware/checkOwner';
+import validateBody from '../../middleware/validateBody';
+import search from '../../middleware/search';
+import commentsController from '../controllers/comments';
+import comment from '../../middleware/validComment';
+import RatingController from '../controllers/ratingController';
+import slugExist from '../../middleware/slugExist';
+import isAlreadBlocked from '../../middleware/blockedarticleExist';
+import isNotBlocked from '../../middleware/articleNotBlocked';
+import isThisArticleBlocked from '../../middleware/isThisArticleBlocked';
+import bookmarkController from '../controllers/bookmark';
+import checkLikesandDislikes from '../../middleware/checkLikesDislikes';
+import paginate from '../../middleware/paginate';
+import shareArticle from '../../middleware/shareArticle';
+import stats from '../controllers/stats';
+import highlight from '../controllers/highlightController';
+import highlightExist from '../../middleware/highlightExist';
+import textExist from '../../middleware/textExist';
+import upload from '../../handlers/multer';
+import shareHighlight from '../../middleware/shareHighlights';
+import shareHighlightController from '../controllers/shareHighlights';
+import checkHighlight from '../../middleware/checkHighlight';
+
+const articlesRouter = Router();
+const {
+ getViews, commentNumber, facebookShares, twitterShares, emailShares, shares
+} = stats;
+const { shareHighlights } = shareHighlightController;
+const {
+ createArticle,
+ getAllArticle,
+ getOneArticle,
+ updateArticle,
+ deleteArticle,
+ likeArticle,
+ dislikeArticle,
+ getLikes,
+ getDislikes,
+ reportArticle,
+ blockArticle,
+ unBlockArticle,
+ share,
+ getBlockedArticles,
+ getReportedArticles
+} = articlesController;
+const { verifyToken, checkIsModerator } = Auth;
+const { createRatings, UpdateRatings } = RatingController;
+const { bookmark } = bookmarkController;
+const { createHighlights } = highlight;
+const { searchForArticle } = search;
+const {
+ createComment, editComment, deleteComment, getComment, commentAcomment,
+ likeComment, dislikeComment, countLikes, countDislikes, commentHistory
+} = commentsController;
+const { checkComment, checkParameter, articleExists } = comment;
+const { liked, disliked } = checkLikesandDislikes;
+const { highlights } = checkHighlight;
+
+articlesRouter
+ .post('/', verifyToken, upload.fields([{ name: 'gallery', maxCount: 10 }]), validateBody('createArticle'), createArticle)
+ .get('/', paginate, searchForArticle, getAllArticle);
+
+articlesRouter
+ .get('/:slug', slugExist, isThisArticleBlocked, getOneArticle)
+ .put('/:slug', verifyToken, check.articleOwner, upload.fields([{ name: 'gallery', maxCount: 10 }]), validateBody('updateArticle'), updateArticle)
+ .delete('/:slug', verifyToken, check.articleOwner, deleteArticle);
+
+articlesRouter
+ .get('/:slug/like', getLikes)
+ .post('/:slug/like', verifyToken, likeArticle);
+
+articlesRouter
+ .get('/:slug/dislike', getDislikes)
+ .post('/:slug/dislike', verifyToken, dislikeArticle);
+
+// Comments routes
+articlesRouter.post('/:slug/comments', verifyToken, validateBody('checkComment'), articleExists, checkComment, createComment);
+articlesRouter.post('/:slug/comments/:commentId', verifyToken, validateBody('checkComment'), articleExists, checkComment, commentAcomment);
+articlesRouter.patch('/comments/:commentId', verifyToken, validateBody('checkComment'), checkParameter, editComment);
+articlesRouter.delete('/comments/:commentId', verifyToken, checkParameter, deleteComment);
+articlesRouter.get('/:slug/comments', getComment);
+articlesRouter.post('/:slug/rating', verifyToken, validateBody('validateRating'), slugExist, createRatings);
+articlesRouter.put('/:slug/rating', verifyToken, validateBody('validateRating'), slugExist, UpdateRatings);
+
+// Bookmarks routes
+
+articlesRouter.post('/:slug/bookmark', verifyToken, slugExist, bookmark);
+// like and dislike comments
+articlesRouter.post('/comments/:commentId/like', verifyToken, checkParameter, liked, likeComment);
+articlesRouter.post('/comments/:commentId/dislike', verifyToken, checkParameter, disliked, dislikeComment);
+
+// get likes and dislikes of comments
+
+articlesRouter.get('/comments/:commentId/dislikes', checkParameter, countDislikes);
+articlesRouter.get('/comments/:commentId/likes', checkParameter, countLikes);
+// sharing articles
+articlesRouter.get('/:slug/share/twitter', verifyToken, slugExist, shareArticle, share);
+articlesRouter.get('/:slug/share/facebook', verifyToken, slugExist, shareArticle, share);
+articlesRouter.get('/:slug/share/linkedin', verifyToken, slugExist, shareArticle, share);
+articlesRouter.get('/:slug/share/pinterest', verifyToken, slugExist, shareArticle, share);
+articlesRouter.get('/:slug/share/email', verifyToken, slugExist, shareArticle, share);
+
+// sharing highlights
+
+articlesRouter.get('/:slug/highlights/:highlightId/share/twitter', verifyToken, slugExist, highlights, shareHighlight, shareHighlights);
+articlesRouter.get('/:slug/highlights/:highlightId/share/facebook', verifyToken, slugExist, highlights, shareHighlight, shareHighlights);
+articlesRouter.get('/:slug/highlights/:highlightId/share/email', verifyToken, slugExist, highlights, shareHighlight, shareHighlights);
+articlesRouter.post('/:slug/bookmark', verifyToken, slugExist, bookmark);
+
+articlesRouter.post('/:slug/report', verifyToken, validateBody('checkComment'), slugExist, reportArticle);
+// get comment edit history
+
+articlesRouter.get('/comments/:commentId/history', verifyToken, checkParameter, commentHistory);
+
+// articles reading stats
+
+articlesRouter.get('/:slug/comments/count', slugExist, commentNumber);
+articlesRouter.get('/:slug/views', slugExist, getViews);
+articlesRouter.get('/:slug/shares/facebook', slugExist, facebookShares);
+articlesRouter.get('/:slug/shares/twitter', slugExist, twitterShares);
+articlesRouter.get('/:slug/shares/email', slugExist, emailShares);
+articlesRouter.get('/:slug/shares', slugExist, shares);
+
+// block reported articles
+articlesRouter
+ .post('/:slug/block', verifyToken, checkIsModerator, validateBody('checkDescription'), slugExist, isAlreadBlocked, blockArticle)
+ .post('/:slug/unblock', verifyToken, checkIsModerator, slugExist, isNotBlocked, unBlockArticle)
+ .get('/blocked/all', verifyToken, checkIsModerator, getBlockedArticles)
+ .get('/reported/all', verifyToken, checkIsModerator, getReportedArticles);
+// highlight the text in the article
+
+articlesRouter.post('/:slug/highlight', verifyToken, validateBody('validateHighlight'), slugExist, highlightExist, textExist, createHighlights);
+
+export default articlesRouter;
diff --git a/src/api/routes/authRouter.js b/src/api/routes/authRouter.js
new file mode 100644
index 0000000..6fd0668
--- /dev/null
+++ b/src/api/routes/authRouter.js
@@ -0,0 +1,99 @@
+import { Router } from 'express';
+import passport from 'passport';
+// eslint-disable-next-line import/no-named-as-default
+import authController from '../controllers/auth';
+import validateBody from '../../middleware/validateBody';
+import userValidation from '../../middleware/validUser';
+import validateGender from '../../middleware/validateGender';
+import Auth from '../../middleware/auth';
+import socialLogin from '../controllers/socialLogin';
+import socialAccount from '../../middleware/socialAccountExists';
+import socialMiddleware from '../../middleware/socialTest';
+import termsAndConditions from '../controllers/termsAndConditions';
+
+const authRouter = Router();
+
+const {
+ RequestPasswordReset,
+ ConfirmPasswordReset,
+ ApplyPasswordReset,
+ register,
+ verifyAccount,
+ SignOut,
+ login
+} = authController;
+const { usernameExists, emailExists } = userValidation;
+const { verifyToken } = Auth;
+
+const { google, twitter } = socialAccount;
+const { getTermsAndConditions } = termsAndConditions;
+
+// terms and conditions
+
+authRouter.get('/termsandconditions/:id', getTermsAndConditions);
+
+// social login test routes
+
+authRouter.post(
+ '/login/google/test',
+ socialMiddleware,
+ google,
+ socialLogin.googleLogin
+);
+authRouter.post(
+ '/login/facebook/test',
+ socialMiddleware,
+ google,
+ socialLogin.facebookLogin
+);
+authRouter.post(
+ '/login/twitter/test',
+ socialMiddleware,
+ twitter,
+ socialLogin.twitterLogin
+);
+
+// social login
+
+authRouter.get(
+ '/login/google',
+ passport.authenticate('google', { scope: ['profile', 'email'] })
+);
+authRouter.get(
+ '/login/google/redirect',
+ passport.authenticate('google', { session: false }),
+ google,
+ socialLogin.googleLogin
+);
+
+authRouter.get(
+ '/login/facebook',
+ passport.authenticate('facebook', { scope: ['email'] })
+);
+authRouter.get(
+ '/login/facebook/redirect',
+ passport.authenticate('facebook', { session: false }),
+ google,
+ socialLogin.facebookLogin
+);
+
+authRouter.get(
+ '/login/twitter',
+ passport.authenticate('twitter', { scope: ['profile', 'email'] })
+);
+authRouter.get(
+ '/login/twitter/redirect',
+ passport.authenticate('twitter', { session: false }),
+ twitter,
+ socialLogin.twitterLogin
+);
+
+authRouter.get('/signout', verifyToken, SignOut);
+authRouter.post('/login', validateBody('login'), login);
+authRouter.post('/signup', validateBody('signup'), validateGender, usernameExists, emailExists, register);
+authRouter.get('/verify', verifyAccount);
+authRouter.post('/reset', validateBody('passwordReset'), RequestPasswordReset);
+authRouter.get('/reset/:token', ConfirmPasswordReset);
+authRouter.patch('/reset/:aprvToken', validateBody('applyPassword'), ApplyPasswordReset);
+
+export default authRouter;
diff --git a/src/api/routes/bookmark.js b/src/api/routes/bookmark.js
new file mode 100644
index 0000000..05dbe0f
--- /dev/null
+++ b/src/api/routes/bookmark.js
@@ -0,0 +1,10 @@
+import { Router } from 'express';
+import Auth from '../../middleware/auth';
+import BookmarkController from '../controllers/bookmark';
+
+const router = Router();
+const { getOwnerBookmarks } = BookmarkController;
+const { verifyToken } = Auth;
+
+router.get('/', verifyToken, getOwnerBookmarks);
+export default router;
diff --git a/src/api/routes/chatRouter.js b/src/api/routes/chatRouter.js
new file mode 100644
index 0000000..aace006
--- /dev/null
+++ b/src/api/routes/chatRouter.js
@@ -0,0 +1,13 @@
+import { Router } from 'express';
+import chat from '../controllers/chatController';
+import auth from '../../middleware/auth';
+
+const { getUsers, getMessages, getCurrentUser } = chat;
+const { verifyToken } = auth;
+const chatRouter = Router();
+
+chatRouter.get('/users', verifyToken, getUsers);
+chatRouter.get('/currentUser', getCurrentUser);
+chatRouter.get('/:username', verifyToken, getMessages);
+
+export default chatRouter;
diff --git a/src/api/routes/index.js b/src/api/routes/index.js
new file mode 100644
index 0000000..fcc3f9c
--- /dev/null
+++ b/src/api/routes/index.js
@@ -0,0 +1,23 @@
+import express from 'express';
+import authRouter from './authRouter';
+import userRouter from './userRouter';
+import profilesRouter from './profilesRouter';
+import articlesRouter from './articlesRouter';
+import ratingsRouter from './ratingsRouter';
+import bookmarkRouter from './bookmark';
+import termsAndConditionsRouter from './termsConditionsRouter';
+import chatRouter from './chatRouter';
+
+const api = express();
+
+// Routers go here
+api.use('/auth', authRouter);
+api.use('/user', userRouter);
+api.use('/profiles', profilesRouter);
+api.use('/articles', articlesRouter);
+api.use('/ratings', ratingsRouter);
+api.use('/bookmarks', bookmarkRouter);
+api.use('/termsandconditions', termsAndConditionsRouter);
+api.use('/chats', chatRouter);
+
+export default api;
diff --git a/src/api/routes/profilesRouter.js b/src/api/routes/profilesRouter.js
new file mode 100644
index 0000000..d68d716
--- /dev/null
+++ b/src/api/routes/profilesRouter.js
@@ -0,0 +1,36 @@
+import { Router } from 'express';
+// eslint-disable-next-line import/no-named-as-default
+import ProfilesController from '../controllers/profiles';
+import Auth from '../../middleware/auth';
+import validUser from '../../middleware/validUser';
+
+const { verifyToken } = Auth;
+const { userNameExist } = validUser;
+const profilesRouter = Router();
+const {
+ getProfile,
+ follow,
+ unfollow,
+ followers,
+ following,
+ getAllProfile
+} = ProfilesController;
+
+profilesRouter.get(
+ '/following',
+ verifyToken,
+ following
+);
+profilesRouter.get('/followers', verifyToken, followers);
+
+profilesRouter.get('/', getAllProfile);
+profilesRouter.get('/:username', getProfile);
+profilesRouter.patch('/:username/follow', verifyToken, userNameExist, follow);
+profilesRouter.patch(
+ '/:username/unfollow',
+ verifyToken,
+ userNameExist,
+ unfollow
+);
+
+export default profilesRouter;
diff --git a/src/api/routes/ratingsRouter.js b/src/api/routes/ratingsRouter.js
new file mode 100644
index 0000000..cf6ab69
--- /dev/null
+++ b/src/api/routes/ratingsRouter.js
@@ -0,0 +1,13 @@
+import { Router } from 'express';
+import ratingsController from '../controllers/ratingController';
+import auth from '../../middleware/auth';
+
+const { verifyToken } = auth;
+const ratingsRouter = Router();
+const {
+ calculateArticleRatings
+} = ratingsController;
+
+ratingsRouter.post('/articles/:slug', verifyToken, calculateArticleRatings);
+
+export default ratingsRouter;
diff --git a/src/api/routes/termsConditionsRouter.js b/src/api/routes/termsConditionsRouter.js
new file mode 100644
index 0000000..2b462d7
--- /dev/null
+++ b/src/api/routes/termsConditionsRouter.js
@@ -0,0 +1,15 @@
+import { Router } from 'express';
+import termsAndConditions from '../controllers/termsAndConditions';
+import validateBody from '../../middleware/validateBody';
+import Auth from '../../middleware/auth';
+
+const termsRouter = Router();
+
+const { updateTermsAndConditions } = termsAndConditions;
+const { verifyToken, checkIsAdmin } = Auth;
+
+// terms and conditions
+
+termsRouter.patch('/:id', verifyToken, checkIsAdmin, validateBody('validateTerms'), updateTermsAndConditions);
+
+export default termsRouter;
diff --git a/src/api/routes/userRouter.js b/src/api/routes/userRouter.js
new file mode 100644
index 0000000..d234b4a
--- /dev/null
+++ b/src/api/routes/userRouter.js
@@ -0,0 +1,24 @@
+import { Router } from 'express';
+import ProfilesController from '../controllers/profiles';
+import Auth from '../../middleware/auth';
+import validateBody from '../../middleware/validateBody';
+import upload from '../../handlers/multer';
+import OptController from '../controllers/optController';
+
+const userRouter = Router();
+const { updateProfile, deleteProfile } = ProfilesController;
+const {
+ optOutEmail, optOutApp, OptInApp, OptInEmail
+} = OptController;
+const { verifyToken, checkOwnership, checkIsAdmin } = Auth;
+
+userRouter.post('/optinemail', verifyToken, OptInEmail);
+userRouter.post('/optinapp', verifyToken, OptInApp);
+userRouter.delete('/optinemail', verifyToken, optOutEmail);
+userRouter.delete('/optinapp', verifyToken, optOutApp);
+userRouter
+ .route('/:id')
+ .put(verifyToken, checkOwnership, upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'cover', maxCount: 1 }]), validateBody('updateUser'), updateProfile)
+ .delete(verifyToken, checkIsAdmin, deleteProfile);
+
+export default userRouter;
diff --git a/src/config/config.js b/src/config/config.js
new file mode 100644
index 0000000..5558330
--- /dev/null
+++ b/src/config/config.js
@@ -0,0 +1,25 @@
+import dotenv from 'dotenv';
+
+const config = {};
+dotenv.config();
+config.development = {
+ use_env_variable: 'DEV_DATABASE_URL',
+ host: '127.0.0.1',
+ dialect: 'postgres',
+ logging: false
+};
+
+config.staging = {
+ use_env_variable: 'DATABASE_URL',
+};
+
+config.test = {
+ use_env_variable: 'DATABASE_TEST_URL',
+ logging: false,
+};
+
+config.production = {
+ dbUrl: process.env.PROD_DATABASE_URL,
+};
+
+module.exports = config;
diff --git a/src/config/passportSetup.js b/src/config/passportSetup.js
new file mode 100644
index 0000000..6ed9d73
--- /dev/null
+++ b/src/config/passportSetup.js
@@ -0,0 +1,43 @@
+/* eslint-disable */
+import passport from 'passport';
+import GoogleStrategy from 'passport-google-oauth20';
+import FacebookStrategy from 'passport-facebook';
+import TwitterTokenStrategy from 'passport-twitter';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+passport.use(new GoogleStrategy(
+ {
+ clientID: process.env.GOOGLE_ID,
+ clientSecret: process.env.GOOGLE_SECRET,
+ includeEmail: true,
+ callbackURL: '/api/auth/login/google/redirect'
+ },
+ (accessToken, refreshToken, profile, done) => {
+ done(null, profile);
+ }
+));
+passport.use(new FacebookStrategy(
+ {
+ clientID: process.env.FCBK_ID,
+ clientSecret: process.env.FCBK_APP_SECRET,
+ callbackURL: '/api/auth/login/facebook/redirect',
+ profileFields: ['id', 'displayName', 'photos', 'email']
+ },
+ (accessToken, refreshToken, profile, done) => {
+ done(null, profile);
+ }
+));
+passport.use(
+ new TwitterTokenStrategy(
+ {
+ consumerKey: process.env.TWITTER_ID,
+ consumerSecret: process.env.TWITTER_APP_SECRET,
+ callbackURL: `${process.env.BASE_URL}/api/auth/login/twitter/redirect`
+ },
+ (token, tokenSecret, profile, done) => {
+ done(null, profile);
+ }
+ )
+);
diff --git a/src/handlers/cloudinary.js b/src/handlers/cloudinary.js
new file mode 100644
index 0000000..e55fb26
--- /dev/null
+++ b/src/handlers/cloudinary.js
@@ -0,0 +1,10 @@
+import dotenv from 'dotenv';
+import cloudinary from 'cloudinary';
+
+dotenv.config();
+
+cloudinary.config({
+ cloud_name: process.env.CLOUD_NAME,
+ api_key: process.env.CLOUDINARY_API_ID,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+});
diff --git a/src/handlers/multer.js b/src/handlers/multer.js
new file mode 100644
index 0000000..32472ac
--- /dev/null
+++ b/src/handlers/multer.js
@@ -0,0 +1,11 @@
+import multer from 'multer';
+
+export default multer({
+ storage: multer.diskStorage({}),
+ fileFilter: (req, file, cb) => {
+ if (!file.mimetype.match(/jpeg|png|gif$i/)) {
+ cb(new Error('File is not supported'), false);
+ }
+ cb(null, true);
+ }
+});
diff --git a/src/helpers/Favourites.js b/src/helpers/Favourites.js
new file mode 100644
index 0000000..0fb4c03
--- /dev/null
+++ b/src/helpers/Favourites.js
@@ -0,0 +1,14 @@
+import db from '../sequelize/models';
+
+const { User } = db;
+
+const favouritedBy = async (userId) => {
+ const user = await User.findOne({
+ where: {
+ id: userId
+ }
+ });
+ return user ? user.dataValues : {};
+};
+
+export default favouritedBy;
diff --git a/src/helpers/FindArticle.js b/src/helpers/FindArticle.js
new file mode 100644
index 0000000..9488cf4
--- /dev/null
+++ b/src/helpers/FindArticle.js
@@ -0,0 +1,14 @@
+import db from '../sequelize/models';
+
+const { Article } = db;
+
+const findArticle = async (article) => {
+ const { datavalues } = await Article.findOne({
+ where: {
+ slug: article.slug
+ }
+ });
+ return datavalues || {};
+};
+
+export default findArticle;
diff --git a/src/helpers/FindUser.js b/src/helpers/FindUser.js
new file mode 100644
index 0000000..ea6388c
--- /dev/null
+++ b/src/helpers/FindUser.js
@@ -0,0 +1,13 @@
+import db from '../sequelize/models';
+
+const { User } = db;
+
+const findUser = async (username) => {
+ const { dataValues } = await User.findOne({
+ where: {
+ username
+ }
+ });
+ return dataValues || {};
+};
+export default findUser;
diff --git a/src/helpers/NotifyAuthorOnArticleBlock.js b/src/helpers/NotifyAuthorOnArticleBlock.js
new file mode 100644
index 0000000..153864e
--- /dev/null
+++ b/src/helpers/NotifyAuthorOnArticleBlock.js
@@ -0,0 +1,40 @@
+import sendEmail from './mailer/SendAnyEmail';
+import Template from './emailTemplateBlock';
+import TemplateUnblock from './emailTemplateUnblock';
+
+/**
+ * @author EmyRukundo
+ * @class AuthController
+ * @description this class performs the whole authentication
+ */
+class notifyAuthor {
+ /**
+ *
+ * @param {Object} data - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async notifyAuthorblock(data) {
+ const mail = {
+ lastName: data.lastName, email: data.email
+ };
+ const htmlToSend = Template.articleBlockedTemplate(mail.lastName);
+ await sendEmail(mail, htmlToSend, 'Notification');
+ }
+
+ /**
+ *
+ * @param {Object} data - Request object
+ * @param {Object} res - Response object
+ * @returns {Object} - Response object
+ */
+ static async notifyAuthorUnblock(data) {
+ const link = `${process.env.BASE_URL}/api/articles/${data.slug}`;
+ const mailUnblock = {
+ lastName: data.lastName, email: data.email
+ };
+ const htmlToSendU = TemplateUnblock.articleUnBlockedTemplate(mailUnblock.lastName, link);
+ await sendEmail(mailUnblock, htmlToSendU, 'Congratulation');
+ }
+}
+export default notifyAuthor;
diff --git a/src/helpers/ReadTime.helper.js b/src/helpers/ReadTime.helper.js
new file mode 100644
index 0000000..422d663
--- /dev/null
+++ b/src/helpers/ReadTime.helper.js
@@ -0,0 +1,14 @@
+const converter = (seconds) => {
+ if (seconds > 60) {
+ return `${Math.ceil(seconds / 60)} min`;
+ }
+ return 'Less than a minute';
+};
+const readTime = (body) => {
+ const numWords = w => w.split(' ').length;
+ const WPS = 4;
+ const words = numWords(body);
+ const sec = words / WPS;
+ return converter(sec);
+};
+export default readTime;
diff --git a/src/helpers/SendMail.helper.js b/src/helpers/SendMail.helper.js
new file mode 100644
index 0000000..aac84bf
--- /dev/null
+++ b/src/helpers/SendMail.helper.js
@@ -0,0 +1,38 @@
+
+import mailer from 'nodemailer';
+import mailTemplate from './MailTemplate.helper';
+
+const transporter = mailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.AUTHOSHAVEN_USER,
+ pass: process.env.AUTHOSHAVEN_PASS
+ }
+});
+/**
+ * @author Elie Mugenzi
+ * @class MailHelper
+ * @description A helper class for sending emails
+ */
+class MailHelper {
+ /**
+ * Send mail
+ * @param {Object} param0 - Object which contains email information
+ * @returns {Object} Results after sending mail
+ */
+ static async sendMail({
+ to, names, subject, message, token
+ }) {
+ const msg = {
+ from: `Authors Haven<${process.env.AUTHOSHAVEN_USER}>`,
+ to,
+ subject,
+ text: message,
+ html: mailTemplate({ to, token, names })
+ };
+ const result = await transporter.sendMail(msg);
+ return result;
+ }
+}
+
+export default MailHelper;
diff --git a/src/helpers/SocketIO.js b/src/helpers/SocketIO.js
new file mode 100644
index 0000000..25b8954
--- /dev/null
+++ b/src/helpers/SocketIO.js
@@ -0,0 +1,88 @@
+import http from 'http';
+import dotenv from 'dotenv';
+import socketIO from 'socket.io';
+import eventEmitter from './notifications/EventEmitter';
+import Tokenizer from './Token.helper';
+import chatHelper from './chat/saveChats';
+
+const { saveMessage, updateReadMessages, getUnreadMessageCount } = chatHelper;
+
+dotenv.config();
+
+const SocketIO = (app) => {
+ http.createServer(app);
+ const port = process.env.SOCKET_PORT;
+ const io = socketIO.listen(app.listen(port, () => {
+ // eslint-disable-next-line no-console
+ console.log(`Socket.IO is running on port ${port}`);
+ }));
+ io.use((socket, next) => {
+ next(null, next);
+ });
+ io.on('connection', (socket) => {
+ eventEmitter.on('new_inapp', (message, user) => {
+ socket.emit('new_message', {
+ message,
+ user
+ });
+ });
+ socket.on('user_back', async (token) => {
+ try {
+ const currentUser = await Tokenizer.decodeToken(token);
+ if (currentUser.username) {
+ const unreadCount = await getUnreadMessageCount(currentUser.username);
+ if (unreadCount !== 0) {
+ socket.emit('message_unread', {
+ receiver: currentUser.username,
+ unreadCount
+ });
+ }
+ }
+ } catch (error) {
+ io.emit('no_auth', {
+ message: 'You are not authenticated'
+ });
+ }
+ });
+ });
+
+ const chats = io.of('/chats');
+ chats.on('connection', (socket) => {
+ socket.on('new_user', async (token) => {
+ try {
+ const currentUser = await Tokenizer.decodeToken(token);
+ const count = await getUnreadMessageCount(currentUser.username);
+ if (count !== 0) {
+ await updateReadMessages(currentUser.username);
+ }
+ if (currentUser.username) {
+ socket.on('chat', async ({ sender, receiver, message }) => {
+ const { id } = await saveMessage({
+ sender, receiver, message, read: false
+ });
+ chats.emit('chat', {
+ id, sender, message, receiver
+ });
+ const unreadCount = await getUnreadMessageCount(currentUser.username);
+ if (unreadCount !== 0) {
+ io.sockets.emit('message_unread', {
+ receiver: currentUser.username,
+ unreadCount
+ });
+ }
+ });
+ socket.on('typing', (data) => {
+ socket.broadcast.emit('typing', data);
+ });
+ }
+ } catch (error) {
+ chats.emit('no_auth', {
+ message: 'You are not authenticated'
+ });
+ }
+ });
+ });
+ return io;
+};
+
+export default SocketIO;
diff --git a/src/helpers/Token.helper.js b/src/helpers/Token.helper.js
new file mode 100644
index 0000000..01524fd
--- /dev/null
+++ b/src/helpers/Token.helper.js
@@ -0,0 +1,20 @@
+/* eslint-disable require-jsdoc */
+import jwt from 'jsonwebtoken';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+class Tokenizer {
+ static async generateToken(payload) {
+ const token = await jwt.sign(payload, process.env.SECRET_KEY, { expiresIn: '10h' });
+
+ return token;
+ }
+
+ static async decodeToken(token) {
+ const user = await jwt.verify(token, process.env.SECRET_KEY);
+
+ return user;
+ }
+}
+export default Tokenizer;
diff --git a/src/helpers/articlesHelper.js b/src/helpers/articlesHelper.js
new file mode 100644
index 0000000..730e6a7
--- /dev/null
+++ b/src/helpers/articlesHelper.js
@@ -0,0 +1,94 @@
+/* eslint-disable require-jsdoc */
+/* eslint-disable valid-jsdoc */
+import slug from 'slug';
+import uniqid from 'uniqid';
+import models from '../sequelize/models';
+import readTime from './ReadTime.helper';
+import workers from '../workers';
+
+const { Article, User } = models;
+const { uploadImageWorker } = workers;
+
+/**
+ * @description Helpers for articles
+ */
+class ArticlesHelper {
+ /**
+ * @param {title}
+ * @returns {newSlug} return a new slug
+ */
+ static createSlug(title) {
+ const newSlug = `${slug(title, { lower: true })}-${uniqid.process()}`;
+ return newSlug;
+ }
+
+ /**
+ * @param {object} req - Request.
+ * @param {object} res - Response.
+ * @returns {object} - Contains an article information.
+ */
+ static async createNewArticle(req) {
+ const {
+ title, body, description, tagList
+ } = req.body;
+ const { id } = req.user;
+ const newSlug = this.createSlug(title);
+ const readtime = readTime(body);
+ const { dataValues } = await Article.create({
+ slug: newSlug,
+ title,
+ description,
+ body,
+ tagList: tagList.split(','),
+ authorId: id,
+ readtime,
+ views: 0,
+ });
+
+ // Uplooad article image
+ if (req.files) {
+ uploadImageWorker(req.files, dataValues.id, 'article', null);
+ }
+
+ const userInfo = await this.getUserInfo(id);
+ const { username, bio, image } = userInfo;
+ const author = { username, bio, image };
+ dataValues.author = author;
+ return dataValues;
+ }
+
+ static async getUserInfo(id) {
+ const { dataValues } = await User.findOne({ where: { id } });
+ return dataValues;
+ }
+
+ static async getAllArticle() {
+ const result = await Article.findAll(
+ { where: { blocked: false } },
+ {
+ include: [{
+ as: 'author',
+ model: User,
+ attributes: ['username', 'bio', 'avatar']
+ }],
+ attributes: ['id', 'slug', 'title', 'description', 'readtime', 'body', 'tagList', 'updatedAt', 'createdAt'],
+ limit: 10
+ }
+ );
+ return result;
+ }
+
+ static async getOneSlug(newSlug) {
+ const result = await Article.findOne({
+ where: { slug: newSlug },
+ include: [{
+ as: 'author',
+ model: User,
+ attributes: ['username', 'bio', 'avatar']
+ }],
+ attributes: ['slug', 'title', 'description', 'readtime', 'body', 'tagList', 'views', 'updatedAt', 'createdAt']
+ });
+ return result;
+ }
+}
+export default ArticlesHelper;
diff --git a/src/helpers/chat/saveChats.js b/src/helpers/chat/saveChats.js
new file mode 100644
index 0000000..be2605f
--- /dev/null
+++ b/src/helpers/chat/saveChats.js
@@ -0,0 +1,66 @@
+import db from '../../sequelize/models';
+import findUser from '../FindUser';
+
+const { Chat } = db;
+
+/**
+ * @author Elie Mugenzi
+ * @class ChatHelper
+ * @description this class performs the whole authentication
+ */
+class ChatHelper {
+ /**
+ *
+ * @param {Object} message - Request object
+ * @returns {Object} - Response object
+ */
+ static async saveMessage(message) {
+ const {
+ sender, receiver, message: chatMessage, read
+ } = message;
+ const { id: senderId } = await findUser(sender);
+ const infos = await findUser(receiver);
+
+ const newChat = await Chat.create({
+ senderId,
+ recieverId: infos.id,
+ message: chatMessage,
+ read
+ });
+ return newChat;
+ }
+
+ /**
+ *
+ * @param {String} username - Request object
+ * @returns {Object} - Response object
+ */
+ static async updateReadMessages(username) {
+ const { id } = await findUser(username);
+ const result = await Chat.update({
+ read: true,
+ }, {
+ where: {
+ recieverId: id,
+ read: false,
+ }
+ });
+ return result;
+ }
+
+ /**
+ *
+ * @param {String} username - Request object
+ * @returns {Number} - Response object
+ */
+ static async getUnreadMessageCount(username) {
+ const { id } = await findUser(username);
+ const result = await Chat.count({
+ where: { recieverId: id, read: false }
+ });
+ return result;
+ }
+}
+
+
+export default ChatHelper;
diff --git a/src/helpers/dbUrlParser.js b/src/helpers/dbUrlParser.js
new file mode 100644
index 0000000..e51be6c
--- /dev/null
+++ b/src/helpers/dbUrlParser.js
@@ -0,0 +1,26 @@
+import dotenv from 'dotenv';
+import url from 'url';
+
+dotenv.config();
+
+export default (dbUrl) => {
+ const urlObj = url.parse(dbUrl);
+ const {
+ auth,
+ port: dbPort,
+ hostname: dbHost,
+ pathname
+ } = urlObj;
+
+ const dbUser = auth.split(':')[0];
+ const dbPassword = auth.split(':')[1] || '';
+ const dbName = pathname.split('/')[1];
+
+ return {
+ dbUser,
+ dbPassword,
+ dbName,
+ dbPort,
+ dbHost
+ };
+};
diff --git a/src/helpers/destroyTables.js b/src/helpers/destroyTables.js
new file mode 100644
index 0000000..da77513
--- /dev/null
+++ b/src/helpers/destroyTables.js
@@ -0,0 +1,38 @@
+import models from '../sequelize/models';
+
+models.ArticleRatings.destroy({
+ where: {},
+ truncate: true
+});
+models.Article.destroy({
+ where: {},
+ truncate: false
+});
+models.Blacklist.destroy({
+ where: {},
+ truncate: false
+});
+models.Comment.destroy({
+ where: {},
+ truncate: false
+});
+models.LikeDislike.destroy({
+ where: {},
+ truncate: false
+});
+models.ReportedArticles.destroy({
+ where: {},
+ truncate: false
+});
+models.User.destroy({
+ where: {},
+ truncate: false
+});
+models.Blacklist.destroy({
+ where: {},
+ truncate: true
+});
+models.Comment.destroy({
+ where: {},
+ truncate: true
+});
diff --git a/src/helpers/emailTemplate.js b/src/helpers/emailTemplate.js
new file mode 100644
index 0000000..f0c7c03
--- /dev/null
+++ b/src/helpers/emailTemplate.js
@@ -0,0 +1,48 @@
+/**
+ * @class ResetPassword
+ * @description Authentication based class
+ * */
+class Template {
+/**
+ * Verify token middleware
+ * @param {String} firstname - Request
+ * @param {String} lastname - Response
+ * @param {String} token -EmailTemplate
+ * @returns {String} The response String
+ */
+ static getPasswordResetTemplete(firstname, lastname, token) {
+ return `
+
+
+
+ Authors Haven
+
+
+
+
Hi ${firstname} ${lastname}
+ You Recently requested a password reset for your Authors Haven Account, Click the the Button below
+ to reset it.
+
+
+ If you did not request a password reset please ignore this email or reply to let us know.
+ This link will be expired in next 10 minutes.
+
+
Visit ahmedkhaled4d's website
+
+
+
+ Authors Haven
+
+
+
+ Copyright, 2019
+ Authors Haven
+
+
+ `;
+ }
+}
+
+export default Template;
diff --git a/src/helpers/emailTemplateBlock.js b/src/helpers/emailTemplateBlock.js
new file mode 100644
index 0000000..b5cd4ca
--- /dev/null
+++ b/src/helpers/emailTemplateBlock.js
@@ -0,0 +1,42 @@
+/**
+ * @class ResetPassword
+ * @description Authentication based class
+ * */
+class Template {
+/**
+ * Verify token middleware
+ * @param {String} lastname - Request
+ * @param {String} token -EmailTemplate
+ * @returns {String} The response String
+ */
+ static articleBlockedTemplate(lastname) {
+ return `
+
+
+
+ Authors Haven
+
+
+
+
Dear ${lastname}
+ Your article is blocked on our site because it doesn't follow our terms and conditions
+
+ If you feel it's just a mistake, you can contact administrator
+
+
Visit ahmedkhaled4d's website
+
+
+
+ Authors Haven
+
+
+
+ Copyright, 2019
+ Authors Haven
+
+
+ `;
+ }
+}
+
+export default Template;
diff --git a/src/helpers/emailTemplateUnblock.js b/src/helpers/emailTemplateUnblock.js
new file mode 100644
index 0000000..b61f489
--- /dev/null
+++ b/src/helpers/emailTemplateUnblock.js
@@ -0,0 +1,41 @@
+/**
+ * @class ResetPassword
+ * @description Authentication based class
+ * */
+class TemplateUnblock {
+/**
+ * Verify token middleware
+ * @param {String} lastname - Request
+ * @param {string} link - Article link
+ * @returns {String} The response String
+ */
+ static articleUnBlockedTemplate(lastname, link) {
+ return `
+
+
+
+ Authors Haven
+
+
+
+
Dear ${lastname}
+ Congratulation!! Your article is unblocked on our site, you can visit your article here ${link}
+
+
+
Visit ahmedkhaled4d's website
+
+
+
+ Authors Haven
+
+
+
+ Copyright, 2019
+ Authors Haven
+
+
+ `;
+ }
+}
+
+export default TemplateUnblock;
diff --git a/src/helpers/emailVerifyTemplate.js b/src/helpers/emailVerifyTemplate.js
new file mode 100644
index 0000000..86f2aec
--- /dev/null
+++ b/src/helpers/emailVerifyTemplate.js
@@ -0,0 +1,48 @@
+/**
+ * @class Template
+ * @description Authentication based class
+ * */
+class Template {
+ /**
+ * Verifiy Template
+ * @param {String} names -EmailTemplate
+ * @param {String} to -EmailTemplate
+ * @param {String} token -EmailTemplate
+ * @returns {String} The response String
+ */
+ static sendVerification(names, to, token) {
+ return `
+
+
+
+ Authors Haven - Team Tesla
+
+
+
+
+
+ Well ${names}, congratulations for choosing AuthorsHaven.
+ To verify that ${to} is your email, could you please click this link below to verify your AuthorsHaven's account?
+
+ Click here to verify your account
+
+ Here there is the link below where you can visit ahmedkhaled4d and get more information about what's ahmedkhaled4d
+
+
+
Visit ahmedkhaled4d's website
+
+
+
+ ahmedkhaled4d, Team @Tesla - Cohort 5
+
+
+
+ Copyright, 2019
+ ahmedkhaled4d, Team Tesla
+
+
+ `;
+ }
+}
+
+export default Template;
diff --git a/src/helpers/hashHelper.js b/src/helpers/hashHelper.js
new file mode 100644
index 0000000..35bf230
--- /dev/null
+++ b/src/helpers/hashHelper.js
@@ -0,0 +1,27 @@
+import bcrypt from 'bcrypt';
+
+/**
+ * @class HashHelper
+ */
+class HashHelper {
+ /**
+ * Hashes password
+ * @param {String} password - Password to hash
+ * @returns {String} - hashed Password
+ */
+ static hashPassword(password) {
+ return bcrypt.hashSync(password, 8);
+ }
+
+ /**
+ * Compares Passwords
+ * @param {String} password - Password provided by a user
+ * @param {String} passwordToCompare - Password from Database
+ * @returns {Boolean} -True if they're equal, otherwise false
+ */
+ static comparePassword(password, passwordToCompare) {
+ return bcrypt.compareSync(password, passwordToCompare);
+ }
+}
+
+export default HashHelper;
diff --git a/src/helpers/hashSuperUserPsw.js b/src/helpers/hashSuperUserPsw.js
new file mode 100644
index 0000000..2e4c8b2
--- /dev/null
+++ b/src/helpers/hashSuperUserPsw.js
@@ -0,0 +1,8 @@
+import dotenv from 'dotenv';
+import password from './hashHelper';
+
+dotenv.config();
+
+const hashed = password.hashPassword(process.env.SUPER_ADMIN_PSW);
+
+export default hashed;
diff --git a/src/helpers/mailer/SendAnyEmail.js b/src/helpers/mailer/SendAnyEmail.js
new file mode 100644
index 0000000..89564c6
--- /dev/null
+++ b/src/helpers/mailer/SendAnyEmail.js
@@ -0,0 +1,25 @@
+import dotenv from 'dotenv';
+import nodemailer from 'nodemailer';
+
+dotenv.config();
+
+const sendEmail = async (mail, htmlToSend, subject) => {
+ const transport = nodemailer.createTransport({
+ service: 'gmail',
+ auth: {
+ user: process.env.AUTHOSHAVEN_USER,
+ pass: process.env.AUTHOSHAVEN_PASS
+ }
+ });
+ const mailOptions = {
+ from: 'Authors Haven',
+ to: `${mail.email}`,
+ subject,
+ text: '',
+ html: htmlToSend
+ };
+
+ transport.sendMail(mailOptions, async () => true);
+};
+
+export default sendEmail;
diff --git a/src/helpers/mailer/templates/index.html b/src/helpers/mailer/templates/index.html
new file mode 100644
index 0000000..fa4003c
--- /dev/null
+++ b/src/helpers/mailer/templates/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ Socket.IO Client
+
+
+
+
+ Authors Haven
+
+
+
+
+
+
+
+
Welcome to Socket.IO client
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/helpers/mailer/templates/index.js b/src/helpers/mailer/templates/index.js
new file mode 100644
index 0000000..649cc8e
--- /dev/null
+++ b/src/helpers/mailer/templates/index.js
@@ -0,0 +1,3 @@
+import notification from './notification';
+
+export default { notification };
diff --git a/src/helpers/mailer/templates/js/main.js b/src/helpers/mailer/templates/js/main.js
new file mode 100644
index 0000000..2395508
--- /dev/null
+++ b/src/helpers/mailer/templates/js/main.js
@@ -0,0 +1,13 @@
+/* eslint-disable no-console */
+/* eslint-disable no-undef */
+$(document).ready(() => {
+ const socket = io('http://localhost:5000');
+ socket.on('connect', () => {
+ console.log('Connected to Socket.IO server');
+ });
+
+ socket.on('new_message', ({ message, user }) => {
+ $('.publish-info').html(message);
+ console.log(`Current User: ${user}`);
+ });
+});
diff --git a/src/helpers/mailer/templates/notification.js b/src/helpers/mailer/templates/notification.js
new file mode 100644
index 0000000..3314439
--- /dev/null
+++ b/src/helpers/mailer/templates/notification.js
@@ -0,0 +1,6 @@
+export default (data) => {
+ const message = {};
+ message.subject = 'Authors Haven - Notification';
+ message.html = `${data.message}
`;
+ return message;
+};
diff --git a/src/helpers/notifications/EventEmitter.js b/src/helpers/notifications/EventEmitter.js
new file mode 100644
index 0000000..4742900
--- /dev/null
+++ b/src/helpers/notifications/EventEmitter.js
@@ -0,0 +1,3 @@
+import EventEmitter from 'events';
+
+export default new EventEmitter();
diff --git a/src/helpers/notifications/EventListener.js b/src/helpers/notifications/EventListener.js
new file mode 100644
index 0000000..c903a37
--- /dev/null
+++ b/src/helpers/notifications/EventListener.js
@@ -0,0 +1,7 @@
+import eventEmitter from './EventEmitter';
+import commentArticle from './commentArticle';
+import publishArticle from './publishArticle';
+
+eventEmitter.on('commentArticle', commentArticle);
+eventEmitter.on('publishArticle', publishArticle);
+eventEmitter.on('error', err => process.stdout.write('Oops! an event error occurred') && err);
diff --git a/src/helpers/notifications/Notify.js b/src/helpers/notifications/Notify.js
new file mode 100644
index 0000000..a4fe757
--- /dev/null
+++ b/src/helpers/notifications/Notify.js
@@ -0,0 +1,57 @@
+import db from '../../sequelize/models';
+import sendMail from '../mailer/SendAnyEmail';
+import eventEmitter from './EventEmitter';
+
+const { Notification, Opt, User } = db;
+
+const notify = async (data) => {
+ let inAppNotification = {};
+ let emailNotification = {};
+
+ const {
+ resource, user, inAppMessage, emailMessage
+ } = data;
+ const optedin = await Opt.findAll({
+ where: {
+ userId: user.followerId
+ }
+ });
+ optedin.map(async (subscription) => {
+ const { dataValues } = await User.findOne({
+ where: {
+ id: user.userId
+ }
+ });
+ switch (subscription.type) {
+ case 'email':
+ emailNotification = await Notification.create({
+ userId: user.userId,
+ resource,
+ message: emailMessage,
+ type: subscription.type
+ });
+ await sendMail(dataValues.email, 'notification', {
+ message: emailMessage
+ });
+ break;
+ case 'inapp':
+ inAppNotification = await Notification.create({
+ userId: user.userId,
+ resource,
+ message: inAppMessage,
+ type: subscription.type
+ });
+ eventEmitter.emit('new_inapp', inAppMessage, dataValues);
+ break;
+ default:
+ break;
+ }
+ });
+ const response = {
+ inAppNotification,
+ emailNotification
+ };
+ return response;
+};
+
+export default notify;
diff --git a/src/helpers/notifications/NotifyForBookmarks.js b/src/helpers/notifications/NotifyForBookmarks.js
new file mode 100644
index 0000000..ba34a16
--- /dev/null
+++ b/src/helpers/notifications/NotifyForBookmarks.js
@@ -0,0 +1,57 @@
+import db from '../../sequelize/models';
+import sendMail from '../mailer/SendAnyEmail';
+import eventEmitter from './EventEmitter';
+
+const { Notification, Opt, User } = db;
+
+const notify = async (data) => {
+ let inAppNotification = {};
+ let emailNotification = {};
+
+ const {
+ resource, user, inAppMessage, emailMessage
+ } = data;
+ const optedin = await Opt.findAll({
+ where: {
+ userId: user.id
+ }
+ });
+ optedin.map(async (subscription) => {
+ const { dataValues } = await User.findOne({
+ where: {
+ id: user.id
+ }
+ });
+ switch (subscription.type) {
+ case 'email':
+ await sendMail(dataValues.email, 'notification', {
+ message: emailMessage
+ });
+ emailNotification = await Notification.create({
+ userId: user.id,
+ resource,
+ message: emailMessage,
+ type: subscription.type
+ });
+ break;
+ case 'inapp':
+ inAppNotification = await Notification.create({
+ userId: user.id,
+ resource,
+ message: inAppMessage,
+ type: subscription.type
+ });
+ eventEmitter.emit('new_inapp', inAppMessage, dataValues);
+ break;
+ default:
+ break;
+ }
+ });
+ const response = {
+ inAppNotification,
+ emailNotification
+ };
+ return response;
+};
+
+export default notify;
diff --git a/src/helpers/notifications/commentArticle.js b/src/helpers/notifications/commentArticle.js
new file mode 100644
index 0000000..712190d
--- /dev/null
+++ b/src/helpers/notifications/commentArticle.js
@@ -0,0 +1,49 @@
+import db from '../../sequelize/models';
+import favouritedBy from '../Favourites';
+import notify from './NotifyForBookmarks';
+
+const { User, Bookmarks } = db;
+
+const commentArticle = async (comment) => {
+ try {
+ const author = await User.findOne({
+ where: {
+ id: comment.userId
+ }
+ });
+ const favourites = await Bookmarks.findAll({
+ where: {
+ slug: comment.slug,
+ }
+ });
+ if (favourites) {
+ favourites.map(async (fav) => {
+ const user = await favouritedBy(fav.userId);
+ const inAppMessage = `Dear ${user.firstName} ${user.lastName}, ${author.dataValues.lastName} commented on the article you bookmarked!
+ `;
+ const emailMessage = `
+ Hello ${user.firstName}, ${author.lastName} commented on the article you bookmarked!
+ Please click below to read the comment:
+ View all comments
+
+ `;
+
+ const data = {
+ resource: 'articles',
+ action: 'comment',
+ user,
+ inAppMessage,
+ emailMessage
+ };
+ const res = await notify(data);
+ return res;
+ });
+ }
+ } catch (err) {
+ return {
+ errors: err
+ };
+ }
+};
+
+export default commentArticle;
diff --git a/src/helpers/notifications/config.js b/src/helpers/notifications/config.js
new file mode 100644
index 0000000..e4ff928
--- /dev/null
+++ b/src/helpers/notifications/config.js
@@ -0,0 +1,14 @@
+export default {
+ inApp: {
+ articles: {
+ show: true,
+ on: ['publish', 'comment', 'like']
+ }
+ },
+ email: {
+ articles: {
+ show: true,
+ on: ['publish', 'comment', 'like']
+ }
+ }
+};
diff --git a/src/helpers/notifications/publishArticle.js b/src/helpers/notifications/publishArticle.js
new file mode 100644
index 0000000..6abf748
--- /dev/null
+++ b/src/helpers/notifications/publishArticle.js
@@ -0,0 +1,47 @@
+import dotenv from 'dotenv';
+import notify from './Notify';
+import db from '../../sequelize/models';
+
+const { User, follows } = db;
+dotenv.config();
+
+export default async (authorId, slug) => {
+ try {
+ const author = await User.findOne({
+ where: {
+ id: authorId
+ }
+ });
+ const url = `${process.env.BASE_URL}/api/articles/${slug}`;
+ const followers = await follows.findAll({
+ where: {
+ userId: authorId
+ }
+ });
+ followers.map(async (follower) => {
+ const user = await User.findOne({
+ where: {
+ id: follower.dataValues.followerId
+ }
+ });
+ const inAppMessage = `Hello ${user.dataValues.firstName}, ${author.dataValues.firstName} ${author.dataValues.lastName} published a new article`;
+ const emailMessage = `Hello ${user.dataValues.firstName}, ${author.dataValues.firstName} ${author.dataValues.lastName} published a new article
+
+ Read an article `;
+ const data = {
+ resource: 'articles',
+ action: 'publish',
+ user: follower.dataValues,
+ inAppMessage,
+ emailMessage,
+ url
+ };
+ const res = await notify(data);
+ return res;
+ });
+ } catch (error) {
+ return {
+ errors: error
+ };
+ }
+};
diff --git a/src/helpers/validationSchemas.js b/src/helpers/validationSchemas.js
new file mode 100644
index 0000000..ed492c6
--- /dev/null
+++ b/src/helpers/validationSchemas.js
@@ -0,0 +1,156 @@
+import Joi from '@hapi/joi';
+
+const password = Joi.string()
+ .trim()
+ .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/)
+ .required()
+ .label('Password is required and must be at least 8 letters containing'
+ + ' at least a number a Lowercase letter and an Uppercase letter');
+const email = Joi.string()
+ .trim()
+ .lowercase()
+ .email()
+ .required()
+ .label('Email is required and should look like this : example@email.com!');
+
+export default {
+ passwordReset: Joi.object().keys({
+ email
+ }),
+ applyPassword: Joi.object().keys({
+ newpassword: password
+ }),
+ login: Joi.object().keys({
+ email: Joi.string()
+ .trim()
+ .lowercase()
+ .required()
+ .min(3)
+ .label('Username or email are required, they must have at least 3 letters'),
+ password
+ }),
+ signup: Joi.object().keys({
+ firstName: Joi.string()
+ .trim()
+ .required()
+ .regex(/^[A-Za-z_-]+$/)
+ .min(3)
+ .label('First name is required, it must have at least 3 letters and must contain only letters, underscores(_) and hyphens (-)'),
+ lastName: Joi.string()
+ .trim()
+ .required()
+ .regex(/^[A-Za-z_.-]+$/)
+ .min(3)
+ .label('Last name is required, it must have at least 3 letters and must contain only letters, underscores(_) and hyphens (-)'),
+ username: Joi.string()
+ .trim()
+ .lowercase()
+ .required()
+ .regex(/^[a-zA-Z0-9_.-]+$/)
+ .min(3)
+ .label('Username is required, it must have at least 3 letters and must contain only letters, numbers, underscores(_), hyphens (-) and points (.)'),
+ email,
+ password,
+ confirmPassword: Joi.any()
+ .required()
+ .valid(Joi.ref('password'))
+ .label('Password and Confirm Password do not match'),
+ bio: Joi.string(),
+ image: Joi.string(),
+ dateOfBirth: Joi.string(),
+ gender: Joi.string(),
+ socialId: Joi.string(),
+ provider: Joi.string()
+ }),
+ updateUser: Joi.object().keys({
+ firstName: Joi.string()
+ .trim()
+ .regex(/^[A-Za-z_-]+$/)
+ .min(3)
+ .label('First name should have at least 3 letters and must contain only letters, underscores(_) and hyphens (-)'),
+ lastName: Joi.string()
+ .trim()
+ .regex(/^[A-Za-z_.-]+$/)
+ .min(3)
+ .label('Last name should have at least 3 letters and must contain only letters, underscores(_) and hyphens (-)'),
+ username: Joi.string()
+ .trim()
+ .lowercase()
+ .regex(/^[a-zA-Z0-9_.-]+$/)
+ .min(3)
+ .label('Username should have at least 3 letters and must contain only letters, numbers, underscores(_), hyphens (-) and points (.)'),
+ email: Joi.string()
+ .trim()
+ .lowercase()
+ .email()
+ .label('Email should look like this : example@email.com!'),
+ bio: Joi.string(),
+ image: Joi.string(),
+ dateOfBirth: Joi.string(),
+ gender: Joi.string(),
+ socialId: Joi.string(),
+ provider: Joi.string()
+ }),
+ createArticle: Joi.object().keys({
+ title: Joi.string()
+ .required()
+ .label('title is required and should be a string'),
+ body: Joi.string()
+ .required()
+ .label('body is required and should be a string'),
+ description: Joi.string()
+ .required()
+ .label('description is required and should be a string'),
+ tagList: Joi.string()
+ }),
+ updateArticle: Joi.object().keys({
+ title: Joi.string().label('title should be a string'),
+ body: Joi.string().label('body should be a string'),
+ description: Joi.string().label('description should be a string'),
+ tagList: Joi.string()
+ }),
+ checkComment: Joi.object().keys({
+ comment: Joi.string()
+ .trim()
+ .required()
+ .min(3)
+ .label('The comment is required and should have at least 3 letters!')
+ }),
+ checkDescription: Joi.object().keys({
+ description: Joi.string()
+ .trim()
+ .required()
+ .min(3)
+ .label('Description is required and should have at least 3 letters!')
+ }),
+ validateRating: Joi.object().keys({
+ rating: Joi.number()
+ .integer()
+ .positive()
+ .min(1)
+ .max(5)
+ .required()
+ .label('Rating must be from 1 to 5 ')
+ }),
+ validateHighlight: Joi.object().keys({
+ highlightText: Joi.string()
+ .trim()
+ .required()
+ .label('The highlightText is required and should be a string'),
+ comment: Joi.string()
+ .trim()
+ .min(3)
+ .label('The comment should have at least 3 letters!'),
+ occurencyNumber: Joi.number()
+ .label('Occurrency should be a number')
+
+
+ }),
+ validateTerms: Joi.object().keys({
+ termsAndConditions: Joi.string()
+ .trim()
+ .min(10)
+ .required()
+ .label('Terms and Conditions are required are required and should at least have 10 letters')
+ })
+};
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..fa6016c
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,60 @@
+import 'regenerator-runtime';
+import express from 'express';
+import dotenv from 'dotenv';
+import passport from 'passport';
+import session from 'express-session';
+import swaggerUi from 'swagger-ui-express';
+import cron from 'node-cron';
+import api from './api/routes/index';
+import globalMiddleware from './middleware/globalMiddleware';
+import './config/passportSetup';
+import db from './sequelize/models/index';
+import swaggerDoc from '../swagger.json';
+import SocketIO from './helpers/SocketIO';
+import './helpers/notifications/EventListener';
+
+
+import './handlers/cloudinary';
+import workers from './workers';
+
+dotenv.config();
+
+const port = process.env.PORT || 3000;
+const app = express();
+const { sequelize } = db;
+const { purgeWorker, sendMailWorker } = workers;
+
+app.use(session({
+ secret: process.env.SECRET,
+ saveUninitialized: true
+}));
+SocketIO(app);
+app.use(passport.initialize());
+app.use(passport.session());
+globalMiddleware(app);
+app.get('/', (req, res) => { res.redirect('/docs'); });
+app.use('/api', api);
+app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDoc));
+app.use((req, res) => {
+ res.status(404).send({
+ status: 404,
+ error: {
+ message: 'Page Not found',
+ }
+ });
+});
+
+sequelize.sync().then(() => {
+ cron.schedule('*/59 * * * *', () => {
+ purgeWorker();
+ });
+ cron.schedule('*/5 * * * *', () => {
+ sendMailWorker();
+ });
+ app.listen(port, () => {
+ // eslint-disable-next-line no-console
+ console.log(`Database succesfully connected\nPID: ${process.pid} Server listening on port: ${port} in ${process.env.NODE_ENV} mode`);
+ });
+});
+
+export default app;
diff --git a/src/middleware/articleNotBlocked.js b/src/middleware/articleNotBlocked.js
new file mode 100644
index 0000000..3110749
--- /dev/null
+++ b/src/middleware/articleNotBlocked.js
@@ -0,0 +1,21 @@
+import db from '../sequelize/models/index';
+
+const { BlockedArticles } = db;
+
+const notblocked = async (req, res, next) => {
+ const { article } = req;
+ const blockedArticle = await BlockedArticles.findOne({
+ where: { articleId: article.id }
+ });
+ if (!blockedArticle) {
+ return res.status(400).send({
+ status: 400,
+ error: {
+ message: 'The article you are trying to unblock is not blocked.'
+ }
+ });
+ }
+ next();
+};
+
+export default notblocked;
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
new file mode 100644
index 0000000..cec8d78
--- /dev/null
+++ b/src/middleware/auth.js
@@ -0,0 +1,94 @@
+import authHelper from '../helpers/Token.helper';
+import db from '../sequelize/models';
+
+const { User, Blacklist } = db;
+/**
+ * @class Auth
+ * @description Authentication based class
+ */
+export default class Auth {
+ /**
+ * Verify token middleware
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async verifyToken(req, res, next) {
+ const { token } = req.headers;
+ if (!token) {
+ return res.status(401).json({ status: 401, error: 'Token is missing' });
+ }
+
+ try {
+ const decoded = await authHelper.decodeToken(token);
+ try {
+ const user = await User.findAll({ where: { id: decoded.id } });
+ const blackListed = await Blacklist.findAll({ where: { token } });
+ if (!user[0] || blackListed[0]) {
+ return res.status(401).json({ status: 401, error: 'Token is invalid' });
+ }
+ req.token = token;
+ req.user = user[0].dataValues;
+ return next();
+ } catch (error) {
+ return res.status(500).json({ error: `${error}` });
+ }
+ } catch (error) {
+ if (error.name && error.name === 'TokenExpiredError') {
+ return res.status(401).json({ status: 401, message: error.message });
+ }
+ return res.status(500).json({ error: `${error}` });
+ }
+ }
+
+ /**
+ * Check ownership
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async checkOwnership(req, res, next) {
+ const { user, params } = req;
+ if ((user.id === parseInt(params.id, 10)) || (user.roles.includes('admin'))) {
+ return next();
+ }
+ return res.status(403).json({ message: 'You are not allowed to perform this operation' });
+ }
+
+ /**
+ * Check checkIsAdmin
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async checkIsAdmin(req, res, next) {
+ const { user } = req;
+ if (user && user.roles.includes('admin')) {
+ return next();
+ }
+ return res.status(403).json({ message: 'You are not allowed to perform this operation' });
+ }
+
+ /**
+ * Check checkIsModerator
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async checkIsModerator(req, res, next) {
+ const { user } = req;
+ if (user.roles.includes('moderator') || user.roles.includes('admin')) {
+ return next();
+ }
+ return res.status(403).send({
+ status: 403,
+ data: {
+ message: 'You are not allowed to perform this operation',
+ },
+ });
+ }
+}
diff --git a/src/middleware/blockedarticleExist.js b/src/middleware/blockedarticleExist.js
new file mode 100644
index 0000000..117d1b1
--- /dev/null
+++ b/src/middleware/blockedarticleExist.js
@@ -0,0 +1,21 @@
+import db from '../sequelize/models/index';
+
+const { BlockedArticles } = db;
+
+const blockedExist = async (req, res, next) => {
+ const { article } = req;
+ const blockedArticle = await BlockedArticles.findOne({
+ where: { articleId: article.id }
+ });
+ if (blockedArticle) {
+ return res.status(400).send({
+ status: 400,
+ error: {
+ message: 'This article is already blocked'
+ }
+ });
+ }
+ next();
+};
+
+export default blockedExist;
diff --git a/src/middleware/checkHighlight.js b/src/middleware/checkHighlight.js
new file mode 100644
index 0000000..f7db65e
--- /dev/null
+++ b/src/middleware/checkHighlight.js
@@ -0,0 +1,41 @@
+import models from '../sequelize/models';
+
+const { Article, Highlights } = models;
+
+/**
+ * @class checkHighlight
+ * @description a class to check if a highlight belongs to an article
+ */
+export default class checkHighlight {
+ /**
+ * Verify if a highlight belongs to an article
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async highlights(req, res, next) {
+ const { slug, highlightId } = req.params;
+ const checkHighlights = await Highlights.findAll({
+ where: {
+ id: highlightId
+ }
+ });
+ const getArticleId = await Article.findAll({
+ where: {
+ slug
+ }
+ });
+ if (!checkHighlights[0]) {
+ return res.status(400).json({
+ message: 'This higlight does not exist!'
+ });
+ }
+ if (checkHighlights[0].dataValues.articleId !== getArticleId[0].dataValues.id) {
+ return res.status(400).json({
+ message: 'This higlight does not belong to that article!'
+ });
+ }
+ next();
+ }
+}
diff --git a/src/middleware/checkLikesDislikes.js b/src/middleware/checkLikesDislikes.js
new file mode 100644
index 0000000..a40b494
--- /dev/null
+++ b/src/middleware/checkLikesDislikes.js
@@ -0,0 +1,59 @@
+import models from '../sequelize/models';
+
+/**
+ * @class checkLikesDislikes
+ * @description a class to check if a user has already like or disliked a comment
+ */
+export default class checkLikesDislikes {
+ /**
+ * Verify if the user has already liked the comment
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async liked(req, res, next) {
+ const { commentId } = req.params;
+ const { id, firstName } = req.user;
+ const hasLiked = await models.LikeDislike.findAll({
+ where: {
+ commentId,
+ userId: id,
+ likes: 1
+ }
+ });
+ // If the user has already liked that comment
+ if (hasLiked[0]) {
+ return res.status(400).json({
+ message: `Dear ${firstName}, You have already liked this comment!`
+ });
+ }
+ next();
+ }
+
+ /**
+ * Verify if the user has already disliked the comment
+ * @param {Object} req - Request
+ * @param {Object} res - Response
+ * @param {Function} next -Next
+ * @returns {Object} The response object
+ */
+ static async disliked(req, res, next) {
+ const { commentId } = req.params;
+ const { id, firstName } = req.user;
+ const hasDisliked = await models.LikeDislike.findAll({
+ where: {
+ commentId,
+ userId: id,
+ dislikes: 1
+ }
+ });
+ // If the user has already disliked that comment
+ if (hasDisliked[0]) {
+ return res.status(400).json({
+ message: `Dear ${firstName}, You have already disliked this comment!`
+ });
+ }
+ next();
+ }
+}
diff --git a/src/middleware/checkOwner.js b/src/middleware/checkOwner.js
new file mode 100644
index 0000000..04e508a
--- /dev/null
+++ b/src/middleware/checkOwner.js
@@ -0,0 +1,40 @@
+/* eslint-disable valid-jsdoc */
+import models from '../sequelize/models';
+
+const { Article, User } = models;
+
+/**
+ * @description Helpers for articles
+ */
+class checkOwner {
+ /**
+ * @author Audace Uhiriwe
+ * @param {req} - request
+ * @param {res} - response
+ */
+ static async articleOwner(req, res, next) {
+ const { id, roles } = req.user;
+ const { slug } = req.params;
+
+ // @check if the article's slug exist
+ const result = await Article.findOne({ where: { slug } });
+ if (result === null) return res.status(404).send({ error: 'Slug Not found!' });
+
+ if (roles.includes('moderator' || 'admin')) {
+ req.foundArticle = result.dataValues;
+ return next();
+ }
+
+ // @check if that user is verified
+ const { dataValues } = await User.findOne({ where: { id } });
+ if (dataValues.verified === false) return res.status(403).send({ error: 'Please Verify your account, first!' });
+
+ // @check if the user who logged in - is the owner of that slug
+ const response = await Article.findOne({ where: { slug, authorId: id } });
+ if (!response) return res.status(403).send({ message: 'Sorry!, You are not the owner of this article' });
+
+ req.foundArticle = response.dataValues;
+ return next();
+ }
+}
+export default checkOwner;
diff --git a/src/middleware/checkUsernameExist.js b/src/middleware/checkUsernameExist.js
new file mode 100644
index 0000000..e78b477
--- /dev/null
+++ b/src/middleware/checkUsernameExist.js
@@ -0,0 +1,21 @@
+import models from '../sequelize/models';
+
+const usernameAvailability = {
+ async usernameExist(req, res, next) {
+ const { username } = req.params;
+ const user = await models.User.findAll({
+ where: {
+ username
+ }
+ });
+ if (!user.length) {
+ return res.status(404).json({
+ error: 'username does not exist'
+ });
+ }
+
+ next();
+ }
+};
+
+export default usernameAvailability;
diff --git a/src/middleware/globalMiddleware.js b/src/middleware/globalMiddleware.js
new file mode 100644
index 0000000..7d76345
--- /dev/null
+++ b/src/middleware/globalMiddleware.js
@@ -0,0 +1,15 @@
+import bodyParser from 'body-parser';
+import cors from 'cors';
+import morgan from 'morgan';
+
+export default (app) => {
+ app
+ // Parse req object and make data available on req.body
+ .use(bodyParser.json())
+ .use(bodyParser.urlencoded({ extended: true }))
+ // Allow cross origin requests
+ .use(cors());
+ if (process.env.NODE_ENV !== 'test') {
+ app.use(morgan('dev'));
+ }
+};
diff --git a/src/middleware/highlightExist.js b/src/middleware/highlightExist.js
new file mode 100644
index 0000000..4d5d847
--- /dev/null
+++ b/src/middleware/highlightExist.js
@@ -0,0 +1,25 @@
+import db from '../sequelize/models/index';
+
+const { Highlights } = db;
+const highlightExist = async (req, res, next) => {
+ const {
+ article, user: { id },
+ body: { highlightText, occurencyNumber }
+ } = req; // This contains the article
+ const response = await Highlights.findOne({
+ where: {
+ articleId: article.id,
+ userId: id,
+ highlightText,
+ occurencyNumber
+ }
+ });
+ if (!response) {
+ return next();
+ }
+ return res.status(403).json({
+ Message: 'you have already highlighted this text.'
+ });
+};
+
+export default highlightExist;
diff --git a/src/middleware/isThisArticleBlocked.js b/src/middleware/isThisArticleBlocked.js
new file mode 100644
index 0000000..6e1f99d
--- /dev/null
+++ b/src/middleware/isThisArticleBlocked.js
@@ -0,0 +1,21 @@
+import db from '../sequelize/models/index';
+
+const { BlockedArticles } = db;
+
+const isthisAricleBlocked = async (req, res, next) => {
+ const { article } = req;
+ const blockedArticle = await BlockedArticles.findOne({
+ where: { articleId: article.id }
+ });
+ if (blockedArticle) {
+ return res.status(200).send({
+ status: 200,
+ error: {
+ message: 'This article is blocked'
+ }
+ });
+ }
+ next();
+};
+
+export default isthisAricleBlocked;
diff --git a/src/middleware/paginate.js b/src/middleware/paginate.js
new file mode 100644
index 0000000..fd6f223
--- /dev/null
+++ b/src/middleware/paginate.js
@@ -0,0 +1,32 @@
+import db from '../sequelize/models';
+
+const { Article } = db;
+
+const paginate = async (req, res, next) => {
+ const { page, limit } = req.query;
+ const pageNumber = parseInt(page, 10);
+ const limitNumber = parseInt(limit, 10);
+ if (
+ typeof pageNumber === 'number'
+ && typeof limitNumber === 'number'
+ && typeof page !== 'undefined'
+ && typeof limit !== 'undefined'
+ ) {
+ if (pageNumber <= 0 || limitNumber <= 0) {
+ return res.status(400).json({
+ error: 'Invalid request'
+ });
+ }
+ const offset = limitNumber * (pageNumber - 1);
+ const foundArticles = await Article.findAll({
+ limit: limitNumber,
+ offset
+ });
+ return res.json({
+ data: foundArticles
+ });
+ }
+ next();
+};
+
+export default paginate;
diff --git a/src/middleware/search.js b/src/middleware/search.js
new file mode 100644
index 0000000..f54637e
--- /dev/null
+++ b/src/middleware/search.js
@@ -0,0 +1,196 @@
+/* eslint-disable no-prototype-builtins */
+import sequelize from 'sequelize';
+import models from '../sequelize/models';
+
+const { Op } = sequelize;
+const { User, Article } = models;
+/**
+ * @description search by different parameters
+ */
+class search {
+ /**
+ * @param {object} req - Request.
+ * @param {object} res - Response.
+ * @param {function} next - passing to other middlewares
+ * @returns {object} - Contains an article information.
+ */
+ static async searchForArticle(req, res, next) {
+ const {
+ title, author, keywords, tag
+ } = req.query;
+
+ if (!Object.keys(req.query).length) {
+ return next();
+ }
+
+ if (!title && !tag && !author && !keywords) {
+ return res.status(400).send({ error: 'You made a Bad Request!' });
+ }
+
+ if (author && !keywords && !tag && !title) {
+ // check if the author has a value and has at least three characters
+ if (author.length < 3) return res.status(400).send({ error: 'You should have provided at least 3 characters long for author\'s name' });
+ // @find the author
+ const result = await User.findOne({
+ where: { username: { [Op.iLike]: `%${author}%` } }
+ });
+ if (result === null) {
+ return res.status(404).send({
+ error: `This Author with username of ${author} not exists!`
+ });
+ }
+
+ // @find All article written by the found Author
+ const response = await Article.findAll({
+ where: { authorId: result.dataValues.id },
+ include: [
+ {
+ as: 'author',
+ model: User,
+ attributes: ['username', 'bio', 'image']
+ }
+ ],
+ attributes: [
+ 'slug',
+ 'title',
+ 'description',
+ 'readtime',
+ 'body',
+ 'tagList',
+ 'updatedAt',
+ 'createdAt'
+ ]
+ });
+
+ if (!response[0]) {
+ return res.status(404).send({
+ error: `Author : ${author} - doesn't have any article, so far!`
+ });
+ }
+
+ // @returning the response
+ return res.status(200).send({
+ message: `Here's All article written by Author who is like ${author}`,
+ data: response
+ });
+ }
+
+ if (title && !author && !tag && !keywords) {
+ // check if the title has at least three characters
+ if (title.length < 3) return res.status(400).send({ error: 'You should have provided at least 3 characters long for title' });
+
+ const titleFound = await Article.findAll({
+ where: { title: { [Op.iLike]: `%${title}%` } },
+ include: [
+ {
+ as: 'author',
+ model: User,
+ attributes: ['username', 'bio', 'image']
+ }
+ ],
+ attributes: [
+ 'slug',
+ 'title',
+ 'description',
+ 'readtime',
+ 'body',
+ 'readtime',
+ 'tagList',
+ 'updatedAt',
+ 'createdAt'
+ ]
+ });
+ if (!titleFound[0]) {
+ return res.status(200).send({
+ error: 'No Articles with that title, so far!'
+ });
+ }
+
+ return res
+ .status(200)
+ .send({
+ message: `Here's All Articles which has the same title like this ${title}`,
+ data: titleFound
+ });
+ }
+
+ if (!title && !author && tag && !keywords) {
+ // check if the tag has at least three characters
+ if (tag.length < 3) return res.status(400).send({ error: 'You should have provided at least 3 characters long for tag' });
+
+ const tagFound = await Article.findAll({
+ where: { tagList: { [Op.contains]: [tag.toLowerCase()] } },
+ include: [
+ {
+ as: 'author',
+ model: User,
+ attributes: ['username', 'bio', 'image']
+ }
+ ],
+ attributes: [
+ 'slug',
+ 'title',
+ 'description',
+ 'readtime',
+ 'body',
+ 'readtime',
+ 'tagList',
+ 'updatedAt',
+ 'createdAt'
+ ]
+ });
+ if (!tagFound[0]) {
+ return res.status(200).send({
+ error: 'No Articles with that tag, so far!'
+ });
+ }
+
+ return res
+ .status(200)
+ .send({
+ message: `Here's All Articles which has the same tag like this ${tag}`,
+ data: tagFound
+ });
+ }
+
+ if (!title && !author && !tag && keywords) {
+ // check if the keyword has at least three characters
+ if (keywords.length < 3) return res.status(400).send({ error: 'You should have provided at least 3 characters long for keywords' });
+
+ const keywordFound = await Article.findAll({
+ where: {
+ [Op.or]: [
+ { title: { [Op.iLike]: `%${keywords.toLowerCase()}%` } },
+ { body: { [Op.iLike]: `%${keywords.toLowerCase()}%` } },
+ { description: { [Op.iLike]: `%${keywords.toLowerCase()}%` } },
+ { tagList: { [Op.contains]: [keywords.toLowerCase()] } }
+ ]
+ },
+ attributes: [
+ 'slug',
+ 'title',
+ 'description',
+ 'readtime',
+ 'body',
+ 'readtime',
+ 'tagList',
+ 'updatedAt',
+ 'createdAt'
+ ]
+ });
+ if (!keywordFound[0]) {
+ return res.status(200).send({
+ error: 'No Articles with that Keyword found, so far!'
+ });
+ }
+
+ return res
+ .status(200)
+ .send({
+ data: keywordFound
+ });
+ }
+ }
+}
+
+export default search;
diff --git a/src/middleware/shareArticle.js b/src/middleware/shareArticle.js
new file mode 100644
index 0000000..56c745d
--- /dev/null
+++ b/src/middleware/shareArticle.js
@@ -0,0 +1,33 @@
+import open from 'open';
+import dotenv from 'dotenv';
+import share from 'social-share';
+import db from '../sequelize/models';
+
+dotenv.config();
+const { Article } = db;
+const { APP_URL_FRONTEND } = process.env;
+
+export default async (req, res, next) => {
+ let provider;
+ const { slug } = req.params;
+ const { title } = await Article.findOne({ where: { slug } });
+ if (req.url.search(/\/twitter/g) > 0) {
+ const url = share('twitter', { url: `${APP_URL_FRONTEND}/api/articles/${slug}` });
+ await open(`${url}`, { wait: false });
+ provider = 'twitter';
+ } else if (req.url.search(/\/facebook/g) > 0) {
+ const url = share('facebook', { url: `${APP_URL_FRONTEND}/api/articles/${slug}` });
+ await open(`${url}`, { wait: false });
+ provider = 'facebook';
+ } else if (req.url.search(/\/email/g) > 0) {
+ await open(
+ `mailto:?subject=${title}&body=${APP_URL_FRONTEND}/articles/${slug}`,
+ {
+ wait: false
+ }
+ );
+ provider = 'email';
+ }
+ req.share = { slug, provider };
+ next();
+};
diff --git a/src/middleware/shareHighlights.js b/src/middleware/shareHighlights.js
new file mode 100644
index 0000000..da3ba2a
--- /dev/null
+++ b/src/middleware/shareHighlights.js
@@ -0,0 +1,40 @@
+import open from 'open';
+import dotenv from 'dotenv';
+import share from 'social-share';
+import db from '../sequelize/models';
+
+dotenv.config();
+const { Article, Highlights, User } = db;
+const { APP_URL_FRONTEND } = process.env;
+
+export default async (req, res, next) => {
+ const { slug, highlightId } = req.params;
+ const { title, authorId, gallery } = await Article.findOne({ where: { slug } });
+ const { firstName, lastName } = await User.findOne({ where: { id: authorId } });
+ const { highlightText } = await Highlights.findOne({ where: { id: highlightId } });
+ if (gallery) {
+ const image = gallery[0];
+ if (req.url.search(/\/twitter/g) > 0) {
+ const url = share('twitter', {
+ url: `${APP_URL_FRONTEND}/api/articles/${slug}`,
+ title: `${highlightText} - written by ${firstName} ${lastName} ${gallery[0]}`
+ });
+ await open(`${url}`, `${title}`, { wait: false });
+ } else if (req.url.search(/\/facebook/g) > 0) {
+ const url = share('facebook', {
+ url: `${APP_URL_FRONTEND}/api/articles/${slug}`,
+ title: `${highlightText} - written by ${firstName} ${lastName} ${image}`
+ });
+ await open(`${url}`, `${title}`, { wait: false });
+ } else if (req.url.search(/\/email/g) > 0) {
+ const highTitle = `${highlightText} - written by ${firstName} ${lastName} ${image}`;
+ await open(
+ `mailto:?subject=${title}&body=${highTitle} ${APP_URL_FRONTEND}/articles/${slug}`,
+ {
+ wait: false
+ }
+ );
+ }
+ }
+ next();
+};
diff --git a/src/middleware/slugExist.js b/src/middleware/slugExist.js
new file mode 100644
index 0000000..d00de31
--- /dev/null
+++ b/src/middleware/slugExist.js
@@ -0,0 +1,22 @@
+import db from '../sequelize/models/index';
+
+const { Article } = db;
+
+const slugExist = async (req, res, next) => {
+ const { slug } = req.params;
+ const currentArticle = await Article.findOne({
+ where: { slug }
+ });
+ if (!currentArticle) {
+ return res.status(404).send({
+ status: 404,
+ error: {
+ message: 'The article does not exist!!!'
+ }
+ });
+ }
+ req.article = currentArticle;
+ next();
+};
+
+export default slugExist;
diff --git a/src/middleware/socialAccountExists.js b/src/middleware/socialAccountExists.js
new file mode 100644
index 0000000..adece2b
--- /dev/null
+++ b/src/middleware/socialAccountExists.js
@@ -0,0 +1,59 @@
+import models from '../sequelize/models';
+import tokenGeneration from '../helpers/Token.helper';
+
+const userExists = {
+ async google(req, res, next) {
+ const { emails, displayName } = req.user;
+ const currentUser = await models.User.findAll({
+ where: {
+ email: emails[0].value,
+ },
+ });
+ if (currentUser.length > 0) {
+ const token = await tokenGeneration.generateToken(currentUser[0].dataValues);
+ const {
+ id, firstName, lastName, email, socialId, provider
+ } = currentUser[0].dataValues;
+ return res.status(200).json({
+ message: `Welcome to Authors Haven ${displayName} `,
+ data: {
+ token,
+ id,
+ firstName,
+ lastName,
+ email,
+ socialId,
+ provider
+ },
+ });
+ }
+ next();
+ },
+ async twitter(req, res, next) {
+ const { displayName } = req.user;
+ const currentUser = await models.User.findAll({
+ where: {
+ socialId: req.user.id,
+ },
+ });
+ if (currentUser.length > 0) {
+ const token = await tokenGeneration.generateToken(currentUser[0].dataValues);
+ const {
+ id, firstName, lastName, socialId, provider
+ } = currentUser[0].dataValues;
+ return res.status(200).json({
+ message: `Welcome to Authors Haven ${displayName} `,
+ data: {
+ token,
+ id,
+ firstName,
+ lastName,
+ socialId,
+ provider,
+ },
+ });
+ }
+ next();
+ },
+};
+export default userExists;
diff --git a/src/middleware/socialTest.js b/src/middleware/socialTest.js
new file mode 100644
index 0000000..ecf3dda
--- /dev/null
+++ b/src/middleware/socialTest.js
@@ -0,0 +1,16 @@
+export default (req, res, next) => {
+ req.user = {
+ id: req.body.id,
+ displayName: 'Mireille Niwemuhuza',
+ name: { familyName: 'Niwemuhuza', givenName: 'Mireille' },
+ emails:
+ [{ value: req.body.email, verified: true }],
+ photos:
+ [{
+ value:
+ 'https://lh4.googleusercontent.com/-FZSypt5JyRU/AAAAAAAAAAI/AAAAAAAAAAA/ACHi3repeJYC3C7JQWReWg7zqfLcOxh0Qg/mo/photo.jpg'
+ }],
+ provider: 'google'
+ };
+ next();
+};
diff --git a/src/middleware/textExist.js b/src/middleware/textExist.js
new file mode 100644
index 0000000..7233baa
--- /dev/null
+++ b/src/middleware/textExist.js
@@ -0,0 +1,19 @@
+
+const textExist = async (req, res, next) => {
+ const {
+ article,
+ body: { highlightText, occurencyNumber }
+ } = req; // This contains the article
+
+
+ const { body } = article;
+ const regX = new RegExp(highlightText, 'g');
+ const count = (body.match(regX) || []).length;
+ if (count === 0 || occurencyNumber > count) {
+ return res.status(404).json({
+ Message: 'Highlight text not found'
+ });
+ }
+ next();
+};
+export default textExist;
diff --git a/src/middleware/validComment.js b/src/middleware/validComment.js
new file mode 100644
index 0000000..7df59ff
--- /dev/null
+++ b/src/middleware/validComment.js
@@ -0,0 +1,56 @@
+import models from '../sequelize/models';
+
+const validComment = {
+ async checkComment(req, res, next) {
+ const { slug, commentId } = req.params;
+ if (commentId) {
+ const findComment = await models.Comment.findAll({
+ where: {
+ slug,
+ id: commentId
+ }
+ });
+ if (findComment.length === 0) {
+ return res.status(400).json({
+ message: 'That comment does not belong to this article!'
+ });
+ }
+ }
+ next();
+ },
+ async checkParameter(req, res, next) {
+ const { commentId } = req.params;
+ const isInteger = /^[0-9]+$/;
+ if (!isInteger.test(commentId)) {
+ return res.status(400).json({
+ message: 'The comment Id should be an integer!'
+ });
+ }
+ const findComment = await models.Comment.findAll({
+ where: {
+ id: commentId
+ }
+ });
+ if (findComment.length === 0) {
+ return res.status(404).json({
+ message: 'That comment does not exist!'
+ });
+ }
+ next();
+ },
+ async articleExists(req, res, next) {
+ const { slug } = req.params;
+ const data = await models.Article.findAll({
+ where: {
+ slug
+ },
+ });
+ if (data.length === 0) {
+ return res.status(404).json({
+ message: 'The article you are trying to comment was not found'
+ });
+ }
+ return next();
+ }
+};
+export default validComment;
diff --git a/src/middleware/validUser.js b/src/middleware/validUser.js
new file mode 100644
index 0000000..5be7a99
--- /dev/null
+++ b/src/middleware/validUser.js
@@ -0,0 +1,56 @@
+import models from '../sequelize/models';
+
+const validUser = {
+ async emailExists(req, res, next) {
+ const { email } = req.body;
+ await models.User.findAll({
+ where: {
+ email
+ },
+ }).then((data) => {
+ if (data.length > 0) {
+ return res.status(400).json({
+ status: 400,
+ message: 'This email is already in use',
+ });
+ }
+ next();
+ });
+ },
+ async usernameExists(req, res, next) {
+ const { username } = req.body;
+ await models.User.findAll({
+ where: {
+ username
+ },
+ }).then((data) => {
+ if (data.length > 0) {
+ return res.status(400).json({
+ status: 400,
+ message: 'This username is not available, Please choose another one!',
+ });
+ }
+ next();
+ });
+ },
+ async userNameExist(req, res, next) {
+ const { username } = req.params;
+ // console.log(username);
+ const user = await models.User.findAll({
+ where: {
+ username
+ }
+ });
+ // console.log(user);
+ if (!user.length) {
+ return res.status(404).json({
+ error: 'username does not exist'
+ });
+ }
+
+ next();
+ }
+
+};
+
+export default validUser;
diff --git a/src/middleware/validateBody.js b/src/middleware/validateBody.js
new file mode 100644
index 0000000..13c5a03
--- /dev/null
+++ b/src/middleware/validateBody.js
@@ -0,0 +1,31 @@
+import Joi from '@hapi/joi';
+import _ from 'lodash';
+import Schemas from '../helpers/validationSchemas';
+
+const validateBody = schema => (req, res, next) => {
+ const data = req.body;
+
+ if (_.has(Schemas, schema)) {
+ const chosenSchema = _.get(Schemas, schema);
+ const validationResult = Joi.validate(data, chosenSchema, { abortEarly: false });
+
+ if (!validationResult.error) {
+ req.body = data;
+ next();
+ } else {
+ const allErrors = [];
+ validationResult.error.details.forEach((errors) => {
+ const findError = allErrors.filter(error => error === errors.context.label);
+ if (findError.length === 0) {
+ allErrors.push(errors.context.label);
+ }
+ });
+ return res.status(400).send({
+ status: 400,
+ data: { message: allErrors },
+ });
+ }
+ }
+};
+
+export default validateBody;
diff --git a/src/middleware/validateGender.js b/src/middleware/validateGender.js
new file mode 100644
index 0000000..6bd86a2
--- /dev/null
+++ b/src/middleware/validateGender.js
@@ -0,0 +1,17 @@
+const validategender = (req, res, next) => {
+ const { gender } = req.body;
+ if (gender) {
+ if (gender === 'M' || gender === 'F') {
+ next();
+ } else {
+ res.status(400).json({
+ status: 400,
+ error: 'Gender should be represented by either "M" or "F" '
+ });
+ }
+ } else {
+ next();
+ }
+};
+
+export default validategender;
diff --git a/src/sequelize/migrations/20190611082539-create-blacklist.js b/src/sequelize/migrations/20190611082539-create-blacklist.js
new file mode 100644
index 0000000..fc4ad4d
--- /dev/null
+++ b/src/sequelize/migrations/20190611082539-create-blacklist.js
@@ -0,0 +1,25 @@
+export default {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Blacklists', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ token: {
+ type: Sequelize.TEXT
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Blacklists')
+};
diff --git a/src/sequelize/migrations/20190612172050-create-user.js b/src/sequelize/migrations/20190612172050-create-user.js
new file mode 100644
index 0000000..4d0caf9
--- /dev/null
+++ b/src/sequelize/migrations/20190612172050-create-user.js
@@ -0,0 +1,56 @@
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Users', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ firstName: {
+ type: Sequelize.STRING
+ },
+ lastName: {
+ type: Sequelize.STRING
+ },
+ username: {
+ type: Sequelize.STRING
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ password: {
+ type: Sequelize.STRING
+ },
+ bio: {
+ type: Sequelize.TEXT
+ },
+ image: {
+ type: Sequelize.TEXT
+ },
+ dateOfBirth: {
+ type: Sequelize.DATE
+ },
+ gender: {
+ type: Sequelize.STRING
+ },
+ provider: {
+ type: Sequelize.STRING
+ },
+ socialId: {
+ type: Sequelize.STRING
+ },
+ verified: {
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Users')
+};
diff --git a/src/sequelize/migrations/20190613355309-create-article.js b/src/sequelize/migrations/20190613355309-create-article.js
new file mode 100644
index 0000000..3316163
--- /dev/null
+++ b/src/sequelize/migrations/20190613355309-create-article.js
@@ -0,0 +1,58 @@
+/* eslint-disable no-unused-vars */
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ slug: {
+ allowNull: false,
+ type: Sequelize.STRING
+ },
+ title: {
+ allowNull: false,
+ type: Sequelize.STRING
+ },
+ readtime: {
+ allowNull: false,
+ type: Sequelize.STRING
+ },
+ description: {
+ type: Sequelize.STRING,
+ allowNull: false
+ },
+ body: {
+ allowNull: false,
+ type: Sequelize.TEXT
+ },
+ tagList: {
+ type: Sequelize.ARRAY(Sequelize.STRING),
+ allowNull: true,
+ defaultValue: []
+ },
+ image: {
+ type: Sequelize.STRING,
+ allowNull: true
+ },
+ authorId: {
+ type: Sequelize.INTEGER,
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE',
+ references: {
+ model: 'Users',
+ key: 'id'
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: (queryInterface, Sequelize) => queryInterface.dropTable('Articles')
+};
diff --git a/src/sequelize/migrations/20190617110332-create-comment.js b/src/sequelize/migrations/20190617110332-create-comment.js
new file mode 100644
index 0000000..1a14555
--- /dev/null
+++ b/src/sequelize/migrations/20190617110332-create-comment.js
@@ -0,0 +1,22 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Comments', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ comment: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Comments')
+};
diff --git a/src/sequelize/migrations/20190617142742-add-comment-user-association.js b/src/sequelize/migrations/20190617142742-add-comment-user-association.js
new file mode 100644
index 0000000..6e86789
--- /dev/null
+++ b/src/sequelize/migrations/20190617142742-add-comment-user-association.js
@@ -0,0 +1,20 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn(
+ 'Comments', // name of Source model
+ 'userId', // name of the key we're adding
+ {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Users', // name of Target model
+ key: 'id' // key in Target model that we're referencing
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ }
+ ),
+
+ down: queryInterface => queryInterface.removeColumn(
+ 'Comments', // name of Source model
+ 'userId' // key we want to remove
+ )
+};
diff --git a/src/sequelize/migrations/20190617142756-add-comment-article-association.js b/src/sequelize/migrations/20190617142756-add-comment-article-association.js
new file mode 100644
index 0000000..4b50560
--- /dev/null
+++ b/src/sequelize/migrations/20190617142756-add-comment-article-association.js
@@ -0,0 +1,20 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn(
+ 'Comments', // name of Source model
+ 'articleId', // name of the key we're adding
+ {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Articles', // name of Target model
+ key: 'id' // key in Target model that we're referencing
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ }
+ ),
+
+ down: queryInterface => queryInterface.removeColumn(
+ 'Comments', // name of Source model
+ 'articleId' // key we want to remove
+ )
+};
diff --git a/src/sequelize/migrations/20190617142809-add-comment-comment-association.js b/src/sequelize/migrations/20190617142809-add-comment-comment-association.js
new file mode 100644
index 0000000..1de9e88
--- /dev/null
+++ b/src/sequelize/migrations/20190617142809-add-comment-comment-association.js
@@ -0,0 +1,20 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn(
+ 'Comments', // name of Source model
+ 'commentId', // name of the key we're adding
+ {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Comments', // name of Target model
+ key: 'id' // key in Target model that we're referencing
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'SET NULL'
+ }
+ ),
+
+ down: queryInterface => queryInterface.removeColumn(
+ 'Comments', // name of Source model
+ 'commentId' // key we want to remove
+ )
+};
diff --git a/src/sequelize/migrations/20190617184505-add-isAdmin-field-users.js b/src/sequelize/migrations/20190617184505-add-isAdmin-field-users.js
new file mode 100644
index 0000000..c873e4c
--- /dev/null
+++ b/src/sequelize/migrations/20190617184505-add-isAdmin-field-users.js
@@ -0,0 +1,5 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('Users', 'isAdmin', { type: Sequelize.STRING }),
+
+ down: queryInterface => queryInterface.removeColumn('Users', 'isAdmin')
+};
diff --git a/src/sequelize/migrations/20190617202025-create-follows.js b/src/sequelize/migrations/20190617202025-create-follows.js
new file mode 100644
index 0000000..5e1c3cd
--- /dev/null
+++ b/src/sequelize/migrations/20190617202025-create-follows.js
@@ -0,0 +1,33 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('follows', {
+ userId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ references: {
+ model: 'Users',
+ key: 'id'
+ }
+ },
+ followerId: {
+ type: Sequelize.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ references: {
+ model: 'Users',
+ key: 'id'
+ }
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('follows')
+};
diff --git a/src/sequelize/migrations/20190619090814-create-like-dislike.js b/src/sequelize/migrations/20190619090814-create-like-dislike.js
new file mode 100644
index 0000000..497ebf8
--- /dev/null
+++ b/src/sequelize/migrations/20190619090814-create-like-dislike.js
@@ -0,0 +1,63 @@
+export default {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.createTable('LikeDislikes', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ articleId: {
+ allowNull: true,
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Articles',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE',
+ },
+ userId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Users',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE',
+ },
+ likes: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ },
+ dislikes: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ });
+
+ await queryInterface.addConstraint('LikeDislikes', ['likes'], {
+ type: 'check',
+ where: {
+ likes: [0, 1]
+ }
+ });
+
+ return queryInterface.addConstraint('LikeDislikes', ['dislikes'], {
+ type: 'check',
+ where: {
+ dislikes: [0, 1]
+ }
+ });
+ },
+ down: queryInterface => queryInterface.dropTable('LikeDislikes')
+};
diff --git a/src/sequelize/migrations/20190619094032-add-slug-in-comments.js b/src/sequelize/migrations/20190619094032-add-slug-in-comments.js
new file mode 100644
index 0000000..20632b9
--- /dev/null
+++ b/src/sequelize/migrations/20190619094032-add-slug-in-comments.js
@@ -0,0 +1,4 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('Comments', 'slug', { type: Sequelize.STRING }),
+ down: queryInterface => queryInterface.removeColumn('Comments', 'slug')
+};
diff --git a/src/sequelize/migrations/20190619100157-create-article-ratings.js b/src/sequelize/migrations/20190619100157-create-article-ratings.js
new file mode 100644
index 0000000..903373e
--- /dev/null
+++ b/src/sequelize/migrations/20190619100157-create-article-ratings.js
@@ -0,0 +1,30 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('ArticleRatings', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ slug: {
+ type: Sequelize.STRING
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ ratings: {
+ type: Sequelize.INTEGER
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('ArticleRatings')
+};
diff --git a/src/sequelize/migrations/20190623093716-create-user.js b/src/sequelize/migrations/20190623093716-create-user.js
new file mode 100644
index 0000000..04f9b95
--- /dev/null
+++ b/src/sequelize/migrations/20190623093716-create-user.js
@@ -0,0 +1,60 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Users', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ firstName: {
+ type: Sequelize.STRING
+ },
+ lastName: {
+ type: Sequelize.STRING
+ },
+ username: {
+ type: Sequelize.STRING
+ },
+ email: {
+ type: Sequelize.STRING
+ },
+ password: {
+ type: Sequelize.STRING
+ },
+ bio: {
+ type: Sequelize.TEXT
+ },
+ image: {
+ type: Sequelize.STRING
+ },
+ dateOfBirth: {
+ type: Sequelize.DATE
+ },
+ gender: {
+ type: Sequelize.STRING
+ },
+ provider: {
+ type: Sequelize.STRING
+ },
+ socialId: {
+ type: Sequelize.STRING
+ },
+ verified: {
+ type: Sequelize.BOOLEAN
+ },
+ isAdmin: {
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Users')
+};
diff --git a/src/sequelize/migrations/20190623102859-create-opt.js b/src/sequelize/migrations/20190623102859-create-opt.js
new file mode 100644
index 0000000..98baadb
--- /dev/null
+++ b/src/sequelize/migrations/20190623102859-create-opt.js
@@ -0,0 +1,33 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Opts', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ resource: {
+ type: Sequelize.STRING
+ },
+ type: {
+ type: Sequelize.STRING
+ },
+ resourceId: {
+ type: Sequelize.INTEGER
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Opts')
+};
diff --git a/src/sequelize/migrations/20190623103532-create-notification.js b/src/sequelize/migrations/20190623103532-create-notification.js
new file mode 100644
index 0000000..610e189
--- /dev/null
+++ b/src/sequelize/migrations/20190623103532-create-notification.js
@@ -0,0 +1,42 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Notifications', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ type: {
+ type: Sequelize.STRING
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ resource: {
+ type: Sequelize.STRING
+ },
+ resourceId: {
+ type: Sequelize.INTEGER
+ },
+ message: {
+ type: Sequelize.STRING
+ },
+ url: {
+ type: Sequelize.STRING
+ },
+ status: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Notifications')
+};
diff --git a/src/sequelize/migrations/20190624075935-create-bookmarks.js b/src/sequelize/migrations/20190624075935-create-bookmarks.js
new file mode 100644
index 0000000..2c1b23a
--- /dev/null
+++ b/src/sequelize/migrations/20190624075935-create-bookmarks.js
@@ -0,0 +1,27 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Bookmarks', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ slug: {
+ type: Sequelize.STRING
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Bookmarks')
+};
diff --git a/src/sequelize/migrations/20190624081558-add-commentId-likeDislike.js b/src/sequelize/migrations/20190624081558-add-commentId-likeDislike.js
new file mode 100644
index 0000000..85ce6fd
--- /dev/null
+++ b/src/sequelize/migrations/20190624081558-add-commentId-likeDislike.js
@@ -0,0 +1,13 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('LikeDislikes', 'commentId', {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Comments',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ }),
+
+ down: queryInterface => queryInterface.removeColumn('LikeDislikes', 'commentId')
+};
diff --git a/src/sequelize/migrations/20190624093422-add-admin-moderator.js b/src/sequelize/migrations/20190624093422-add-admin-moderator.js
new file mode 100644
index 0000000..f9b8170
--- /dev/null
+++ b/src/sequelize/migrations/20190624093422-add-admin-moderator.js
@@ -0,0 +1,14 @@
+export default {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.removeColumn('Users', 'isAdmin');
+ return queryInterface.addColumn('Users', 'roles', {
+ type: Sequelize.ARRAY(Sequelize.STRING),
+ defaultValue: ['user'],
+ });
+ },
+
+ down: async (queryInterface, Sequelize) => {
+ await queryInterface.removeColumn('Users', 'roles');
+ return queryInterface.addColumn('Users', 'isAdmin', { type: Sequelize.STRING });
+ }
+};
diff --git a/src/sequelize/migrations/20190624102025-create-reportedarticles.js b/src/sequelize/migrations/20190624102025-create-reportedarticles.js
new file mode 100644
index 0000000..e9de4db
--- /dev/null
+++ b/src/sequelize/migrations/20190624102025-create-reportedarticles.js
@@ -0,0 +1,28 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('ReportedArticles', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ slug: {
+ type: Sequelize.STRING
+ },
+ comment: {
+ type: Sequelize.STRING
+ },
+ username: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('ReportedArticles')
+};
diff --git a/src/sequelize/migrations/20190625184332-create-comments-history.js b/src/sequelize/migrations/20190625184332-create-comments-history.js
new file mode 100644
index 0000000..fd2ba8c
--- /dev/null
+++ b/src/sequelize/migrations/20190625184332-create-comments-history.js
@@ -0,0 +1,44 @@
+
+
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('CommentsHistories', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Users',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ },
+ editedComment: {
+ type: Sequelize.STRING
+ },
+ commentId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Comments',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('CommentsHistories')
+};
diff --git a/src/sequelize/migrations/20190627081610-add-iat-to-blaclist.js b/src/sequelize/migrations/20190627081610-add-iat-to-blaclist.js
new file mode 100644
index 0000000..74ae467
--- /dev/null
+++ b/src/sequelize/migrations/20190627081610-add-iat-to-blaclist.js
@@ -0,0 +1,8 @@
+export default {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('Blacklists', 'expiresAt', {
+ type: Sequelize.BIGINT,
+ allowNull: false
+ }),
+
+ down: queryInterface => queryInterface.removeColumn('Blacklists', 'expiresAt')
+};
diff --git a/src/sequelize/migrations/20190627142139-create-highlights.js b/src/sequelize/migrations/20190627142139-create-highlights.js
new file mode 100644
index 0000000..0fa631f
--- /dev/null
+++ b/src/sequelize/migrations/20190627142139-create-highlights.js
@@ -0,0 +1,34 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Highlights', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ articleId: {
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ type: Sequelize.INTEGER
+ },
+ highlightText: {
+ type: Sequelize.TEXT
+ },
+ comment: {
+ type: Sequelize.TEXT
+ },
+ occurencyNumber: {
+ type: Sequelize.INTEGER
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Highlights')
+};
diff --git a/src/sequelize/migrations/20190627181513-add-views-article.js b/src/sequelize/migrations/20190627181513-add-views-article.js
new file mode 100644
index 0000000..74c32d6
--- /dev/null
+++ b/src/sequelize/migrations/20190627181513-add-views-article.js
@@ -0,0 +1,7 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.addColumn('Articles', 'views', {
+ type: Sequelize.INTEGER
+ }),
+
+ down: queryInterface => queryInterface.removeColumn('Articles', 'views')
+};
diff --git a/src/sequelize/migrations/20190701162127-create-share.js b/src/sequelize/migrations/20190701162127-create-share.js
new file mode 100644
index 0000000..e2fe76c
--- /dev/null
+++ b/src/sequelize/migrations/20190701162127-create-share.js
@@ -0,0 +1,34 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Shares', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ userId: {
+ type: Sequelize.INTEGER,
+ references: {
+ model: 'Users',
+ key: 'id'
+ },
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ },
+ slug: {
+ type: Sequelize.STRING
+ },
+ provider: {
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Shares')
+};
diff --git a/src/sequelize/migrations/20190702104558-create-emails.js b/src/sequelize/migrations/20190702104558-create-emails.js
new file mode 100644
index 0000000..b49712e
--- /dev/null
+++ b/src/sequelize/migrations/20190702104558-create-emails.js
@@ -0,0 +1,26 @@
+export default {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Emails', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ content: {
+ type: Sequelize.JSON,
+ },
+ sent: {
+ type: Sequelize.BOOLEAN,
+ defaultValue: false
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Emails')
+};
diff --git a/src/sequelize/migrations/20190706123820-update-mail-queue-fields.js b/src/sequelize/migrations/20190706123820-update-mail-queue-fields.js
new file mode 100644
index 0000000..4492f34
--- /dev/null
+++ b/src/sequelize/migrations/20190706123820-update-mail-queue-fields.js
@@ -0,0 +1,13 @@
+export default {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.renameColumn('Emails', 'content', 'mail');
+ await queryInterface.addColumn('Emails', 'html', Sequelize.TEXT);
+ return queryInterface.addColumn('Emails', 'subject', Sequelize.STRING);
+ },
+
+ down: async (queryInterface) => {
+ await queryInterface.renameColumn('Emails', 'mail', 'content');
+ await queryInterface.removeColumn('Emails', 'html');
+ return queryInterface.removeColumn('Emails', 'subject');
+ }
+};
diff --git a/src/sequelize/migrations/20190715070539-upload-multiple-images.js b/src/sequelize/migrations/20190715070539-upload-multiple-images.js
new file mode 100644
index 0000000..f0ce1f5
--- /dev/null
+++ b/src/sequelize/migrations/20190715070539-upload-multiple-images.js
@@ -0,0 +1,24 @@
+export default {
+ up: async (queryInterface, Sequelize) => {
+ await queryInterface.removeColumn('Users', 'image');
+ await queryInterface.removeColumn('Articles', 'image');
+ await queryInterface.addColumn('Users', 'avatar', {
+ type: Sequelize.STRING,
+ });
+ await queryInterface.addColumn('Users', 'cover', {
+ type: Sequelize.STRING,
+ });
+ return queryInterface.addColumn('Articles', 'gallery', {
+ type: Sequelize.ARRAY(Sequelize.STRING),
+ defaultValue: []
+ });
+ },
+
+ down: async (queryInterface) => {
+ await queryInterface.removeColumn('Users', 'avatar');
+ await queryInterface.removeColumn('Users', 'cover');
+ await queryInterface.removeColumn('Articles', 'gallery');
+ await queryInterface.addColumn('Users', 'image');
+ await queryInterface.addColumn('Articles', 'image');
+ }
+};
diff --git a/src/sequelize/migrations/20190715073823-create-terms-and-condition.js b/src/sequelize/migrations/20190715073823-create-terms-and-condition.js
new file mode 100644
index 0000000..7fccbb2
--- /dev/null
+++ b/src/sequelize/migrations/20190715073823-create-terms-and-condition.js
@@ -0,0 +1,22 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('termsAndConditions', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ termsAndConditions: {
+ type: Sequelize.TEXT
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('termsAndConditions')
+};
diff --git a/src/sequelize/migrations/20190715084903-create-chat.js b/src/sequelize/migrations/20190715084903-create-chat.js
new file mode 100644
index 0000000..2e46032
--- /dev/null
+++ b/src/sequelize/migrations/20190715084903-create-chat.js
@@ -0,0 +1,31 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('Chats', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ senderId: {
+ type: Sequelize.INTEGER
+ },
+ message: {
+ type: Sequelize.STRING
+ },
+ recieverId: {
+ type: Sequelize.INTEGER
+ },
+ read: {
+ type: Sequelize.BOOLEAN
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('Chats')
+};
diff --git a/src/sequelize/migrations/20200624102025-create-blockedarticle.js b/src/sequelize/migrations/20200624102025-create-blockedarticle.js
new file mode 100644
index 0000000..880c684
--- /dev/null
+++ b/src/sequelize/migrations/20200624102025-create-blockedarticle.js
@@ -0,0 +1,43 @@
+module.exports = {
+ up: (queryInterface, Sequelize) => queryInterface.createTable('BlockedArticles', {
+ id: {
+ allowNull: false,
+ autoIncrement: true,
+ primaryKey: true,
+ type: Sequelize.INTEGER
+ },
+ reporterId: {
+ allowNull: true,
+ type: Sequelize.INTEGER
+ },
+ articleId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ },
+ authorId: {
+ allowNull: false,
+ type: Sequelize.INTEGER,
+ },
+ moderatorId: {
+ allowNull: false,
+ type: Sequelize.INTEGER
+ },
+ blockedDay: {
+ allowNull: false,
+ type: Sequelize.STRING
+ },
+ description: {
+ allowNull: false,
+ type: Sequelize.STRING
+ },
+ createdAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ },
+ updatedAt: {
+ allowNull: false,
+ type: Sequelize.DATE
+ }
+ }),
+ down: queryInterface => queryInterface.dropTable('ReportedArticles')
+};
diff --git a/src/sequelize/migrations/20210624081558-add-blocked-article.js b/src/sequelize/migrations/20210624081558-add-blocked-article.js
new file mode 100644
index 0000000..f882034
--- /dev/null
+++ b/src/sequelize/migrations/20210624081558-add-blocked-article.js
@@ -0,0 +1,8 @@
+export default {
+ up: async (queryInterface, Sequelize) => queryInterface.addColumn('Articles', 'blocked', {
+ type: Sequelize.BOOLEAN,
+ defaultValue: false,
+ }),
+
+ down: async queryInterface => queryInterface.removeColumn('Articles', 'blocked')
+};
diff --git a/src/sequelize/models/article.js b/src/sequelize/models/article.js
new file mode 100644
index 0000000..a9a6c8f
--- /dev/null
+++ b/src/sequelize/models/article.js
@@ -0,0 +1,64 @@
+/* eslint-disable func-names */
+module.exports = (sequelize, DataTypes) => {
+ const Article = sequelize.define(
+ 'Article',
+ {
+ slug: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ title: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ readtime: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ description: {
+ type: DataTypes.STRING,
+ allowNull: false
+ },
+ body: {
+ type: DataTypes.TEXT,
+ allowNull: false
+ },
+ blocked: {
+ type: DataTypes.BOOLEAN,
+ allowNull: true
+ },
+ tagList: {
+ type: DataTypes.ARRAY(DataTypes.STRING),
+ allowNull: true
+ },
+ gallery: {
+ type: DataTypes.ARRAY(DataTypes.STRING),
+ allowNull: true
+ },
+ authorId: {
+ type: DataTypes.INTEGER,
+ allowNull: false
+ },
+ views: {
+ type: DataTypes.INTEGER
+ },
+ },
+ {}
+ );
+ Article.associate = function (models) {
+ // associations can be defined here
+ Article.belongsTo(models.User, {
+ as: 'author',
+ foreignKey: 'authorId',
+ targetKey: 'id',
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ });
+ Article.hasMany(models.Comment, {
+ foreignKey: 'articleId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ };
+ return Article;
+};
diff --git a/src/sequelize/models/articleratings.js b/src/sequelize/models/articleratings.js
new file mode 100644
index 0000000..f9ac068
--- /dev/null
+++ b/src/sequelize/models/articleratings.js
@@ -0,0 +1,11 @@
+
+
+module.exports = (sequelize, DataTypes) => {
+ const ArticleRatings = sequelize.define('ArticleRatings', {
+ slug: DataTypes.STRING,
+ userId: DataTypes.INTEGER,
+ ratings: DataTypes.INTEGER
+ }, {});
+ ArticleRatings.associate = () => {};
+ return ArticleRatings;
+};
diff --git a/src/sequelize/models/blacklist.js b/src/sequelize/models/blacklist.js
new file mode 100644
index 0000000..67ec2ed
--- /dev/null
+++ b/src/sequelize/models/blacklist.js
@@ -0,0 +1,11 @@
+export default (sequelize, DataTypes) => {
+ const Blacklist = sequelize.define('Blacklist', {
+ userId: DataTypes.INTEGER,
+ token: DataTypes.TEXT,
+ expiresAt: DataTypes.BIGINT,
+ }, {});
+ Blacklist.associate = () => {
+ // associations can be defined here
+ };
+ return Blacklist;
+};
diff --git a/src/sequelize/models/blockedarticle.js b/src/sequelize/models/blockedarticle.js
new file mode 100644
index 0000000..b6e792e
--- /dev/null
+++ b/src/sequelize/models/blockedarticle.js
@@ -0,0 +1,15 @@
+
+module.exports = (sequelize, DataTypes) => {
+ const blockedArticles = sequelize.define('BlockedArticles', {
+ reporterId: DataTypes.INTEGER,
+ articleId: DataTypes.INTEGER,
+ authorId: DataTypes.INTEGER,
+ moderatorId: DataTypes.INTEGER,
+ blockedDay: DataTypes.STRING,
+ description: DataTypes.STRING
+ }, {});
+ blockedArticles.associate = () => {
+ // associations can be defined here
+ };
+ return blockedArticles;
+};
diff --git a/src/sequelize/models/bookmarks.js b/src/sequelize/models/bookmarks.js
new file mode 100644
index 0000000..0ed90ac
--- /dev/null
+++ b/src/sequelize/models/bookmarks.js
@@ -0,0 +1,11 @@
+
+module.exports = (sequelize, DataTypes) => {
+ const Bookmarks = sequelize.define('Bookmarks', {
+ slug: DataTypes.STRING,
+ userId: DataTypes.INTEGER
+ }, {});
+ Bookmarks.associate = () => {
+ // associations can be defined here
+ };
+ return Bookmarks;
+};
diff --git a/src/sequelize/models/chat.js b/src/sequelize/models/chat.js
new file mode 100644
index 0000000..70c2187
--- /dev/null
+++ b/src/sequelize/models/chat.js
@@ -0,0 +1,25 @@
+/* eslint-disable func-names */
+module.exports = (sequelize, DataTypes) => {
+ const Chat = sequelize.define('Chat', {
+ senderId: DataTypes.INTEGER,
+ message: DataTypes.STRING,
+ recieverId: DataTypes.INTEGER,
+ read: DataTypes.BOOLEAN
+ }, {});
+ Chat.associate = function (models) {
+ // associations can be defined here
+ Chat.belongsTo(models.User, {
+ as: 'sender',
+ foreignKey: 'senderId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ Chat.belongsTo(models.User, {
+ as: 'receiver',
+ foreignKey: 'recieverId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ };
+ return Chat;
+};
diff --git a/src/sequelize/models/comment.js b/src/sequelize/models/comment.js
new file mode 100644
index 0000000..9dcd4cf
--- /dev/null
+++ b/src/sequelize/models/comment.js
@@ -0,0 +1,42 @@
+module.exports = (sequelize, DataTypes) => {
+ const Comment = sequelize.define(
+ 'Comment',
+ {
+ comment: DataTypes.STRING,
+ slug: DataTypes.STRING,
+ userId: DataTypes.INTEGER,
+ articleId: DataTypes.INTEGER,
+ commentId: DataTypes.INTEGER
+ },
+ {}
+ );
+ // eslint-disable-next-line func-names
+ Comment.associate = function (models) {
+ Comment.hasMany(models.Comment, {
+ foreignKey: 'commentId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ Comment.belongsTo(models.User, {
+ as: 'commentAuthor',
+ foreignKey: 'userId',
+ targetKey: 'id',
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ });
+ Comment.belongsTo(models.Article, {
+ as: 'Article',
+ foreignKey: 'articleId',
+ targetKey: 'id',
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ });
+ Comment.belongsTo(models.Comment, {
+ foreignKey: 'commentId',
+ targetKey: 'id',
+ onUpdate: 'CASCADE',
+ onDelete: 'CASCADE'
+ });
+ };
+ return Comment;
+};
diff --git a/src/sequelize/models/commentshistory.js b/src/sequelize/models/commentshistory.js
new file mode 100644
index 0000000..bd0c520
--- /dev/null
+++ b/src/sequelize/models/commentshistory.js
@@ -0,0 +1,8 @@
+module.exports = (sequelize, DataTypes) => {
+ const CommentsHistory = sequelize.define('CommentsHistory', {
+ userId: DataTypes.INTEGER,
+ editedComment: DataTypes.STRING,
+ commentId: DataTypes.INTEGER
+ }, {});
+ return CommentsHistory;
+};
diff --git a/src/sequelize/models/emails.js b/src/sequelize/models/emails.js
new file mode 100644
index 0000000..04b34dc
--- /dev/null
+++ b/src/sequelize/models/emails.js
@@ -0,0 +1,11 @@
+export default (sequelize, DataTypes) => {
+ const Emails = sequelize.define('Emails', {
+ mail: DataTypes.JSON,
+ html: DataTypes.TEXT,
+ subject: DataTypes.STRING,
+ sent: DataTypes.BOOLEAN,
+ createdAt: DataTypes.DATE,
+ updatedAt: DataTypes.DATE
+ }, {});
+ return Emails;
+};
diff --git a/src/sequelize/models/follows.js b/src/sequelize/models/follows.js
new file mode 100644
index 0000000..d630760
--- /dev/null
+++ b/src/sequelize/models/follows.js
@@ -0,0 +1,40 @@
+/* eslint-disable func-names */
+
+module.exports = (sequelize, DataTypes) => {
+ const follows = sequelize.define(
+ 'follows',
+ {
+ userId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ references: {
+ model: 'Users',
+ key: 'id'
+ },
+ followerId: {
+ type: DataTypes.INTEGER,
+ allowNull: false,
+ primaryKey: true,
+ references: {
+ model: 'Users',
+ key: 'id'
+ }
+ }
+ }
+ },
+ {}
+ );
+ follows.associate = function (models) {
+ // associations can be defined here
+ follows.belongsTo(models.User, {
+ foreignKey: 'userId',
+ as: 'followedUser'
+ });
+ follows.belongsTo(models.User, {
+ foreignKey: 'followerId',
+ as: 'follower'
+ });
+ };
+ return follows;
+};
diff --git a/src/sequelize/models/highlights.js b/src/sequelize/models/highlights.js
new file mode 100644
index 0000000..2fbb270
--- /dev/null
+++ b/src/sequelize/models/highlights.js
@@ -0,0 +1,13 @@
+export default (sequelize, DataTypes) => {
+ const Highlights = sequelize.define('Highlights', {
+ articleId: DataTypes.INTEGER,
+ userId: DataTypes.INTEGER,
+ highlightText: DataTypes.TEXT,
+ comment: DataTypes.TEXT,
+ occurencyNumber: DataTypes.INTEGER
+ }, {});
+ Highlights.associate = () => {
+ // associations can be defined here
+ };
+ return Highlights;
+};
diff --git a/src/sequelize/models/index.js b/src/sequelize/models/index.js
new file mode 100644
index 0000000..e212a58
--- /dev/null
+++ b/src/sequelize/models/index.js
@@ -0,0 +1,42 @@
+import fs from 'fs';
+import path from 'path';
+import Sequelize from 'sequelize';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const basename = path.basename(__filename);
+const env = process.env.NODE_ENV || 'development';
+const config = require('../../config/config.js')[env];
+
+const db = {};
+
+let sequelize;
+if (config.use_env_variable) {
+ sequelize = new Sequelize(process.env[config.use_env_variable], config);
+} else {
+ sequelize = new Sequelize(config.database, config.username, config.password, {
+ host: config.host,
+ dialect: 'postgres',
+ logging: false,
+ });
+}
+
+fs
+ .readdirSync(__dirname)
+ .filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'))
+ .forEach((file) => {
+ const model = sequelize.import(path.join(__dirname, file));
+ db[model.name] = model;
+ });
+
+Object.keys(db).forEach((modelName) => {
+ if (db[modelName].associate) {
+ db[modelName].associate(db);
+ }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+export default db;
diff --git a/src/sequelize/models/likedislike.js b/src/sequelize/models/likedislike.js
new file mode 100644
index 0000000..07b44b5
--- /dev/null
+++ b/src/sequelize/models/likedislike.js
@@ -0,0 +1,14 @@
+export default (sequelize, DataTypes) => {
+ const LikeDislike = sequelize.define(
+ 'LikeDislike',
+ {
+ userId: DataTypes.INTEGER,
+ articleId: DataTypes.INTEGER,
+ likes: DataTypes.INTEGER,
+ dislikes: DataTypes.INTEGER,
+ commentId: DataTypes.INTEGER
+ },
+ {}
+ );
+ return LikeDislike;
+};
diff --git a/src/sequelize/models/notification.js b/src/sequelize/models/notification.js
new file mode 100644
index 0000000..9bd85f4
--- /dev/null
+++ b/src/sequelize/models/notification.js
@@ -0,0 +1,19 @@
+
+
+module.exports = (sequelize, DataTypes) => {
+ const Notification = sequelize.define('Notification', {
+ type: DataTypes.STRING,
+ userId: DataTypes.INTEGER,
+ resource: DataTypes.STRING,
+ resourceId: DataTypes.INTEGER,
+ message: DataTypes.STRING,
+ url: DataTypes.STRING,
+ status: DataTypes.STRING
+ }, {});
+ // eslint-disable-next-line func-names
+ // eslint-disable-next-line no-unused-vars
+ Notification.associate = (models) => {
+ // associations can be defined here
+ };
+ return Notification;
+};
diff --git a/src/sequelize/models/opt.js b/src/sequelize/models/opt.js
new file mode 100644
index 0000000..f351dfd
--- /dev/null
+++ b/src/sequelize/models/opt.js
@@ -0,0 +1,15 @@
+
+
+module.exports = (sequelize, DataTypes) => {
+ const Opt = sequelize.define('Opt', {
+ userId: DataTypes.INTEGER,
+ resource: DataTypes.STRING,
+ type: DataTypes.STRING,
+ resourceId: DataTypes.INTEGER
+ }, {});
+ // eslint-disable-next-line no-unused-vars
+ Opt.associate = (models) => {
+ // associations can be defined here
+ };
+ return Opt;
+};
diff --git a/src/sequelize/models/reportedarticles.js b/src/sequelize/models/reportedarticles.js
new file mode 100644
index 0000000..b58d0bc
--- /dev/null
+++ b/src/sequelize/models/reportedarticles.js
@@ -0,0 +1,12 @@
+
+module.exports = (sequelize, DataTypes) => {
+ const reportedarticles = sequelize.define('ReportedArticles', {
+ slug: DataTypes.STRING,
+ comment: DataTypes.STRING,
+ username: DataTypes.STRING
+ }, {});
+ reportedarticles.associate = () => {
+ // associations can be defined here
+ };
+ return reportedarticles;
+};
diff --git a/src/sequelize/models/share.js b/src/sequelize/models/share.js
new file mode 100644
index 0000000..a2e6896
--- /dev/null
+++ b/src/sequelize/models/share.js
@@ -0,0 +1,8 @@
+export default (sequelize, DataTypes) => {
+ const Share = sequelize.define('Share', {
+ userId: DataTypes.INTEGER,
+ slug: DataTypes.STRING,
+ provider: DataTypes.STRING
+ }, {});
+ return Share;
+};
diff --git a/src/sequelize/models/termsandcondition.js b/src/sequelize/models/termsandcondition.js
new file mode 100644
index 0000000..a1299b4
--- /dev/null
+++ b/src/sequelize/models/termsandcondition.js
@@ -0,0 +1,6 @@
+export default (sequelize, DataTypes) => {
+ const termsAndCondition = sequelize.define('termsAndCondition', {
+ termsAndConditions: DataTypes.TEXT
+ }, {});
+ return termsAndCondition;
+};
diff --git a/src/sequelize/models/user.js b/src/sequelize/models/user.js
new file mode 100644
index 0000000..0624c9d
--- /dev/null
+++ b/src/sequelize/models/user.js
@@ -0,0 +1,39 @@
+/* eslint-disable func-names */
+module.exports = (sequelize, DataTypes) => {
+ const User = sequelize.define(
+ 'User',
+ {
+ firstName: DataTypes.STRING,
+ lastName: DataTypes.STRING,
+ username: DataTypes.STRING,
+ email: DataTypes.STRING,
+ password: DataTypes.STRING,
+ bio: DataTypes.TEXT,
+ avatar: DataTypes.STRING,
+ cover: DataTypes.STRING,
+ dateOfBirth: DataTypes.DATE,
+ gender: DataTypes.STRING,
+ provider: DataTypes.STRING,
+ socialId: DataTypes.STRING,
+ verified: DataTypes.BOOLEAN,
+ roles: DataTypes.ARRAY(DataTypes.STRING),
+ },
+ {}
+ );
+ User.associate = function (models) {
+ // associations can be defined here
+ User.hasMany(models.Article, {
+ as: 'author',
+ foreignKey: 'authorId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ User.hasMany(models.Comment, {
+ as: 'commentAuthor',
+ foreignKey: 'userId',
+ onDelete: 'CASCADE',
+ onUpdate: 'CASCADE'
+ });
+ };
+ return User;
+};
diff --git a/src/sequelize/seeders/20190618062925-test-user.js b/src/sequelize/seeders/20190618062925-test-user.js
new file mode 100644
index 0000000..a8a7913
--- /dev/null
+++ b/src/sequelize/seeders/20190618062925-test-user.js
@@ -0,0 +1,220 @@
+import superUserPsw from '../../helpers/hashSuperUserPsw';
+
+module.exports = {
+ up: queryInterface => queryInterface.bulkInsert('Users', [{
+ firstName: 'Mireille',
+ lastName: 'Niwemuhuza',
+ username: 'mifeille',
+ email: 'nimilleer@gmail.com',
+ password: 'Mireille1!',
+ bio: '',
+ avatar: '',
+ cover: '',
+ dateOfBirth: '12/12/2000',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ }, {
+ firstName: 'eric',
+ lastName: 'rukundo',
+ username: 'ericrundo',
+ email: 'hhhhhhhhhhhhhh3h3hh3@gmail.com',
+ password: 'eric123',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['admin']
+ },
+ {
+ firstName: 'Mireille',
+ lastName: 'Niwemuhuza',
+ username: 'mifeille',
+ email: 'nimilleer@gmail.com',
+ password: 'Mireille1!',
+ bio: '',
+ avatar: '',
+ cover: '',
+ dateOfBirth: '12/12/2000',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: 'false',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+ firstName: 'Uhiriwe',
+ lastName: 'Audace',
+ username: 'Audace',
+ email: 'u.audace@gmail.com',
+ password: 'Uhiriwe1!',
+ bio: '',
+ avatar: '',
+ cover: '',
+ dateOfBirth: '12/12/1999',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: 'true',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+ firstName: 'Eric',
+ lastName: 'Prestein',
+ username: 'ericprestein',
+ email: 'gprestein055@gmail.com',
+ password: '$2b$08$aKninSz.39G5SxBE5QOro.xBJXIEtnXkrOBriWfVCpqND8AzeJMaC',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+ firstName: 'Eric',
+ lastName: 'Prestein',
+ username: 'ericpresteinjjj',
+ email: 'gprestein555@gmail.com',
+ password: '$2b$08$aKninSz.39G5SxBE5QOro.xBJXIEtnXkrOBriWfVCpqND8AzeJMaC',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['admin']
+ },
+ {
+ firstName: 'diane',
+ lastName: 'mahoro',
+ username: 'test_user',
+ email: 'mahorodiane@gmail.com',
+ password: 'cooler12345',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+ firstName: 'diego',
+ lastName: 'hirwa',
+ username: 'diego',
+ email: 'diegohirwa@gmail.com',
+ password: 'coolest12345',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+
+ firstName: 'espoire',
+ lastName: 'mugenzie',
+ username: 'espoire',
+ email: 'espoirmugenzie@gmail.com',
+ password: 'ericprestein',
+ bio: 'nan',
+ avatar: '',
+ cover: '',
+ dateOfBirth: new Date(),
+ gender: 'male',
+ provider: 'nan',
+ socialId: '3434343',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['user']
+ },
+ {
+ firstName: 'SuperUser',
+ lastName: 'SuperUser',
+ username: 'superUser',
+ email: 'superuser@gmail.com',
+ password: superUserPsw,
+ bio: '',
+ avatar: '',
+ cover: '',
+ dateOfBirth: '12/12/2000',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['admin', 'moderator']
+ },
+ {
+ firstName: 'SuperEmy',
+ lastName: 'SuperEmy',
+ username: 'EmyRukundo',
+ email: 'rukundoemma@gmail.com',
+ password: 'EmyRukundo00',
+ bio: '',
+ avatar: '',
+ cover: '',
+ dateOfBirth: '12/12/1900',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['admin', 'moderator']
+ },
+ {
+ firstName: 'SuperAdmin',
+ lastName: 'SuperAdmin',
+ username: 'superAdmin',
+ email: 'superadmin@gmail.com',
+ password: superUserPsw,
+ bio: '',
+ avatar: '',
+ dateOfBirth: '12/12/2000',
+ gender: '',
+ provider: '',
+ socialId: '',
+ verified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ roles: ['admin']
+ },
+ ], {}),
+};
diff --git a/src/sequelize/seeders/20190618152420-articleRatings.js b/src/sequelize/seeders/20190618152420-articleRatings.js
new file mode 100644
index 0000000..97563d8
--- /dev/null
+++ b/src/sequelize/seeders/20190618152420-articleRatings.js
@@ -0,0 +1,50 @@
+module.exports = {
+ up: queryInterface => queryInterface.bulkInsert(
+ 'ArticleRatings',
+ [
+ {
+ userId: 1,
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ ratings: 5,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 2,
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ ratings: 5,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 3,
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ ratings: 5,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 1,
+ slug: 'This_is_muhabura_2433546h34',
+ ratings: 4,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 2,
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ ratings: 3,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 6,
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ ratings: 4,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ ],
+ {}
+ )
+};
diff --git a/src/sequelize/seeders/20190620204605-articles-test.js b/src/sequelize/seeders/20190620204605-articles-test.js
new file mode 100644
index 0000000..413ac8b
--- /dev/null
+++ b/src/sequelize/seeders/20190620204605-articles-test.js
@@ -0,0 +1,44 @@
+module.exports = {
+ up: async queryInterface => queryInterface.bulkInsert('Articles', [
+ {
+ slug: '73H7812',
+ title: 'How to survive at ahmedkhaled4d',
+ description: 'YoYo',
+ authorId: 11,
+ blocked: false,
+ body:
+ 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ readtime: '2 min',
+ views: 0
+ },
+ {
+ slug: '73H99992',
+ title: 'Wow',
+ description: 'YoYo',
+ authorId: 2,
+ blocked: false,
+ body:
+ 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ readtime: '1min',
+ views: 0
+ },
+ {
+ slug: 'This_is_ahmedkhaled4d_2433546h34',
+ title: 'Wow',
+ description: 'YoYo',
+ readtime: '2 min',
+ body:
+ 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ authorId: 4,
+ views: 0
+ }
+ ]),
+
+ down: queryInterface => queryInterface.bulkDelete('Articles', null, {})
+};
diff --git a/src/sequelize/seeders/20190715075157-termsAndConditions.js b/src/sequelize/seeders/20190715075157-termsAndConditions.js
new file mode 100644
index 0000000..b23a2f7
--- /dev/null
+++ b/src/sequelize/seeders/20190715075157-termsAndConditions.js
@@ -0,0 +1,54 @@
+
+module.exports = {
+ up: queryInterface => queryInterface.bulkInsert('termsAndConditions', [{
+ termsAndConditions: `Authors Haven Terms and conditions
+
+Welcome to Authors Haven! The following terms and conditions govern all use of the Authors Haven website and all content and services available at or through the website.
+Please read this Agreement carefully before accessing or using the Website. By accessing or using any part of the web site, you agree to become bound by the terms and conditions of this agreement. If you do not agree to all the terms and conditions of this agreement, then you may not use this website services. If these terms and conditions are considered an offer by Authors Haven, acceptance is expressly limited to these terms.
+
+Publication policy
+Terms and conditions of publishing articles on Authors Haven
+By submitting any article for publication on Authors Haven, the author has undertaken and agreed with the following terms and conditions:
+That the article is original work of the author and has not been published anywhere before.
+That if there is any plagiarism or violation of copyright due to publication of the article, then the author will be solely responsible for it, and will hold Intelligent Legal Risk Management Solutions LLP, the owner of Authors Haven indemnified against any damages and legal action.
+That in case of adverse public opinion, governmental action, criticism or harm to the image of the website, the site moderator reserves the right to take down the article.
+That the author is responsible for the content he/she posts. This means he/she assumes all risks related to it, including someone else’s reliance on its accuracy, or claims relating to intellectual property or other legal rights.
+
+Right on the content posted on Authors Haven
+
+The content posted on Authors haven, The author has a non-exclusive license to publish it on Author’s haven. Including anything reasonably related to publishing it (like storing, displaying, reformatting, and distributing it).In consideration for Authors haven granting you access, you agree that Authors haven may use your content to promote Authors haven products. However, Authors haven will never sell your content to third parties without your permission.
+
+Moderation of comments
+We encourage users to have a lively debate and discussion of their work and ideas. However, by submitting a comment you agree to abide by the following rules. We reserve the right to delete any comments that contravene any of these rules.
+Identification
+Anonymous comments are not allowed. For a user to comment he/she must be authenticated.
+Conduct and use of language
+Comments should be relevant to the article. All off-topic, abusive or defamatory comments will not be accepted. Comments should not attack an individual’s personality or character.
+Spamming and advertising
+No advertising or promotion is allowed except when an event, service or product has direct relevance to the topic of discussion.
+Reporting abuse
+Any author or commenter who posts content which is deemed to be inappropriate will be notified and the article will be blocked or comment will be deleted. The Authors Haven moderator has the right to remove all inappropriate articles or comments.
+If a user abuses his right to report articles or comments, the site moderator reserves the right to delete his account from Authors Haven.
+Site terms of use modifications
+Authors Haven reserves the right, at its sole discretion, to modify or replace any part of this Agreement. It is your responsibility to check this Agreement periodically for changes. Your continued use of or access to the Website following the posting of any changes to this Agreement constitutes acceptance of those changes.
+
+
+Termination
+Authors Haven may terminate your access to all or any part of the Website at any time, with or without cause, with or without notice, effective immediately.
+
+Security
+If you find a security vulnerability on Author’s Haven, Please let us know to be able to fix it.Do not use that vulnerability to breach into our system.
+
+Non transferable
+Any password or right given to you to obtain Materials from the Site is not transferable. You are responsible for maintaining the confidentiality of your account number and/or password, if applicable. You are responsible for all uses of your account, whether or not actually or expressly authorized by you.
+
+Entire agreement
+These Terms and conditions are the whole agreement between Author’s Haven and you concerning its services.
+
+`,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ }], {}),
+
+ down: queryInterface => queryInterface.bulkDelete('termsAndConditions', null, {})
+};
diff --git a/src/sequelize/seeders/20200618062950-follows.js b/src/sequelize/seeders/20200618062950-follows.js
new file mode 100644
index 0000000..904b254
--- /dev/null
+++ b/src/sequelize/seeders/20200618062950-follows.js
@@ -0,0 +1,21 @@
+module.exports = {
+ up: queryInterface => queryInterface.bulkInsert('follows', [{
+ userId: 5,
+ followerId: 10,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 10,
+ followerId: 5,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ userId: 3,
+ followerId: 5,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ }
+ ], {}),
+};
diff --git a/src/sequelize/seeders/20200718062950-chats.js b/src/sequelize/seeders/20200718062950-chats.js
new file mode 100644
index 0000000..8214b6f
--- /dev/null
+++ b/src/sequelize/seeders/20200718062950-chats.js
@@ -0,0 +1,27 @@
+module.exports = {
+ up: queryInterface => queryInterface.bulkInsert('Chats', [{
+ senderId: 10,
+ recieverId: 5,
+ message: 'hello tyhere',
+ read: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ senderId: 5,
+ recieverId: 10,
+ message: 'How are you?',
+ read: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ },
+ {
+ senderId: 10,
+ recieverId: 5,
+ message: 'Im fine',
+ read: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ }
+ ], {}),
+};
diff --git a/src/workers/index.js b/src/workers/index.js
new file mode 100644
index 0000000..1ee1a73
--- /dev/null
+++ b/src/workers/index.js
@@ -0,0 +1,10 @@
+const workerFarm = require('worker-farm');
+
+const workers = {};
+
+workers.purgeWorker = workerFarm(require.resolve('./purgeDeadTokens'));
+workers.queueEmailWorker = workerFarm(require.resolve('./queueEmail'));
+workers.sendMailWorker = workerFarm(require.resolve('./sendMail'));
+workers.uploadImageWorker = workerFarm(require.resolve('./uploadImage'));
+
+export default workers;
diff --git a/src/workers/purgeDeadTokens.js b/src/workers/purgeDeadTokens.js
new file mode 100644
index 0000000..12127f9
--- /dev/null
+++ b/src/workers/purgeDeadTokens.js
@@ -0,0 +1,20 @@
+/* eslint-disable no-console */
+import 'regenerator-runtime';
+import Sequelize from 'sequelize';
+import db from '../sequelize/models';
+
+const { Blacklist } = db;
+const { Op } = Sequelize;
+
+module.exports = async () => {
+ console.log(`PID: ${process.pid} === STARTING EXPIRED TOKEN PURGE ===`);
+ await Blacklist.destroy({
+ where: {
+ expiresAt: {
+ [Op.lte]: Date.now()
+ }
+ }
+ });
+ console.log('=== DONE PURGING ===');
+ return 0;
+};
diff --git a/src/workers/queueEmail.js b/src/workers/queueEmail.js
new file mode 100644
index 0000000..a3edb5c
--- /dev/null
+++ b/src/workers/queueEmail.js
@@ -0,0 +1,18 @@
+/* eslint-disable no-console */
+import 'regenerator-runtime';
+import models from '../sequelize/models';
+
+const { Emails } = models;
+
+module.exports = async (emailObject, html, subject) => {
+ console.log(`PID: ${process.pid} === QUEING EMAIL ===`);
+ await Emails.create({
+ mail: emailObject,
+ html,
+ subject,
+ sent: false,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ });
+ console.log(`PID: ${process.pid} === FINISHED QUEING EMAIL ===`);
+};
diff --git a/src/workers/sendMail.js b/src/workers/sendMail.js
new file mode 100644
index 0000000..2e65bfe
--- /dev/null
+++ b/src/workers/sendMail.js
@@ -0,0 +1,28 @@
+/* eslint-disable no-console */
+import 'regenerator-runtime';
+import { forEach } from 'lodash';
+import models from '../sequelize/models';
+import sendMail from '../helpers/mailer/SendAnyEmail';
+
+const { Emails } = models;
+
+const sendMailsInQueue = async () => {
+ console.log(`PID: ${process.pid} === SENDING EMAILS ===`);
+ const mailsToSend = await Emails.findAll({ where: { sent: false } });
+
+ forEach(mailsToSend, async (mailItem) => {
+ sendMail(mailItem.mail, mailItem.html, mailItem.subject);
+ await Emails.update(
+ {
+ sent: true
+ },
+ {
+ where: {
+ id: mailItem.id
+ }
+ }
+ );
+ });
+};
+
+module.exports = sendMailsInQueue;
diff --git a/src/workers/uploadImage.js b/src/workers/uploadImage.js
new file mode 100644
index 0000000..a7e9ae6
--- /dev/null
+++ b/src/workers/uploadImage.js
@@ -0,0 +1,60 @@
+/* eslint-disable no-console */
+import 'regenerator-runtime';
+import { v2 as cloudinary } from 'cloudinary';
+import dotenv from 'dotenv';
+import { forEach } from 'lodash';
+import { each } from 'async';
+import models from '../sequelize/models';
+
+const { User, Article } = models;
+
+
+dotenv.config();
+
+cloudinary.config({
+ cloud_name: process.env.CLOUD_NAME,
+ api_key: process.env.CLOUDINARY_API_ID,
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+});
+
+
+const uploadHelper = async (uploads, id, model) => {
+ console.log(`PID: ${process.pid} === UPLOADING IMAGE ===`);
+ let uploadedImage;
+
+ forEach(uploads, async (upload) => {
+ const gallery = [];
+
+ switch (model) {
+ case 'user':
+ forEach(upload, async (file) => {
+ const { path, fieldname } = file;
+ uploadedImage = await cloudinary.uploader.upload(path);
+ await User.update({ [fieldname]: uploadedImage.secure_url }, { where: { id } });
+ console.log('UPLOAD COMPLETED');
+ });
+ break;
+
+ case 'article':
+ each(upload, async (file, callback) => {
+ const { path } = file;
+ uploadedImage = await cloudinary.uploader.upload(path);
+ gallery.push(uploadedImage.secure_url);
+ callback();
+ }, async (err) => {
+ if (!err) {
+ const article = await Article.findOne({ where: { id } });
+ const newGallery = [...article.gallery, ...gallery];
+ await Article.update({ gallery: newGallery }, { where: { id } });
+ console.log('UPLOAD COMPLETED');
+ }
+ });
+ break;
+
+ default:
+ break;
+ }
+ });
+};
+
+module.exports = uploadHelper;
diff --git a/swagger.json b/swagger.json
new file mode 100644
index 0000000..cff49a7
--- /dev/null
+++ b/swagger.json
@@ -0,0 +1,2234 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "version": "2",
+ "title": "Tesla-AH",
+ "description": "Create a community of like minded authors to foster inspiration and innovation by leveraging the modern web."
+ },
+ "host": "localhost:3000",
+ "basePath": "/api",
+ "tags": [
+ {
+ "name": "auth",
+ "description": " User Authentication "
+ },
+ {
+ "name": "article",
+ "description": " Articles "
+ },
+ {
+ "name": "profile",
+ "description": " User profile "
+ },
+ {
+ "name": "follow",
+ "description": " User should be able to follow and unfollow each other."
+ }
+ ],
+ "schemes": [
+ "http",
+ "https"
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "definitions": {
+ "user": {
+ "type": "object",
+ "properties": {
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "bio": {
+ "type": "string"
+ },
+ "image": {
+ "type": "string"
+ },
+ "gender": {
+ "type": "M"
+ },
+ "dateOfBirth": {
+ "type": "string"
+ }
+ }
+ },
+ "signup": {
+ "required": [
+ "firstName",
+ "lastName",
+ "email",
+ "username",
+ "password",
+ "confirmPassword"
+ ],
+ "properties": {
+ "firstName": {
+ "type": "string"
+ },
+ "lastName": {
+ "type": "string"
+ },
+ "email": {
+ "type": "string"
+ },
+ "username": {
+ "type": "string"
+ },
+ "password": {
+ "type": "string"
+ },
+ "confirmPassword": {
+ "type": "string"
+ }
+ }
+ },
+ "article": {
+ "required": [
+ "title",
+ "description",
+ "body",
+ "tagList"
+ ],
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "body": {
+ "type": "string"
+ },
+ "tagList": {
+ "type": "string",
+ "example": "reactJs, AngularJs, NodeJs"
+ }
+ }
+ },
+ "requestPasswordReset": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ericrukundo005@gmail.com"
+ }
+ }
+ },
+ "login": {
+ "type": "object",
+ "properties": {
+ "email": {
+ "type": "string",
+ "example": "ericrukundo005@gmail.com"
+ },
+ "password": {
+ "type": "string",
+ "example": "eric@12345"
+ }
+ }
+ },
+ "comment": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string",
+ "example": "This is a comment"
+ }
+ }
+ },
+ "description": {
+ "type": "object",
+ "properties": {
+ "comment": {
+ "type": "string",
+ "example": "This is the description or the reason why article is blocked."
+ }
+ }
+ }
+ },
+ "paths": {
+ "/auth/signup": {
+ "post": {
+ "tags": [
+ "auth"
+ ],
+ "description": "User is able to create an account",
+ "parameters": [
+ {
+ "name": "user",
+ "in": "body",
+ "description": "User create a new account",
+ "schema": {
+ "$ref": "#/definitions/signup"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": " User Account created successfully",
+ "schema": {
+ "$ref": "#/definitions/signup"
+ }
+ }
+ }
+ }
+ },
+ "/api/user": {
+ "put": {
+ "tags": [
+ "profile"
+ ],
+ "description": "User profile update",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "Headers",
+ "description": "Client authentication token",
+ "required": true
+ },
+ {
+ "name": "user",
+ "in": "Body",
+ "description": "Object containing user information to update",
+ "schema": "#/definitions/user"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "User profile sucessfully updated"
+ },
+ "404": {
+ "description": "User profile not found"
+ },
+ "500": {
+ "description": "Error while updating profile"
+ }
+ }
+ }
+ },
+ "/profiles/{username}": {
+ "get": {
+ "tags": [
+ "profile"
+ ],
+ "description": "Get user profile",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "Headers",
+ "description": "Client authentication token",
+ "required": false
+ },
+ {
+ "name": "username",
+ "in": "path",
+ "description": "Username of user to get",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "User profile returned"
+ },
+ "404": {
+ "description": "User profile not found"
+ },
+ "500": {
+ "description": "Error while fetching profile"
+ }
+ }
+ }
+ },
+ "/auth/signout": {
+ "get": {
+ "tags": [
+ "auth"
+ ],
+ "description": "User is able to signout from the system when she/he is logged in by blacklisting the Token ",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "You are now signed Out!"
+ },
+ "401": {
+ "description": " You are not Authorised to blacklist this token "
+ },
+ "500": {
+ "description": "Server Error"
+ }
+ }
+ }
+ },
+ "/articles": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "user should be able to create an article",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "Article",
+ "in": "body",
+ "description": "create an article on author's haven",
+ "schema": {
+ "$ref": "#/definitions/article"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Article created successfully",
+ "schema": {
+ "$ref": "#/definitions/article"
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": [
+ "article"
+ ],
+ "description": "user should be able to view all articles",
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Articles fetched successfully"
+ }
+ }
+ }
+ },
+ "/articles/{slug}": {
+ "get": {
+ "tags": [
+ "article"
+ ],
+ "description": "user should be able to view a specific slug",
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "The article's slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Single Article fetched successfully"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "article"
+ ],
+ "description": "user should be able to update his/her own article - slug",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "The article's slug",
+ "required": true
+ },
+ {
+ "name": "Article",
+ "in": "body",
+ "description": "update your own article on author's haven",
+ "schema": {
+ "$ref": "#/definitions/article"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Article updated successfully",
+ "schema": {
+ "$ref": "#/definitions/article"
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "article"
+ ],
+ "description": "user should be able to delete a specific slug",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "The article's slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Article deleted successfully!"
+ }
+ }
+ }
+ },
+ "/articles/{slug}/like": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "User should be able to like an article",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Article liked successfully"
+ },
+ "403": {
+ "description": "Article already liked"
+ }
+ }
+ }
+ },
+ "/articles/{slug}/dislike": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "User should be able to dislike an article",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Article disliked successfully"
+ },
+ "403": {
+ "description": "Article already disliked"
+ }
+ }
+ }
+ },
+ "/ratings/articles/:slug": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "User is able to view ratings report on a specific article",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "example": "This_is_ahmedkhaled4d_2433546h34",
+ "description": "Authentication key",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "report": {
+ "type": "object",
+ "properties": {
+ "1st": {
+ "type": "integer",
+ "example": 1
+ },
+ "2st": {
+ "type": "integer",
+ "example": 4
+ },
+ "3st": {
+ "type": "integer",
+ "example": 3
+ },
+ "4st": {
+ "type": "integer",
+ "example": 1
+ },
+ "5st": {
+ "type": "integer",
+ "example": 5
+ },
+ "Number of User": {
+ "type": "integer",
+ "example": 1
+ },
+ "Total Ratings": {
+ "type": "integer",
+ "example": 1
+ },
+ "Average": {
+ "type": "integer",
+ "example": 1
+ }
+ }
+ },
+ "percentage": {
+ "type": "object",
+ "properties": {
+ "1st": {
+ "type": "string",
+ "example": "10%"
+ },
+ "2st": {
+ "type": "string",
+ "example": "0%"
+ },
+ "3st": {
+ "type": "string",
+ "example": "0%"
+ },
+ "4st": {
+ "type": "string",
+ "example": "30%"
+ },
+ "5st": {
+ "type": "string",
+ "example": "60%"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "No rating found for specific article",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 404
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "No any rating found for that article"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/login": {
+ "post": {
+ "tags": [
+ "auth"
+ ],
+ "description": "User is able to login",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "in": "body",
+ "name": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/login"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "User logged in successful"
+ },
+ "token": {
+ "type": "string",
+ "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NDU3LCJmaXJzdE5hbWUiOiJFcmljIiwibGFzdE5hbWUiOiJQcmVzdGVpbiIsInVzZXJuYW1lIjoiZXJpY3ByZXN0ZWluIiwiZW1haWwiOiJncHJlc3RlaW4wNTVAZ21haWwuY29tIiwicGFzc3dvcmQiOiIkMmIkMDgkemlUU3RqL0piU2NWb2M3R2tTaksxT1VqalgyNnh3UnBWSUdTeC9QVDVpUXBoeXduVFJSRkMiLCJiaW8iOiJuYW4iLCJpbWFnZSI6Im5hbiIsImRhdGVPZkJpcnRoIjoiMjAxOS0wNi0xN1QxMzo0MDozMi4yODJaIiwiZ2VuZGVyIjoibWFsZSIsInByb3ZpZGVyIjoibmFuIiwic29jaWFsSWQiOiIzNDM0MzQzIiwidmVyaWZpZWQiOnRydWUsImNyZWF0ZWRBdCI6IjIwMTktMDYtMTdUMTM6NDA6MzIuMjgyWiIsInVwZGF0ZWRBdCI6IjIwMTktMDYtMTdUMTM6NDA6MzIuMjgyWiIsImlhdCI6MTU2MDc3ODg0OCwiZXhwIjoxNTYwOTUxNjQ4fQ.uLQmiZZ0u7TQYYAkvb9_XjhBQp88xJbSb_84Bt9BfRw"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Inputs",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "child email fails because [email must be a valid email]"
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "User not found",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 404
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "User with that email does not exist"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/reset": {
+ "post": {
+ "tags": [
+ "auth"
+ ],
+ "description": "User is able to request password reset",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "in": "body",
+ "name": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/requestPasswordReset"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 201
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "Reset link sent to your email "
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Inputs",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "child email fails because [email must be a valid email]"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/reset/:token": {
+ "get": {
+ "tags": [
+ "auth"
+ ],
+ "description": "When user click on Reset Button on his/her email, Reset link will be generated.",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "token",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": "Below is your reset link",
+ "link": "http://localhost:3000/auth/reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTU1OTg5NDQ1OCwiZXhwIjoxNTYwMDY3MjU4fQ.yLEX1WeDAwcjVzvag5guJGz-GmzwecVUOZYVYyeYajM"
+ }
+ }
+ }
+ }
+ },
+ "304": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 304
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "token is invalid or expired"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/auth/reset/:aprvToken": {
+ "patch": {
+ "tags": [
+ "auth"
+ ],
+ "description": "User is able to apply password reset and change his or her password",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "aprvToken",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 201
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": "Password changed successful"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "child newpassword fails because [newpassword length must be at least 8 characters long]"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/articles/{slug}/comments": {
+ "post": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User is able to add a comment on an article",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "Client authentication token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ },
+ {
+ "name": "comment",
+ "in": "body",
+ "required": true,
+ "description": "User comments an article",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Dear ..., Thank you for contributing to this article",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User is able to get an article with its comments",
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/articles/{slug}/comments/{commentId}": {
+ "post": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User is able to comment a comment",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "Client authentication token",
+ "required": true
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "The comment Id",
+ "required": true
+ },
+ {
+ "name": "comment",
+ "in": "body",
+ "required": true,
+ "description": "User comments a comment",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Dear ..., Thank you for contributing to this comment",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ }
+ }
+ },
+ "/articles/comments/{commentId}": {
+ "patch": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User is able to edit a comment",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "Client authentication token",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "The comment Id",
+ "required": true
+ },
+ {
+ "name": "comment",
+ "in": "body",
+ "required": true,
+ "description": "User edits a comment",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Your comment has been edited",
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User is able to delete a comment",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "Client authentication token",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "The comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Comment deleted!"
+ }
+ }
+ }
+ },
+ "/articles/comments/{commentId}/dislike": {
+ "post": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User should be able to dislike a comment",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "Comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Dear ..., Thank you for disliking this comment"
+ },
+ "403": {
+ "description": "Dear ..., You have already disliked this comment!"
+ }
+ }
+ }
+ },
+ "/articles/comments/{commentId}/like": {
+ "post": {
+ "tags": [
+ "comment"
+ ],
+ "description": "User should be able to like a comment",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "Comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "Dear ..., Thank you for liking this comment"
+ },
+ "403": {
+ "description": "Dear ..., You have already liked this comment!"
+ }
+ }
+ }
+ },
+ "/articles/comments/{commentId}/dislikes": {
+ "get": {
+ "tags": [
+ "comment"
+ ],
+ "description": "Authors Haven visitors and users should be able see all dislikes on a comment",
+ "parameters": [
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "Comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/articles/comments/{commentId}/history": {
+ "get": {
+ "tags": [
+ "comment"
+ ],
+ "description": "Authors Haven users should be able see all track their comments edit history",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The user token",
+ "required": true
+ },
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "Comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/articles/comments/{commentId}/likes": {
+ "get": {
+ "tags": [
+ "comment"
+ ],
+ "description": "Authors Haven visitors and users should be able to see all likes on a comment",
+ "parameters": [
+ {
+ "name": "commentId",
+ "in": "path",
+ "description": "Comment Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/profiles": {
+ "get": {
+ "tags": [
+ "profile"
+ ],
+ "description": "Any User will be able to view a list of User's Profiles",
+ "parameters": [],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "the list of All User's profile has been successfully fetched"
+ }
+ }
+ }
+ },
+ "/profiles/:username/follow": {
+ "patch": {
+ "tags": [
+ "follow"
+ ],
+ "description": "User should be able to follow other users",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "follow",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": "Congratulation, now you followed the person"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "you already follow this person"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/profiles/:username/unfollow": {
+ "patch": {
+ "tags": [
+ "follow"
+ ],
+ "description": "User should be able to unfollow other users",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "unfollow",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": " you unfollowed the person"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "you do not follow this person"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/profiles/:username/following": {
+ "get": {
+ "tags": [
+ "follow"
+ ],
+ "description": "User should be able to fetch all users he or she follows",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "following",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": " these are the people you follow"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "you do not follow any person"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/profiles/:username/followers": {
+ "get": {
+ "tags": [
+ "follow"
+ ],
+ "description": "User should be able to fetch all users who follow him or her",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "followers",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "example": {
+ "message": " these are the people you follow"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid or Expired token",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "error": {
+ "type": "object",
+ "example": {
+ "message": "you do not follow any person"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/articles/:slug/report": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "User is able to report an article",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "token",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "in": "body",
+ "name": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/comment"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 201
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "example": "2"
+ },
+ "slug": {
+ "type": "string",
+ "example": "73H7812"
+ },
+ "comment": {
+ "type": "string",
+ "example": "some thing"
+ },
+ "username": {
+ "type": "string",
+ "example": "ericprestein"
+ },
+ "createdAt": {
+ "type": "string",
+ "example": "2019-06-24T12:05:20.116Z"
+ },
+ "updatedAt": {
+ "type": "string",
+ "example": "2019-06-24T12:05:20.116Z"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Inputs",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The comment is required and should have at least 3 letters!"
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Article Not Found",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 404
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The article does not exist!!!"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/articles/:slug/block": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "Super admin or moderator are able to block the article",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "token",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "in": "body",
+ "name": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/description"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 201
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "object",
+ "example": "Article blocked successfully"
+ },
+ "response": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "example": "2"
+ },
+ "reporterId": {
+ "type": "integer",
+ "example": 5
+ },
+ "articleId": {
+ "type": "integer",
+ "example": 3
+ },
+ "authorId": {
+ "type": "integer",
+ "example": 2
+ },
+ "moderatorId": {
+ "type": "integer",
+ "example": 6
+ },
+ "blockedDay": {
+ "type": "string",
+ "example": "Thursday"
+ },
+ "createdAt": {
+ "type": "string",
+ "example": "2019-06-24T12:05:20.116Z"
+ },
+ "updatedAt": {
+ "type": "string",
+ "example": "2019-06-24T12:05:20.116Z"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid Inputs",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The description is required and should have at least 3 letters!"
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Article Not Found",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 404
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The article does not exist!!!"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/articles/:slug/unblock": {
+ "post": {
+ "tags": [
+ "article"
+ ],
+ "description": "Super admin or moderator are able to unblock the article",
+ "produces": [
+ "application/json"
+ ],
+ "parameters": [
+ {
+ "name": "token",
+ "in": "path",
+ "description": "Token key",
+ "required": true,
+ "type": "string"
+ },
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation.",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 200
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "Article unblocked successfully"
+ }
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Unblock an article which is not blocked",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 400
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The article you are trying to unblock is not blocked."
+ }
+ }
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Article Not Found",
+ "schema": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "integer",
+ "example": 404
+ },
+ "data": {
+ "type": "object",
+ "properties": {
+ "messages": {
+ "type": "string",
+ "example": "The article does not exist!!!"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/articles?author={author}": {
+ "get": {
+ "tags": [
+ "Search for an article"
+ ],
+ "description": "AAny User will be able to view a list of all articles which matches with the provided parameter",
+ "parameters": [
+ {
+ "name": "author",
+ "in": "path",
+ "description": "Author's Username",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "The List of All Articles related to the provided author's Username"
+ }
+ }
+ }
+ },
+ "/articles?tag={tag}": {
+ "get": {
+ "tags": [
+ "Search for an article"
+ ],
+ "description": "Any User will be able to view a list of all articles which matches with the provided parameter",
+ "parameters": [
+ {
+ "name": "tag",
+ "in": "path",
+ "description": "article's tag",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "The List of All Articles related to the provided tag"
+ }
+ }
+ }
+ },
+ "/articles?title={title}": {
+ "get": {
+ "tags": [
+ "Search for an article"
+ ],
+ "description": "Any User will be able to view a list of all articles which matches with the provided parameter",
+ "parameters": [
+ {
+ "name": "title",
+ "in": "path",
+ "description": "article's title",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "The List of All Articles related to the provided title"
+ }
+ }
+ }
+ },
+ "/articles/{slug}/views": {
+ "get": {
+ "tags": [
+ "stats"
+ ],
+ "description": "User is able to view a specific article reading stats",
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/articles?keywords={keywords}": {
+ "get": {
+ "tags": [
+ "Search for an article"
+ ],
+ "description": "Any User will be able to view a list of all articles which matches with the provided parameter",
+ "parameters": [
+ {
+ "name": "keywords",
+ "in": "path",
+ "description": "article's title",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "The List of All Articles related to the provided keywords"
+ }
+ }
+ }
+ },
+ "/user/optinemail": {
+ "post": {
+ "tags": [
+ "Subscribe to Email Notifications"
+ ],
+ "description": "Enable user to subscribe email notification",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "You successfully subscribed to email notifications"
+ },
+ "400": {
+ "description": "You already subscribed to email notifications"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Unsubscribe to Email Notifications"
+ ],
+ "description": "Enable user to unsubscribe email notification",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "You successfully unsubscribed to email notifications"
+ },
+ "400": {
+ "description": "You are not opted-in"
+ }
+ }
+ }
+ },
+ "/user/optinapp": {
+ "post": {
+ "tags": [
+ "Subscribe to Inapp Notifications"
+ ],
+ "description": "Enable user to subscribe Inapp notification",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "201": {
+ "description": "You successfully subscribed to inapp notifications"
+ },
+ "400": {
+ "description": "You already subscribed to inapp notifications"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "Unsubscribe to inapp Notifications"
+ ],
+ "description": "Enable user to unsubscribe inapp notification",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "You successfully unsubscribed to inapp notifications"
+ },
+ "400": {
+ "description": "You are not opted-in"
+ }
+ }
+ }
+ },
+ "/articles/{slug}/comments/count": {
+ "get": {
+ "tags": [
+ "stats"
+ ],
+ "description": "User is able to view a specific article reading stats",
+ "parameters": [
+ {
+ "name": "slug",
+ "in": "path",
+ "description": "Article slug",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ },
+ "/articles/{articleId}/highlights/{highlightId}/share/twitter": {
+ "get": {
+ "tags": [
+ "Share highlights"
+ ],
+ "description": "Authors Haven users are able to share highlights across several platforms like twitter, facebook and email",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ },
+ {
+ "name": "articleId",
+ "in": "path",
+ "description": "article's slug",
+ "required": true
+ },
+ {
+ "name": "highlightId",
+ "in": "path",
+ "description": "Highlight Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Highlight shared!"
+ }
+ }
+ }
+ },
+ "/articles/{articleId}/highlights/{highlightId}/share/facebook": {
+ "get": {
+ "tags": [
+ "Share highlights"
+ ],
+ "description": "Authors Haven users are able to share highlights across several platforms like twitter, facebook and email",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ },
+ {
+ "name": "articleId",
+ "in": "path",
+ "description": "article's slug",
+ "required": true
+ },
+ {
+ "name": "highlightId",
+ "in": "path",
+ "description": "Highlight Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Highlight shared!"
+ }
+ }
+ }
+ },
+ "/articles/{articleId}/highlights/{highlightId}/share/email": {
+ "get": {
+ "tags": [
+ "Share highlights"
+ ],
+ "description": "Authors Haven users are able to share highlights across several platforms like twitter, facebook and email",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ },
+ {
+ "name": "articleId",
+ "in": "path",
+ "description": "article's slug",
+ "required": true
+ },
+ {
+ "name": "highlightId",
+ "in": "path",
+ "description": "Highlight Id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {
+ "description": "Highlight shared!"
+ }
+ }
+ }
+ },
+ "/auth/termsandconditions/{termsId}": {
+ "get": {
+ "tags": [
+ "Terms and Conditions"
+ ],
+ "description": "User is able to see terms and conditions while signing up",
+ "parameters": [
+ {
+ "name": "termsId",
+ "in": "path",
+ "description": "Terms and Conditions id",
+ "required": true
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ },
+ "patch": {
+ "tags": [
+ "Terms and Conditions"
+ ],
+ "description": "Admin should be able to update terms and conditions",
+ "parameters": [
+ {
+ "name": "token",
+ "in": "header",
+ "description": "The access token",
+ "required": true
+ },
+ {
+ "name": "termsId",
+ "in": "path",
+ "description": "Terms and Conditions id",
+ "required": true
+ },
+ {
+ "in": "body",
+ "name": "termsAndConditions",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/terms"
+ }
+ }
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "responses": {
+ "200": {}
+ }
+ }
+ }
+ }
+}
diff --git a/test/articles.test.js b/test/articles.test.js
new file mode 100644
index 0000000..89ee02f
--- /dev/null
+++ b/test/articles.test.js
@@ -0,0 +1,556 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../src/index';
+import models from '../src/sequelize/models';
+import authHelper from '../src/helpers/Token.helper';
+import signupMock from './mock/signup';
+import articleMock from './mock/articles';
+
+const { User, Article } = models;
+const { generateToken } = authHelper;
+
+// @Mock-Data
+const { validInfo, unverifiedInfo, invalidInfo } = signupMock;
+const { validArticle, invalidArticle, updatedArticle } = articleMock;
+
+chai.use(chaiHttp);
+
+let validToken, slug, unverifiedToken;
+let updatedSlug, invalidToken;
+describe('POST and GET /api/articles', () => {
+ before('Before any test, Create A new user', async () => {
+ const user = await User.create({
+ bio: validInfo.bio,
+ email: validInfo.email,
+ username: validInfo.username,
+ verified: true
+ });
+ const user1 = await User.create({
+ bio: unverifiedInfo.bio,
+ email: unverifiedInfo.email,
+ username: unverifiedInfo.username,
+ verified: false
+ });
+ validToken = await generateToken(user.dataValues);
+ unverifiedToken = await generateToken(user1.dataValues);
+ });
+
+ it('it should return an error if there is no any article', (done) => {
+ chai
+ .request(app)
+ .get('/api/articles')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ // expect(res.body.error).to.deep.equal('Whoops! No Articles found!');
+ done();
+ });
+ });
+
+ it('it should return an error if the user is not verified', (done) => {
+ chai
+ .request(app)
+ .post('/api/articles')
+ .set('token', `${unverifiedToken}`)
+ .send(validArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.be.an('string');
+ expect(res.body.error).to.deep.equal('Please Verify your account, first!');
+ done();
+ });
+ });
+
+ it('it should return an error if the request is not valid', (done) => {
+ chai
+ .request(app)
+ .post('/api/articles')
+ .set('token', `${validToken}`)
+ .send(invalidArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data.message).to.be.an('array');
+ done();
+ });
+ });
+
+ it('it should create a new article', (done) => {
+ chai
+ .request(app)
+ .post('/api/articles')
+ .set('token', `${validToken}`)
+ .send(validArticle)
+ .end((err, res) => {
+ // eslint-disable-next-line prefer-destructuring
+ slug = res.body.article.slug;
+ expect(res.body).to.be.an('object');
+ expect(res.body.article).to.be.an('object');
+ done();
+ });
+ });
+
+ it('it should get all articles', (done) => {
+ chai
+ .request(app)
+ .get('/api/articles')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body).to.have.property('articles');
+ expect(res.body.articles).to.be.an('array');
+ done();
+ });
+ });
+
+ it('it should get one article', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${slug}`)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.article).to.be.an('object');
+ done();
+ });
+ });
+});
+
+describe('PUT and DELETE /api/articles/:slug', () => {
+ before('Before any test, Create A new user', async () => {
+ const user = await User.create({
+ bio: invalidInfo.bio,
+ email: invalidInfo.email,
+ username: invalidInfo.username,
+ verified: true
+ });
+ const user1 = await User.create({
+ bio: unverifiedInfo.bio,
+ email: unverifiedInfo.email,
+ username: unverifiedInfo.username,
+ verified: false
+ });
+ invalidToken = await generateToken(user.dataValues);
+ unverifiedToken = await generateToken(user1.dataValues);
+ });
+
+ it('', (done) => {
+ chai
+ .request(app)
+ .post('/api/articles')
+ .set('token', `${validToken}`)
+ .send(validArticle)
+ .end((err, res) => {
+ updatedSlug = res.body.article.slug;
+ expect(res.body).to.be.an('object');
+ expect(res.body.article).to.be.an('object');
+ done();
+ });
+ });
+
+ it('it should return an error if the user is not verified', (done) => {
+ chai
+ .request(app)
+ .post('/api/articles')
+ .set('token', `${unverifiedToken}`)
+ .send(validArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(403);
+ expect(res.body.error).to.be.an('string');
+ expect(res.body.error).to.deep.equal('Please Verify your account, first!');
+ done();
+ });
+ });
+
+ it('it should return error msg if the logged in user is not the owner of the article', (done) => {
+ chai
+ .request(app)
+ .put(`/api/articles/${slug}`)
+ .set('token', `${invalidToken}`)
+ .send(updatedArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(403);
+ expect(res.body.message).to.deep.equal('Sorry!, You are not the owner of this article');
+ done();
+ });
+ });
+
+ it('it should return an error message for the nonexisting slug', (done) => {
+ chai
+ .request(app)
+ .put('/api/articles/adndfnlkafnamfm')
+ .set('token', `${validToken}`)
+ .send(updatedArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(404);
+ expect(res.body.error).to.deep.equal('Slug Not found!');
+ done();
+ });
+ });
+
+ it('it should return an error if the request for updating an article is not valid', (done) => {
+ chai
+ .request(app)
+ .put(`/api/articles/${slug}`)
+ .set('token', `${validToken}`)
+ .send(invalidArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(400);
+ expect(res.body.data.message).to.be.an('array');
+ done();
+ });
+ });
+
+ it('it should update an existing article', (done) => {
+ chai
+ .request(app)
+ .put(`/api/articles/${slug}`)
+ .set('token', `${validToken}`)
+ .send(updatedArticle)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(200);
+ expect(res.body.message).to.deep.equal('Article updated successfully');
+ expect(res.body.article).to.be.an('object');
+ done();
+ });
+ });
+ it('Should Return 201 when article reported successfully', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${updatedSlug}/report`)
+ .set('token', `${validToken}`)
+ .send({
+ comment: 'some thing'
+ })
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(201);
+ done();
+ });
+ });
+ it('it should delete an existing article', (done) => {
+ chai
+ .request(app)
+ .delete(`/api/articles/${updatedSlug}`)
+ .set('token', `${validToken}`)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.deep.equal(200);
+ expect(res.body.message).to.deep.equal('Article deleted successfully!');
+ done();
+ });
+ });
+});
+
+describe('Like/Unlike Articles', () => {
+ let userToken;
+ let userObject;
+ let articleObject;
+ let testUser;
+ let testArticle;
+
+ before(async () => {
+ // create test user
+ userObject = {
+ firstName: 'Luffyiu',
+ lastName: 'Monkeyf',
+ username: 'thep_irate_king',
+ email: 'monkeyd@luffy.co',
+ password: 'qwerty123445',
+ confirmPassword: 'qwerty123445',
+ };
+
+ testUser = await User.create(userObject);
+
+ // generate test token
+ userToken = await generateToken({ id: testUser.dataValues.id });
+
+ await Article.destroy({
+ where: {},
+ truncate: false
+ });
+
+ articleObject = {
+ title: 'this is article one',
+ body: 'this is article is supposed to have two paragraph',
+ description: 'the paragraph one has many character than before',
+ tagList: ['reactjs', 'angularjs', 'expressjs'],
+ slug: 'lsug32344',
+ authorId: testUser.dataValues.id,
+ readtime: '1 min'
+ };
+
+ // create test article
+ testArticle = await Article.create(articleObject);
+ });
+
+ it('it should like an article for an authenticated user', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug}/like`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+
+ it('it should dislike an article for an authenticated user', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug}/dislike`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+
+ it('it should not like an article which does not exist', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug + 10}/like`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+
+ it('it should not dislike an article which does not exist', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug + 10}/dislike`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+
+ it('it should get the number of likes for an article', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/like`)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('it should get the number of dislikes for an article', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/dislike`)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('it should not get dislikes for an article which does not exist', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug + 10}/dislike`)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+
+ it('it should not get likes for an article which does not exist', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug + 10}/like`)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+
+ // share article test
+ it('should share an article on twitter', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/share/twitter`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ res.should.have.status(200);
+ done();
+ });
+ });
+
+ it('should share an article on facebook', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/share/facebook`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ expect(res.status).to.be.equal(200);
+ done();
+ });
+ });
+
+ it('should share an article on linkedin', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/share/linkedin`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ expect(res.status).to.be.equal(200);
+ done();
+ });
+ });
+
+ it('should share an article on pinterest', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/share/linkedin`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ expect(res.status).to.be.equal(200);
+ done();
+ });
+ });
+
+ it('should share an article on email', (done) => {
+ chai
+ .request(app)
+ .get(`/api/articles/${testArticle.slug}/share/email`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ expect(res.status).to.be.equal(200);
+ done();
+ });
+ });
+});
+
+describe('Block article', () => {
+ let UserToten;
+ let AdminToken;
+ let testUser;
+ let testArticle;
+ before(async () => {
+ // create test user
+ testUser = await User.create({
+ firstName: 'Luffyiu',
+ lastName: 'Monkeyf',
+ username: 'thep_irate_king',
+ email: 'monkeyd@luffy.co',
+ password: 'qwerty123445',
+ confirmPassword: 'qwerty123445',
+ });
+ // create test article
+ testArticle = await Article.create({
+ title: 'this is article one',
+ body: 'this is article is supposed to have two paragraph',
+ description: 'the paragraph one has many character than before',
+ tagList: ['reactjs', 'angularjs', 'expressjs'],
+ slug: 'lsug38769',
+ authorId: testUser.dataValues.id,
+ readtime: '1 min'
+ });
+ });
+
+ describe('POST /api/arctile/:slug/block', () => {
+ it('Should return 200 when user logged in successful', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/login')
+ .send({
+ email: 'gprestein055@gmail.com',
+ password: 'Eric.00005'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ UserToten = res.body.data.token;
+ done();
+ });
+ });
+ it('Should return 200 when admin logged in successful', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/login')
+ .send({
+ email: 'gprestein555@gmail.com',
+ password: 'Eric.00005'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ AdminToken = res.body.data.token;
+ done();
+ });
+ });
+ it('User should not be able to block an article', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug}/block`)
+ .set('token', UserToten)
+ .end((err, res) => {
+ res.should.have.status(403);
+ done();
+ });
+ });
+ it('Admin should be able to block an article', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug}/block`)
+ .set('token', AdminToken)
+ .send({ description: 'some reasons' })
+ .end((err, res) => {
+ res.should.have.status(201);
+ done();
+ });
+ });
+ it('Admin should be able to unblock an article', (done) => {
+ chai
+ .request(app)
+ .post(`/api/articles/${testArticle.slug}/unblock`)
+ .set('token', AdminToken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ done();
+ });
+ });
+
+ it('it should let admin or moderator get blocked articles', (done) => {
+ chai
+ .request(app)
+ .get('/api/articles/blocked/all')
+ .set('token', AdminToken)
+ .end((err, res) => {
+ res.body.should.have.status(200);
+ done();
+ });
+ });
+
+ it('it should let admin or moderator get reported articles', (done) => {
+ chai
+ .request(app)
+ .get('/api/articles/reported/all')
+ .set('token', AdminToken)
+ .end((err, res) => {
+ res.body.should.have.status(200);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/auth.test.js b/test/auth.test.js
new file mode 100644
index 0000000..ceaa719
--- /dev/null
+++ b/test/auth.test.js
@@ -0,0 +1,343 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import db from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+const { User } = db;
+
+const { expect } = chai;
+chai.use(chaiHttp);
+
+dotenv.config();
+let userToken;
+describe('User Registration', () => {
+ before(async () => {
+ await User.destroy({
+ where: {
+ email: 'elie@gmail.com'
+ }
+ });
+ });
+ it('should not let a user signup without valid credentials ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/signup')
+ .send({
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'emy2',
+ email: 'rukundogmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+
+ it('it should let user signup', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/signup')
+ .send({
+ firstName: 'Elie',
+ lastName: 'Mugenzi',
+ username: 'elie',
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!',
+ gender: 'M'
+ })
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.equal(201);
+ expect(res.body.data.token).to.be.a('string');
+ userToken = res.body.data.token;
+ done();
+ });
+ });
+
+ it('should not let a user signup with an already existing email ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/signup')
+ .send({
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mfeillee',
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+
+ it('should not let a user signup with an already existing username ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/signup')
+ .send({
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'elie',
+ email: 'nimiller@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+
+ it('should not let a user signup with gender chich is not M or F', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/signup')
+ .send({
+ firstName: 'Elie',
+ lastName: 'Mugenzi',
+ username: 'elie',
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!',
+ gender: 'G'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ done();
+ });
+ });
+ it('should not verify an account because of invalid token', (done) => {
+ chai
+ .request(server)
+ .get('/api/auth/verify/?token=ffhsfjsf')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.equal(400);
+ done();
+ });
+ });
+
+ it('it should verify account', (done) => {
+ chai.request(server).get(`/api/auth/verify/?token=${userToken}`)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.equal(202);
+ done();
+ });
+ });
+});
+
+describe('User SignOut', () => {
+ let token;
+ before(async () => {
+ const user = {
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mifeillee',
+ email: 'nimilleer@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ };
+
+ const newUser = await User.create(user);
+
+ token = await tokenHelper.generateToken({ id: newUser.id });
+ });
+
+ it('should logout with a valid token', (done) => {
+ chai
+ .request(server)
+ .get('/api/auth/signout')
+ .set('token', token)
+ .end((err, res) => {
+ res.should.have.status(200);
+ expect(res.body.message).to.be.a('string');
+ done();
+ });
+ });
+ it('should return an error when there is no token', (done) => {
+ chai
+ .request(server)
+ .get('/api/auth/signout')
+ .set('token', ' ')
+ .end((err, res) => {
+ res.should.have.status(401);
+ done();
+ });
+ });
+});
+describe('Social Login', () => {
+ before('Before any test, Create A new user', async () => {
+ await User.destroy({
+ where: {
+ email: 'nimilii@yahoo.fr'
+ },
+ truncate: false
+ });
+ await User.destroy({
+ where: {
+ socialId: '56789'
+ },
+ truncate: false
+ });
+ await User.destroy({
+ where: {
+ email: 'nimillr@yahoo.fr'
+ },
+ truncate: false
+ });
+ });
+ it('should let a user log in with google, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/google/test')
+ .send({
+ id: '1234',
+ email: 'nimilii@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let save a user if he is already in the database, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/google/test')
+ .send({
+ id: '1234',
+ email: 'nimilii@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let a user log in with facebook, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/facebook/test')
+ .send({
+ id: '12345',
+ email: 'nimillr@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let save a user if he is already in the database, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/facebook/test')
+ .send({
+ id: '12345',
+ email: 'nimillr@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let a user log in with twitter, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/twitter/test')
+ .send({
+ id: '56789',
+ email: 'nimil@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let save a user if he is already in the database, test! ', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login/twitter/test')
+ .send({
+ id: '56789',
+ email: 'nimil@yahoo.fr'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});
+describe('User login', () => {
+ it('Should return 400 when user entered invalid creditial', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'ru',
+ password: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('Should return 404 when email does not exist', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'hhhhhhhhehhhhh@gmail.com',
+ password: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(404);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('Should return 400 when user entered wrong password', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!esdfjksh'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('Should return 200 when user logged in successful', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});
diff --git a/test/bookmark.test.js b/test/bookmark.test.js
new file mode 100644
index 0000000..f00f045
--- /dev/null
+++ b/test/bookmark.test.js
@@ -0,0 +1,84 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import db from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+
+const { User, Article } = db;
+// eslint-disable-next-line no-unused-vars
+const should = chai.should();
+
+chai.use(chaiHttp);
+const { expect } = chai;
+
+dotenv.config();
+
+let newArticle;
+
+describe('User should bookmark article', () => {
+ let token;
+ before(async () => {
+ const user = {
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mifeillee',
+ email: 'nimilleer@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ };
+ const newUser = await User.create(user);
+
+ token = await tokenHelper.generateToken({ id: newUser.id });
+
+ const article = {
+ title: 'ahmedkhaled4d Kigali Launch',
+ description: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block. ',
+ body: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block. The firm made their debut in Rwanda in July last year, with pan-African hub, the first of its kind. This was followed by the announcement in October of Clement Uwajeneza as the Country Director. The firm has a Memorandum of Understanding with the government to recruit, train and connect to market about 500 young software engineers in the country',
+ tagList: ['Tech', 'Kigali'],
+ authorId: newUser.id,
+ slug: 'slug',
+ readtime: 'Less than a minute'
+ };
+ newArticle = await Article.create(article);
+ });
+ it('should bookmark the article ', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/bookmark`)
+ .set('token', token)
+ .end((err, res) => {
+ res.should.have.status(201);
+ expect(res.body.message).to.be.a('string');
+ done();
+ });
+ });
+ it('should delete the bookmark if the user bookmarks the same bookmarked article again', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/bookmark`)
+ .set('token', token)
+ .end((err, res) => {
+ res.should.have.status(200);
+ expect(res.body.message).to.be.a('string');
+ done();
+ });
+ });
+ it('should raise an error when the user is not logged in', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/bookmark`)
+ .set('token', ' ')
+ .end((err, res) => {
+ res.should.have.status(401);
+ done();
+ });
+ });
+ it('should get all bookmarked article by a specific user', (done) => {
+ chai.request(server)
+ .get('/api/bookmarks')
+ .set('token', token)
+ .end((err, res) => {
+ res.should.have.status(200);
+ done();
+ });
+ });
+});
diff --git a/test/chat.test.js b/test/chat.test.js
new file mode 100644
index 0000000..c6603bf
--- /dev/null
+++ b/test/chat.test.js
@@ -0,0 +1,111 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import chatHelper from '../src/helpers/chat/saveChats';
+
+const { saveMessage, updateReadMessages, getUnreadMessageCount } = chatHelper;
+
+const { expect } = chai;
+chai.use(chaiHttp);
+
+dotenv.config();
+let userToken;
+let userToken2;
+describe('User Registration', () => {
+ describe('Logoin', () => {
+ it('Should return 200 when user logged in successful', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'ericprestein',
+ password: 'Eric.00005'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ const { token } = res.body.data;
+ userToken = token;
+ done();
+ });
+ });
+ it('Should return 200 when user logged in successful', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'ericpresteinjjj',
+ password: 'Eric.00005'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ const { token } = res.body.data;
+ userToken2 = token;
+ done();
+ });
+ });
+ });
+ describe('Get all users', () => {
+ it('Should return 200 when users loaded successful', (done) => {
+ chai
+ .request(server)
+ .get('/api/chats/users')
+ .set('token', userToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('Should return 200 when you have no follower', (done) => {
+ chai
+ .request(server)
+ .get('/api/chats/users')
+ .set('token', userToken2)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+ describe('Get chat ', () => {
+ it('Should return message', (done) => {
+ chai
+ .request(server)
+ .get('/api/chats/ericprestein')
+ .set('token', userToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+ describe('Send message chat ', () => {
+ it('Should create message', (done) => {
+ saveMessage({
+ sender: 'superUser',
+ receiver: 'ericpresteinjjj',
+ message: 'Hello'
+ }).then((result) => {
+ expect(result).to.be.an('object');
+ });
+ done();
+ });
+ it('Should update message', (done) => {
+ updateReadMessages('superUser').then((result) => {
+ expect(result).to.be.a('number');
+ });
+ done();
+ });
+ it('Should update message', (done) => {
+ getUnreadMessageCount('superUser').then((result) => {
+ expect(result).to.be.a('number');
+ });
+ done();
+ });
+ });
+});
diff --git a/test/comments.test.js b/test/comments.test.js
new file mode 100644
index 0000000..e6e65f5
--- /dev/null
+++ b/test/comments.test.js
@@ -0,0 +1,342 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import models from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+
+const { expect } = chai;
+chai.use(chaiHttp);
+dotenv.config();
+let userOneToken,
+ userTwoToken,
+ adminToken,
+ adminToken1,
+ userOneObject,
+ userTwoObject,
+ adminObject,
+ testUserOne,
+ testUserTwo,
+ commentId,
+ commentTwoId,
+ articleOne,
+ articleTwo,
+ testAdmin;
+
+describe('Comments', () => {
+ before(async () => {
+ userOneObject = {
+ firstName: 'Aurore',
+ lastName: 'Kay',
+ username: 'kay',
+ email: 'kay@luffy.co',
+ password: process.env.TEST_USER_PSW,
+ confirmPassword: process.env.TEST_USER_PSW
+ };
+
+ testUserOne = await models.User.create(userOneObject);
+ const userOneId = testUserOne.dataValues.id;
+ // generate test token
+ userOneToken = await tokenHelper.generateToken({ id: userOneId });
+
+ userTwoObject = {
+ firstName: 'Emily',
+ lastName: 'Ben',
+ username: 'Ben',
+ email: 'ben@luffy.co',
+ password: process.env.TEST_USER_PSW,
+ confirmPassword: process.env.TEST_USER_PSW
+ };
+
+ testUserTwo = await models.User.create(userTwoObject);
+ // generate test token
+ userTwoToken = await tokenHelper.generateToken({ id: testUserTwo.dataValues.id });
+
+ adminObject = {
+ firstName: 'admin',
+ lastName: 'admin',
+ username: 'admin',
+ email: 'admin@luffy.co',
+ password: process.env.TEST_USER_PSW,
+ confirmPassword: process.env.TEST_USER_PSW,
+ roles: ['admin'],
+ };
+
+ testAdmin = await models.User.create(adminObject);
+
+ // generate test token
+ adminToken = await tokenHelper.generateToken({ id: testAdmin.dataValues.id });
+ articleOne = {
+ slug: '73H7812',
+ title: 'How to survive at ahmedkhaled4d',
+ description: 'YoYo',
+ readtime: '1min',
+ body:
+ 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
+ image: '',
+ authorId: userOneId,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ await models.Article.create(articleOne);
+
+ articleTwo = {
+ slug: '73H99992',
+ title: 'Wow',
+ description: 'YoYo',
+ readtime: 'Less than a minute',
+ body:
+ 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.',
+ image: '',
+ authorId: userOneId,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+ await models.Article.create(articleTwo);
+ });
+ it('should let a user comment an article', (done) => {
+ chai
+ .request(server)
+ .post('/api/articles/73H7812/comments')
+ .set('token', userOneToken)
+ .send({
+ comment: 'I love this article!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(201);
+ expect(res.body).to.be.an('object');
+ commentId = res.body.data.Id;
+ done();
+ });
+ });
+ it('should let a user comment an article', (done) => {
+ chai
+ .request(server)
+ .post('/api/articles/73H7812/comments')
+ .set('token', userTwoToken)
+ .send({
+ comment: 'Amazing!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(201);
+ expect(res.body).to.be.an('object');
+ commentTwoId = res.body.data.Id;
+ done();
+ });
+ });
+ it('should notify the use if the comment to track does not exist', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/comments/${commentId}/history`)
+ .set('token', userOneToken)
+ .end((err, res) => {
+ // expect(res.status).to.equal(404);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let a user edit a comment', (done) => {
+ chai
+ .request(server)
+ .patch(`/api/articles/comments/${commentId}`)
+ .set('token', userOneToken)
+ .send({
+ comment: 'Wooow! I love this article!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let a user track edit history', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/comments/${commentId}/history`)
+ .set('token', userOneToken)
+ .end((err, res) => {
+ // expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let only the owner of the comment track it', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/comments/${commentId}/history`)
+ .set('token', userTwoToken)
+ .end((err, res) => {
+ // expect(res.status).to.equal(403);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let only the owner of the comment edit it!', (done) => {
+ chai
+ .request(server)
+ .patch(`/api/articles/comments/${commentId}`)
+ .set('token', userTwoToken)
+ .send({
+ comment: 'Wooow! I love this article!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(403);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the user like a comment!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/comments/${commentId}/like`)
+ .set('token', userTwoToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(201);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should not let a user like a comment twice!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/comments/${commentId}/like`)
+ .set('token', userTwoToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the user dislike a comment!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/comments/${commentId}/dislike`)
+ .set('token', userTwoToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the user dislike a comment!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/comments/${commentId}/dislike`)
+ .set('token', userOneToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(201);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should not let a user dislike a comment twice!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/comments/${commentId}/dislike`)
+ .set('token', userTwoToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should get all likes on a comment!', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/comments/${commentId}/dislikes`)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should get all dislikes on a comment!', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/comments/${commentId}/likes`)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the owner of the comment delete it!', (done) => {
+ chai
+ .request(server)
+ .delete(`/api/articles/comments/${commentId}`)
+ .set('token', userOneToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should only allow the admin and the owner of the comment to delete it!', (done) => {
+ chai
+ .request(server)
+ .delete(`/api/articles/comments/${commentTwoId}`)
+ .set('token', userOneToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(403);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should give an error if the article does not exist!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/73H99992/comments/${commentTwoId}`)
+ .set('token', adminToken)
+ .send({
+ comment: 'You are right!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the user comment on a comment!', (done) => {
+ chai
+ .request(server)
+ .post(`/api/articles/73H7812/comments/${commentTwoId}`)
+ .set('token', adminToken)
+ .send({
+ comment: 'You are right!'
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(201);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let the admin delete any comment!', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({ email: 'superuser@gmail.com', password: process.env.SUPER_ADMIN_PSW })
+ .end(async (err, res) => {
+ adminToken1 = res.body.data.token;
+ chai
+ .request(server)
+ .delete(`/api/articles/comments/${commentTwoId}`)
+ .set('token', adminToken1)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+ it('should let the user get an article with its comments!', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/comments')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});
diff --git a/test/emailTemplate.test.js b/test/emailTemplate.test.js
new file mode 100644
index 0000000..0c7c7d8
--- /dev/null
+++ b/test/emailTemplate.test.js
@@ -0,0 +1,8 @@
+import { expect } from 'chai';
+import template from '../src/helpers/mailer/templates/notification';
+
+describe('Email notification template tests..,', () => {
+ it('Should return an object of email data', () => {
+ expect(template({ message: 'Hello world' })).to.be.an('object');
+ });
+});
diff --git a/test/eventEmitter.test.js b/test/eventEmitter.test.js
new file mode 100644
index 0000000..c449741
--- /dev/null
+++ b/test/eventEmitter.test.js
@@ -0,0 +1,10 @@
+/* eslint-disable no-unused-expressions */
+import { expect } from 'chai';
+import eventEmitter from '../src/helpers/notifications/EventEmitter';
+
+describe('Event Listener tests', () => {
+ it('Should listen to errors', () => {
+ const event = eventEmitter.emit('error', new Error('Random error!'));
+ expect(event).to.be.true;
+ });
+});
diff --git a/test/favourite.test.js b/test/favourite.test.js
new file mode 100644
index 0000000..a044882
--- /dev/null
+++ b/test/favourite.test.js
@@ -0,0 +1,18 @@
+import { expect } from 'chai';
+import favourite from '../src/helpers/Favourites';
+import users from './mock/users';
+import db from '../src/sequelize/models';
+
+const { User } = db;
+let currentUser;
+
+describe('Tests for finding who favourited an article', () => {
+ before(async () => {
+ currentUser = await User.create(users.user1);
+ });
+
+ it('Should return an object', async () => {
+ const response = await favourite(currentUser.id);
+ expect(response).to.be.an('object');
+ });
+});
diff --git a/test/findUser.test.js b/test/findUser.test.js
new file mode 100644
index 0000000..3da6cdc
--- /dev/null
+++ b/test/findUser.test.js
@@ -0,0 +1,17 @@
+/* eslint-disable no-unused-expressions */
+import { expect } from 'chai';
+import findUser from '../src/helpers/FindUser';
+import users from './mock/users';
+import db from '../src/sequelize/models';
+
+const { User } = db;
+let currentUser;
+describe('Test for finding user by username', () => {
+ before(async () => {
+ currentUser = await User.create(users.user2);
+ });
+ it('Should return a user object', async () => {
+ const user = await findUser(currentUser.username);
+ expect(user).to.be.an('object');
+ });
+});
diff --git a/test/hash.test.js b/test/hash.test.js
new file mode 100644
index 0000000..e98a7a6
--- /dev/null
+++ b/test/hash.test.js
@@ -0,0 +1,22 @@
+import chai from 'chai';
+import hashHelper from '../src/helpers/hashHelper';
+
+
+const { expect } = chai;
+
+describe('Password hash based tests', () => {
+ let hashed;
+ const password = 'whatever123';
+ it('should hash password', () => {
+ hashed = hashHelper.hashPassword(password);
+ expect(hashed).to.be.a('string');
+ });
+ it('should compare password', () => {
+ const isDone = hashHelper.comparePassword(password, hashed);
+ expect(isDone).to.equal(true);
+ });
+ it('should compare the password but returns false', () => {
+ const isDone = hashHelper.comparePassword('afdfjdhasd', hashed);
+ expect(isDone).to.equal(false);
+ });
+});
diff --git a/test/highlight.test.js b/test/highlight.test.js
new file mode 100644
index 0000000..64cade5
--- /dev/null
+++ b/test/highlight.test.js
@@ -0,0 +1,143 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import db from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+
+const { User, Article } = db;
+// eslint-disable-next-line no-unused-vars
+const should = chai.should();
+
+chai.use(chaiHttp);
+const { expect } = chai;
+
+dotenv.config();
+
+let newArticle;
+const highlight = {
+ highlightText: 'Rwanda',
+ comment: 'Thank you',
+ occurencyNumber: 1
+};
+const invalidHighlight = {
+ highlightText: 'Blablablabla',
+ comment: 'Thank you',
+ occurencyNumber: 1
+};
+
+describe('Highlight the Article', () => {
+ let token, highlightId;
+ before(async () => {
+ const user = {
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mifeillee',
+ email: 'nimilleer@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ };
+ const newUser = await User.create(user);
+
+ token = await tokenHelper.generateToken({ id: newUser.id });
+
+ const article = {
+ title: 'ahmedkhaled4d Kigali Launch',
+ description: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block.',
+ body: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block. The firm made their debut in Rwanda in July last year, with pan-African hub, the first of its kind. This was followed by the announcement in October of Clement Uwajeneza as the Country Director. The firm has a Memorandum of Understanding with the government to recruit, train and connect to market about 500 young software engineers in the country',
+ tagList: ['Tech', 'Kigali'],
+ authorId: newUser.id,
+ slug: 'slyg',
+ readtime: '1 min'
+ };
+ newArticle = await Article.create(article);
+ });
+ it('should highlight with comment or without', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/highlight`)
+ .set('token', token)
+ .send(highlight)
+ .end((err, res) => {
+ res.should.have.status(201);
+ expect(res.body.Message).to.be.a('string');
+ highlightId = res.body.data.id;
+ done();
+ });
+ });
+ it('should raise an error when the user highlights the same text ', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/highlight`)
+ .set('token', token)
+ .send(highlight)
+ .end((err, res) => {
+ res.should.have.status(403);
+ expect(res.body.Message).to.be.a('string');
+ done();
+ });
+ });
+ it('should raise an error when the highlighter text exist', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/highlight`)
+ .set('token', token)
+ .send(invalidHighlight)
+ .end((err, res) => {
+ res.should.have.status(404);
+ expect(res.body.Message).to.be.a('string');
+ done();
+ });
+ });
+ it('should raise an error when the user is not logged in', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/highlight`)
+ .set('token', ' ')
+ .end((err, res) => {
+ res.should.have.status(401);
+ done();
+ });
+ });
+ it('should be able to share hightlights', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/${newArticle.dataValues.slug}/highlights/${highlightId}/share/twitter`)
+ .set('token', token)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to share hightlights', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/${newArticle.slug}/highlights/${highlightId}/share/facebook`)
+ .set('token', token)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to share hightlights', (done) => {
+ chai
+ .request(server)
+ .get(`/api/articles/${newArticle.dataValues.slug}/highlights/${highlightId}/share/email`)
+ .set('token', token)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should check if the highlight belongs to the article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/highlights/1/share/email')
+ .set('token', token)
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});
diff --git a/test/index.test.js b/test/index.test.js
new file mode 100644
index 0000000..2c1fa2e
--- /dev/null
+++ b/test/index.test.js
@@ -0,0 +1,26 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../src/index';
+
+chai.use(chaiHttp);
+describe('Unavailable URL', () => {
+ it('Should return 200 when youbtry to access root url', (done) => {
+ chai
+ .request(app)
+ .get('/')
+ .end((err, res) => {
+ expect(res.status).to.eql(200);
+ done();
+ });
+ });
+ it('Should return an error message, Once you tried to access the unavailable URL', (done) => {
+ chai
+ .request(app)
+ .get('/fadfasfasfafafdafafda')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error.message).to.deep.equal('Page Not found');
+ done();
+ });
+ });
+});
diff --git a/test/mock/articles.js b/test/mock/articles.js
new file mode 100644
index 0000000..ca2ea96
--- /dev/null
+++ b/test/mock/articles.js
@@ -0,0 +1,25 @@
+export default {
+ validArticle: {
+ title: 'this is article one',
+ body: 'this is article is supposed to have two paragraph',
+ description: 'the paragraph one has many character than before',
+ tagList: 'reactjs , angularjs, expressjs'
+ },
+ updatedArticle: {
+ title: 'this is article two',
+ body: 'this is article is supposed to have at least three paragraph'
+ },
+ invalidArticle: {
+ title: 123412421,
+ description: 'this is the article one that has been created by uhiriwe audace',
+ body: 'this s article is the one article i have ever seen amazing',
+ tagList: 'angularjs, nodejs, mongodb'
+ },
+ toPublish: {
+ title: 'This is ahmedkhaled4d',
+ slug: 'this-is-ahmedkhaled4d-jfsf',
+ description: 'Hello @people, This is ahmedkhaled4d',
+ body: 'bk ksjkjfvvsohgsonhsfogkfghfgkfnokgfkhsgknfgkfsghfsigfgifgfkfgsfgfgkfgfgkfgukhfgkhgkg',
+ tagList: 'this,is,ahmedkhaled4d'
+ }
+};
diff --git a/test/mock/sample.png b/test/mock/sample.png
new file mode 100644
index 0000000..049edea
Binary files /dev/null and b/test/mock/sample.png differ
diff --git a/test/mock/signup.js b/test/mock/signup.js
new file mode 100644
index 0000000..d1a2e87
--- /dev/null
+++ b/test/mock/signup.js
@@ -0,0 +1,34 @@
+export default {
+ validInfo: {
+ email: 'u.audace@test.com',
+ bio: "i'm software engineer",
+ username: 'Uhiriwe',
+ firstName: 'Uhiriwe',
+ lastName: 'Audace',
+ image: 'null',
+ gender: 'F',
+ password: 'Uhiriwe1',
+ confirmPassword: 'Uhiriwe1',
+ },
+ invalidInfo: {
+ email: 'test@test.com',
+ bio: "I'm a junior software developer",
+ username: 'Junior',
+ firstName: 'Junior',
+ lastName: 'Kramalynx',
+ gender: 'M',
+ confirmPassword: 'Uhiriwe1',
+ verified: true
+ },
+ unverifiedInfo: {
+ email: 'test1@test.com',
+ bio: "I'm a junior software developer",
+ username: 'Juniors',
+ firstName: 'Juniors',
+ lastName: 'Kramalynx',
+ image: 'null',
+ gender: 'F',
+ confirmPassword: 'Uhiriwe1',
+ verified: false
+ }
+};
diff --git a/test/mock/users.js b/test/mock/users.js
new file mode 100644
index 0000000..5289694
--- /dev/null
+++ b/test/mock/users.js
@@ -0,0 +1,16 @@
+export default {
+ user1: {
+ firstName: 'Elie',
+ lastName: 'Mugenzi',
+ email: 'elie@tesla.ah',
+ username: 'elie38',
+ password: 'My@ahmedkhaled4d5',
+ },
+ user2: {
+ firstName: 'Shia',
+ lastName: 'Roberts',
+ email: 'shia@gmail.com',
+ username: 'shiaroberts',
+ password: 'Shi@R0berts',
+ }
+};
diff --git a/test/opt.test.js b/test/opt.test.js
new file mode 100644
index 0000000..e0b86ce
--- /dev/null
+++ b/test/opt.test.js
@@ -0,0 +1,83 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../src';
+import users from './mock/users';
+import db from '../src/sequelize/models';
+import Tokenizer from '../src/helpers/Token.helper';
+
+const { User } = db;
+
+let token;
+chai.use(chaiHttp);
+chai.should();
+
+describe('Subscribe and unsubscribe tests', () => {
+ before(async () => {
+ const newUser = await User.create(users.user2);
+ token = await Tokenizer.generateToken({ id: newUser.id });
+ });
+
+ it('Should opt-in a user for email notifications', (done) => {
+ chai.request(app).post('/api/user/optinemail').set('token', token)
+ .end((err, res) => {
+ res.should.have.status(201);
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+ it('Should not opt-in a user for email notifications again', (done) => {
+ chai.request(app).post('/api/user/optinemail').set('token', token)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+
+ it('Should opt-in a user for in-app notifications', (done) => {
+ chai.request(app).post('/api/user/optinapp').set('token', token)
+ .end((err, res) => {
+ res.should.have.status(201);
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+ it('Should not opt-in a user for in-app notifications again', (done) => {
+ chai.request(app).post('/api/user/optinapp').set('token', token)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+
+ it('Should opt-out a user for email notifications', (done) => {
+ chai.request(app).delete('/api/user/optinemail').set('token', token)
+ .end((err, res) => {
+ res.should.have.status(200);
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+ it('Should not opt-out a user for email notifications if they hadn\'t opted-in', (done) => {
+ chai.request(app).delete('/api/user/optinemail').set('token', token)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+
+ it('Should opt-out a user for in-app notifications', (done) => {
+ chai.request(app).delete('/api/user/optinapp').set('token', token)
+ .end((err, res) => {
+ res.should.have.status(200);
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+ it('Should not opt-out a user for in-app notifications if they had not opted-in', (done) => {
+ chai.request(app).delete('/api/user/optinapp').set('token', token)
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ });
+ done();
+ });
+});
diff --git a/test/paginate.test.js b/test/paginate.test.js
new file mode 100644
index 0000000..309f178
--- /dev/null
+++ b/test/paginate.test.js
@@ -0,0 +1,34 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+
+import app from '../src/index';
+
+chai.use(chaiHttp);
+describe('GET /api/articles/page=&limit=', () => {
+ it('It should return an error message if you provided the page number and limit which is less than or equal to zero ', () => {
+ chai
+ .request(app)
+ .get('/api/articles?page=0&limit=0')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.statusCode).to.deep.equal(400);
+ expect(res.body.error).to.deep.equal('Invalid request');
+ });
+ });
+
+ it('It should make a search and fetch the articles based on the provided pageNumber and limit number of the pages', () => {
+ chai
+ .request(app)
+ .get('/api/articles?page=1&limit=10')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data).to.be.an('array');
+ });
+ });
+ it('Should return an error message', () => {
+ chai.request(app).get('/api/articles?page=-1&limit=2').end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.status).to.equal(400);
+ });
+ });
+});
diff --git a/test/passwordreset.test.js b/test/passwordreset.test.js
new file mode 100644
index 0000000..fb2a0af
--- /dev/null
+++ b/test/passwordreset.test.js
@@ -0,0 +1,85 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import app from '../src';
+
+chai.use(chaiHttp);
+const { expect } = chai;
+let token;
+describe('Password reset', () => {
+ describe('Request password reset POST /api/auth/reset ', () => {
+ it('it should return 400 when email is not varidated', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/reset')
+ .send({ email: 'gprestein055' })
+ .end((err, res) => {
+ expect(res.body.status).to.eql(400);
+ done();
+ });
+ });
+ it('it should return 404 when email is not exist', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/reset')
+ .send({ email: 'ericprestein055@gmail.com' })
+ .end((err, res) => {
+ expect(res.body.status).to.eql(404);
+ done();
+ });
+ });
+ it('it should return 201 when password reset lint, sent successful', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/reset')
+ .send({ email: 'nimilleer@gmail.com' })
+ .end(async (err, res) => {
+ expect(res.body.status).to.eql(201);
+ ({ token } = res.body.data);
+ done();
+ });
+ });
+ });
+ describe('Confirm password reset GET /api/auth/reset:token', () => {
+ it('it should return 500 or 304 when invalid token or expired token passed', (done) => {
+ chai
+ .request(app)
+ .get(`/api/auth/reset/${token}ghjfufuf`)
+ .end(async (err, res) => {
+ expect(res.body.status).to.eql(500 || 304);
+ done();
+ });
+ });
+ it('it should return 200 when password reset confirmed', (done) => {
+ chai
+ .request(app)
+ .get(`/api/auth/reset/${token}`)
+ .end(async (err, res) => {
+ expect(res.body.status).to.eql(200);
+ ({ token } = res.body.data);
+ done();
+ });
+ });
+ });
+ describe('Apply password reset PATCH /api/auth/reset:aprvToken', () => {
+ it('it should return 500 or 400 when email is invalid', (done) => {
+ chai
+ .request(app)
+ .patch(`/api/auth/reset/${token}`)
+ .send({ newpassword: 'Eric' })
+ .end(async (err, res) => {
+ expect(res.body.status).to.eql(400);
+ done();
+ });
+ });
+ it('it should return 201 when reset is successful', (done) => {
+ chai
+ .request(app)
+ .patch(`/api/auth/reset/${token}`)
+ .send({ newpassword: 'Eric0000005555' })
+ .end(async (err, res) => {
+ expect(res.body.status).to.eql(201);
+ done();
+ });
+ });
+ });
+});
diff --git a/test/profiles.test.js b/test/profiles.test.js
new file mode 100644
index 0000000..f915d19
--- /dev/null
+++ b/test/profiles.test.js
@@ -0,0 +1,436 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+import fs from 'fs';
+import dotenv from 'dotenv';
+import app from '../src';
+import db from '../src/sequelize/models';
+import authHelper from '../src/helpers/Token.helper';
+
+dotenv.config();
+const { User } = db;
+
+// eslint-disable-next-line no-unused-vars
+const should = chai.should();
+
+chai.use(chaiHttp);
+
+let userToken;
+let userToken2;
+let userObject;
+let userObject2;
+let testUser;
+let testUser2;
+let followObject;
+let testFollow;
+let fobject;
+let ftest;
+let ftoken;
+let bobject;
+let btest;
+let btoken;
+let dobject;
+let dtest;
+let dtoken;
+describe('User Profiles', () => {
+ before(async () => {
+ // eslint-disable-next-line no-unused-expressions
+ // create test user
+ userObject = {
+ firstName: 'Luffy',
+ lastName: 'Monkey',
+ username: 'pirate_king',
+ email: 'monkey@luffy.co',
+ password: 'qwerty123445',
+ confirmPassword: 'qwerty123445'
+ // eslint-disable-next-line no-sequences
+ };
+
+ // create test user 2
+ userObject2 = {
+ firstName: 'Luffy2',
+ lastName: 'Monkey2',
+ username: 'pirate_king2',
+ email: 'monkey2@luffy.co',
+ password: 'qwerty1234452',
+ confirmPassword: 'qwerty1234452'
+ };
+
+ fobject = {
+ firstName: 'espoire',
+ lastName: 'mugenzie',
+ username: 'espoire',
+ email: 'espoiremugenzie@gmail.com',
+ password: 'ericprestein',
+ confirmPassword: 'ericprestein'
+ };
+ bobject = {
+ firstName: 'diane',
+ lastName: 'mahoro',
+ username: 'test_user',
+ email: 'mahorodiane@gmail.com',
+ password: 'cooler12345',
+ confirmPassword: 'cooler12345'
+ };
+ dobject = {
+ firstName: 'diego',
+ lastName: 'hirwa',
+ username: 'diego',
+ email: 'diegohirwa@gmail.com',
+ password: 'coolest12345',
+ confirmPassword: 'coolest12345'
+ };
+
+ testUser = await User.create(userObject);
+ testUser2 = await User.create(userObject2);
+ // generate test token
+ userToken = await authHelper.generateToken({ id: testUser.id });
+ userToken2 = await authHelper.generateToken({ id: testUser2.id });
+
+ ftest = await User.create(fobject);
+ ftoken = await authHelper.generateToken({ id: ftest.id });
+
+ btest = await User.create(bobject);
+ btoken = await authHelper.generateToken({ id: btest.id });
+
+ dtest = await User.create(dobject);
+ dtoken = await authHelper.generateToken({ id: dtest.id });
+
+ ftest = await User.create(fobject);
+ ftoken = await authHelper.generateToken({ id: ftest.id });
+
+ await chai
+ .request(app)
+ .patch('/api/profiles/test_user/follow')
+ .set('token', ftoken)
+ .send();
+ });
+
+
+ after(async () => {
+ followObject = {
+ firstName: 'espoire',
+ lastName: 'mugenzie',
+ username: 'espoire',
+ email: 'espoiremugenzie@gmail.com',
+ password: 'ericprestein',
+ confirmPassword: 'ericprestein'
+ };
+
+ testFollow = await User.create(followObject);
+ // followToken = await authHelper.generateToken({ id: testUser.id });
+ await db.follows.destroy({
+ where: { userId: testFollow.id },
+ truncate: false
+ });
+ await db.User.destroy({
+ where: { id: testFollow.id },
+ truncate: false
+ });
+ });
+
+ describe('Get a user profile', () => {
+ it('it should get a user profile', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/pirate_king')
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('it should get a non existant user profile', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/kotowaru')
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+ });
+ describe('Update user profile', () => {
+ it('it should update user profile', (done) => {
+ const data = {
+ username: 'northern_lights',
+ bio: 'Stargazing and Food',
+ image: 'image'
+ };
+
+ chai
+ .request(app)
+ .put(`/api/user/${testUser.id}`)
+ .set('token', userToken)
+ .send(data)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+ it('it should not update user profile', (done) => {
+ const data = {
+ username: 'northern_lights',
+ bio: 'Stargazing and Food',
+ image: 'image'
+ };
+
+ chai
+ .request(app)
+ .put(`/api/user/${testUser.id}`)
+ .set('token', userToken2)
+ .send(data)
+ .end((err, res) => {
+ res.should.have.status(403);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+ it('it should update user profile with images', (done) => {
+ chai
+ .request(app)
+ .put(`/api/user/${testUser.id}`)
+ .set('token', userToken)
+ .attach('avatar', fs.readFileSync(`${__dirname}/mock/sample.png`), 'sample.png')
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+
+ it('it should return an error message, Once you don\'t update anything to your profile', (done) => {
+ chai
+ .request(app)
+ .put(`/api/user/${testUser.id}`)
+ .set('token', userToken)
+ .send()
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.message).to.deep.equal('Cannot update empty object');
+ expect(res.statusCode).to.deep.equal(400);
+ done();
+ });
+ });
+ });
+
+ it('it should get a user profile', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/pirate_king')
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('it should get a non existant user profile', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/kotowaru')
+ .end((err, res) => {
+ res.should.have.status(404);
+ done();
+ });
+ });
+
+ it('should give you do not follow anyone', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/following')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('follow users', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/follow')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('should unfollow user', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/espoire/unfollow')
+ .set('token', btoken)
+ .end((err, res) => {
+ res.should.have.status(404);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('should follow users', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/follow')
+ .set('token', ftoken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+ it('should fail to follow a user who is already follow', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/follow')
+ .set('token', ftoken)
+ .end((err, res) => {
+ res.status.should.be.equal(400);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('should unfollow user', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/unfollow')
+ .set('token', ftoken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('should give all your followers', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/followers')
+ .set('token', btoken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ done();
+ });
+ });
+
+ it('should fail to follow a user who is already follow', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/follow')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.status.should.be.equal(400);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it(' should not follow yourself', (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/espoire/follow')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(400);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it("should not get user's followers", (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/followers')
+ .set('token', dtoken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it('should give who you follows', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles/following')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(200);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+ it("should not unfollow user who doesn' exist", (done) => {
+ chai
+ .request(app)
+ .patch('/api/profiles/mayoo/unfollow')
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(404);
+ res.body.should.be.an('object');
+ done();
+ });
+ });
+
+
+ it('should get all profile', () => {
+ chai
+ .request(app)
+ .get('/api/profiles')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.profiles).to.be.an('array');
+ });
+
+ it('it should get all profile', (done) => {
+ chai
+ .request(app)
+ .get('/api/profiles')
+ .end((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.profiles).to.be.an('array');
+ done();
+ });
+ });
+ });
+
+ describe('Delete user profile', () => {
+ let adminToken;
+
+ it('it should not delete profile if user is not admin', (done) => {
+ chai
+ .request(app)
+ .delete(`/api/user/${testUser.id}`)
+ .set('token', userToken)
+ .end((err, res) => {
+ res.should.have.status(403);
+ res.body.should.be.a('object');
+ done();
+ });
+ });
+ it('it should delete profile if user is admin', (done) => {
+ chai
+ .request(app)
+ .post('/api/auth/login')
+ .send({ email: 'superuser@gmail.com', password: process.env.SUPER_ADMIN_PSW })
+ .end(async (err, res) => {
+ adminToken = res.body.data.token;
+ chai
+ .request(app)
+ .delete(`/api/user/${testUser2.id}`)
+ .set('token', adminToken)
+ .end((err, testRes) => {
+ testRes.should.have.status(200);
+ testRes.body.should.be.a('object');
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/ratings.test.js b/test/ratings.test.js
new file mode 100644
index 0000000..465c563
--- /dev/null
+++ b/test/ratings.test.js
@@ -0,0 +1,84 @@
+import chai from 'chai';
+import chaiHttp from 'chai-http';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import db from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+
+const { User, Article } = db;
+// eslint-disable-next-line no-unused-vars
+const should = chai.should();
+
+chai.use(chaiHttp);
+const { expect } = chai;
+
+dotenv.config();
+const rating = {
+ rating: 5
+};
+let newArticle;
+
+describe('Rating Article', () => {
+ let token;
+ before(async () => {
+ const user = {
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mifeillee',
+ email: 'nimilleer@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ };
+ const newUser = await User.create(user);
+
+ token = await tokenHelper.generateToken({ id: newUser.id });
+
+ const article = {
+ title: 'ahmedkhaled4d Kigali Launch',
+ description: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block.',
+ body: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block. The firm made their debut in Rwanda in July last year, with pan-African hub, the first of its kind. This was followed by the announcement in October of Clement Uwajeneza as the Country Director. The firm has a Memorandum of Understanding with the government to recruit, train and connect to market about 500 young software engineers in the country',
+ tagList: ['Tech', 'Kigali'],
+ authorId: newUser.id,
+ slug: 'slyg',
+ readtime: '1 min'
+ };
+ newArticle = await Article.create(article);
+ });
+
+
+ it('should create rating for the article ', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/rating`)
+ .set('token', token)
+ .send(rating)
+ .end((err, res) => {
+ res.should.have.status(201);
+ expect(res.body.message).to.be.a('string');
+
+ done();
+ });
+ });
+ it('should Update the ratings if the user has rated the article before ', (done) => {
+ chai.request(server)
+ .put(`/api/articles/${newArticle.slug}/rating`)
+ .set('token', token)
+ .send({ rating: 4 })
+ .end((err, res) => {
+ res.should.have.status(200);
+ expect(res.body.message).to.be.a('string');
+
+ done();
+ });
+ });
+ it('should raise an error when the user is not logged in', (done) => {
+ chai.request(server)
+ .post(`/api/articles/${newArticle.slug}/rating`)
+ .set('token', ' ')
+ .send(rating)
+ .end((err, res) => {
+ res.should.have.status(401);
+ done();
+ });
+ });
+});
diff --git a/test/ratingscarc.test.js b/test/ratingscarc.test.js
new file mode 100644
index 0000000..cd63dd7
--- /dev/null
+++ b/test/ratingscarc.test.js
@@ -0,0 +1,80 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import db from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+const { User, Article } = db;
+
+const { expect } = chai;
+chai.use(chaiHttp);
+
+let token;
+
+dotenv.config();
+describe('Articles ratings', () => {
+ let newArticle;
+ let Atoken;
+ before(async () => {
+ const user = {
+ firstName: 'Emy',
+ lastName: 'Rukundo',
+ username: 'mifeillee',
+ email: 'nimilleer@gmail.com',
+ password: 'Rukundo1!',
+ confirmPassword: 'Rukundo1!'
+ };
+ const newUser = await User.create(user);
+
+ Atoken = await tokenHelper.generateToken({ id: newUser.id });
+
+ const article = {
+ title: 'ahmedkhaled4d Kigali Launch',
+ description: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block.',
+ body: 'ahmedkhaled4d Rwanda, a technology company specializing in training software engineers on Thursday last week launched its Kigali office. Its new offices are housed within the University of Rwanda - College of Science and Technology, in Muhabura Block. The firm made their debut in Rwanda in July last year, with pan-African hub, the first of its kind. This was followed by the announcement in October of Clement Uwajeneza as the Country Director. The firm has a Memorandum of Understanding with the government to recruit, train and connect to market about 500 young software engineers in the country',
+ tagList: ['Tech', 'Kigali'],
+ authorId: newUser.id,
+ slug: 'slyg',
+ readtime: '1 min'
+ };
+ newArticle = await Article.create(article);
+ });
+ describe('Ratings Report', () => {
+ it('Should return 200 when user logged in successful', (done) => {
+ chai.request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'elie@gmail.com',
+ password: 'Rukundo1!',
+ })
+ .end((err, res) => {
+ ({ token } = res.body.data);
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+
+ it('should return 200 when article ratings found and calcurated successfuly', (done) => {
+ chai.request(server)
+ .post(`/api/ratings/articles/${newArticle.slug}`)
+ .set('token', `${Atoken}`)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should return 404 when article ratings not found', (done) => {
+ chai.request(server)
+ .post('/api/ratings/articles/gf2433546h34')
+ .set('token', `${token}`)
+ .end((err, res) => {
+ expect(res.status).to.equal(404);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ });
+});
diff --git a/test/readtime.test.js b/test/readtime.test.js
new file mode 100644
index 0000000..0aa922a
--- /dev/null
+++ b/test/readtime.test.js
@@ -0,0 +1,31 @@
+import { expect } from 'chai';
+import readTime from '../src/helpers/ReadTime.helper';
+
+
+describe('Read time tests', () => {
+ it('should return a beautiful read time', () => {
+ const body = `Since joining ahmedkhaled4d’s Bootcamp,
+ that was the first time to know how to test my codes,
+ because there are some situations where the software( like a website)
+ is being broken in production(means when the users are using that product and face the technical bugs).
+ The way this was a challenge to me is that I had to learn it fast and implement them immediately.
+ I found how important it is, the way you target every block of code as input and
+ expect each possible output in order to catch some errors and correct them before the product
+ is going to get deployed. The main thing I learned from this challenge is that
+ I have to make sure my codes are bug-free before getting deployed and make sure my tests are covering every
+ block of codes. How I adapted to this challenge is, I spent a lot of sleepless nights figuring out how
+ to write my tests, I didn’t know anything about Travis CI and I got several emails that my builds were failing.
+ The main key is working hard, ask around and do more research to get your work done.\nGit workflow was another challenge I faced.
+ I tried it before, but I never tried the feature-branch workflow.
+ I found that workflow was awesome because it helps you manage the project tasks and work them into branches.
+ The reason it was a challenge is that we had to work in several branches and merge our work into the main branch and sometimes you face some merge conflicts.
+ I didn’t know how to resolve conflicts, but I tried to make some research about it, ask my colleagues how to resolve them and luckily
+ I got my work really organized on Github.`;
+ expect(readTime(body)).to.equal('2 min');
+ });
+
+ it('should get a less than a minute read time', () => {
+ const body = 'This is an amazing project we are working on, Authors Haven';
+ expect(readTime(body)).to.equal('Less than a minute');
+ });
+});
diff --git a/test/search.test.js b/test/search.test.js
new file mode 100644
index 0000000..b192d75
--- /dev/null
+++ b/test/search.test.js
@@ -0,0 +1,145 @@
+import chai, { expect } from 'chai';
+import chaiHttp from 'chai-http';
+
+import app from '../src/index';
+
+chai.use(chaiHttp);
+
+describe('GET /api/articles/author=', () => {
+ it('It should return an error message if you provided the key which is not author,title,tag or keywords', () => {
+ chai
+ .request(app)
+ .get('/api/articles?fdsfasf=noffffffffffffff')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('You made a Bad Request!');
+ });
+ });
+
+ it('It should return an error message if you provided author,title,tag or keywords value which does not have at least three character', () => {
+ chai
+ .request(app)
+ .get('/api/articles?author=nf')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal("You should have provided at least 3 characters long for author's name");
+ });
+ });
+
+ it('It should return an error message if it doesn\'t find the provided title', () => {
+ chai
+ .request(app)
+ .get('/api/articles?author=noffffffffffffff')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('This Author with username of noffffffffffffff not exists!');
+ });
+ });
+
+ it('It should make a search and fetch all articles written by a certain author', () => {
+ chai
+ .request(app)
+ .get('/api/articles?author=Uhiriwe')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data).to.be.an('array');
+ expect(res.body.message).to.be.a('string');
+ });
+ });
+});
+
+describe('GET /api/articles/title=', () => {
+ it('It should return an error message if you provided author,title,tag or keywords value which does not have at least three character', () => {
+ chai
+ .request(app)
+ .get('/api/articles?title=nf')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('You should have provided at least 3 characters long for title');
+ });
+ });
+ it('It should return an error message if it doesn\'t find the provided title', () => {
+ chai
+ .request(app)
+ .get('/api/articles?title=noffffffffffffff')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('No Articles with that title, so far!');
+ });
+ });
+
+ it('It should make a search and fetch all articles which has the same provided title', () => {
+ chai
+ .request(app)
+ .get('/api/articles?title=article two')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data).to.be.an('array');
+ expect(res.body.message).to.be.a('string');
+ });
+ });
+});
+
+describe('GET /api/articles/tag=', () => {
+ it('It should return an error message if you provided author,title,tag or keywords value which does not have at least three character', () => {
+ chai
+ .request(app)
+ .get('/api/articles?tag=nf')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('You should have provided at least 3 characters long for tag');
+ });
+ });
+ it('It should return an error message if it doesn\'t find the provided tag', () => {
+ chai
+ .request(app)
+ .get('/api/articles?tag=noffffffffffffff')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('No Articles with that tag, so far!');
+ });
+ });
+
+ it('It should make a search and fetch all articles which has the same provided tag', () => {
+ chai
+ .request(app)
+ .get('/api/articles?tag=reactjs')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data).to.be.an('array');
+ expect(res.body.message).to.be.a('string');
+ });
+ });
+});
+
+describe('GET /api/articles/keywords=', () => {
+ it('It should return an error message if you provided author,title,tag or keywords value which does not have at least three character', () => {
+ chai
+ .request(app)
+ .get('/api/articles?keywords=nf')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('You should have provided at least 3 characters long for keywords');
+ });
+ });
+ it('It should return an error message if there\'s any response for the request made', () => {
+ chai
+ .request(app)
+ .get('/api/articles?keywords=noffffffffffffff')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.error).to.deep.equal('No Articles with that Keyword found, so far!');
+ });
+ });
+
+ it('It should make a search and fetch all articles which has the same provided tag', () => {
+ chai
+ .request(app)
+ .get('/api/articles?keywords=reactjs')
+ .send((err, res) => {
+ expect(res.body).to.be.an('object');
+ expect(res.body.data).to.be.an('array');
+ expect(res.body.message).to.be.a('string');
+ });
+ });
+});
diff --git a/test/stats.test.js b/test/stats.test.js
new file mode 100644
index 0000000..7973717
--- /dev/null
+++ b/test/stats.test.js
@@ -0,0 +1,69 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import server from '../src/index';
+
+const { expect } = chai;
+chai.use(chaiHttp);
+
+describe('Article reading stats', () => {
+ it('should be able to view the number of people who read the article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/views')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to view the number of comments on an article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/comments/count')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to view the number of facebook shares on an article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/shares/facebook')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to view the number of twitter shares on an article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/shares/twitter')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to view the number of email shares on an article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/shares/email')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should be able to view the number of shares on an article', (done) => {
+ chai
+ .request(server)
+ .get('/api/articles/73H7812/shares')
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});
diff --git a/test/terms.test.js b/test/terms.test.js
new file mode 100644
index 0000000..6aa6703
--- /dev/null
+++ b/test/terms.test.js
@@ -0,0 +1,110 @@
+import chaiHttp from 'chai-http';
+import chai from 'chai';
+import dotenv from 'dotenv';
+import server from '../src/index';
+import models from '../src/sequelize/models';
+import tokenHelper from '../src/helpers/Token.helper';
+
+dotenv.config();
+
+const { expect } = chai;
+chai.use(chaiHttp);
+
+let adminToken, termsId, userObject, testUser, userToken;
+
+describe('Terms and Conditions', () => {
+ before(async () => {
+ const termsConditions = await models.termsAndCondition.create({ termsAndConditions: 'Authors Haven Terms and conditions' });
+ termsId = termsConditions.dataValues.id;
+ userObject = {
+ firstName: 'user',
+ lastName: 'user',
+ username: 'user',
+ email: 'user@luffy.co',
+ password: 'User123!',
+ confirmPassword: 'User123!',
+ roles: ['user'],
+ };
+
+ testUser = await models.User.create(userObject);
+
+ // generate test token
+ userToken = await tokenHelper.generateToken({ id: testUser.dataValues.id });
+ });
+ it('should be able to get terms and conditions', (done) => {
+ chai
+ .request(server)
+ .get(`/api/auth/termsandconditions/${termsId}`)
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should notify the user when terms and conditions are not found', (done) => {
+ chai
+ .request(server)
+ .get('/api/auth/termsandconditions/55555')
+ .end((err, res) => {
+ expect(res.status).to.equal(404);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should let an admin log in', (done) => {
+ chai
+ .request(server)
+ .post('/api/auth/login')
+ .send({
+ email: 'superadmin@gmail.com',
+ password: process.env.SUPER_ADMIN_PSW
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ adminToken = res.body.data.token;
+ done();
+ });
+ });
+ it('should be able to update terms and conditions', (done) => {
+ chai
+ .request(server)
+ .patch(`/api/termsandconditions/${termsId}`)
+ .set('token', adminToken)
+ .send({
+ termsAndConditions: `Authors Haven Terms and conditions
+Welcome to Authors Haven`
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(200);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('terms and conditions are required', (done) => {
+ chai
+ .request(server)
+ .patch(`/api/termsandconditions/${termsId}`)
+ .set('token', adminToken)
+ .end((err, res) => {
+ expect(res.status).to.equal(400);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+ it('should not let a normal user update the terms and conditions', (done) => {
+ chai
+ .request(server)
+ .patch(`/api/termsandconditions/${termsId}`)
+ .set('token', userToken)
+ .send({
+ termsAndConditions: `Authors Haven Terms and conditions
+Welcome to Authors Haven`
+ })
+ .end((err, res) => {
+ expect(res.status).to.equal(403);
+ expect(res.body).to.be.an('object');
+ done();
+ });
+ });
+});