From fed0e638fbedda9a6406a6bc86f7b8e1ab11d818 Mon Sep 17 00:00:00 2001 From: Rupesh Krishna Jha Date: Sun, 13 Sep 2020 23:00:27 +0530 Subject: [PATCH] Minor changes (#193) * added update restrictions, techStacks in project and orgId to a user * added edit restriction mechanism * intial deactivation mechanism * intial logging mechanism and deactive account * issue fix * fixing org creation issue * Fixes issues in testing (#160) * fix failing test file user.test.js * fixes EADDRINUSE while testing * fixes issues in prposal route * fixed issues in org route and added new tests * Moving Google Analytics requests to backend (#154) * Moving Google Analytics requetss to backend * Requested Changes * Minor changes * Adding code to prevent attacks (#153) * modified user's api (#168) * modified user's api * login options during login * Changes for reactions (#166) * Revert "Adding code to prevent attacks (#153)" (#170) This reverts commit 57a0cf99867792ee3082408582535a83293e11ca. * Fixed and written all the missing test cases (#172) * update code * fixed failing test cases and missing test cases * User activity tracker using redis (#174) * update code * initial mechanism for user tracking initial mechanism for user tracking * Security issue (#177) * update code * fix security flaws * fixed test cases * minor fixes (#178) * add docs for new contributors (#181) Co-authored-by: Devesh Verma Co-authored-by: Kumar Saurabh Raj <39027928+ksraj123@users.noreply.github.com> Co-authored-by: Asel Peiris Co-authored-by: pranjals149 <42092291+pranjals149@users.noreply.github.com> Co-authored-by: Vaibhav D. Aren --- .env.dev | 8 +- README.md | 126 +++++++++++----- app.js | 46 +++--- app/controllers/activity.js | 32 ++++ app/controllers/auth.js | 8 +- app/controllers/comment.js | 12 +- app/controllers/event.js | 11 +- app/controllers/organization.js | 11 +- app/controllers/post.js | 14 +- app/controllers/project.js | 4 + app/controllers/user.js | 57 +++++-- app/middleware/activity.js | 56 +++++++ app/middleware/rateLimiter.js | 81 ++++++++++ app/middleware/sanitise.js | 7 + app/models/Activity.js | 33 +++++ app/routes/activity.js | 16 ++ app/routes/organisation.js | 1 - app/routes/user.js | 4 +- app/utils/activity-helper.js | 58 ++++++++ app/utils/collections.js | 10 ++ config/mongoose.js | 1 + config/redis.js | 14 ++ package-lock.json | 255 ++++++++++++++------------------ package.json | 8 +- test/comment.test.js | 7 + test/event.test.js | 8 +- test/organisation.test.js | 98 +++++++++++- test/post.test.js | 34 ++++- test/project.test.js | 8 +- test/proposal.test.js | 8 + test/rateLimit.test.js | 117 +++++++++++++++ test/url.test.js | 4 + test/user.test.js | 92 +++++++++--- 33 files changed, 993 insertions(+), 256 deletions(-) create mode 100644 app/controllers/activity.js create mode 100644 app/middleware/activity.js create mode 100644 app/middleware/rateLimiter.js create mode 100644 app/middleware/sanitise.js create mode 100644 app/models/Activity.js create mode 100644 app/routes/activity.js create mode 100644 app/utils/activity-helper.js create mode 100644 app/utils/collections.js create mode 100644 config/redis.js create mode 100644 test/rateLimit.test.js diff --git a/.env.dev b/.env.dev index 39acea1..269d233 100644 --- a/.env.dev +++ b/.env.dev @@ -4,4 +4,10 @@ JWT_SECRET="thisismysupersecrettokenjustkidding" DATABASE_URL="mongodb://mongo:27017/donut-development" SENDGRID_API_KEY='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' SOCKET_PORT=8810 -clientbaseurl = "http://localhost:3000/" +clientbaseurl = "http://localhost:3000" +SOCKET_PORT=8810 +REDIS_HOST="redis" +REDIS_PORT=6379 +REDIS_PASSWORD="auth" +REDIS_DB=0 +REDIS_ACTIVITY_DB=1 diff --git a/README.md b/README.md index 336d35f..db82fe6 100644 --- a/README.md +++ b/README.md @@ -21,47 +21,91 @@

-### STEPS -```.sh -npm install +## Prerequisite: + +These are the requirement that should be installed locally on your machine. + +- Node.js +- MongoDB +- Redis + + +## How to setup node.js on your machine? + +- Move to: [link](https://nodejs.org/en/download/) choose the operating system as per your machine and start downloading and setup by clicking recommended settings in the installation wizard. + +## How to setup MongoDB on your machine? + +- Move to: [link](https://docs.mongodb.com/manual/administration/install-community/) look at the left sidebar and choose the operating system according to your machine. Then follow the steps recommended in the official docs. + +``` +Note: You can also use the MongoDB servers like Mlab or MongoDB Cluster server ``` -#### Package descriptions +## How to setup redis on your machine? -- bcrypt :- hash your plain password and store hashed password in database. -- body-parser :- Parse incoming request bodies in a middleware before your handlers, available under the req.bodyproperty. -- express :- Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. -- jsonwebtoken :- JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. -- mongoose :- Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment. -- morgan :- HTTP request logger middleware for node.js. -- nodemon :- nodemon will watch the files in the directory in which nodemon was started, and if any files change, nodemon will automatically restart your node application. +- Follow the steps provided in the [link](https://auth0.com/blog/introduction-to-redis-install-cli-commands-and-data-types/) to install redis on your operating system +## How to set up this project locally? -These are the Donut APIs +- Move to: https://github.com/codeuino/social-platform-donut-backend +- Fork the repo +- Clone the repo using: +```sh + git clone https://github.com/codeuino/social-platform-donut-backend.git +``` +- Now move to the project directory on your machine. +``` + cd social-platform-donut-backend +``` +- Now use ```git checkout development``` to move to the development branch. +- Install all the dependencies using: +```sh +npm install +``` +- Run the development server using: +```sh +npm run dev +``` +- Now the server is running on PORT 5000 or the PORT mentioned in the environment **.env.dev** variables -## API Response Format -The response body for all the APIs will use the following format and it will contain one of them ( data | errors ). -
-{
-    data: {
-        /../
-    },
-    errors: {
-        /../
-    }
-}
-
- -## Error Objects -Error objects provide additional information about problems encountered while performing an operation. Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document. +``` +Note: Setup the environment variables as mentioned below +``` + + +## Run unit test +Use the given below command to run all the unit test cases. +``` +npm run test +``` -### An error object MAY have the following members: -- `status` : the HTTP status code applicable to this problem, expressed as a string value. -- `code` : an application-specific error code, expressed as a string value. -- `title` : a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. -- `detail` : a human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. +## What are the environment variables required to run the project locally on a machine? +- Follow these steps to set the environment variable for development: +- Move to the root directory of the project and open **.env.dev** (for development) or **.env.test** (for testing) +- PORT = 5000 +- NODE_ENV = "development" +- JWT_SECRET="" +- DATABASE_URL="" +- SENDGRID_API_KEY = '' +- SOCKET_PORT = 8810 + +Note: To get **SENDGRID_API_KEY** follow the Sendgrid official [docs](https://sendgrid.com/docs/ui/account-and-settings/api-keys/#creating-an-api-key) + +## Workflow (After setup) + +Must follow the steps to use the platform: +1. Create an organization - [API](https://docs.codeuino.org/donut-social-networking-platform/rest-apis/organization-api#create-an-organization) +2. Register as an admin - [API](https://docs.codeuino.org/donut-social-networking-platform/rest-apis/post-api#create-a-user) +3. Now login and use the features implemented - [API](https://docs.codeuino.org/donut-social-networking-platform/rest-apis/post-api#login-user) +4. To know more about features please go through the docs - [Docs](https://docs.codeuino.org/donut-social-networking-platform/rest-apis/post-api) + +``` +NOTE: Please make sure when you setup for the first time your database is empty. +``` + ## Allowed HTTPs requests:
@@ -122,5 +166,21 @@ DELETE  : To delete resource
         409
         Conflict
         resourse with given id already exist.
+      
+    
+        429
+        Too many requests
+        sent too many requests to the server in short span of time
         
-
\ No newline at end of file
+
+

+ +## Contributing 💻 +We are happy to see you here and we welcome your contributions towards Donut-Platform. +Contributions are not limited to coding only, you can help in many other ways which includes leaving constructive feedback to people's Pull Request threads also. + +Donut platform also provides an extensive list of issues, some of them includes labels like good-first-issue, help-wanted. You can take a look at good-first-issue issues if you are new here but you are free to choose any issue you would like to work on. + +If there's no issue available currently, you can setup the project locally and find out the bugs/new features and open issues for that and discuss the bugs or features with the project maintainers or admins. + +After choosing an issue and doing changes in the code regarding that, you can open up a Pull Request (PR) to development branch to get your work reviewed and merged! \ No newline at end of file diff --git a/app.js b/app.js index 19b1162..d2b8f18 100644 --- a/app.js +++ b/app.js @@ -4,15 +4,17 @@ const morgan = require('morgan') const cookieParser = require('cookie-parser') const createError = require('http-errors') const path = require('path') - const socket = require('socket.io') const multer = require('multer') const bodyParser = require('body-parser') const cors = require('cors') +const helmet = require('helmet') +const hpp = require('hpp') var winston = require('./config/winston') +const rateLimiter = require('./app/middleware/rateLimiter') +const sanitizer = require('./app/middleware/sanitise') const fileConstants = require('./config/fileHandlingConstants') - const indexRouter = require('./app/routes/index') const authRouter = require('./app/routes/auth') const usersRouter = require('./app/routes/user') @@ -25,6 +27,7 @@ const projectRouter = require('./app/routes/project') const notificationRouter = require('./app/routes/notification') const proposalRouter = require('./app/routes/proposal') const analyticsRouter = require('./app/routes/analytics') +const activityRouter = require('./app/routes/activity') const app = express() const server = require('http').Server(app) @@ -32,6 +35,7 @@ const server = require('http').Server(app) app.use(cors()) app.use(bodyParser.json({ limit: '200mb' })) +app.use(cookieParser()) app.use(bodyParser.urlencoded(fileConstants.fileParameters)) const memoryStorage = multer.memoryStorage() @@ -49,26 +53,6 @@ io.on('connection', (socket) => { io.emit('user connected') }) -app.use(helmet()); -app.use(hpp()); - -const csrfMiddleware = csurf({ - cookie: true -}); - -app.use(session({ - secret: 'codeuino', - resave: false, - saveUninitialized: true, - cookie: { - secure: true, - httpOnly: true - } -})); - -app.use(cookieParser()); -app.use(csrfMiddleware); - // view engine setup app.set('views', path.join(__dirname, 'views')) app.set('view engine', 'ejs') @@ -93,6 +77,23 @@ app.use((req, res, next) => { next() }) +// TO PREVENT DOS ATTACK AND RATE LIMITER +app.use(rateLimiter.customRateLimiter) + +// TO PREVENT XSS ATTACK +app.use(sanitizer.cleanBody) +app.use(helmet()) + +// TO PREVENT CLICK JACKING +app.use((req, res, next) => { + res.append('X-Frame-Options', 'Deny') + res.set('Content-Security-Policy', "frame-ancestors 'none';") + next() +}) + +// TO PREVENT THE QUERY PARAMETER POLLUTION +app.use(hpp()) + app.use('/notification', notificationRouter) app.use('/', indexRouter) app.use('/auth', authRouter) @@ -105,6 +106,7 @@ app.use('/comment', commentRouter) app.use('/project', projectRouter) app.use('/proposal', proposalRouter) app.use('/analytics', analyticsRouter) +app.use('/activity', activityRouter) // catch 404 and forward to error handler app.use(function (req, res, next) { diff --git a/app/controllers/activity.js b/app/controllers/activity.js new file mode 100644 index 0000000..4ad6f1a --- /dev/null +++ b/app/controllers/activity.js @@ -0,0 +1,32 @@ +const User = require('../models/User') +const Activity = require('../models/Activity') + +const HttpStatus = require('http-status-codes') +module.exports = { + + getActivity: async (req, res, next) => { + // userID whose activity will be fetched by admin + const { id } = req.params + + try { + // Check if user exists + const user = await User.findById(req.user._id) + if (!user) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'No such user exists!' }) + } + + // check if not admin + if (user.isAdmin !== true) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'You don\'t have permission!' }) + } + + const data = await Activity.findOne({ userId: id }) + return res.status(HttpStatus.OK).json({ activity: data.activity }) + } catch (error) { + if (process.env.NODE_ENV !== 'production') { + console.log(error.name, '-', error.message) + } + return res.status(HttpStatus.BAD_REQUEST).json({ error: error.message }) + } + } +} diff --git a/app/controllers/auth.js b/app/controllers/auth.js index 20f101f..cd0138f 100644 --- a/app/controllers/auth.js +++ b/app/controllers/auth.js @@ -1,9 +1,11 @@ const User = require('../models/User') const HttpStatus = require('http-status-codes') +const activityHelper = require('../utils/activity-helper') + module.exports = { authenticateUser: async (req, res, next) => { - const email = req.body.email - const password = req.body.password + const email = escape(req.body.email) + const password = escape(req.body.password) try { const user = await User.findByCredentials(email, password) const token = await user.generateAuthToken() @@ -13,9 +15,11 @@ module.exports = { } }, logout: (req, res, next) => { + activityHelper.addActivityToDb(req, res) res.status(HttpStatus.OK).json({ success: 'ok' }) }, logoutAll: (req, res, next) => { + activityHelper.addActivityToDb(req, res) res.status(HttpStatus.OK).json({ success: 'ok' }) } } diff --git a/app/controllers/comment.js b/app/controllers/comment.js index 7517731..b4e7ed9 100644 --- a/app/controllers/comment.js +++ b/app/controllers/comment.js @@ -3,6 +3,8 @@ const HttpStatus = require('http-status-codes') const CommentModel = require('../models/Comment') const permission = require('../utils/permission') const helper = require('../utils/paginate') +const activityTracker = require('../utils/activity-helper') +const collectionTypes = require('../utils/collections') module.exports = { // CREATE COMMENT (ISSUE IN CREATE COMMENT ) @@ -14,7 +16,8 @@ module.exports = { comment.userId = userId comment.postId = id // added postId await comment.save() - res.status(HttpStatus.CREATED).json({ comment: comment }) + activityTracker.addToRedis(req, res, next, collectionTypes.COMMENT, comment._id) + return res.status(HttpStatus.CREATED).json({ comment: comment }) } catch (error) { HANDLER.handleError(res, error) } @@ -63,7 +66,8 @@ module.exports = { comment[update] = req.body[update] }) await comment.save() - res.status(HttpStatus.OK).json({ comment: comment }) + activityTracker.addToRedis(req, res, next, collectionTypes.COMMENT, comment._id) + return res.status(HttpStatus.OK).json({ comment: comment }) } catch (error) { HANDLER.handleError(res, error) } @@ -78,10 +82,10 @@ module.exports = { .sort({ updatedAt: -1 }) .lean() .exec() - if (!comments) { + if (comments === undefined || comments.length === 0) { return res.status(HttpStatus.NOT_FOUND).json({ error: 'No such post' }) } - res.status(HttpStatus.OK).json({ comments: comments }) + return res.status(HttpStatus.OK).json({ comments: comments }) } catch (error) { HANDLER.handleError(res, error) } diff --git a/app/controllers/event.js b/app/controllers/event.js index b7e48ea..fc53cfa 100644 --- a/app/controllers/event.js +++ b/app/controllers/event.js @@ -5,6 +5,9 @@ const permission = require('../utils/permission') const helper = require('../utils/paginate') const notificationHelper = require('../utils/notif-helper') const settingsHelper = require('../utils/settingHelpers') +const activityTracker = require('../utils/activity-helper') +const collectionTypes = require('../utils/collections') + const notification = { heading: '', content: '', @@ -22,6 +25,7 @@ module.exports = { notification.content = `${event.eventName} is added!` notification.tag = 'New!' notificationHelper.addToNotificationForAll(req, res, notification, next) + activityTracker.addToRedis(req, res, next, collectionTypes.EVENT, event._id) res.status(HttpStatus.CREATED).json({ event: event }) } catch (error) { res.status(HttpStatus.BAD_REQUEST).json({ error: error }) @@ -53,6 +57,7 @@ module.exports = { notification.content = `${event.eventName} is updated!` notification.tag = 'Update' notificationHelper.addToNotificationForAll(req, res, notification, next) + activityTracker.addToRedis(req, res, next, collectionTypes.EVENT, event._id) res.status(HttpStatus.OK).json({ event: event }) } catch (error) { HANDLER.handleError(res, error) @@ -163,7 +168,11 @@ module.exports = { notification.content = `Event ${deleteEvent.eventName} is deleted!` notification.tag = 'Deleted' notificationHelper.addToNotificationForAll(req, res, notification, next) - return res.status(HttpStatus.OK).json({ deleteEvent: deleteEvent, message: 'Deleted the event' }) + activityTracker.addToRedis(req, res, next, collectionTypes.EVENT, deleteEvent._id) + return res.status(HttpStatus.OK).json({ + deleteEvent: deleteEvent, + message: 'Deleted the event' + }) } return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Not permitted!' }) } catch (error) { diff --git a/app/controllers/organization.js b/app/controllers/organization.js index 1791547..f307bbb 100644 --- a/app/controllers/organization.js +++ b/app/controllers/organization.js @@ -10,6 +10,9 @@ const Event = require('../models/Event') const permission = require('../utils/permission') const TAGS = require('../utils/notificationTags') const Organisation = require('../models/Organisation') +const activityTracker = require('../utils/activity-helper') +const collectionTypes = require('../utils/collections') + const notification = { heading: '', content: '', @@ -29,6 +32,7 @@ module.exports = { notification.content = `${orgData.name} is created!` notification.tag = TAGS.NEW notificationHelper.addToNotificationForAll(req, res, notification, next) + // activityTracker.addToRedis(req, res, next, collectionTypes.ORG, org._id) return res.status(HttpStatus.CREATED).json({ orgData }) } catch (error) { HANDLER.handleError(res, error) @@ -71,6 +75,7 @@ module.exports = { helper.mapToDb(req, org) } await org.save() + activityTracker.addToRedis(req, res, next, collectionTypes.ORG, org._id) return res.status(HttpStatus.OK).json({ organization: org }) } catch (error) { HANDLER.handleError(res, error) @@ -127,6 +132,7 @@ module.exports = { notification.content = `${org.name} is deleted!` notification.tag = TAGS.DELETE notificationHelper.addToNotificationForAll(req, res, notification, next) + activityTracker.addToRedis(req, res, next, collectionTypes.ORG, org._id) return res.status(HttpStatus.OK).json({ organization: org }) } catch (error) { HANDLER.handleError(res, error) @@ -228,6 +234,7 @@ module.exports = { .status(HttpStatus.BAD_REQUEST) .json({ msg: 'Invalid update' }) } + activityTracker.addToRedis(req, res, next, collectionTypes.ORG, organization._id) // else not admin return res .status(HttpStatus.BAD_REQUEST) @@ -335,11 +342,11 @@ module.exports = { const org = await Organisation.find({}) .lean() .exec() - if (org.length == 0) { + if (org.length === 0) { return res.status(HttpStatus.NOT_FOUND).json({ error: 'No such organization exists!' }) } return res.status(HttpStatus.OK).json({ methods: org[0].options.authentication }) - } catch(error) { + } catch (error) { HANDLER.handleError(res, error) } } diff --git a/app/controllers/post.js b/app/controllers/post.js index 1521f4d..aa024f8 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -6,6 +6,8 @@ const imgUploadHelper = require('../utils/uploader') const permission = require('../utils/permission') const helper = require('../utils/paginate') const settingsHelper = require('../utils/settingHelpers') +const activityTracker = require('../utils/activity-helper') +const collectionTypes = require('../utils/collections') module.exports = { // CREATE POST @@ -19,6 +21,7 @@ module.exports = { try { await post.save() // req.io.emit('new post created', { data: post.content }) + activityTracker.addToRedis(req, res, next, collectionTypes.POST, post._id) return res.status(HttpStatus.CREATED).json({ post }) } catch (error) { HANDLER.handleError(res, error) @@ -181,7 +184,7 @@ module.exports = { default: } await post.save() - res.status(HttpStatus.OK).json({ post: post }) + return res.status(HttpStatus.OK).json({ post: post }) } catch (error) { HANDLER.handleError(res, error) } @@ -233,7 +236,7 @@ module.exports = { default: } await post.save() - res.status(HttpStatus.OK).json({ post: post }) + return res.status(HttpStatus.OK).json({ post: post }) } catch (error) { HANDLER.handleError(res, error) } @@ -241,11 +244,8 @@ module.exports = { getPostByUser: async (req, res, next) => { try { - const posts = await PostModel.find( - { userId: req.params.id }, - {}, - helper.paginate(req) - ) + const { id } = req.params + const posts = await PostModel.find({ userId: id }, {}, helper.paginate(req)) .populate('comments', ['content', 'votes']) .populate('userId', [ 'name.firstName', diff --git a/app/controllers/project.js b/app/controllers/project.js index 203dc8f..a398d69 100644 --- a/app/controllers/project.js +++ b/app/controllers/project.js @@ -4,6 +4,8 @@ const HttpStatus = require('http-status-codes') const helper = require('../utils/paginate') const permission = require('../utils/permission') const settingsHelper = require('../utils/settingHelpers') +const activityTracker = require('../utils/activity-helper') +const collectionTypes = require('../utils/collections') module.exports = { createProject: async (req, res, next) => { @@ -11,6 +13,7 @@ module.exports = { const project = await new Project(req.body) project.createdBy = req.user._id await project.save() + activityTracker.addToRedis(req, res, next, collectionTypes.PROJECT, project._id) return res.status(HttpStatus.CREATED).json({ project }) } catch (error) { HANDLER.handleError(res, error) @@ -98,6 +101,7 @@ module.exports = { await Project.findByIdAndRemove(id) return res.status(HttpStatus.OK).json({ msg: 'Project deleted!' }) } + activityTracker.addToRedis(req, res, next, collectionTypes.PROJECT, project._id) return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Not permitted!' }) } catch (error) { HANDLER.handleError(res, error) diff --git a/app/controllers/user.js b/app/controllers/user.js index d581b7d..33b674c 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -10,6 +10,9 @@ const Events = require('../models/Event') const Organization = require('../models/Organisation') const TAGS = require('../utils/notificationTags') const settingHelper = require('../utils/settingHelpers') +const Activity = require('../models/Activity') +const activityHelper = require('../utils/activity-helper') + const notification = { heading: '', content: '', @@ -38,6 +41,12 @@ module.exports = { const token = await user.generateAuthToken() // Added fn to send email to activate account with warm message await emailController.sendEmail(req, res, next, token) + + // create redis db for activity for the user + const activity = new Activity({ userId: data._id }) + await activity.save() + // hide password + user.password = undefined return res.status(HttpStatus.CREATED).json({ user: user, token: token }) } catch (error) { return res.status(HttpStatus.NOT_ACCEPTABLE).json({ error: error }) @@ -46,9 +55,9 @@ module.exports = { // GET USER PROFILE userProfile: async (req, res, next) => { try { - const id = req.params.id ? req.params.id : req.user._id + const id = req.params.id || req.user._id const user = await User.findById({ _id: id }) - .populate('followings', [ + .populate('followings', [ 'name.firstName', 'name.lastName', 'info.about.designation', @@ -73,6 +82,9 @@ module.exports = { if (!user) { return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exist!' }) } + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(res, error) @@ -86,6 +98,7 @@ module.exports = { 'phone', 'info', 'about', + 'socialMedia', 'isDeactivated' ] // added control as per org settings @@ -110,6 +123,9 @@ module.exports = { user[update] = req.body[update] }) await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ data: user }) } catch (error) { return res.status(HttpStatus.BAD_REQUEST).json({ error }) @@ -169,6 +185,8 @@ module.exports = { try { req.user.tokens = [] await req.user.save() + // add all activity to db after successfully logged out + activityHelper.addActivityToDb(req, res) return res.status(HttpStatus.OK).json({ msg: 'User logged out Successfully!' }) } catch (error) { HANDLER.handleError(res, error) @@ -225,6 +243,7 @@ module.exports = { const inviteLink = `${req.protocol}://${req.get('host')}/user/invite/${token}` return res.status(HttpStatus.OK).json({ inviteLink: inviteLink }) } catch (error) { + console.log('error in req', error) HANDLER.handleError(res, error) } }, @@ -255,16 +274,16 @@ module.exports = { // ADD TO THE FOLLOWINGS LIST addFollowing: async (req, res, next) => { - const { followId } = req.body + const { id } = req.params try { - if (followId === req.user._id) { + if (id === req.user._id) { return res.status(HttpStatus.OK).json({ msg: 'You can not follow yourself!' }) } const user = await User.findById(req.user.id) if (!user) { return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'No such user exists!' }) } - user.followings.unshift(followId) + user.followings.unshift(id) await user.save() next() } catch (error) { @@ -274,9 +293,10 @@ module.exports = { // ADD TO FOLLOWERS LIST addFollower: async (req, res, next) => { - const { followId } = req.body + console.log('follow request!') + const { id } = req.params try { - const user = await User.findById(followId) + const user = await User.findById(id) if (!user) { return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'No such user exists!' }) } @@ -297,6 +317,9 @@ module.exports = { .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .exec() + // hide password and tokens + userData.password = undefined + userData.tokens = [] return res.status(HttpStatus.OK).json({ user: userData }) } catch (error) { HANDLER.handleError(res, error) @@ -305,7 +328,7 @@ module.exports = { // REMOVE FROM FOLLOWINGS LIST removeFollowing: async (req, res, next) => { - const { followId } = req.body + const { id } = req.params try { const user = await User.findById(req.user._id) if (!user) { @@ -313,7 +336,7 @@ module.exports = { } // check if followId is in following list or not const followingIdArray = user.followings.map(followingId => followingId._id) - const isFollowingIdIndex = followingIdArray.indexOf(followId) + const isFollowingIdIndex = followingIdArray.indexOf(id) if (isFollowingIdIndex === -1) { return res.status(HttpStatus.OK).json({ msg: 'You haven\'t followed the user!' }) } else { @@ -329,9 +352,9 @@ module.exports = { // REMOVE FROM FOLLOWERS LIST removeFollower: async (req, res, next) => { - const { followId } = req.body + const { id } = req.params try { - const user = await User.findById(followId) + const user = await User.findById(id) if (!user) { return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exists!' }) } @@ -347,6 +370,9 @@ module.exports = { .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) .exec() + // hide password and tokens + userData.password = undefined + userData.tokens = [] return res.status(HttpStatus.OK).json({ user: userData }) } catch (error) { HANDLER.handleError(res, error) @@ -393,6 +419,9 @@ module.exports = { if (unblockIndex !== -1) { user.blocked.splice(unblockIndex, 1) await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } return res.status(HttpStatus.NOT_FOUND).json({ user }) @@ -430,6 +459,9 @@ module.exports = { } user.isRemoved = true await user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(res, error) @@ -440,6 +472,9 @@ module.exports = { try { req.user.isActivated = !req.user.isActivated const user = await req.user.save() + // hide password and tokens + user.password = undefined + user.tokens = [] return res.status(HttpStatus.OK).json({ user }) } catch (error) { HANDLER.handleError(error) diff --git a/app/middleware/activity.js b/app/middleware/activity.js new file mode 100644 index 0000000..a0f2b91 --- /dev/null +++ b/app/middleware/activity.js @@ -0,0 +1,56 @@ +const User = require('../models/User') +const redis = require('../../config/redis') + +const activity = async (req, res, next) => { + var redisClient = redis.redisClient + var route = req.originalUrl.replace(/\?.*$/, '') + var method = req.method + var userID = req.user.id.toString() + console.log('req.body ', req.body) + console.log('res.locals.data ', res.locals.data) + console.log('route ', route) + console.log('methods ', method) + + if (route === '/user/logout') { + var activityData = await redisClient.lrange(userID, 0, -1) + const data = await User.findOne({ + _id: userID + }) + var activityElement = { + route: '', + method: '', + collectionType: '', + id: '', + timestamp: '' + } + for (let index = 0; index < activityData.length; index++) { + var activityDataElement = activityData[index].split(',') + activityElement.route = activityDataElement[0] + activityElement.method = activityDataElement[1] + activityElement.collectionType = activityDataElement[2] + activityElement.id = activityDataElement[3] + activityElement.timestamp = activityDataElement[4] + data.activity.unshift(activityElement) + } + await data.update() + console.log('DATA') + console.log(data) + // clear data from redis + await redisClient.del(userID) + } else if (method !== 'GET') { + var objectID = res.locals.data._id + userID = objectID + var timeStamp = Date() + var collectionType = res.locals.collectionType + if (typeof res.locals.data.userId !== 'undefined') { + userID = res.locals.data.userId + } + // example /auth/login,POST,user,5ed09e9d446f2b1c208b6ba8,Thu Jul 23 2020 20:28:29 GMT+0530 (India Standard Time) + activityElement = route.concat(',', method, ',', collectionType, ',', objectID, ',', timeStamp) + // userID => [(route, collection, method, objectID), (route,method, collection, objectID) ...] + await redisClient.rpush(userID, activityElement) + next() + } +} + +module.exports = activity diff --git a/app/middleware/rateLimiter.js b/app/middleware/rateLimiter.js new file mode 100644 index 0000000..1778853 --- /dev/null +++ b/app/middleware/rateLimiter.js @@ -0,0 +1,81 @@ +const redis = require('../../config/redis') +const redisClient = redis.redisClient +const moment = require('moment') +const WINDOW_SIZE_IN_HOURS = 24 +const MAX_WINDOW_REQUEST_COUNT = process.env.NODE_ENV === 'testing' ? 20 : 500 +const WINDOW_LOG_INTERVAL_IN_HOURS = 1 + +module.exports = { + customRateLimiter: (req, res, next) => { + try { + // check if redis exists + if (!redisClient) { + throw new Error('RedisClient not found on the server') + } + // if exists check if request made earlier from same ip + redisClient.get(req.ip, (err, reply) => { + if (err) { + console.log('Error in fetching data from redis', err) + } + const currentRequestTime = moment() + // if no reply from redis then store the users request to the server in redis + if (reply === null || reply === undefined) { + const newRecord = [] + const info = { + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1 + } + newRecord.unshift(info) + // set to redis => ip => [{ requestTimeStamp, requestCount }] + redisClient.set(req.ip, JSON.stringify(newRecord)) + next() + } else { + // if record is found, parse it's value and calculate number of requests users has made within the last window + const data = JSON.parse(reply) + + const windowStartTimestamp = moment() + .subtract(WINDOW_SIZE_IN_HOURS, 'hours') + .unix() + + const requestsWithinWindow = data.filter(entry => { + return entry.requestTimeStamp > windowStartTimestamp + }) + + const totalWindowRequestsCount = requestsWithinWindow.reduce((accumulator, entry) => { + return accumulator + entry.requestCount + }, 0) + + // if number of requests made is greater than or equal to the desired maximum, return error + if (totalWindowRequestsCount >= MAX_WINDOW_REQUEST_COUNT) { + return res.status(429).json({ + error: `You have exceeded the ${MAX_WINDOW_REQUEST_COUNT} requests in ${WINDOW_SIZE_IN_HOURS} hrs limit!` + }) + } else { + // if number of requests made is less than allowed maximum, log new entry + const lastRequestLog = data[data.length - 1] + const potentialCurrentWindowIntervalStartTimeStamp = currentRequestTime + .subtract(WINDOW_LOG_INTERVAL_IN_HOURS, 'hours') + .unix() + + // if interval has not passed since last request log, increment counter + if (lastRequestLog.requestTimeStamp > potentialCurrentWindowIntervalStartTimeStamp) { + lastRequestLog.requestCount++ + data[data.length - 1] = lastRequestLog + } else { + // if interval has passed, log new entry for current user and timestamp + data.unshift({ + requestTimeStamp: currentRequestTime.unix(), + requestCount: 1 + }) + } + redisClient.set(req.ip, JSON.stringify(data)) + next() + } + } + }) + } catch (error) { + console.log('Error in rateLimiter', error) + next(error) + } + } +} diff --git a/app/middleware/sanitise.js b/app/middleware/sanitise.js new file mode 100644 index 0000000..980ae45 --- /dev/null +++ b/app/middleware/sanitise.js @@ -0,0 +1,7 @@ +const sanitize = require('mongo-sanitize') +module.exports = { + cleanBody: (req, res, next) => { + req.body = sanitize(req.body) + next() + } +} diff --git a/app/models/Activity.js b/app/models/Activity.js new file mode 100644 index 0000000..9302111 --- /dev/null +++ b/app/models/Activity.js @@ -0,0 +1,33 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const activitySchema = new Schema({ + userId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + }, + activity: [{ + route: { + type: String, + required: true + }, + method: { + type: String, + required: true + }, + collectionType: { + type: String, + required: true + }, + id: { + type: String, + required: true + }, + timestamp: { + type: String, + required: true + } + }] +}) + +module.exports = mongoose.model('Activity', activitySchema) diff --git a/app/routes/activity.js b/app/routes/activity.js new file mode 100644 index 0000000..509d7b1 --- /dev/null +++ b/app/routes/activity.js @@ -0,0 +1,16 @@ +const express = require('express') +const router = express.Router() + +const activityController = require('../controllers/activity') +const isUnderMaintenance = require('../middleware/maintenance') +const auth = require('../middleware/auth') + +// get a User activity +router.get( + '/user/:id', + isUnderMaintenance, + auth, + activityController.getActivity +) + +module.exports = router diff --git a/app/routes/organisation.js b/app/routes/organisation.js index 80b7ba7..74ba17b 100644 --- a/app/routes/organisation.js +++ b/app/routes/organisation.js @@ -10,7 +10,6 @@ router.post( '/', isUnderMaintenance, uploader.upload.single('image'), - auth, OrgController.createOrganization ) diff --git a/app/routes/user.js b/app/routes/user.js index 76de54c..8436ac1 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -82,7 +82,7 @@ router.post( // follow the user router.patch( - '/follow', + '/follow/:id', isUnderMaintenance, auth, userController.addFollowing, @@ -91,7 +91,7 @@ router.patch( // unFollow the user router.patch( - '/unfollow', + '/unfollow/:id', isUnderMaintenance, auth, userController.removeFollowing, diff --git a/app/utils/activity-helper.js b/app/utils/activity-helper.js new file mode 100644 index 0000000..27dbc8c --- /dev/null +++ b/app/utils/activity-helper.js @@ -0,0 +1,58 @@ +const Activity = require('../models/Activity') +const redis = require('../../config/redis') +var redisClient = redis.redisClient +var activityElement = {} + +module.exports = { + addToRedis: async (req, res, next, collection, objectId) => { + var route = req.originalUrl.replace(/\?.*$/, '') + var method = req.method + var userID = req.user.id.toString() + console.log('route ', route) + console.log('methods ', method) + + if (method !== 'GET') { + var objectID = objectId // post, event, project id + var timeStamp = Date() + var collectionType = collection + // example /auth/login,POST,user,5ed09e9d446f2b1c208b6ba8,Thu Jul 23 2020 20:28:29 GMT+0530 (India Standard Time) + activityElement = route.concat(',', method, ',', collectionType, ',', objectID, ',', timeStamp) + console.log('activityElement ', activityElement) + // userID => [(route, collection, method, objectID), (route,method, collection, objectID) ...] + await redisClient.rpush(userID, activityElement) + } + }, + addActivityToDb: async (req, res) => { + console.log('add activity to db called!') + + var userID = req.user.id.toString() + var activityData = await redisClient.lrange(userID, 0, -1) + console.log('activityData ', activityData) + + const data = await Activity.findOne({ userId: userID }) + console.log('data from db ', data) + + for (let index = 0; index < activityData.length; index++) { + var activityDataElement = activityData[index].split(',') + activityElement.route = activityDataElement[0] + activityElement.method = activityDataElement[1] + activityElement.collectionType = activityDataElement[2] + activityElement.id = activityDataElement[3] + activityElement.timestamp = activityDataElement[4] + data.activity.unshift(activityElement) + } + + if (data !== null || data !== undefined) { + await data.save() + } + console.log('Activity saved to db ', data) + + // clear data from redis + await redisClient.del(userID, (err, reply) => { + if (err) { + console.log('Error in removing key ', userID) + } + console.log('Delete reply ', reply) + }) + } +} diff --git a/app/utils/collections.js b/app/utils/collections.js new file mode 100644 index 0000000..224612b --- /dev/null +++ b/app/utils/collections.js @@ -0,0 +1,10 @@ +const activity = { + USER: 'User', + POST: 'Post', + EVENT: 'Event', + PROJECT: 'Project', + ORG: 'Organization', + COMMENT: 'Comment' +} + +module.exports = activity diff --git a/config/mongoose.js b/config/mongoose.js index 25aa95b..cba5c16 100644 --- a/config/mongoose.js +++ b/config/mongoose.js @@ -8,6 +8,7 @@ mongoose useFindAndModify: false }) .then(() => { + console.log(`${process.env.DATABASE_URL}`) console.log('mongodb connection successful') }) .catch((err) => { diff --git a/config/redis.js b/config/redis.js new file mode 100644 index 0000000..10f7fb0 --- /dev/null +++ b/config/redis.js @@ -0,0 +1,14 @@ +const Redis = require('ioredis') +const redisClient = new Redis() + +redisClient.on('connect', () => { + console.log('redis connected!') +}) + +redisClient.on('error', (error) => { + console.log(process.env.REDIS_PORT) + console.log(process.env.REDIS_HOST) + console.log('redis error', error) +}) + +exports.redisClient = redisClient diff --git a/package-lock.json b/package-lock.json index 8be42e1..47ead99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1145,11 +1145,6 @@ } } }, - "bowser": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz", - "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA==" - }, "boxen": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", @@ -1364,11 +1359,6 @@ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", "dev": true }, - "camelize": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz", - "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=" - }, "capture-exit": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", @@ -1526,6 +1516,11 @@ } } }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==" + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1681,11 +1676,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" }, - "content-security-policy-builder": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz", - "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ==" - }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -1706,12 +1696,19 @@ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "cookie-parser": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", - "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz", + "integrity": "sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==", "requires": { - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } } }, "cookie-signature": { @@ -1855,11 +1852,6 @@ "assert-plus": "^1.0.0" } }, - "dasherize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz", - "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg=" - }, "data-urls": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", @@ -1981,6 +1973,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -2058,11 +2055,6 @@ "webidl-conversions": "^4.0.2" } }, - "dont-sniff-mimetype": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz", - "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug==" - }, "dot-prop": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", @@ -2985,38 +2977,6 @@ } } }, - "express-session": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.1.tgz", - "integrity": "sha512-UbHwgqjxQZJiWRTMyhvWGvjBQduGCSBDhhZXYenziMFjxst5rMV+aJZ6hKPHZnPyHGsrqRICxtX8jtEbm/z36Q==", - "requires": { - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.0.2", - "parseurl": "~1.3.3", - "safe-buffer": "5.2.0", - "uid-safe": "~2.1.5" - }, - "dependencies": { - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" - } - } - }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -3143,16 +3103,16 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, - "fast-text-encoding": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", - "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" - }, "fast-safe-stringify": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" + }, "fb-watchman": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", @@ -4321,50 +4281,9 @@ } }, "helmet": { - "version": "3.23.3", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.23.3.tgz", - "integrity": "sha512-U3MeYdzPJQhtvqAVBPntVgAvNSOJyagwZwyKsFdyRa8TV3pOKVFljalPOCxbw5Wwf2kncGhmP0qHjyazIdNdSA==", - "requires": { - "depd": "2.0.0", - "dont-sniff-mimetype": "1.1.0", - "feature-policy": "0.3.0", - "helmet-crossdomain": "0.4.0", - "helmet-csp": "2.10.0", - "hide-powered-by": "1.1.0", - "hpkp": "2.0.0", - "hsts": "2.2.0", - "nocache": "2.1.0", - "referrer-policy": "1.2.0", - "x-xss-protection": "1.3.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, - "helmet-crossdomain": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz", - "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA==" - }, - "helmet-csp": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz", - "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==", - "requires": { - "bowser": "2.9.0", - "camelize": "1.0.0", - "content-security-policy-builder": "2.1.0", - "dasherize": "2.0.0" - } - }, - "hide-powered-by": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz", - "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.1.0.tgz", + "integrity": "sha512-KWy75fYN8hOG2Rhl8e5B3WhOzb0by1boQum85TiddIE9iu6gV+TXbUjVC17wfej0o/ZUpqB9kxM0NFCZRMzf+Q==" }, "hosted-git-info": { "version": "2.8.5", @@ -4372,11 +4291,6 @@ "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==", "dev": true }, - "hpkp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz", - "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI=" - }, "hpp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", @@ -4386,21 +4300,6 @@ "type-is": "^1.6.12" } }, - "hsts": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz", - "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==", - "requires": { - "depd": "2.0.0" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } - } - }, "html-encoding-sniffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", @@ -4659,6 +4558,37 @@ "loose-envify": "^1.0.0" } }, + "ioredis": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.17.3.tgz", + "integrity": "sha512-iRvq4BOYzNFkDnSyhx7cmJNOi1x/HWYe+A4VXHBu4qpwJaGT1Mp+D2bVGJntH9K/Z/GeOM/Nprb8gB3bmitz1Q==", + "requires": { + "cluster-key-slot": "^1.1.0", + "debug": "^4.1.1", + "denque": "^1.1.0", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "redis-commands": "1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ipaddr.js": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", @@ -5847,6 +5777,16 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -6109,6 +6049,11 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" }, + "mongo-sanitize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.1.0.tgz", + "integrity": "sha512-6gB9AiJD+om2eZLxaPKIP5Q8P3Fr+s+17rVWso7hU0+MAzmIvIMlgTYuyvalDLTtE/p0gczcvJ8A3pbN1XmQ/A==" + }, "mongodb": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.3.3.tgz", @@ -6287,11 +6232,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "nocache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz", - "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==" - }, "node-fetch": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", @@ -6937,6 +6877,11 @@ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, + "random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -7056,10 +7001,34 @@ "util.promisify": "^1.0.0" } }, - "referrer-policy": { + "redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.0.2.tgz", + "integrity": "sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ==", + "requires": { + "denque": "^1.4.1", + "redis-commands": "^1.5.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + } + }, + "redis-commands": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz", + "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg==" + }, + "redis-errors": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz", - "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA==" + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } }, "regex-not": { "version": "1.0.2", @@ -7930,6 +7899,11 @@ "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, + "standard-as-callback": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.0.1.tgz", + "integrity": "sha512-NQOxSeB8gOI5WjSaxjBgog2QFw55FV8TkS6Y07BiB3VJ8xNTvUYm0wl0s8ObgQ5NhdpnNfigMIKjgPESzgr4tg==" + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -8933,11 +8907,6 @@ "async-limiter": "~1.0.0" } }, - "x-xss-protection": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz", - "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg==" - }, "xdg-basedir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", diff --git a/package.json b/package.json index f99e0e6..6e78e43 100644 --- a/package.json +++ b/package.json @@ -15,20 +15,26 @@ "aws-sdk": "^2.691.0", "bcrypt": "^3.0.6", "body-parser": "^1.19.0", - "cookie-parser": "~1.4.4", + "cookie-parser": "^1.4.5", "cors": "^2.8.5", "crypto": "^1.0.1", + "csurf": "^1.11.0", "debug": "~2.6.9", "dotenv": "^8.2.0", "ejs": "~2.6.1", "express": "^4.16.4", "googleapis": "^56.0.0", + "helmet": "^4.1.0", + "hpp": "^0.2.3", "http-status-codes": "^1.4.0", + "ioredis": "^4.17.3", "jsonwebtoken": "^8.5.1", "moment": "^2.27.0", + "mongo-sanitize": "^1.1.0", "mongoose": "^5.7.7", "morgan": "^1.9.1", "multer": "^1.4.2", + "redis": "^3.0.2", "socket.io": "^2.3.0", "validator": "^10.11.0", "winston": "^3.3.3" diff --git a/test/comment.test.js b/test/comment.test.js index cb8e64b..a58bc4d 100644 --- a/test/comment.test.js +++ b/test/comment.test.js @@ -6,6 +6,7 @@ const request = require('supertest') const Post = require('../app/models/Post') const User = require('../app/models/User') const Comment = require('../app/models/Comment') +const redis = require('../config/redis').redisClient const randomDigit = Math.floor(Math.random() * 90 + 10) const testUserId = new mongoose.Types.ObjectId() @@ -131,6 +132,8 @@ let server */ beforeAll(async (done) => { await Comment.deleteMany() + await User.deleteMany() + await redis.flushall() await new User(testUser).save() await new Post(demoPost).save() server = app.listen(4000, () => { @@ -308,6 +311,10 @@ afterAll(async () => { await server.close() // delete all the posts post testing await Comment.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/event.test.js b/test/event.test.js index 672168a..0eda3f4 100644 --- a/test/event.test.js +++ b/test/event.test.js @@ -5,6 +5,7 @@ const HttpStatus = require('http-status-codes') const request = require('supertest') const Event = require('../app/models/Event') const User = require('../app/models/User') +const redis = require('../config/redis').redisClient const randomDigit = Math.floor(Math.random() * 90 + 10) const testUserId = new mongoose.Types.ObjectId() @@ -98,6 +99,7 @@ let server beforeAll(async (done) => { await Event.deleteMany() await User.deleteMany() + await redis.flushall() server = app.listen(4000, () => { global.agent = request.agent(server) done() @@ -293,7 +295,7 @@ test('Should get all the upcoming event', async (done) => { test('Should get all the events created by user', async (done) => { await request(app) - .get('/event/me/all') + .get(`/event/${testUserId}/all`) .set('Authorization', `Bearer ${testUser.tokens[0].token}`) .send() .expect(HttpStatus.OK) @@ -312,6 +314,10 @@ afterAll(async () => { await server.close() // delete all the events post testing await Event.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/organisation.test.js b/test/organisation.test.js index f159266..073bd0c 100644 --- a/test/organisation.test.js +++ b/test/organisation.test.js @@ -5,6 +5,7 @@ const HttpStatus = require('http-status-codes') const Organization = require('../app/models/Organisation') const User = require('../app/models/User') const jwt = require('jsonwebtoken') +const redis = require('../config/redis').redisClient const adminId = new mongoose.Types.ObjectId() const moderatorId = new mongoose.Types.ObjectId() const randomDigit = Math.floor(Math.random() * 90 + 10) @@ -39,6 +40,26 @@ const updatedTestOrg = { } } +const updateSettings = { + settings: { + enableEmail: true, + language: 'German', + timeFormat: '24' + }, + permissions: { + sendInvite: 'ADMINS', + canCreateManage: 'MEMBERS', + canChangeEmail: true, + canChangeName: true + }, + authentication: { + email: true, + google: true, + github: true, + gitlab: true + } +} + const testUser = { name: { firstName: 'test', @@ -73,6 +94,7 @@ const testUser = { location: 'location' } }, + isAdmin: true, tokens: [{ token: jwt.sign({ _id: `${adminId}` @@ -86,6 +108,7 @@ let server */ beforeAll(async (done) => { await Organization.deleteMany() + await redis.flushall() await new User(testUser).save() server = app.listen(4000, () => { global.agent = request.agent(server) @@ -105,7 +128,6 @@ describe('POST /org/', () => { test('should create a new Organization', async (done) => { const response = await request(app) .post('/org/') - .set('Authorization', `Bearer ${token}`) .send(testOrg) .expect(HttpStatus.CREATED) orgId = response.body.orgData._id @@ -157,6 +179,76 @@ describe('PATCH /org/:id', () => { }) }) +/** GET ORGANIZATION LOGIN OPTIONS**/ +describe('GET login options', () => { + test('Should retrieve the login options', async (done) => { + const res = await request(app) + .get('/org/login/options') + .expect(HttpStatus.OK) + expect(res.body).not.toBeNull() + done() + }) +}) + +/** UPDATE ORGANIZATION SETTINGS**/ +describe('UPDATE org-settings', () => { + test('Should update org-settings', async (done) => { + const res = await request(app) + .patch(`/org/${orgId}/settings/update`) + .set('Authorization', `Bearer ${token}`) + .send(updateSettings) + .expect(HttpStatus.OK) + + // check res + expect(res.body).not.toBeNull() + done() + }) +}) + +/** GET ORGANIZATION OVERVIEW**/ +describe('GET org overview', () => { + test('Should retrieve the organization overview', async (done) => { + const res = await request(app) + .get('/org/overview/all') + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + // check response + expect(res.body).not.toBeNull() + done() + }) +}) + +/** GET ALL MEMBERS **/ +describe('GET all members', () => { + test('Should retrieve all the members of the org', async (done) => { + const res = await request(app) + .get('/org/members/all') + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + // check res + expect(res.body).not.toBeNull() + done() + }) +}) + +/** REMOVE ADMIN**/ +describe('PATCH /org/remove/:orgId/:userId', () => { + console.log('adminId ', adminId) + test('Should remove the user', async (done) => { + const res = await request(app) + .patch(`/org/remove/${orgId}/${adminId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.BAD_REQUEST) + expect(res.body).not.toBeNull() + done() + }) +}) + /** DELETE ORGANIZATION**/ describe('DELETE /org/:id', () => { test('Should delete the organization', async (done) => { @@ -180,6 +272,10 @@ afterAll(async () => { await server.close() // delete all the organization post testing await Organization.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/post.test.js b/test/post.test.js index 3095604..fcdf518 100644 --- a/test/post.test.js +++ b/test/post.test.js @@ -5,6 +5,7 @@ const HttpStatus = require('http-status-codes') const request = require('supertest') const Post = require('../app/models/Post') const User = require('../app/models/User') +const redis = require('../config/redis').redisClient const randomDigit = Math.floor(Math.random() * 90 + 10) const testUserId = new mongoose.Types.ObjectId() @@ -100,6 +101,7 @@ let server */ beforeAll(async (done) => { await Post.deleteMany() + await redis.flushall() await new User(testUser).save() server = app.listen(4000, () => { global.agent = request.agent(server) @@ -230,7 +232,33 @@ test('Should update the Post data', async (done) => { test('Should retrieve all posts created by a user', async (done) => { await request(app) - .get('/post/me/all') + .get(`/post/${testUserId}/all`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing pin post of by particular user + */ + +test('Should pin the post', async (done) => { + await request(app) + .patch(`/post/pin/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing get all pinned post + */ + +test('Should retrieve all the pinned post', async (done) => { + await request(app) + .get('/post/all/pinned?pagination=10&page=1') .set('Authorization', `Bearer ${token}`) .send() .expect(HttpStatus.OK) @@ -249,6 +277,10 @@ afterAll(async () => { await server.close() // delete all the posts post testing await Post.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/project.test.js b/test/project.test.js index dd22655..5d6eab9 100644 --- a/test/project.test.js +++ b/test/project.test.js @@ -5,6 +5,7 @@ const HttpStatus = require('http-status-codes') const request = require('supertest') const Project = require('../app/models/Project') const User = require('../app/models/User') +const redis = require('../config/redis').redisClient const randomDigit = Math.floor(Math.random() * 90 + 10) const pagination = 10 const page = 1 @@ -96,6 +97,7 @@ let server */ beforeAll(async (done) => { await Project.deleteMany() + await redis.flushall() await new User(testUser).save() server = app.listen(4000, () => { global.agent = request.agent(server) @@ -184,7 +186,7 @@ test('Should get project by id', async (done) => { test('Should get all the project created by a user', async (done) => { await request(app) - .get('/project/me/all') + .get(`/project/${testUserId}/all`) .set('Authorization', `Bearer ${token}`) .send() .expect(HttpStatus.OK) @@ -215,6 +217,10 @@ afterAll(async () => { await server.close() // delete all the projects project testing await Project.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/proposal.test.js b/test/proposal.test.js index 5a0b70d..2e71687 100644 --- a/test/proposal.test.js +++ b/test/proposal.test.js @@ -6,6 +6,7 @@ const request = require('supertest') const User = require('../app/models/User') const Organization = require('../app/models/Organisation') const Proposal = require('../app/models/Proposal') +const redis = require('../config/redis').redisClient const randomDigit = Math.floor(Math.random() * 90 + 10) const testUserId = new mongoose.Types.ObjectId() @@ -106,6 +107,7 @@ let server beforeAll(async (done) => { await Proposal.deleteMany() + await redis.flushall() await new User(testUser).save() await new Organization(testOrganization).save() server = app.listen(4000, () => { @@ -201,6 +203,12 @@ afterAll(async () => { await new Promise((resolve) => setTimeout(() => resolve(), 500)) // close server await server.close() + // delete proposal + await Proposal.deleteMany() + // delete all the user created + await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/rateLimit.test.js b/test/rateLimit.test.js new file mode 100644 index 0000000..4839562 --- /dev/null +++ b/test/rateLimit.test.js @@ -0,0 +1,117 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const request = require('supertest') +const jwt = require('jsonwebtoken') +const User = require('../app/models/User') +const HttpStatus = require('http-status-codes') +const redis = require('../config/redis') +const userId = mongoose.Types.ObjectId() +const randomDigit = Math.floor(Math.random() * 90 + 10) + +var token = '' +const demoUser = { + name: { + firstName: 'test', + lastName: 'test' + }, + email: `test${randomDigit}@mailinator.com`, + phone: `12345678${randomDigit}`, + password: 'abc12345', + info: { + about: { + shortDescription: 'this is short description', + longDescription: 'this is a very long description', + website: 'https://www.google.com', + designation: 'software engg', + skills: [ + 'c++', + 'java' + ], + education: [{ + school: { + schoolName: 'firstSchoolName', + year: '2017-2021' + } + }, + { + school: { + schoolName: 'secondSchoolName', + year: '2007-2014' + } + } + ], + location: 'location' + } + } +} + +const testUser = { + _id: userId, + ...demoUser, + email: `test${randomDigit}@mailinator.com`, + phone: `12345678${randomDigit}`, + tokens: [{ + token: jwt.sign({ + _id: userId + }, 'process.env.JWT_SECRET') + }] +} + +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await User.deleteMany() + await redis.flushall() + await new User(testUser).save() + jest.setTimeout(500000) + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app) + .post('/auth/login') + .send({ + email: demoUser.email, + password: demoUser.password + }) + token = response.body.token + done() +}) + +/** + * Testing Rate limiter + */ +test('Should exceed the no of request', async (done) => { + // eslint-disable-next-line no-unused-vars + var response + var status = 200 + while (status !== 429) { + response = await request(app) + .get('/user/link/invite?role=user') + .set('Authorization', `Bearer ${token}`) + .send() + status = response.status + } + console.log('response ', response.status) + expect(response.status).toBe(HttpStatus.TOO_MANY_REQUESTS) + // flush redis + redis.redisClient.del(demoUser._id, (err, res) => { + if (err) { + console.log('error in redis flush ', err) + } + console.log('res ', res) + }) + done() +}) + +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // flush redis + await redis.flushall() + // Closing the DB connection allows Jest to exit successfully. + await mongoose.connection.close() +}) diff --git a/test/url.test.js b/test/url.test.js index 1105634..efca9d9 100644 --- a/test/url.test.js +++ b/test/url.test.js @@ -3,6 +3,7 @@ const mongoose = require('mongoose') const request = require('supertest') const UrlModel = require('../app/models/UrlShortner') const HttpStatus = require('http-status-codes') +const redis = require('../config/redis').redisClient const testUrl = 'http://codeuino.org/codeofconduct' // let shortUrl = '' @@ -12,6 +13,7 @@ let server */ beforeAll(async (done) => { await UrlModel.deleteMany() + await redis.flushall() server = app.listen(4000, () => { global.agent = request.agent(server) done() @@ -62,6 +64,8 @@ afterAll(async () => { await new Promise((resolve) => setTimeout(() => resolve(), 500)) // close server await server.close() + // flush all + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() }) diff --git a/test/user.test.js b/test/user.test.js index f2df44a..8d5effe 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -3,10 +3,12 @@ const mongoose = require('mongoose') const jwt = require('jsonwebtoken') const request = require('supertest') const User = require('../app/models/User') +const redis = require('../config/redis').redisClient const HttpStatus = require('http-status-codes') let token = '' let passwordToken = '' let inviteLink = '' +let unAuthUserId = '' const demoUser = { name: { @@ -46,6 +48,8 @@ const demoUser = { const testUserId = new mongoose.Types.ObjectId() const testFollowUserId = new mongoose.Types.ObjectId() +console.log('testUserID ', testUserId) +console.log('testFollowUserID ', testFollowUserId) const testUser = { _id: testUserId, ...demoUser, @@ -88,6 +92,7 @@ let server beforeAll(async (done) => { await User.deleteMany() + await redis.flushall() server = app.listen(4000, () => { global.agent = request.agent(server) done() @@ -102,6 +107,7 @@ beforeEach(async () => { await User.deleteMany() await new User(testUser).save() await new User(testFollowUser).save() + await redis.flushall() }) /** @@ -114,6 +120,7 @@ test('Should signup new user', async () => { .expect(HttpStatus.CREATED) // Assert that db was changed + unAuthUserId = response.body.user._id const user = await User.findById(response.body.user._id) expect(user).not.toBeNull() @@ -171,18 +178,51 @@ test('Should not login non-existing user', async () => { /** Fetch authenticated user profile */ test('Should get profile for user', async () => { await request(app) - .get('/user/me') + .get(`/user/${testUserId}`) .set('Authorization', `Bearer ${testUser.tokens[0].token}`) .send() .expect(HttpStatus.OK) }) /** Fail in getting unathenticated user profile */ -test('Should not get profile for unauthenticated user', async () => { +test('Should not get profile for unauthenticated user', async (done) => { await request(app) - .get('/user/me') + .get(`/user/${unAuthUserId}`) .send() .expect(HttpStatus.UNAUTHORIZED) + done() +}) + +/** Should update user profile */ +test('Should update profile or authenticated user', async () => { + await request(app) + .patch(`/user/${testUserId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send({ + email: 'updated@example.com' + }) + .expect(HttpStatus.OK) +}) + +/** Should fail to make updates that are not allowed to user profile */ +test('Should be able to make only allowed updates to authenticated user', async () => { + await request(app) + .patch(`/user/${testUserId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send({ + gender: 'Male' + }) + .expect(HttpStatus.BAD_REQUEST) +}) + +/** Should Fail updating profile of unauthenticate user */ +test('Should not update profile or unauthenticated user', async () => { + await request(app) + .patch(`/user/${unAuthUserId}`) + .send({ + email: 'updated@example.com' + }) + .expect(HttpStatus.UNAUTHORIZED) }) /** Should update user profile */ @@ -275,7 +315,7 @@ test('Should activate the account ', async (done) => { /* Get invite link */ test('Should generate an invite link and send', async () => { const response = await request(app) - .get('/user/invite?role=user') + .get('/user/link/invite?role=user') .set('Authorization', `Bearer ${testUser.tokens[0].token}`) .send() .expect(HttpStatus.OK) @@ -293,24 +333,11 @@ test('Should validate the invite link token ', async () => { .expect(HttpStatus.MOVED_TEMPORARILY) }) -/* Logout the user */ -test('Should logout the user ', async (done) => { - await request(app) - .post('/user/logout') - .set('Authorization', `Bearer ${testUser.tokens[0].token}`) - .send() - .expect(HttpStatus.OK) - done() -}) - /* Follow the user */ test('Should follow the user', async (done) => { await request(app) - .patch('/user/follow') + .patch(`/user/follow/${testFollowUserId}`) .set('Authorization', `Bearer ${testUser.tokens[0].token}`) - .send({ - followId: testFollowUserId - }) .expect(HttpStatus.OK) // Assert the db change const user = await User.findById(testFollowUserId) @@ -321,11 +348,8 @@ test('Should follow the user', async (done) => { /* unFollow the user */ test('Should unFollow the user', async (done) => { await request(app) - .patch('/user/unfollow') + .patch(`/user/unfollow/${testFollowUserId}`) .set('Authorization', `Bearer ${testUser.tokens[0].token}`) - .send({ - followId: testFollowUserId - }) .expect(HttpStatus.OK) // Assert that db change const user = await User.findById(testFollowUserId) @@ -356,6 +380,28 @@ test('Should UnBlock the user', async (done) => { done() }) +/* Get user activity on the platform */ +test('Should fetch all the activities of a user on the platform', async (done) => { + const response = await request(app) + .get(`/activity/user/${testUserId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + // Assert the db changed + expect(response.body).not.toBeNull() + done() +}) + +/* Logout the user */ +test('Should logout the user ', async (done) => { + await request(app) + .post('/user/logout') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + /** * TODO: FIX ERROR * This is a temporary fix to issue: @@ -368,6 +414,8 @@ afterAll(async () => { await server.close() // delete all the users post testing await User.deleteMany() + // flush redis + await redis.flushall() // Closing the DB connection allows Jest to exit successfully. await mongoose.connection.close() })