diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..842de51 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +.git +.github diff --git a/.env.dev b/.env.dev index a4df8b4..b5e54cd 100644 --- a/.env.dev +++ b/.env.dev @@ -1,4 +1,6 @@ PORT=5000 NODE_ENV="development" JWT_SECRET="thisismysupersecrettokenjustkidding" -DATABASE_URL="mongodb://localhost:27017/donut-development" +DATABASE_URL="mongodb://mongo:27017/donut-development" +SENDGRID_API_KEY='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' +SOCKET_PORT=8810 diff --git a/.env.test b/.env.test index 42459d6..dd4d183 100644 --- a/.env.test +++ b/.env.test @@ -1,4 +1,6 @@ PORT=3000 NODE_ENV=testing JWT_SECRET=thisismysupersecrettokenjustkidding -DATABASE_URL=mongodb+srv://donut-admin:5cdS2C2g3wRAdQWp@donut-users-hdawt.mongodb.net/donut-testing?retryWrites=true&w=majority \ No newline at end of file +DATABASE_URL=mongodb+srv://donut-admin:5cdS2C2g3wRAdQWp@donut-users-hdawt.mongodb.net/donut-testing?retryWrites=true&w=majority +SENDGRID_API_KEY = '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' +SOCKET_PORT = 8810 \ No newline at end of file diff --git a/.github/workflows/image-workflow.yaml b/.github/workflows/image-workflow.yaml new file mode 100644 index 0000000..acea905 --- /dev/null +++ b/.github/workflows/image-workflow.yaml @@ -0,0 +1,39 @@ +name: donut-server-image-ci + +on: + push: + branches: + - development + + tags: + - v* + +env: + IMAGE_NAME: donut-server:latest + REPO_NAME: codeuino1 + REGISTRY_NAME: registry.hub.docker.com + +jobs: + push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Build image + run: docker build . --file Dockerfile.prod --tag $IMAGE_NAME + + - name: Log into registry + run: docker login --username {{ secrets.DOCKER_USERNAME }} --password {{ secrets.DOCKER_PASSWORD }} + - name: Push image + run: | + IMAGE_ID=$REGISTRY_NAME/$REPO_NAME/$IMAGE_NAME + + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + + echo IMAGE_ID=$IMAGE_ID + echo VERSION=$VERSION + + docker tag $IMAGE_NAME $IMAGE_ID:$VERSION + docker push $IMAGE_ID:$VERSION + docker tag $IMAGE_NAME $IMAGE_ID:latest + docker push $IMAGE_ID:$VERSION diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..3c81e0c --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,21 @@ +FROM node:14 + +ENV NODE_ENV="development" + +# Copy package.json file into container +COPY package.json package.json +COPY package-lock.json package-lock.json + +# Install node modules +RUN npm install && \ + npm install --only=dev && \ + npm cache clean --force --loglevel=error + +# Volume to mount source code into container +VOLUME [ "/server" ] + +# move to the source code directory +WORKDIR /server + +# Start the server +CMD mv ../node_modules . && npm run dev diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..f5d7097 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,16 @@ +FROM node:14 + +ENV NODE_ENV="production" + +WORKDIR /server + +RUN git clone https://github.com/codeuino/social-platform-donut-backend.git + +WORKDIR /server/social-platform-donut-backend + +RUN npm install && \ + npm install pm2@latest -g && \ + npm cache clean --force --loglevel=error + +# Start the server +CMD [ "pm2", "start", "./bin/www", "--time", "--no-daemon" ] diff --git a/app.js b/app.js index 0171e14..a743490 100644 --- a/app.js +++ b/app.js @@ -4,13 +4,44 @@ const logger = 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 fileConstants = require('./config/fileHandlingConstants') const indexRouter = require('./app/routes/index') const authRouter = require('./app/routes/auth') const usersRouter = require('./app/routes/user') const postRouter = require('./app/routes/post') +const eventRouter = require('./app/routes/event') +const shortUrlRouter = require('./app/routes/urlShortner') +const organizationRouter = require('./app/routes/organisation') +const commentRouter = require('./app/routes/comment') +const projectRouter = require('./app/routes/project') +const notificationRouter = require('./app/routes/notification') +const proposalRouter = require('./app/routes/proposal') const app = express() +const server = require('http').Server(app) + +app.use(cors()) + +app.use(bodyParser.json({ limit: '200mb' })) +app.use(bodyParser.urlencoded(fileConstants.fileParameters)) + +const memoryStorage = multer.memoryStorage() +app.use(multer({ storage: memoryStorage }).single('file')) + +server.listen(process.env.SOCKET_PORT || 8810) +// WARNING: app.listen(80) will NOT work here! + +const io = socket.listen(server) +let count = 0 +io.on('connection', (socket) => { + console.log('socket connected count ', count++) + io.emit('user connected') +}) // view engine setup app.set('views', path.join(__dirname, 'views')) @@ -21,15 +52,26 @@ app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(cookieParser()) app.use(express.static(path.join(__dirname, 'public'))) +app.use((req, res, next) => { + req.io = io + next() +}) +app.use('/notification', notificationRouter) app.use('/', indexRouter) app.use('/auth', authRouter) app.use('/user', usersRouter) app.use('/post', postRouter) +app.use('/org', organizationRouter) +app.use('/event', eventRouter) +app.use('/shortUrl', shortUrlRouter) +app.use('/comment', commentRouter) +app.use('/project', projectRouter) +app.use('/proposal', proposalRouter) // catch 404 and forward to error handler app.use(function (req, res, next) { - next(createError(404, 'route doesn\'t exist')) + next(createError(404, "route doesn't exist")) }) // error handler @@ -43,4 +85,4 @@ app.use(function (err, req, res, next) { res.render('error') }) -module.exports = app +module.exports = { app, io } diff --git a/app/controllers/auth.js b/app/controllers/auth.js index dd5ea0e..744c8dc 100644 --- a/app/controllers/auth.js +++ b/app/controllers/auth.js @@ -1,5 +1,5 @@ const User = require('../models/User') - +const HttpStatus = require('http-status-codes') module.exports = { authenticateUser: async (req, res, next) => { const email = req.body.email @@ -10,15 +10,15 @@ module.exports = { res.send({ user: user, token: token }) } catch (error) { if (process.env.NODE_ENV !== 'production') { - // console.log('error ', error) + console.log(error.name, '-', error.message) } - res.status(400).send({ error: error }) + res.status(HttpStatus.BAD_REQUEST).json({ error: error.message }) } }, logout: (req, res, next) => { - res.json({ success: 'ok' }) + res.status(HttpStatus.OK).json({ success: 'ok' }) }, logoutAll: (req, res, next) => { - res.json({ success: 'ok' }) + res.status(HttpStatus.OK).json({ success: 'ok' }) } } diff --git a/app/controllers/comment.js b/app/controllers/comment.js new file mode 100644 index 0000000..f78bb6b --- /dev/null +++ b/app/controllers/comment.js @@ -0,0 +1,151 @@ +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const CommentModel = require('../models/Comment') +const permission = require('../utils/permission') +const helper = require('../utils/paginate') + +module.exports = { + // CREATE COMMENT (ISSUE IN CREATE COMMENT ) + comment: async (req, res, next) => { + const { id } = req.params + const userId = req.user.id.toString() + try { + const comment = new CommentModel(req.body) + comment.userId = userId + comment.postId = id // added postId + await comment.save() + res.status(HttpStatus.CREATED).json({ comment: comment }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // DELETE COMMENT + delete: async (req, res, next) => { + const { id } = req.params + try { + const comment = await CommentModel.findById(id) + if (!comment) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No comment exist' }) + } + // Add rights for admins and moderators as well (TODO) + if (!permission.check(req, res, comment.userId)) { + return res.status(HttpStatus.FORBIDDEN).json({ message: 'Bad delete request' }) + } + await CommentModel.findByIdAndRemove(id) + res.status(HttpStatus.OK).json({ comment: comment }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // UPDATE COMMENT + update: async (req, res, next) => { + const { id } = req.params + const updates = Object.keys(req.body) + const valid = ['content'] + const isValidOperation = updates.every((update) => { + return valid.includes(update) + }) + if (!isValidOperation) { + return res.status(HttpStatus.BAD_REQUEST).json({ error: 'Wrong Update Request' }) + } + try { + const comment = await CommentModel.findById(id) + if (!comment) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No comment exist' }) + } + // also add admin or moderator control (TODO) + if (!permission.check(req, res, comment.userId)) { + return res.status(HttpStatus.BAD_REQUEST).json({ error: 'Wrong update' }) + } + updates.forEach(update => { + comment[update] = req.body[update] + }) + await comment.save() + res.status(HttpStatus.OK).json({ comment: comment }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // GET ALL COMMENTS OF A POST BY postId + getCommentByPost: async (req, res, next) => { + const { id } = req.params + try { + const comments = await CommentModel.find({ postId: id }, {}, helper.paginate(req)) + .populate('userId', ['name.firstName', 'name.lastName']) + .sort({ updatedAt: -1 }) + .lean() + .exec() + if (!comments) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No such post' }) + } + res.status(HttpStatus.OK).json({ comments: comments }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // UPVOTE COMMENT + upvote: async (req, res, next) => { + const { id } = req.params + const userId = req.user.id.toString() + try { + const comment = await CommentModel.findById(id) + if (!comment) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No comment found' }) + } + // CHECKS IF THE USER HAS ALREADY UPVOTED THE COMMENT + comment.votes.upVotes.user.filter(user => { + if (JSON.stringify(user) === JSON.stringify(userId)) { + return res.status(HttpStatus.BAD_REQUEST).json({ + error: 'Bad request' + }) + } + }) + // CHECKS IF THE USER HAS ALREADY DOWNVOTED THE COMMENT + comment.votes.downVotes.user.filter(user => { + if (JSON.stringify(user) === JSON.stringify(userId)) { + comment.votes.downVotes.user.remove(user) + } + }) + comment.votes.upVotes.user.unshift(userId) + await comment.save() + res.status(HttpStatus.OK).json({ comment: comment }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // DOWNVOTE COMMENT + downvote: async (req, res, next) => { + const { id } = req.params + const userId = req.user.id.toString() + try { + const comment = await CommentModel.findById(id) + if (!comment) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No comment found' }) + } + // CHECKS IF THE USER HAS ALREADY DOWNVOTED THE COMMENT + comment.votes.downVotes.user.filter(user => { + if (JSON.stringify(user) === JSON.stringify(userId)) { + return res.status(HttpStatus.BAD_REQUEST).json({ + error: 'Bad request' + }) + } + }) + // CHECKS IF THE USER HAS ALREADY UPVOTED THE COMMENT + comment.votes.upVotes.user.filter(user => { + if (JSON.stringify(user) === JSON.stringify(userId)) { + comment.votes.upVotes.user.remove(user) + } + }) + comment.votes.downVotes.user.unshift(userId) + await comment.save() + res.status(HttpStatus.OK).json({ comment: comment }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/email.js b/app/controllers/email.js new file mode 100644 index 0000000..382a5c3 --- /dev/null +++ b/app/controllers/email.js @@ -0,0 +1,35 @@ +const sendgridMail = require('@sendgrid/mail') +const ejs = require('ejs') +const path = require('path') +const sendGridApi = process.env.SENDGRID_API_KEY || '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' + +sendgridMail.setApiKey(sendGridApi) + +module.exports = { + sendEmail: async (req, res, next, token) => { + const filePath = path.join(__dirname, '/../../views/emailTemplate.ejs') + ejs.renderFile(filePath, { token: token }, (err, data) => { + if (err) { + console.log('Error in renderFile ', err) + } else { + const message = { + to: req.body.email, + from: 'services@codeuino.com', + subject: `Welcome to Donut ${req.body.name.firstName}`, + html: data + } + sendgridMail.send(message).then( + () => { + console.log('sending email') + }, + (error) => { + console.log('error in sending email ', error) + if (error.response) { + console.error(error.response.body) + } + } + ) + } + }) + } +} diff --git a/app/controllers/event.js b/app/controllers/event.js new file mode 100644 index 0000000..b8898cb --- /dev/null +++ b/app/controllers/event.js @@ -0,0 +1,189 @@ +const Event = require('../models/Event') +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const permission = require('../utils/permission') +const helper = require('../utils/paginate') +const notificationHelper = require('../utils/notif-helper') +const notification = { + heading: '', + content: '', + tag: '' +} + +module.exports = { + createEvent: async (req, res, next) => { + const event = new Event(req.body) + try { + event.createdBy = req.user._id + await event.save() + req.io.emit('new event created', { data: event.eventName }) + notification.heading = 'New Event!' + notification.content = `${event.eventName} is added!` + notification.tag = 'New!' + notificationHelper.addToNotificationForAll(req, res, notification, next) + res.status(HttpStatus.CREATED).json({ event: event }) + } catch (error) { + res.status(HttpStatus.BAD_REQUEST).json({ error: error }) + } + }, + + updateEvent: async (req, res, next) => { + const { id } = req.params + const updates = Object.keys(req.body) + try { + const event = await Event.findById(id) + if (!event) { + return res.status(HttpStatus.BAD_REQUEST).json({ message: 'No post exists' }) + } + // check for permission (TODO AFTER PREVIOUS PR MERGED) + updates.forEach(update => { + event[update] = req.body[update] + }) + await event.save() + req.io.emit('event update', { data: `Event: ${event.eventName} is updated!` }) + notification.heading = 'Event update!' + notification.content = `${event.eventName} is updated!` + notification.tag = 'Update' + notificationHelper.addToNotificationForAll(req, res, notification, next) + res.status(HttpStatus.OK).json({ event: event }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + rsvp: async (req, res, next) => { + const { yes, no, maybe } = req.body + const { id } = req.params + notification.tag = 'RSVP' + try { + const data = await Event.findById(id) + if (!data) { + res.status(HttpStatus.BAD_REQUEST).json({ error: 'No Event is available' }) + return + } + if (data.rsvpMaybe.includes(req.user.id) || + data.rsvpNo.includes(req.user.id) || + data.rsvpYes.includes(req.user.id)) { + req.io.emit('already rsvp', { data: 'You have already done the rsvp' }) + notification.heading = 'Already rsvp!' + notification.content = 'You have already done the rsvp' + notificationHelper.addToNotificationForUser(req.user._id, res, notification, next) + res.status(HttpStatus.OK).json({ msg: 'You have already done the rsvp' }) + return + } + const event = await Event.findByIdAndUpdate(id) + if (yes) { + try { + event.rsvpYes.push(req.user.id) + await event.save() + req.io.emit('rsvp done', { data: 'RSVP successfully done!' }) + notification.heading = 'RSVP done!' + notification.content = 'RSVP successfully done!' + notificationHelper.addToNotificationForUser(req.user._id, res, notification, next) + res.status(HttpStatus.OK).json({ rsvpData: data }) + } catch (error) { + return res.status(HttpStatus.BAD_REQUEST).json({ error: error }) + } + } + if (no) { + try { + event.rsvpNo.push(req.user.id) + await event.save() + req.io.emit('rsvp done', { data: 'RSVP successfully done!' }) + notification.heading = 'RSVP done!' + notification.content = 'RSVP successfully done!' + notificationHelper.addToNotificationForUser(req.user._id, res, notification, next) + res.status(HttpStatus.OK).json({ rsvpData: data }) + } catch (error) { + return res.status(HttpStatus.BAD_REQUEST).json({ error: error }) + } + } + if (maybe) { + try { + event.rsvpMaybe.push(req.user.id) + await event.save() + req.io.emit('rsvp done', { data: 'RSVP successfully done!' }) + notification.heading = 'RSVP done!' + notification.content = 'RSVP successfully done!' + notificationHelper.addToNotificationForUser(req.user._id, res, notification, next) + res.status(HttpStatus.OK).json({ rsvpData: data }) + } catch (error) { + return res.status(HttpStatus.BAD_REQUEST).json({ error: error }) + } + } + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + GetEventById: async (req, res, next) => { + const { id } = req.params + try { + const EventData = await Event.findById(id) + if (!EventData) { + return res.status(HttpStatus.NOT_FOUND).json({ error: 'No such Event is available!' }) + } + return res.status(HttpStatus.OK).json({ event: EventData }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + GetAllEvent: async (req, res, next) => { + try { + const EventData = await Event.find({}, {}, helper.paginate(req)) + .populate('createdBy', ['name.firstName', 'name.lastName', '_id', 'isAdmin']) + .sort({ eventDate: -1 }) + .lean() + return res.status(HttpStatus.OK).json({ events: EventData }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + deleteEvent: async (req, res, next) => { + const { id } = req.params + try { + const deleteEvent = await Event.findById(id) + if (!deleteEvent) { + return res.status(HttpStatus.NOT_FOUND).json({ message: 'No Event exists' }) + } + if (permission.check(req, res, deleteEvent.createdBy)) { + await Event.findByIdAndRemove(id) + req.io.emit('event deleted', { data: deleteEvent.eventName }) + notification.heading = 'Event deleted!' + 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' }) + } + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Not permitted!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + UpComingEvents: async (req, res, next) => { + try { + const events = await Event.find({ eventDate: { $gt: Date.now() } }, {}, helper.paginate(req)) + .sort({ eventDate: -1 }) + .exec() + console.log('Upcoming events ', events) + return res.status(HttpStatus.OK).json({ events }) + } catch (error) { + HANDLER.handleError(res, next) + } + }, + + getAllEventByUser: async (req, res, next) => { + try { + const events = await Event.find({ createdBy: req.user._id }, {}, helper.paginate(req)) + .sort({ eventDate: -1 }) + .populate('createdBy', '_id name.firstName name.lastName') + .exec() + return res.status(HttpStatus.OK).json({ events }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/notification.js b/app/controllers/notification.js new file mode 100644 index 0000000..96147b4 --- /dev/null +++ b/app/controllers/notification.js @@ -0,0 +1,55 @@ +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const Notifications = require('../models/Notifications') +const helper = require('../utils/paginate') +const User = require('../models/User') +const ProposalNotifications = require('../models/ProposalNotification') + +module.exports = { + // GET ALL THE NOTIFICATIONS FOR ALL + getOrgNotifications: async (req, res, next) => { + try { + const notifications = await Notifications.find( + {}, + {}, + helper.paginate(req) + ) + .lean() + .sort({ createdAt: -1 }) + .exec() + return res.status(HttpStatus.OK).json({ notifications }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + // GET LOGGED IN USER NOTIFICATIONS + getUserNotification: async (req, res, next) => { + const userId = req.user._id + try { + const user = await User.findById(userId) + if (!user) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: 'No such user exists!' }) + } + // get all notifications of existing user + const notifications = user.notifications + if (notifications.length === 0) { + return res.status(HttpStatus.OK).json({ msg: 'No new notifications!' }) + } + return res.status(HttpStatus.OK).json({ notifications }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + getProposalNotifications: async (req, res, next) => { + try { + const notifications = await ProposalNotifications.find({}) + console.log(notifications) + return res.status(HttpStatus.OK).json({ notifications }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/organization.js b/app/controllers/organization.js new file mode 100644 index 0000000..92627d8 --- /dev/null +++ b/app/controllers/organization.js @@ -0,0 +1,337 @@ +const Organization = require('../models/Organisation') +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const helper = require('../utils/uploader') +const notificationHelper = require('../utils/notif-helper') +const User = require('../models/User') +const Project = require('../models/Project') +const Event = require('../models/Event') +const permission = require('../utils/permission') +const TAGS = require('../utils/notificationTags') +const notification = { + heading: '', + content: '', + tag: '' +} + +module.exports = { + createOrganization: async (req, res, next) => { + const org = new Organization(req.body) + if (req.file) { + helper.mapToDb(req, org) + } + try { + await org.save() + req.io.emit('new org created', { data: org.name }) + notification.heading = 'New org!' + notification.content = `${org.name} is created!` + notification.tag = TAGS.NEW + notificationHelper.addToNotificationForAll(req, res, notification, next) + return res.status(HttpStatus.CREATED).json({ org }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + updateOrgDetails: async (req, res, next) => { + const { id } = req.params + const updates = Object.keys(req.body) + const allowedUpdates = [ + 'name', + 'description', + 'contactInfo', + 'image', + 'imgUrl', + 'adminInfo', + 'moderatorInfo' + ] + const isValidOperation = updates.every((update) => { + return allowedUpdates.includes(update) + }) + + if (!isValidOperation) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ error: 'invalid update' }) + } + try { + const org = await Organization.findById(id) + // check for permission (ONLY ADMINS CAN UPDATE) + if (!permission.check(req, res)) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: "You don't have the permission" }) + } + updates.forEach((update) => { + org[update] = req.body[update] + }) + if (req.file) { + helper.mapToDb(req, org) + } + await org.save() + res.status(HttpStatus.OK).json({ organization: org }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + getOrgDetailsById: async (req, res, next) => { + const { id } = req.params + try { + const orgData = await Organization.findById(id) + .populate('adminInfo', [ + 'name.firstName', + 'name.lastName', + 'email', + 'isAdmin' + ]) + .populate('moderatorInfo', [ + 'name.firstName', + 'name.lastName', + 'email', + 'isAdmin' + ]) + .sort({ createdAt: -1 }) + .lean() + .exec() + if (!orgData) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'No such organization exists!' }) + } + res.status(HttpStatus.OK).json({ organization: orgData }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + deleteOrg: async (req, res, next) => { + const { id } = req.params + try { + const org = await Organization.findByIdAndRemove(id) + if (!org) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'No such organization exists!' }) + } + // check for permission + if (!permission.check(req, res)) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: "You don't have the permission!" }) + } + req.io.emit('org deleted', { data: org.name }) + notification.heading = 'Org deleted!' + notification.content = `${org.name} is deleted!` + notification.tag = TAGS.DELETE + notificationHelper.addToNotificationForAll(req, res, notification, next) + return res.status(HttpStatus.OK).json({ organization: org }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + archiveOrg: async (req, res, next) => { + const { id } = req.params + try { + const org = await Organization.findById(id) + if (!org) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'No such organization exists!' }) + } + org.isArchived = true + await org.save() + return res.status(HttpStatus.OK).json({ organization: org }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + triggerMaintenance: async (req, res, next) => { + const { id } = req.params + try { + const organization = await Organization.findById(id) + // if org exists or not + if (!organization) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'No such organization exists!' }) + } + // if user is admin or not + const adminIds = organization.adminInfo.adminId + const isAdmin = adminIds.indexOf(req.user.id) || req.user.isAdmin + // user is admin then perform operation + if (isAdmin !== -1 || req.user.isAdmin) { + // toggle maintenance mode + organization.isMaintenance = !organization.isMaintenance + await organization.save() + notification.tag = TAGS.MAINTENANCE + + if (organization.isMaintenance) { + req.io.emit('org under maintenance', { data: organization.name }) + notification.heading = 'Maintenance mode on!' + notification.content = `${organization.name} is kept under maintenance!` + notificationHelper.addToNotificationForAll( + req, + res, + notification, + next + ) + return res + .status(HttpStatus.OK) + .json({ msg: 'Organization is kept under the maintenance!!' }) + } + + req.io.emit('org revoked maintenance', { data: organization.name }) + notification.heading = 'Maintenance mode off!' + notification.content = `${organization.name} is revoked from maintenance!` + return res + .status(HttpStatus.OK) + .json({ msg: 'Organization is recovered from maintenance!!' }) + } else { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: "You don't have access to triggerMaintenance!" }) + } + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + updateSettings: async (req, res, next) => { + const { id } = req.params + try { + // check if org exists + const organization = await Organization.findById(id) + if (!organization) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ msg: 'No Organization found!' }) + } + // check if user is admin or not + const adminIds = organization.adminInfo.adminId + const isAdmin = adminIds.indexOf(req.user.id) + const updates = Object.keys(req.body) + console.log('req.body ', req.body) + console.log('isAdmin ', isAdmin) + const allowedUpdates = ['settings', 'permissions', 'authentication'] + // if admin then check if valid update + if (isAdmin !== -1) { + const isValidOperation = updates.every((update) => { + return allowedUpdates.includes(update) + }) + // if valid update + if (isValidOperation) { + updates.forEach((update) => { + organization.options[update] = req.body[update] + }) + await organization.save() + return res + .status(HttpStatus.OK) + .json({ msg: 'Successfully updated!' }) + } + // invalid update + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: 'Invalid update' }) + } + // else not admin + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: "You don't have access to perform this operation!" }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + getOrgOverView: async (req, res, next) => { + const orgOverView = {} + try { + const org = await Organization.find({}) + if (!org) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No org exists!' }) + } + orgOverView.admins = org[0].adminInfo.length + orgOverView.members = await User.find({}).lean().count() + orgOverView.projects = await Project.find({}).lean().count() + orgOverView.events = await Event.find({}).lean().count() + return res.status(HttpStatus.OK).json({ orgOverView }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + // SEARCH FUNCTIONALITY + getMembers: async (req, res, next) => { + try { + const { search } = req.query + if (search) { + const queryTerm = search.split(' ') + const regex = new RegExp('^' + queryTerm + '$', 'i') + const member = await User.find({ + $or: [ + { 'name.firstName': { $regex: regex } }, + { 'name.lastName': { $regex: regex } } + ] + }) + .select('name email isAdmin info.about.designation isRemoved') + .lean() + .sort({ createdAt: -1 }) + .exec() + if (!member) { + return res.status(HttpStatus.OK).json({ msg: 'Member not found!' }) + } + return res.status(HttpStatus.OK).json({ member }) + } else { + const members = await User.find({}) + .select('name email isAdmin info.about.designation isRemoved') + .lean() + .sort({ createdAt: -1 }) + .exec() + if (members.length === 0) { + return res + .status(HttpStatus.OK) + .json({ msg: 'No members joined yet!' }) + } + return res.status(HttpStatus.OK).json({ members }) + } + } catch (error) { + HANDLER.handleError(res, error) + } + }, + // REMOVE ADMIN + removeAdmin: async (req, res, next) => { + try { + const { userId, orgId } = req.params + const org = await Organization.findById(orgId) + if (!org) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No org exists!' }) + } + // only permitted for admins + if (!req.user.isAdmin) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: 'You are not permitted!' }) + } + // console.log('Permitted to removeAdmin') + // REMOVE ADMINS FROM ADMINS LIST + const admins = org.adminInfo.adminId + console.log('adminIds ', admins) + const removableIndex = admins.indexOf(userId) + if (removableIndex === -1) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ msg: 'User is not an admin!' }) + } + // user is admin so remove + org.adminInfo.adminId.splice(removableIndex, 1) + await org.save() + // also make isAdmin false + const user = await User.findById(userId) + user.isAdmin = false + await user.save() + return res.status(HttpStatus.OK).json({ org }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/post.js b/app/controllers/post.js index 36c1ff2..affa4dc 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -1,32 +1,238 @@ -const userModel = require('../models/User') -const bcrypt = require('bcrypt') -const jwt = require('jsonwebtoken') +const PostModel = require('../models/Post') +const UserModel = require('../models/User') +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const imgUploadHelper = require('../utils/uploader') +const permission = require('../utils/permission') +const helper = require('../utils/paginate') module.exports = { - create: function (req, res, next) { - userModel.create({ name: req.body.name, email: req.body.email, password: req.body.password }, function (err, result) { - if (err) { - next(err) - } else { - res.json({ status: 'success', message: 'User added successfully!!!', data: null }) + // CREATE POST + create: async (req, res, next) => { + const post = new PostModel(req.body) + const userId = req.user.id.toString() + post.userId = userId + if (req.file) { + imgUploadHelper.mapToDb(req, post) + } + try { + await post.save() + // req.io.emit('new post created', { data: post.content }) + return res.status(HttpStatus.CREATED).json({ post }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // DELETE POST + delete: async (req, res, next) => { + const { id } = req.params + try { + const post = await PostModel.findById(id) + if (!post) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No post exists' }) + } + if (!permission.check(req, res, post.userId)) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'Bad delete request' }) } + await PostModel.findByIdAndRemove(id) + res.status(HttpStatus.OK).json({ post: post, msg: 'Deleted!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // UPDATE POST + updatePost: async (req, res, next) => { + const { id } = req.params + const updates = Object.keys(req.body) + const allowedUpdates = ['content', 'imgUrl'] + const isValidOperation = updates.every((update) => { + return allowedUpdates.includes(update) }) + + if (!isValidOperation) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'Invalid Update' }) + } + try { + const post = await PostModel.findById(id) + if (!post) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'No post exists' }) + } + if (!permission.check(req, res, post.userId)) { + return res + .status(HttpStatus.FORBIDDEN) + .json({ message: 'Bad update request' }) + } + updates.forEach((update) => { + post[update] = req.body[update] + }) + if (req.file) { + imgUploadHelper.mapToDb(req, post) + } + await post.save() + res.status(HttpStatus.OK).json({ post: post }) + } catch (error) { + HANDLER.handleError(res, error) + } }, - authenticate: function (req, res, next) { - userModel.findOne({ email: req.body.email }, function (err, userInfo) { - if (err) { - next(err) - } else { - if (bcrypt.compareSync(req.body.password, userInfo.password)) { - const token = jwt.sign({ id: userInfo._id }, req.app.get('secretKey'), { expiresIn: '1h' }) - res.json({ status: 'success', message: 'user found!!!', data: { user: userInfo, token: token } }) - } else { - res.json({ status: 'error', message: 'Invalid email/password!!!', data: null }) + + // GET POST BY ID + getPostById: async (req, res, next) => { + const { id } = req.params + try { + const post = await PostModel.findById(id) + .populate('comments', ['content', 'votes']) + .populate('userId', [ + 'name.firstName', + 'name.lastName', + 'email', + 'isAdmin' + ]) + .lean() + .exec() + if (!post) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'Post not found' }) + } + res.status(HttpStatus.OK).json({ post: post }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // GET ALL THE POSTS + getAllPost: async (req, res, next) => { + try { + const posts = await PostModel.find({}, {}, helper.paginate(req)) + .populate('userId', [ + 'name.firstName', + 'name.lastName', + 'email', + 'isAdmin' + ]) + .sort({ updatedAt: -1 }) + .exec() + if (!posts.length) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No posts found' }) + } + return res.status(HttpStatus.OK).json({ posts: posts }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // UPVOTE POST + upvote: async (req, res, next) => { + const { id } = req.params + const userId = req.user.id.toString() + try { + const post = await PostModel.findById(id) + if (!post) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'No post found' }) + } + // CHECKS IF THE USER HAS ALREADY UPVOTED THE COMMENT + post.votes.upVotes.user.filter((user) => { + if (JSON.stringify(user) === JSON.stringify(userId)) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ error: 'Bad request' }) } + }) + post.votes.upVotes.user.unshift(userId) + await post.save() + res.status(HttpStatus.OK).json({ post: post }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + getPostByUser: async (req, res, next) => { + try { + const posts = await PostModel.find( + { userId: req.user._id }, + {}, + helper.paginate(req) + ) + .populate('comments', ['content', 'votes']) + .populate('userId', [ + 'name.firstName', + 'name.lastName', + '_id', + 'isAdmin' + ]) + .sort({ updatedAt: -1 }) + .exec() + return res.status(HttpStatus.OK).json({ posts }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // PIN THE POST + pinPost: async (req, res, next) => { + const { id } = req.params + try { + const post = await PostModel.findById(id) + const user = await UserModel.findById(req.user._id) + if (!post) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ msg: 'No such post exists!' }) } - }) + // toggle pinned post + post.isPinned = !post.isPinned + // if already pinned then remove from their pinned items + const PinnedItems = user.pinned.postId + if (PinnedItems.length > 0) { + const pinnedPostIndex = PinnedItems.indexOf(id) + user.pinned.postId.splice(pinnedPostIndex, 1) + await user.save() + } else { + // save to the user pinned items + user.pinned.postId.unshift(id) + await user.save() + } + await post.save() + return res.status(HttpStatus.OK).json({ post }) + } catch (error) { + HANDLER.handleError(res, error) + } }, - test: function (req, res, next) { - res.json({ success: 'ulllu' }) + + // GET ALL PINNED POST + getPinned: async (req, res, next) => { + try { + const posts = await PostModel.find({}, {}, helper.paginate(req)) + .populate('userId', [ + 'name.firstName', + 'name.lastName', + 'email', + 'isAdmin' + ]) + .sort({ updatedAt: -1 }) + .exec() + // check for pinned post + const pinnedPost = posts.filter((post) => { + return post.isPinned === true + }) + // else return pinned posts + return res.status(HttpStatus.OK).json({ pinnedPost }) + } catch (error) { + HANDLER.handleError(res, error) + } } } diff --git a/app/controllers/project.js b/app/controllers/project.js new file mode 100644 index 0000000..784f9e4 --- /dev/null +++ b/app/controllers/project.js @@ -0,0 +1,108 @@ +const Project = require('../models/Project') +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const helper = require('../utils/paginate') +const permission = require('../utils/permission') + +module.exports = { + createProject: async (req, res, next) => { + try { + const project = await new Project(req.body) + project.createdBy = req.user._id + await project.save() + return res.status(HttpStatus.CREATED).json({ project }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + getAllProjects: async (req, res, next) => { + try { + const projects = await Project.find({}, {}, helper.paginate(req)) + .populate('createdBy', '_id name.firstName name.lastName email') + .sort({ updatedAt: -1 }) + .exec() + return res.status(HttpStatus.OK).json({ projects }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + getProjectById: async (req, res, next) => { + const { id } = req.params + try { + const project = await Project.findById(id) + .populate('createdBy', '_id name.firstName name.lastName email') + .lean() + .exec() + if (!project) { + return res.status(HttpStatus.OK).json({ msg: 'Post doesn\'t exists!' }) + } + return res.status(HttpStatus.OK).json({ project }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + updateProject: async (req, res, next) => { + const { id } = req.params + const updates = Object.keys(req.body) + const allowedUpdates = [ + 'projectName', + 'description', + 'imgUrl', + 'img', + 'version', + 'links', + 'contributors', + 'maintainers' + ] + const isValidOperation = updates.every((update) => { + return allowedUpdates.includes(update) + }) + try { + const project = await Project.findById(id) + .populate('createdBy', '_id name.firstName name.lastName email') + .exec() + if (!project) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such project exits!' }) + } + if (!isValidOperation) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Invalid update!' }) + } + updates.forEach((update) => { + project[update] = req.body[update] + }) + await project.save() + return res.status(HttpStatus.OK).json({ project }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + deleteProject: async (req, res, next) => { + const { id } = req.params + try { + const project = await Project.findById(id) + if (!project) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such project exits!' }) + } + // check if admin or user who created this project + if (permission.check(req, res, project.createdBy)) { + await Project.findByIdAndRemove(id) + return res.status(HttpStatus.OK).json({ msg: 'Project deleted!' }) + } + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Not permitted!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + projectCreatedByUser: async (req, res, next) => { + try { + const { id } = req.user + const projects = await Project.find({ createdBy: id }, {}, helper.paginate(req)) + .populate('createdBy', '_id name.firstName name.lastName email') + .sort({ updatedAt: -1 }) + .exec() + return res.status(HttpStatus.OK).json({ projects }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/proposal.js b/app/controllers/proposal.js new file mode 100644 index 0000000..d550cf1 --- /dev/null +++ b/app/controllers/proposal.js @@ -0,0 +1,292 @@ +const ProposalModel = require('../models/Proposal') +const UserModal = require('../models/User') +const HANDLER = require('../utils/response-helper') +const HttpStatus = require('http-status-codes') +const AWS = require('aws-sdk') +const TAGS = require('../utils/notificationTags') +const proposalNotificationHelper = require('../utils/proposal-notif-helper') + +module.exports = { + // Creating a proposal + createProposal: async (req, res, next) => { + const proposal = new ProposalModel(req.body) + const creator = req.body.creator + + try { + await proposal.save() + + const user = await UserModal.findById(creator) + const name = `${user.name.firstName} ${user.name.lastName}` + + req.io.emit('new proposal created', { + heading: 'New Proposal Created', + content: `New Proposal ${proposal.title} created by ${name}`, + tag: TAGS.NEW + }) + proposalNotificationHelper.addNotificationForAll( + req, + res, + { + heading: 'New Proposal Created', + content: `New Proposal ${proposal.title} created by ${name}`, + tag: TAGS.NEW + }, + next + ) + + res.status(HttpStatus.CREATED).json({ proposal }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // Updates the content of the proposal + saveProposal: async (req, res, next) => { + const { proposalId } = req.params + const { content, title, description } = req.body + + try { + const proposal = await ProposalModel.findByIdAndUpdate(proposalId, { + content: content, + title: title, + proposalDescription: description + }) + if (!proposal) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No proposal exists under the provided ID' }) + } + res.status(HttpStatus.OK).json({ proposal: proposal }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // attaches a file to the given proposal + attachFile: (req, res, next) => { + const { proposalId } = req.params + const file = req.file + const s3FileURL = process.env.AWS_UPLOADED_FILE_URL_LINK + + const s3bucket = new AWS.S3({ + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + region: process.env.AWS_REGION + }) + + var params = { + Bucket: process.env.AWS_BUCKET_NAME, + Key: file.originalname, + Body: file.buffer, + ContentType: file.mimetype, + ACL: 'public-read' + } + + s3bucket.upload(params, function (err, data) { + if (err) { + res.status(500).json({ error: true, Message: err }) + } else { + var newFileUploaded = { + fileLink: s3FileURL + file.originalname, + s3_key: params.Key + } + + ProposalModel.findOneAndUpdate( + { _id: proposalId }, + { $push: { attachments: newFileUploaded } }, + function (error, success) { + if (error) { + console.log(error) + } else { + console.log(success) + } + } + ) + + res.send({ data }) + } + }) + }, + + // Get proposals by userId + getByUserId: async (req, res, next) => { + const { userId } = req.params + + try { + const proposals = await ProposalModel.find({ creator: userId }) + + if (!proposals) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No proposals found for the given user ID' }) + } + return res.status(HttpStatus.OK).json({ proposal: proposals }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // Delete proposal by proposalId + deleteById: async (req, res, next) => { + try { + const proposalId = req.body.proposalId + + console.log(proposalId) + const result = await ProposalModel.findByIdAndDelete(proposalId) + const creator = result.creator + + const user = await UserModal.findById(creator) + const name = `${user.name.firstName} ${user.name.lastName}` + + proposalNotificationHelper.addNotificationForAll( + req, + res, + { + heading: 'Proposal Deleted', + content: `Proposal: "${result.title}" deleted by ${name}`, + tag: TAGS.NEW + }, + next + ) + req.io.emit('proposal deleted', { + heading: 'Proposal Deleted', + content: `Proposal: "${result.title}" deleted by ${name}`, + tag: TAGS.NEW + }) + + return res.status(HttpStatus.OK).json({ result: result }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // Changes the state of a given proposal + changeState: async (req, res, next) => { + const { proposalId } = req.params + const proposalStatus = req.body.proposalStatus + try { + const proposal = await ProposalModel.findByIdAndUpdate(proposalId, { + proposalStatus: proposalStatus + }) + if (!proposal) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No proposal exists under the provided ID' }) + } + + req.io.emit('proposal submitted', { + heading: 'Proposal Submitted', + content: `Proposal ${proposal.title} was submitted for review`, + tag: TAGS.NEW + }) + proposalNotificationHelper.addNotificationForAll( + req, + res, + { + heading: 'Proposal Submitted', + content: `Proposal "${proposal.title}" was submitted for review`, + tag: TAGS.NEW + }, + next + ) + res.status(HttpStatus.OK).json({ proposal: proposal }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // Obtains the proposal by given proposal ID + getProposalById: async (req, res, next) => { + const { proposalId } = req.params + + try { + const proposal = await ProposalModel.findById(proposalId) + + if (!proposal) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ error: 'Proposal not found' }) + } + return res.status(HttpStatus.OK).json({ proposal: proposal }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + getAllProposals: async (req, res, next) => { + console.log('All proposals called') + try { + const proposals = await ProposalModel.find({}) + if (!proposals.length) { + return res + .status(HttpStatus.NOT_FOUND) + .json({ message: 'No proposals found' }) + } + return res.status(HttpStatus.OK).json({ proposals: proposals }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + commentOnProposal: async (req, res, next) => { + const { proposalId, comment, userId, isAuthor, author } = req.body + + try { + const user = await UserModal.findById(userId) + if (!user) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'No user exists' }) + } + const name = `${user.name.firstName} ${user.name.lastName}` + + const proposal = await ProposalModel.updateOne( + { _id: proposalId }, + { $push: { comments: { userName: name, comment: comment } } } + ) + + const updatedProposal = await ProposalModel.findById(proposalId) + + if (!proposal) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'Proposal could not be found!' }) + } + if (!isAuthor) { + proposalNotificationHelper.addToNotificationForUser( + author, + res, + { + heading: 'New comment', + content: `New comments in your proposal "${updatedProposal.title}" by ${name}`, + tag: TAGS.COMMENT + }, + next + ) + } + + return res.status(HttpStatus.OK).json({ proposal: proposal }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + getProposalNotificationsByUser: async (req, res, next) => { + const userId = req.body.userId + + try { + const user = await UserModal.findById(userId) + if (!user) { + return res + .status(HttpStatus.BAD_REQUEST) + .json({ message: 'No user exists' }) + } + + return res + .status(HttpStatus.OK) + .json({ notifications: user.proposalNotifications }) + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/controllers/urlShortner.js b/app/controllers/urlShortner.js new file mode 100644 index 0000000..0235673 --- /dev/null +++ b/app/controllers/urlShortner.js @@ -0,0 +1,46 @@ +const UrlModel = require('../models/UrlShortner') +const HttpStatus = require('http-status-codes') +const validator = require('validator') + +module.exports = { + redirect: async (req, res) => { + try { + const { urlcode } = req.params + const url = await UrlModel.findOne({ urlCode: urlcode }) + if (url) { + return res.status(HttpStatus.OK).redirect(url.longUrl) + } else { + return res.status(HttpStatus.NOT_FOUND).json('No url found!') + } + } catch (error) { + res.status(HttpStatus.INTERNAL_SERVER_ERROR).json('Server error!') + } + }, + + shorten: async (req, res) => { + var { longUrl } = req.body + var baseurl = req.get('host') + var urlCode = Date.now() + if (validator.isURL(longUrl)) { + try { + var url = await UrlModel.findOne({ longUrl }) + if (url) { + return res.status(HttpStatus.OK).json(url) + } + var shortUrl = baseurl + '/' + urlCode + url = new UrlModel({ + longUrl: longUrl, + shortUrl: shortUrl, + urlCode: urlCode + }) + await url.save() + res.status(HttpStatus.CREATED).json(url) + } catch (error) { + console.log(error) + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json('Server error') + } + } else { + res.status(HttpStatus.NOT_FOUND).json('invalid long url') + } + } +} diff --git a/app/controllers/user.js b/app/controllers/user.js index a3e9f1e..622d812 100644 --- a/app/controllers/user.js +++ b/app/controllers/user.js @@ -1,29 +1,69 @@ const User = require('../models/User') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const emailController = require('./email') +const permission = require('../utils/permission') +const HANDLER = require('../utils/response-helper') +const notificationHelper = require('../utils/notif-helper') +const Projects = require('../models/Project') +const Events = require('../models/Event') +const TAGS = require('../utils/notificationTags') +const notification = { + heading: '', + content: '', + tag: '' +} module.exports = { + // CREATE USER createUser: async (req, res, next) => { const user = new User(req.body) try { + const isRegisteredUserExists = await User.findOne({ firstRegister: true }) + // for the first user who will be setting up the platform for their community + if (!isRegisteredUserExists) { + user.isAdmin = true + user.firstRegister = true + } await user.save() const token = await user.generateAuthToken() - res.status(201).json({ user: user, token: token }) + // Added fn to send email to activate account with warm message + await emailController.sendEmail(req, res, next, token) + return res.status(HttpStatus.CREATED).json({ user: user, token: token }) } catch (error) { console.log(error) - res.status(400).json({ error: error }) + return res.status(HttpStatus.NOT_ACCEPTABLE).json({ error: error }) } }, + // GET USER PROFILE userProfile: async (req, res, next) => { - res.json(req.user) + try { + const user = req.user + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exist!' }) + } + return res.status(HttpStatus.OK).json({ user }) + } catch (error) { + HANDLER.handleError(res, error) + } }, + + // USER PROFILE UPDATE userProfileUpdate: async (req, res, next) => { const updates = Object.keys(req.body) - const allowedUpdates = ['name', 'email', 'password', 'company', 'website', 'location', 'about'] + const allowedUpdates = [ + 'name', + 'email', + 'phone', + 'info', + 'about' + ] const isValidOperation = updates.every((update) => { return allowedUpdates.includes(update) }) if (!isValidOperation) { - return res.status(400).json({ error: 'invalid update' }) + return res.status(HttpStatus.BAD_REQUEST).json({ error: 'invalid update' }) } try { @@ -31,18 +71,323 @@ module.exports = { req.user[update] = req.body[update] }) await req.user.save() - res.status(200).json({ data: req.user }) + return res.status(HttpStatus.OK).json({ data: req.user }) + } catch (error) { + return res.status(HttpStatus.BAD_REQUEST).json({ error }) + } + }, + + // FORGOT PASSWORD REQUEST + forgotPasswordRequest: async (req, res) => { + const { email } = req.body + try { + const user = await User.findOne({ email: email }) + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'User not found!' }) + } + const token = jwt.sign({ _id: user._id, expiry: Date.now() + 10800000 }, process.env.JWT_SECRET) + await user.save() + return res.status(HttpStatus.OK).json({ success: true, token }) + } catch (error) { + if (process.env.NODE_ENV !== 'production' && error) { + console.log('Error in forgotPasswordRequest ', error) + } + return res.status(HttpStatus.BAD_REQUEST).json({ error }) + } + }, + + updatePassword: async (req, res, next) => { + const { password, id } = req.body + const { token } = req.params + try { + const decodedToken = jwt.verify(token, process.env.JWT_SECRET) + + if (Date.now() <= decodedToken.expiry) { + const user = await User.findById({ + _id: id + }) + if (!user) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'No such user' }) + } + user.password = password + await user.save() + const obj = { + userId: user._id + } + req.io.emit('Password update', obj) + notification.heading = 'Forgot password!' + notification.content = 'Password successfully updated!' + notification.tag = TAGS.UPDATE + notificationHelper.addToNotificationForUser(id, res, notification, next) + return res.status(HttpStatus.OK).json({ updated: true }) + } else { + if (process.env.NODE_ENV !== 'production') { + console.log('token expired') + } + res.status(HttpStatus.BAD_REQUEST).json({ error: 'Token expired' }) + } } catch (error) { - res.status(400).json({ error }) + if (process.env.NODE_ENV !== 'production' && error) { + console.log('Something went wrong ', error) + } + res.status(HttpStatus.BAD_REQUEST).json({ error }) } }, + + // LOGOUT USER + logout: async (req, res, next) => { + try { + req.user.tokens = [] + await req.user.save() + return res.status(HttpStatus.OK).json({ msg: 'User logged out Successfully!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // REMOVE USER userDelete: async (req, res, next) => { try { - await req.user.remove() - res.send({ data: 'user deletetion successful', user: req.user }) + if (permission.check(req, res)) { + await req.user.remove() + return res.send({ data: 'user deletion successful', user: req.user }) + } + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'You don\'t have permission!' }) } catch (error) { - console.log(error) - res.status(500).json({ error }) + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ error }) + } + }, + + // USER ACCOUNT ACTIVATION + activateAccount: async (req, res, next) => { + try { + const { token } = req.params + const decodedToken = jwt.verify(token, 'process.env.JWT_SECRET') + const expiryTime = decodedToken.iat + 24 * 3600 * 1000 // 24 hrs + if (expiryTime <= Date.now()) { + const user = await User.findById(decodedToken._id) + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'User not found!' }) + } + // if user found activate the account + user.isActivated = true + await user.save() + const obj = { + userId: user._id + } + req.io.emit('Account activate', obj) + notification.heading = 'Account activate!' + notification.content = 'Account successfully activated!' + notification.tag = TAGS.ACTIVATE + notificationHelper.addToNotificationForUser(user._id, res, notification, next) + return res.status(HttpStatus.OK).json({ msg: 'Succesfully activated!' }) + } + } catch (Error) { + return res.status(HttpStatus.BAD_REQUEST).json({ Error }) + } + }, + + // GET INVITE LINK + getInviteLink: async (req, res, next) => { + const token = jwt.sign({ _id: req.user._id, expiry: Date.now() + 24 * 3600 * 1000 }, process.env.JWT_SECRET) + const inviteLink = `${req.protocol}://${req.get('host')}/user/invite/${token}` + return res.status(HttpStatus.OK).json({ inviteLink: inviteLink }) + }, + + // PROCESS THE INVITE LINK + processInvite: async (req, res, next) => { + const { token } = req.params + const decodedToken = jwt.verify(token, process.env.JWT_SECRET) + // check if token not expired and sender exist in db then valid request + const user = await User.findById(decodedToken._id) + if (user && Date.now() <= decodedToken.expiry) { + console.log('Valid invite!') + return res.status(HttpStatus.OK).json({ success: true, msg: 'Redirect user to register in client side!' }) + } + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Invalid token!' }) + }, + + // ADD TO THE FOLLOWINGS LIST + addFollowing: async (req, res, next) => { + const { followId } = req.body + try { + if (followId === 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) + await user.save() + next() + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // ADD TO FOLLOWERS LIST + addFollower: async (req, res, next) => { + const { followId } = req.body + try { + const user = await User.findById(followId) + if (!user) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'No such user exists!' }) + } + // add to the followers list + user.followers.unshift(req.user.id) + await user.save() + const obj = { + name: req.user.name.firstName, + followId: user._id + } + req.io.emit('New follower', obj) + notification.heading = 'New follower!' + notification.content = `${req.user.name.firstName} started following you!` + notification.tag = TAGS.FOLLOWER + notificationHelper.addToNotificationForUser(user._id, res, notification, next) + const userData = await User.findById(req.user._id) + .populate('followings', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .exec() + return res.status(HttpStatus.OK).json({ user: userData }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // REMOVE FROM FOLLOWINGS LIST + removeFollowing: async (req, res, next) => { + const { followId } = req.body + try { + const user = await User.findById(req.user._id) + if (!user) { + return res.status(HttpStatus.OK).json({ msg: 'No such user exists!' }) + } + // check if followId is in following list or not + const followingIdArray = user.followings.map(followingId => followingId._id) + const isFollowingIdIndex = followingIdArray.indexOf(followId) + if (isFollowingIdIndex === -1) { + return res.status(HttpStatus.OK).json({ msg: 'You haven\'t followed the user!' }) + } else { + // remove from followings list + user.followings.splice(isFollowingIdIndex, 1) + await user.save() + } + next() + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // REMOVE FROM FOLLOWERS LIST + removeFollower: async (req, res, next) => { + const { followId } = req.body + try { + const user = await User.findById(followId) + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exists!' }) + } + const followersIdArray = user.followers.map((follower) => follower._id) + const isFollowingIndex = followersIdArray.indexOf(req.user._id) + if (isFollowingIndex === -1) { + return res.status(HttpStatus.OK).json({ msg: 'User is not following!' }) + } + user.followers.splice(isFollowingIndex, 1) + await user.save() + const userData = await User.findById(req.user._id) + .populate('followings', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .populate('followers', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .populate('blocked', ['name.firstName', 'name.lastName', 'info.about.designation', '_id', 'isAdmin']) + .exec() + return res.status(HttpStatus.OK).json({ user: userData }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // BLOCK THE USER + blockUser: async (req, res, next) => { + const { id } = req.params + try { + const user = await User.findById(req.user._id) + .populate('blocked', ['name.firstName', 'name.lastName', 'email']) + .exec() + if (!user) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'Invalid request!' }) + } + // check if admin + if (user.isAdmin === true) { + user.blocked.unshift(id) + await user.save() + return res.status(HttpStatus.OK).json({ user }) + } + // else not permitted + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'You don\'t have permission!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // UNBLOCK USER + unBlockUser: async (req, res, next) => { + const { id } = req.params + try { + const user = await User.findById(req.user._id) + .populate('blocked', ['name.firstName', 'name.lastName', 'email']) + .exec() + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exists!' }) + } + // if admin + if (user.isAdmin === true) { + const blockedIds = user.blocked.map(item => item._id) + const unblockIndex = blockedIds.indexOf(id) + console.log('UnblockIndex ', unblockIndex) + if (unblockIndex !== -1) { + user.blocked.splice(unblockIndex, 1) + await user.save() + return res.status(HttpStatus.OK).json({ user }) + } + return res.status(HttpStatus.NOT_FOUND).json({ user }) + } + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'You don\'t have permission!' }) + } catch (error) { + HANDLER.handleError(res, error) + } + }, + + // GET OVERALL PERSONAL OVERVIEW + getPersonalOverview: async (req, res, next) => { + const userId = req.user._id + const personalOverview = {} + try { + personalOverview.projects = await Projects.find({ createdBy: userId }).estimatedDocumentCount() + personalOverview.events = await Events.find({ createdBy: userId }).estimatedDocumentCount() + return res.status(HttpStatus.OK).json({ personalOverview }) + } catch (error) { + HANDLER.handleError(req, error) + } + }, + + // REMOVE USER + removeUser: async (req, res, next) => { + const { id } = req.params + try { + const user = await User.findById(id) + if (!user) { + return res.status(HttpStatus.NOT_FOUND).json({ msg: 'No such user exits!' }) + } + // only admins can remove + if (!req.user.isAdmin) { + return res.status(HttpStatus.BAD_REQUEST).json({ msg: 'You are not permitted!' }) + } + user.isRemoved = true + await user.save() + return res.status(HttpStatus.OK).json({ user }) + } catch (error) { + HANDLER.handleError(res, error) } } } diff --git a/app/middleware/activate.js b/app/middleware/activate.js new file mode 100644 index 0000000..5104d83 --- /dev/null +++ b/app/middleware/activate.js @@ -0,0 +1,19 @@ +const User = require('../models/User') +const HttpStatus = require('http-status-codes') + +const isActivated = async (req, res, next) => { + const { email } = req.body + try { + const user = await User.findOne({ email: email }) + if (!user) { + next(new Error('No such user is found!')) + } + if (user && !user.isActivated) { + next(new Error('Please activate the account!')) + } + } catch (Error) { + return res.status(HttpStatus.BAD_REQUEST).json({ Error }) + } +} + +module.exports = isActivated diff --git a/app/middleware/auth.js b/app/middleware/auth.js index 7acd520..67c266c 100644 --- a/app/middleware/auth.js +++ b/app/middleware/auth.js @@ -1,14 +1,38 @@ const jwt = require('jsonwebtoken') const User = require('../models/User') +const HttpStatus = require('http-status-codes') const auth = async (req, res, next) => { try { const token = req.header('Authorization').replace('Bearer ', '') - const decoded = jwt.verify(token, process.env.JWT_SECRET) + const decoded = jwt.verify(token, 'process.env.JWT_SECRET') const user = await User.findOne({ _id: decoded._id, 'tokens.token': token }) + .populate('followings', [ + 'name.firstName', + 'name.lastName', + 'info.about.designation', + '_id', + 'isAdmin' + ]) + .populate('followers', [ + 'name.firstName', + 'name.lastName', + 'info.about.designation', + '_id', + 'isAdmin' + ]) + .populate('blocked', [ + 'name.firstName', + 'name.lastName', + 'info.about.designation', + '_id', + 'isAdmin' + ]) + .exec() + console.log(user) if (!user) { throw new Error() @@ -18,7 +42,7 @@ const auth = async (req, res, next) => { next() } } catch (error) { - res.status(401).send({ error: 'Please authenticate' }) + res.status(HttpStatus.UNAUTHORIZED).send({ error: 'Please authenticate' }) } } diff --git a/app/middleware/maintenance.js b/app/middleware/maintenance.js new file mode 100644 index 0000000..0c2ffda --- /dev/null +++ b/app/middleware/maintenance.js @@ -0,0 +1,22 @@ +const Organization = require('../models/Organisation') +const HttpStatus = require('http-status-codes') + +const isUnderMaintenance = async (req, res, next) => { + try { + const org = await Organization.find({}) + if (!org) { + next(new Error('No org is found!')) + } + if (org[0] && org[0].isMaintenance) { + return res.status(HttpStatus.SERVICE_UNAVAILABLE).json({ + msg: 'Organization is kept under maintenance!' + }) + } else { + next() + } + } catch (Error) { + return res.status(HttpStatus.BAD_REQUEST).json({ Error }) + } +} + +module.exports = isUnderMaintenance diff --git a/app/models/Comment.js b/app/models/Comment.js index 0398a53..7daa41d 100644 --- a/app/models/Comment.js +++ b/app/models/Comment.js @@ -5,23 +5,11 @@ const Schema = mongoose.Schema const commentSchema = new Schema({ userId: { type: Schema.Types.ObjectId, - required: true, - ref: 'User', - validate (userId) { - if (validator.isEmpty(userId)) { - throw new Error('UserId is required!') - } - } + ref: 'User' }, postId: { type: Schema.Types.ObjectId, - required: true, - ref: 'Post', - validate (userId) { - if (validator.isEmpty(userId)) { - throw new Error('PostID is required!') - } - } + ref: 'Post' }, content: { type: String, @@ -35,28 +23,16 @@ const commentSchema = new Schema({ }, votes: { upVotes: { - count: { - type: Number, - default: 0 - }, - user: { - user_id: { - type: Schema.Types.ObjectId, - required: true, - ref: 'User' - } - } + user: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }] }, downVotes: { - count: { - type: Number, - default: 0 - }, - user_id: { + user: [{ type: Schema.Types.ObjectId, - required: true, ref: 'User' - } + }] } }, createdAt: { diff --git a/app/models/Event.js b/app/models/Event.js index 59473e7..782bf5c 100644 --- a/app/models/Event.js +++ b/app/models/Event.js @@ -47,27 +47,27 @@ const eventSchema = new Schema({ } } }, - rsvp: { - yes: { - type: Schema.Types.ObjectId, - ref: 'User' - }, - no: { - type: Schema.Types.ObjectId, - ref: 'User' - }, - mayBe: { - type: Schema.Types.ObjectId, - ref: 'User' - } + eventTime: { + type: String }, + rsvpYes: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + rsvpMaybe: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + rsvpNo: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], slots: { - type: Number, - required: true, + type: String, default: 0, validate (slots) { - if (validator.isEmpty(slots)) { - throw new Error('Slots is required!') + if (!validator.isNumeric(slots)) { + throw new Error('Slots should be a number') } } }, @@ -84,11 +84,15 @@ const eventSchema = new Schema({ eventDate: { type: Date, required: true, - validate (eventDate) { - if (validator.isEmpty(eventDate)) { - throw new Error('Event date is required!') - } - } + default: Date.now() + }, + createdBy: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + isOnline: { + type: Boolean, + default: false }, createdAt: { type: Date, diff --git a/app/models/Notifications.js b/app/models/Notifications.js new file mode 100644 index 0000000..c44c0b7 --- /dev/null +++ b/app/models/Notifications.js @@ -0,0 +1,19 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const NotificationSchema = new Schema({ + heading: { + type: String + }, + content: { + type: String + }, + tag: { + type: String + }, + createdAt: { + type: Date, + default: Date.now() + } +}) +module.exports = mongoose.model('Notification', NotificationSchema) diff --git a/app/models/Organisation.js b/app/models/Organisation.js index fa63da8..8891abe 100644 --- a/app/models/Organisation.js +++ b/app/models/Organisation.js @@ -46,32 +46,36 @@ const orgSchema = new Schema({ } } }, - logo: { - type: Buffer, + image: { + data: Buffer, contentType: String }, - logoUrl: { + imgUrl: { type: String, trim: true, - validator (logoUrl) { - if (!validator.isURL(logoUrl)) { - throw new Error('Invalid logo URL!') + validator (imgUrl) { + if (!validator.isURL(imgUrl)) { + throw new Error('Invalid image URL!') } } }, contactInfo: { - emailId: { + email: { type: String, required: true, - validate (emailId) { - if (validator.isEmpty(emailId)) { + validate (email) { + if (validator.isEmpty(email)) { throw new Error('EmailId or org is required!') } - if (!validator.isEmail(emailId)) { + if (!validator.isEmail(email)) { throw new Error('Invalid emailId') } } }, + adminEmail: { + type: String, + trim: true + }, website: { type: String, trim: true, @@ -87,29 +91,91 @@ const orgSchema = new Schema({ }, chattingPlatform: [ { + _id: false, link: { type: String } } ] }, - adminInfo: { - type: Object, - required: true, - validate (adminInfo) { - if (validator.isEmpty(adminInfo)) { - throw new Error('Admin info is required!') + options: { + _id: false, + settings: { + enableEmail: { + type: Boolean, + default: true + }, + language: { + type: String, + enum: ['English', 'French', 'German'], + default: 'English' + }, + timeFormat: { + type: String, + enum: ['24', '12'], + default: '12' + } + }, + permissions: { + sendInvite: { + type: String, + enum: ['BOTH', 'ADMINS', 'NONE'], + default: 'BOTH' + }, + canCreateManage: { + type: String, + enum: ['BOTH', 'ADMINS', 'MEMBERS'], + default: 'BOTH' + }, + canChangeEmail: { + type: Boolean, + default: true + }, + canChangeName: { + type: Boolean, + default: true + } + }, + authentication: { + email: { + type: Boolean, + default: true + }, + google: { + type: Boolean, + default: false + }, + github: { + type: Boolean, + default: false + }, + gitlab: { + type: Boolean, + default: false } } }, + adminInfo: { + _id: false, + adminId: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }] + }, moderatorInfo: { - type: Object, - required: true, - validate (adminInfo) { - if (validator.isEmpty(adminInfo)) { - throw new Error('Admin info is required!') - } - } + _id: false, + adminId: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }] + }, + isArchived: { + type: Boolean, + default: false + }, + isMaintenance: { + type: Boolean, + default: false }, createdAt: { type: Date, diff --git a/app/models/Post.js b/app/models/Post.js index ae528b0..36b2a7d 100644 --- a/app/models/Post.js +++ b/app/models/Post.js @@ -13,6 +13,10 @@ const PostSchema = new Schema({ } } }, + userId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, image: { data: Buffer, contentType: String @@ -28,36 +32,20 @@ const PostSchema = new Schema({ }, votes: { upVotes: { - count: { - type: Number, - default: 0 - }, - users: { - user: { - type: Schema.Types.ObjectId, - ref: 'User', - required: true - } - } - }, - downVotes: { - count: { - type: Number, - default: 0 - }, - users: { - user: { - type: Schema.Types.ObjectId, - ref: 'User', - required: true - } - } + user: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }] } }, comments: { type: Schema.Types.ObjectId, ref: 'Comment' }, + isPinned: { + type: Boolean, + default: false + }, createdAt: { type: Date, required: true, diff --git a/app/models/Project.js b/app/models/Project.js index cc49ac8..7d5ada8 100644 --- a/app/models/Project.js +++ b/app/models/Project.js @@ -22,14 +22,10 @@ const projectSchema = new Schema({ type: String, required: true, trim: true, - minlength: 10, validate (short) { if (validator.isEmpty(short)) { throw new Error('Short description for the project is required!') } - if (!validator.isLength(short)) { - throw new Error('Short description should be min 10 characters long!') - } } }, long: { @@ -52,24 +48,14 @@ const projectSchema = new Schema({ }, version: { type: String, - trim: true, - required: true, - validate (version) { - if (validator.isEmpty(version)) { - throw new Error('Short description for the project is required!') - } - } + trim: true }, links: [ { githubLink: { type: String, - required: true, trim: true, validate (githubLink) { - if (validator.isEmpty(githubLink)) { - throw new Error('Project github link is required!') - } if (!validator.isURL(githubLink)) { throw new Error('Invalid project url!') } @@ -89,6 +75,10 @@ const projectSchema = new Schema({ type: Schema.Types.ObjectId, ref: 'User' }], + createdBy: { + type: Schema.Types.ObjectId, + ref: 'User' + }, createdAt: { type: Date, default: Date.now() diff --git a/app/models/Proposal.js b/app/models/Proposal.js new file mode 100644 index 0000000..4a1f97b --- /dev/null +++ b/app/models/Proposal.js @@ -0,0 +1,43 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const proposalSchema = new Schema( + { + title: { + type: String, + required: true + }, + content: { + type: String, + required: true + }, + proposalStatus: { + type: String, + default: 'draft' + }, + creator: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + proposalDescription: { + type: String + }, + attachments: [{ fileLink: String, s3_key: String }], + + createdAt: { + type: Date, + required: true, + default: Date.now() + }, + updatedAt: { + type: Date, + required: true, + default: Date.now() + }, + comments: [{ userName: String, comment: String }] + }, + { timestamps: true } +) + +module.exports = mongoose.model('Proposal', proposalSchema) diff --git a/app/models/ProposalNotification.js b/app/models/ProposalNotification.js new file mode 100644 index 0000000..920b39f --- /dev/null +++ b/app/models/ProposalNotification.js @@ -0,0 +1,22 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const ProposalNotification = new Schema({ + heading: { + type: String + }, + proposal: { + type: String + }, + content: { + type: String + }, + tag: { + type: String + }, + createdAt: { + type: Date, + default: new Date().toISOString().substring(0, 25) + } +}) +module.exports = mongoose.model('ProposalNotification', ProposalNotification) diff --git a/app/models/UrlShortner.js b/app/models/UrlShortner.js new file mode 100644 index 0000000..e18b4e0 --- /dev/null +++ b/app/models/UrlShortner.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose') + +const urlShortnerSchema = new mongoose.Schema({ + longUrl: { + type: String, + required: true + }, + urlCode: { + type: String + }, + shortUrl: { + type: String + } +}) + +const shortURL = mongoose.model('shortURL', urlShortnerSchema) +module.exports = shortURL diff --git a/app/models/User.js b/app/models/User.js index a2af919..9dd5946 100644 --- a/app/models/User.js +++ b/app/models/User.js @@ -46,7 +46,6 @@ const UserSchema = new mongoose.Schema({ phone: { type: String, trim: true, - unique: true, minlength: 10, validate (phone) { if (!validator.isLength(phone, { min: 10, max: 10 })) { @@ -89,7 +88,6 @@ const UserSchema = new mongoose.Schema({ about: { shortDescription: { type: String, - required: true, validate (shortDescription) { if (validator.isEmpty(shortDescription)) { throw new Error('Short description is required') @@ -137,6 +135,80 @@ const UserSchema = new mongoose.Schema({ } } }, + notifications: [ + { + heading: { + type: String + }, + content: { + type: String + }, + tag: { + type: String + } + } + ], + proposalNotifications: [ + { + heading: { + type: String + }, + content: { + type: String + }, + tag: { + type: String + }, + createdAt: { + type: Date, + required: true, + default: Date.now() + } + } + ], + followers: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + ], + followings: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + ], + blocked: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'User' + } + ], + pinned: { + _id: false, + postId: [ + { + type: mongoose.Schema.Types.ObjectId, + ref: 'Post' + } + ] + }, + firstRegister: { + type: Boolean, + default: false + }, + isAdmin: { + type: Boolean, + default: false + }, + isActivated: { + type: Boolean, + default: false + }, + isRemoved: { + type: Boolean, + default: false + }, createdAt: { type: Date, required: true, @@ -147,20 +219,24 @@ const UserSchema = new mongoose.Schema({ required: true, default: Date.now() }, - tokens: [{ - token: { - type: String, - required: true + tokens: [ + { + token: { + type: String, + required: true + } } - }] -} -) + ] +}) // generate auth token // Schema Methods, needs to be invoked by an instance of a Mongoose document UserSchema.methods.generateAuthToken = async function () { const user = this - const token = jwt.sign({ _id: user._id.toString() }, process.env.JWT_SECRET) + const token = jwt.sign( + { _id: user._id.toString() }, + 'process.env.JWT_SECRET' + ) user.tokens = user.tokens.concat({ token: token }) await user.save() @@ -175,11 +251,11 @@ UserSchema.statics.findByCredentials = async (email, password) => { }) if (!user) { - throw new Error('Unable to login') + throw new Error('No such user') } else { const isMatch = await bcrypt.compare(password, user.password) if (!isMatch) { - throw new Error('Unable to login') + throw new Error('Incorrect password provided') } else { return user } diff --git a/app/routes/auth.js b/app/routes/auth.js index 2c78f57..f42fd96 100644 --- a/app/routes/auth.js +++ b/app/routes/auth.js @@ -1,10 +1,13 @@ const express = require('express') const router = express.Router() const authController = require('../controllers/auth') +// const isActivated = require('../middleware/activate') +const isUnderMaintenance = require('../middleware/maintenance') // user login router.post( '/login', + isUnderMaintenance, authController.authenticateUser ) diff --git a/app/routes/comment.js b/app/routes/comment.js new file mode 100644 index 0000000..5d16b55 --- /dev/null +++ b/app/routes/comment.js @@ -0,0 +1,55 @@ +const express = require('express') +const router = express.Router() +const auth = require('../middleware/auth') +const commentController = require('../controllers/comment') +const isUnderMaintenance = require('../middleware/maintenance') + +// CREATE COMMENT +router.post( + '/:id', + isUnderMaintenance, + auth, + commentController.comment +) + +// DELETE COMMENT BY ID +router.delete( + '/:id', + isUnderMaintenance, + auth, + commentController.delete +) + +// UPDATE COMMENT BY ID +router.patch( + '/:id', + isUnderMaintenance, + auth, + commentController.update +) + +// GET COMMENT BY POST ID +router.get( + '/:id', + isUnderMaintenance, + auth, + commentController.getCommentByPost +) + +// UPVOTE COMMENT BY COMMENT ID +router.patch( + '/upvote/:id', + isUnderMaintenance, + auth, + commentController.upvote +) + +// DOWNVOTE COMMENT BY COMMENT ID +router.patch( + '/downvote/:id', + isUnderMaintenance, + auth, + commentController.downvote +) + +module.exports = router diff --git a/app/routes/event.js b/app/routes/event.js new file mode 100644 index 0000000..0d0dc02 --- /dev/null +++ b/app/routes/event.js @@ -0,0 +1,67 @@ +const express = require('express') +const auth = require('../middleware/auth') +const router = express.Router() +const eventController = require('../controllers/event') +const isUnderMaintenance = require('../middleware/maintenance') + +// get all the events +router.get( + '/all', + isUnderMaintenance, + auth, + eventController.GetAllEvent +) + +// get all the events +router.get( + '/upcoming', + isUnderMaintenance, + auth, + eventController.UpComingEvents +) + +// create an event +router.post( + '/', + isUnderMaintenance, + auth, + eventController.createEvent +) +// get event by id +router.get( + '/:id', + isUnderMaintenance, + auth, + eventController.GetEventById +) +// update an event +router.patch( + '/:id', + isUnderMaintenance, + auth, + eventController.updateEvent +) +// rsvp by user +router.patch( + '/rsvp/:id', + isUnderMaintenance, + auth, + eventController.rsvp +) +// delete an event +router.delete( + '/:id', + isUnderMaintenance, + auth, + eventController.deleteEvent +) + +// GET ALL EVENT POSTED BY A USER +router.get( + '/me/all', + isUnderMaintenance, + auth, + eventController.getAllEventByUser +) + +module.exports = router diff --git a/app/routes/index.js b/app/routes/index.js index 741fddc..57acc51 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -1,9 +1,14 @@ var express = require('express') var router = express.Router() const documentationUrl = 'https://documenter.getpostman.com/view/1159934/SWDze1Rp' + /* GET home page. */ router.get('/', function (req, res, next) { res.redirect(documentationUrl) }) +// router.get('/:shorturl', (req, res, next) => { +// res.redirect('/shortUrl/' + req.params.shorturl) +// }) + module.exports = router diff --git a/app/routes/notification.js b/app/routes/notification.js new file mode 100644 index 0000000..58152bd --- /dev/null +++ b/app/routes/notification.js @@ -0,0 +1,26 @@ +const express = require('express') +const router = express.Router() +const auth = require('../middleware/auth') +const isUnderMaintenance = require('../middleware/maintenance') +const notificationController = require('../controllers/notification') + +// GET NOTIFICATIONS FOR ALL +router.get( + '/org/all', + isUnderMaintenance, + auth, + notificationController.getOrgNotifications +) + +// GET NOTIFICATIONS FOR LOGGED IN USER +router.get( + '/user/all', + isUnderMaintenance, + auth, + notificationController.getUserNotification +) + +// GET NOTICATIONS FOR PROPOSALS +router.get('/proposal/all', notificationController.getProposalNotifications) + +module.exports = router diff --git a/app/routes/organisation.js b/app/routes/organisation.js new file mode 100644 index 0000000..a69abc9 --- /dev/null +++ b/app/routes/organisation.js @@ -0,0 +1,87 @@ +const express = require('express') +const router = express.Router() +const auth = require('../middleware/auth') +const OrgController = require('../controllers/organization') +const uploader = require('../utils/uploader') +const isUnderMaintenance = require('../middleware/maintenance') + +// CREATE ORG +router.post( + '/', + isUnderMaintenance, + uploader.upload.single('image'), + auth, + OrgController.createOrganization +) + +// GET ORG DETAILS BY ID +router.get( + '/:id', + isUnderMaintenance, + auth, + OrgController.getOrgDetailsById +) + +// UPDATE ORG DETAILS +router.patch( + '/:id', + isUnderMaintenance, + uploader.upload.single('image'), + auth, + OrgController.updateOrgDetails +) + +// DELETE ORG +router.delete( + '/:id', + isUnderMaintenance, + auth, + OrgController.deleteOrg +) + +// ARCHIVE ORG +router.patch( + '/archive/:id', + isUnderMaintenance, + auth, + OrgController.archiveOrg +) + +// TRIGGER MAINTENANCE MODE +router.patch( + '/:id/maintenance', + auth, + OrgController.triggerMaintenance +) + +// GET ORG OVERVIEW FOR INSIGHT PAGE +router.get( + '/overview/all', + auth, + OrgController.getOrgOverView +) + +// GET MEMBERS FOR INSIGHT PAGE +router.get( + '/members/all', + auth, + OrgController.getMembers +) + +// UPDATE THE ORG SETTINGS +router.patch( + '/:id/settings/update', + isUnderMaintenance, + auth, + OrgController.updateSettings +) + +// REMOVE ADMIN +router.patch( + '/remove/:orgId/:userId', + isUnderMaintenance, + auth, + OrgController.removeAdmin +) + +module.exports = router diff --git a/app/routes/post.js b/app/routes/post.js index 1d6d72a..0208282 100644 --- a/app/routes/post.js +++ b/app/routes/post.js @@ -1,35 +1,82 @@ +require('../../config/mongoose') const express = require('express') const router = express.Router() -const userController = require('../controllers/post') +const postController = require('../controllers/post') +const uploader = require('../utils/uploader') +const auth = require('../middleware/auth') +const isUnderMaintenance = require('../middleware/maintenance') // CREATE A POST router.post( '/', - userController.create + isUnderMaintenance, + auth, + uploader.upload.single('image'), + postController.create ) -// GET ALL POSTS OF A USER +// GET ALL POSTS router.get( - '/', - userController.authenticate + '/all_posts', + isUnderMaintenance, + auth, + postController.getAllPost ) -// GET PARTICULAR POST OF A USER -router.get( +// UPDATE POST +router.patch( '/:id', - userController.test + isUnderMaintenance, + auth, + uploader.upload.single('image'), + postController.updatePost ) -// UPDATE A TASK -router.patch( +// DELETE A POST BY ID +router.delete( '/:id', - userController.test + isUnderMaintenance, + auth, + postController.delete ) -// DELETE A TASK -router.delete( +// GET POST BY ID +router.get( '/:id', - userController.test + isUnderMaintenance, + auth, + postController.getPostById +) + +// UPVOTE POST BY POST ID +router.patch( + '/upvote/:id', + isUnderMaintenance, + auth, + postController.upvote +) + +// GET POST PER USER +router.get( + '/me/all', + auth, + postController.getPostByUser +) + +// PIN THE POST +router.patch( + '/pin/:id/', + isUnderMaintenance, + auth, + postController.pinPost +) + +// GET ALL PINNED POSTS +router.get( + '/all/pinned/', + isUnderMaintenance, + auth, + postController.getPinned ) module.exports = router diff --git a/app/routes/project.js b/app/routes/project.js new file mode 100644 index 0000000..178a3f6 --- /dev/null +++ b/app/routes/project.js @@ -0,0 +1,55 @@ +const express = require('express') +const projectController = require('../controllers/project') +const router = express.Router() +const auth = require('../middleware/auth') +const isUnderMaintenance = require('../middleware/maintenance') + +// ADD PROJECT +router.post( + '/', + isUnderMaintenance, + auth, + projectController.createProject +) + +// GET ALL PROJECTS +router.get( + '/', + isUnderMaintenance, + auth, + projectController.getAllProjects +) + +// GET PROJECT BY ID +router.get( + '/:id', + isUnderMaintenance, + auth, + projectController.getProjectById +) + +// UPDATE PROJECT INFO +router.patch( + '/:id', + isUnderMaintenance, + auth, + projectController.updateProject +) + +// DELETE PROJECT +router.delete( + '/:id', + isUnderMaintenance, + auth, + projectController.deleteProject +) + +// GET PROJECTS CREATED BY A USER +router.get( + '/me/all', + isUnderMaintenance, + auth, + projectController.projectCreatedByUser +) + +module.exports = router diff --git a/app/routes/proposal.js b/app/routes/proposal.js new file mode 100644 index 0000000..4a3650c --- /dev/null +++ b/app/routes/proposal.js @@ -0,0 +1,37 @@ +const express = require('express') +const router = express.Router() +const proposalController = require('../controllers/proposal') + +// Create a new proposal +router.post('/', proposalController.createProposal) + +// Save the content of a proposal +router.patch('/:proposalId', proposalController.saveProposal) + +// Attach file to the given proposal +router.post('/attach/:proposalId', proposalController.attachFile) + +// Get proposals by userId +router.get('/user/:userId', proposalController.getByUserId) + +// get proposal by proposalId +router.get('/:proposalId', proposalController.getProposalById) + +// Deletes a proposal by given proposalId +router.delete('/', proposalController.deleteById) + +// Update proposal state +router.patch('/change/:proposalId', proposalController.changeState) + +// Get all the proposals +router.post('/all', proposalController.getAllProposals) + +// Comment on the given proposala +router.post('/comment', proposalController.commentOnProposal) + +router.post( + '/notifications', + proposalController.getProposalNotificationsByUser +) + +module.exports = router diff --git a/app/routes/urlShortner.js b/app/routes/urlShortner.js new file mode 100644 index 0000000..68f2203 --- /dev/null +++ b/app/routes/urlShortner.js @@ -0,0 +1,17 @@ +const express = require('express') +const router = express.Router() +const shortnerController = require('../controllers/urlShortner') + +// Redirects the ShortURL back to LongURL +router.get( + '/:urlcode', + shortnerController.redirect +) + +// Shorten the LongURL and saves in DB +router.post( + '/shorten', + shortnerController.shorten +) + +module.exports = router diff --git a/app/routes/user.js b/app/routes/user.js index 99b8c87..0c9be6e 100644 --- a/app/routes/user.js +++ b/app/routes/user.js @@ -2,16 +2,21 @@ const express = require('express') const router = express.Router() const userController = require('../controllers/user') const auth = require('../middleware/auth') +const isUnderMaintenance = require('../middleware/maintenance') +// const email = require('../middleware/email') // create a user router.post( '/', + isUnderMaintenance, + // email, userController.createUser ) // get user profile router.get( '/me', + isUnderMaintenance, auth, userController.userProfile ) @@ -19,15 +24,110 @@ router.get( // update user info router.patch( '/me', + isUnderMaintenance, auth, userController.userProfileUpdate ) +// user forgot password request +router.patch( + '/password_reset/request', + isUnderMaintenance, + userController.forgotPasswordRequest +) + +// update password +router.patch( + '/password_reset/:token', + isUnderMaintenance, + userController.updatePassword +) + +// get invite link (for sender) +router.get( + '/invite', + isUnderMaintenance, + auth, + userController.getInviteLink +) + +// process invite link (for receiver) +router.get( + '/invite/:token', + isUnderMaintenance, + userController.processInvite +) + +// activate account +router.get( + '/activate/:token', + isUnderMaintenance, + userController.activateAccount +) + // delete a user router.delete( '/me', + isUnderMaintenance, auth, userController.userDelete ) +// LOGOUT USER +router.post( + '/logout', + auth, + userController.logout +) + +// follow the user +router.patch( + '/follow', + isUnderMaintenance, + auth, + userController.addFollowing, + userController.addFollower +) + +// unFollow the user +router.patch( + '/unfollow', + isUnderMaintenance, + auth, + userController.removeFollowing, + userController.removeFollower +) + +// BLOCK THE USER +router.patch( + '/block/:id', + isUnderMaintenance, + auth, + userController.blockUser +) + +// UNBLOCK THE USER +router.patch( + '/unblock/:id', + isUnderMaintenance, + auth, + userController.unBlockUser +) + +// GET PERSONAL OVERVIEW +router.get( + '/overview', + isUnderMaintenance, + auth, + userController.getPersonalOverview +) + +// REMOVE USER +router.patch( + '/remove/:id', + isUnderMaintenance, + auth, + userController.removeUser +) + module.exports = router diff --git a/app/utils/console-helper.js b/app/utils/console-helper.js new file mode 100644 index 0000000..003b207 --- /dev/null +++ b/app/utils/console-helper.js @@ -0,0 +1,6 @@ +const ConsoleHelper = (data) => { + if (process.env.NODE_ENV === 'production') return + console.log(data) +} + +module.exports = ConsoleHelper diff --git a/app/utils/notif-helper.js b/app/utils/notif-helper.js new file mode 100644 index 0000000..19f347b --- /dev/null +++ b/app/utils/notif-helper.js @@ -0,0 +1,26 @@ +const User = require('../models/User') +const Notifications = require('../models/Notifications') + +module.exports = { + // Notifications for a user + addToNotificationForUser: async (userId, res, obj, next) => { + try { + console.log('adding to user\'s notifications') + const user = await User.findById(userId) + user.notifications.unshift(obj) + await user.save() + } catch (error) { + console.log(error) + } + }, + // Notifications for all + addToNotificationForAll: async (req, res, obj, next) => { + const newNotification = new Notifications(obj) + try { + await newNotification.save() + console.log('newNotifications ', newNotification) + } catch (error) { + console.log(error) + } + } +} diff --git a/app/utils/notificationTags.js b/app/utils/notificationTags.js new file mode 100644 index 0000000..b7dbebe --- /dev/null +++ b/app/utils/notificationTags.js @@ -0,0 +1,11 @@ +const tags = { + RSVP: 'RSVP', + UPDATE: 'Update', + DELETE: 'Delete', + NEW: 'New!', + MAINTENANCE: 'Maintenance', + FOLLOWER: 'Follower', + ACTIVATE: 'Activate', + COMMENT: 'Comment' +} +module.exports = tags diff --git a/app/utils/paginate.js b/app/utils/paginate.js new file mode 100644 index 0000000..69c0954 --- /dev/null +++ b/app/utils/paginate.js @@ -0,0 +1,10 @@ +module.exports = { + paginate: (req) => { + const query = {} + const pagination = req.query.pagination ? parseInt(req.query.pagination) : 10 + const currentPage = req.query.page ? parseInt(req.query.page) : 1 + query.skip = (currentPage - 1) * pagination + query.limit = pagination + return query + } +} diff --git a/app/utils/permission.js b/app/utils/permission.js new file mode 100644 index 0000000..f00a933 --- /dev/null +++ b/app/utils/permission.js @@ -0,0 +1,23 @@ +const HANDLER = require('../utils/response-helper') + +module.exports = { + check: async (req, res, creatorId = 0) => { + const userId = req.user.id.toString() + try { + // if user is an admin + if (req.user.isAdmin) { + console.log('user is admin! ') + return true + } + // if user is post/event/project/comment creator + if (JSON.stringify(userId) === JSON.stringify(creatorId)) { + console.log('user is creator!') + return true + } + // else + return false + } catch (error) { + HANDLER.handleError(res, error) + } + } +} diff --git a/app/utils/proposal-notif-helper.js b/app/utils/proposal-notif-helper.js new file mode 100644 index 0000000..bcbcb33 --- /dev/null +++ b/app/utils/proposal-notif-helper.js @@ -0,0 +1,26 @@ +const User = require('../models/User') +const ProposalNotification = require('../models/ProposalNotification') + +module.exports = { + // Notifications for a user + addToNotificationForUser: async (userId, res, obj, next) => { + try { + console.log("adding to user's notifications") + const user = await User.findById(userId) + user.proposalNotifications.unshift(obj) + await user.save() + } catch (error) { + console.log(error) + } + }, + // Notifications for all + addNotificationForAll: async (req, res, obj, next) => { + const newNotification = new ProposalNotification(obj) + + try { + await newNotification.save() + } catch (error) { + console.log(error) + } + } +} diff --git a/app/utils/status-codes.js b/app/utils/status-codes.js deleted file mode 100644 index e69de29..0000000 diff --git a/app/utils/uploader.js b/app/utils/uploader.js new file mode 100644 index 0000000..239abc1 --- /dev/null +++ b/app/utils/uploader.js @@ -0,0 +1,41 @@ +const multer = require('multer') +const fs = require('fs') +const path = require('path') + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, path.join(__dirname, '../uploads/')) + }, + filename: (req, file, cb) => { + console.log('files ', file.originalname) + cb(null, file.originalname) + } +}) + +// type of files allowed +const fileFilter = (req, file, cb) => { + if (file.originalname.match(/\.(jpg|JPG|jpeg|JPEG|png|PNG|gif|GIF)$/)) { + cb(null, true) + } else { + cb(null, false) + } +} + +exports.upload = multer({ + storage: storage, + limits: { + fileSize: 1024 * 1024 * 10 // 10 mb + }, + fileFilter: fileFilter, + upload: (err) => { + if (err instanceof multer.MulterError) { + throw new Error('error in uploading ' + err) + } + } +}) + +exports.mapToDb = (req, db) => { + const img = fs.readFileSync(req.file.path) + db.image.data = img + db.image.contentType = 'image/png' +} diff --git a/bin/www b/bin/www old mode 100755 new mode 100644 index 2b4e4ed..f03de80 --- a/bin/www +++ b/bin/www @@ -5,7 +5,7 @@ */ // for .env file to work require('dotenv').config() -var app = require('../app') +var app = require('../app').app var debug = require('debug')('donut-backend:server') var http = require('http') diff --git a/config/fileHandlingConstants.js b/config/fileHandlingConstants.js new file mode 100644 index 0000000..d760cb5 --- /dev/null +++ b/config/fileHandlingConstants.js @@ -0,0 +1,5 @@ +module.exports.fileParameters = { + limit: '200mb', + extended: true, + parameterLimit: 1000000 +} diff --git a/config/mongoose.js b/config/mongoose.js index 73bd299..25aa95b 100644 --- a/config/mongoose.js +++ b/config/mongoose.js @@ -1,8 +1,15 @@ const mongoose = require('mongoose') -mongoose.connect(process.env.DATABASE_URL, { - useNewUrlParser: true, - useCreateIndex: true, - useUnifiedTopology: true, - useFindAndModify: false -}) +mongoose + .connect(process.env.DATABASE_URL, { + useNewUrlParser: true, + useCreateIndex: true, + useUnifiedTopology: true, + useFindAndModify: false + }) + .then(() => { + console.log('mongodb connection successful') + }) + .catch((err) => { + console.log('mongodb connection error', err) + }) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..469d76b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +version: "3" +services: + server: + container_name: server + restart: always + expose: + - "5000" + build: + context: . + dockerfile: Dockerfile.dev + volumes: + - ./:/server + ports: + - "5000:5000" + links: + - mongo + env_file: + - .env.dev + mongo: + container_name: mongo + image: mongo + volumes: + - db-data:/data/db + ports: + - "27017:27017" +volumes: + db-data: diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..96f2b1c --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,30 @@ +version: "3" +services: + server: + container_name: server-prod + restart: always + expose: + - "5000" + build: + context: . + dockerfile: Dockerfile.prod + ports: + - "5000:5000" + links: + - mongo + environment: + - PORT=5000 + - NODE_ENV="production" + - JWT_SECRET="thisismysupersecrettokenjustkidding" + - DATABASE_URL="mongodb://mongo:27017/donut-development" + - SENDGRID_API_KEY='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' + - SOCKET_PORT=8810 + mongo: + container_name: mongo + image: mongo + volumes: + - db-data:/data/db + ports: + - "27017:27017" +volumes: + db-data: diff --git a/package-lock.json b/package-lock.json index 4547b5b..fd1b330 100644 --- a/package-lock.json +++ b/package-lock.json @@ -430,6 +430,33 @@ "@types/yargs": "^13.0.0" } }, + "@sendgrid/client": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/client/-/client-7.0.0.tgz", + "integrity": "sha512-Wo3Cs8XJ6F2UFYgZJ3WmRVj7y/cTgyBfkwCOxJKml3DnsAbtc0RozkBOvvIAyBKNF1LE6hqQPgckspg59BO6Vg==", + "requires": { + "@sendgrid/helpers": "^7.0.0", + "axios": "^0.19.2" + } + }, + "@sendgrid/helpers": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/helpers/-/helpers-7.0.0.tgz", + "integrity": "sha512-sAoZTH3WBURV5Gep6Y9AhsuLQ1KW4TEzn1MKKBhSGFnYTWBOc562f+ee1j9XI0/7pdI/I2hUWuiw0vhY3joBDA==", + "requires": { + "chalk": "^2.0.1", + "deepmerge": "^4.2.2" + } + }, + "@sendgrid/mail": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sendgrid/mail/-/mail-7.0.0.tgz", + "integrity": "sha512-HlRath7i1imzvB87HvIPJDsv81MuoF4H4OgVMvQO4MTclHe6oJaORJlHFr0+82sCEdQqEOH33rBrEb4j2FfxMA==", + "requires": { + "@sendgrid/client": "^7.0.0", + "@sendgrid/helpers": "^7.0.0" + } + }, "@types/babel__core": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.3.tgz", @@ -573,6 +600,11 @@ "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, + "after": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" + }, "agent-base": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", @@ -651,7 +683,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -677,6 +708,11 @@ } } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -751,6 +787,11 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, + "arraybuffer.slice": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" + }, "asn1": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", @@ -787,8 +828,7 @@ "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" }, "asynckit": { "version": "0.4.0", @@ -802,6 +842,34 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sdk": { + "version": "2.691.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.691.0.tgz", + "integrity": "sha512-HV/iANH5PJvexubWr/oDmWMKtV/n1shtrACrLIUa5vTXIT6O7CzUouExNOvOtFMZw8zJkLmyEpa/0bDpMmo0Zg==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -814,6 +882,14 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -860,6 +936,11 @@ "babel-plugin-jest-hoist": "^24.9.0" } }, + "backo2": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -920,6 +1001,21 @@ } } }, + "base64-arraybuffer": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz", + "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=" + }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -946,12 +1042,25 @@ "tweetnacl": "^0.14.3" } }, + "better-assert": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz", + "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=", + "requires": { + "callsite": "1.0.0" + } + }, "binary-extensions": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true }, + "blob": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", + "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + }, "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", @@ -1121,6 +1230,16 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" }, + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1129,8 +1248,39 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } }, "bytes": { "version": "3.1.0", @@ -1154,6 +1304,11 @@ "unset-value": "^1.0.0" } }, + "callsite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1191,7 +1346,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1362,7 +1516,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -1370,8 +1523,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -1388,17 +1540,37 @@ "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, + "component-bind": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "component-inherit": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "configstore": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz", @@ -1479,6 +1651,15 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1499,6 +1680,11 @@ "which": "^1.2.9" } }, + "crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==" + }, "crypto-random-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", @@ -1584,6 +1770,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1666,6 +1857,38 @@ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", "dev": true }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff-sequences": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", @@ -1758,6 +1981,92 @@ "once": "^1.4.0" } }, + "engine.io": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.4.2.tgz", + "integrity": "sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "0.3.1", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "ws": "^7.1.2" + }, + "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==" + }, + "ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" + } + } + }, + "engine.io-client": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.4.3.tgz", + "integrity": "sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw==", + "requires": { + "component-emitter": "~1.3.0", + "component-inherit": "0.0.3", + "debug": "~4.1.0", + "engine.io-parser": "~2.2.0", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "ws": "~6.1.0", + "xmlhttprequest-ssl": "~1.5.4", + "yeast": "0.1.2" + }, + "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==" + }, + "ws": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.1.4.tgz", + "integrity": "sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, + "engine.io-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.0.tgz", + "integrity": "sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w==", + "requires": { + "after": "0.8.2", + "arraybuffer.slice": "~0.0.7", + "base64-arraybuffer": "0.1.5", + "blob": "0.0.5", + "has-binary2": "~1.0.2" + } + }, "env-cmd": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.0.1.tgz", @@ -1844,8 +2153,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.12.0", @@ -2351,6 +2659,11 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "exec-sh": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", @@ -2740,6 +3053,24 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2764,9 +3095,9 @@ } }, "formidable": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz", - "integrity": "sha512-Fs9VRguL0gqGHkXS5GQiMCr1VhZBxz0JnJs4JmMp/2jL18Fmbzvv7vOFRU+U8TBkHEE/CX1qDXzJplVULgsLeg==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.2.tgz", + "integrity": "sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q==", "dev": true }, "forwarded": { @@ -3528,11 +3859,30 @@ "function-bind": "^1.1.1" } }, + "has-binary2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", + "requires": { + "isarray": "2.0.1" + }, + "dependencies": { + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + } + } + }, + "has-cors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" + }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.0", @@ -3614,6 +3964,11 @@ "sshpk": "^1.7.0" } }, + "http-status-codes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-1.4.0.tgz", + "integrity": "sha512-JrT3ua+WgH8zBD3HEJYbeEgnuQaAnUeRRko/YojPAJjGmIfGD3KPU/asLdsLwKjfxOmQe5nXMQ0pt/7MyapVbQ==" + }, "https-proxy-agent": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", @@ -3649,6 +4004,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3709,6 +4069,11 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "indexof": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4749,6 +5114,11 @@ } } }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5320,6 +5690,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -5564,6 +5949,11 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "object-component": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz", + "integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -5802,6 +6192,22 @@ "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==", "dev": true }, + "parseqs": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz", + "integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=", + "requires": { + "better-assert": "~1.0.0" + } + }, + "parseuri": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz", + "integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=", + "requires": { + "better-assert": "~1.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6005,6 +6411,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6707,6 +7118,145 @@ } } }, + "socket.io": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz", + "integrity": "sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg==", + "requires": { + "debug": "~4.1.0", + "engine.io": "~3.4.0", + "has-binary2": "~1.0.2", + "socket.io-adapter": "~1.1.0", + "socket.io-client": "2.3.0", + "socket.io-parser": "~3.4.0" + }, + "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==" + } + } + }, + "socket.io-adapter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", + "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + }, + "socket.io-client": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.3.0.tgz", + "integrity": "sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA==", + "requires": { + "backo2": "1.0.2", + "base64-arraybuffer": "0.1.5", + "component-bind": "1.0.0", + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "engine.io-client": "~3.4.0", + "has-binary2": "~1.0.2", + "has-cors": "1.1.0", + "indexof": "0.0.1", + "object-component": "0.0.3", + "parseqs": "0.0.5", + "parseuri": "0.0.5", + "socket.io-parser": "~3.3.0", + "to-array": "0.1.4" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "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" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "socket.io-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.0.tgz", + "integrity": "sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~3.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + } + } + }, + "socket.io-parser": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", + "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "requires": { + "component-emitter": "1.2.1", + "debug": "~4.1.0", + "isarray": "2.0.1" + }, + "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "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" + } + }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6861,6 +7411,11 @@ "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", @@ -7000,7 +7555,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -7142,6 +7696,11 @@ "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", "dev": true }, + "to-array": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" + }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -7279,6 +7838,11 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "uglify-js": { "version": "3.6.8", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.8.tgz", @@ -7426,6 +7990,22 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", @@ -7728,6 +8308,30 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "xmlhttprequest-ssl": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", + "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", @@ -7808,6 +8412,11 @@ "dev": true } } + }, + "yeast": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" } } } diff --git a/package.json b/package.json index 6e6cf30..dd8f1d4 100644 --- a/package.json +++ b/package.json @@ -6,25 +6,34 @@ "start": "node ./bin/www", "dev": "env-cmd -f .env.dev nodemon ./bin/www", "pretest": "eslint --ignore-path .gitignore .", + "lint": "eslint .", "test": "env-cmd -f .env.test jest --detectOpenHandles && codecov -t 2b12ad97-07e0-45b2-a569-8aa2fd3e8c54" }, "dependencies": { + "@sendgrid/mail": "^7.0.0", + "aws-sdk": "^2.691.0", "bcrypt": "^3.0.6", "body-parser": "^1.19.0", "cookie-parser": "~1.4.4", + "cors": "^2.8.5", + "crypto": "^1.0.1", "debug": "~2.6.9", "dotenv": "^8.2.0", "ejs": "~2.6.1", "express": "^4.16.4", + "http-status-codes": "^1.4.0", "jsonwebtoken": "^8.5.1", "mongoose": "^5.7.7", "morgan": "^1.9.1", + "multer": "^1.4.2", + "socket.io": "^2.3.0", "validator": "^10.11.0" }, "jest": { "testEnvironment": "node", "coverageDirectory": "./coverage/", - "collectCoverage": true + "collectCoverage": true, + "testTimeout": 30000 }, "devDependencies": { "codecov": "^3.6.1", diff --git a/socket.js b/socket.js new file mode 100644 index 0000000..e91787f --- /dev/null +++ b/socket.js @@ -0,0 +1,53 @@ +const webSocker = require('./app').io + +module.exports = { + socketEvents: (io = webSocker) => { + let count = 0 + + io.on('connection', function (socket) { + console.log('Socket conn count: ' + count++) + io.emit('user connected') + + socket.on('test', (data) => { + console.log('test invoked') + io.emit('test response', { data: data }) + }) + + // PROJECT RELATED NOTIFICATIONS + socket.on('new project added', (data) => { + console.log('New project data ->', data) + io.emit('new project', { data: data }) + }) + + // EVENTS RELATED NOTIFICATIONS + socket.on('new event addeed', (data) => { + io.emit('new event', { data: data }) + }) + + // POST RELATED NOTIFICATIONS + socket.on('create post event', (data) => { + console.log('create post event invoked') + io.emit('new post', { + data: data + }) + }) + + // INTERNET RELATED ISSUE NOTIFICATIONS + socket.on('internet issue emit', (data) => { + console.log('Internet issue in ') + io.emit('internet issue', { data: data }) + }) + + socket.on('internet issue resolved emit', (data) => { + io.emit('internet issue resolved', { data: data }) + }) + + socket.on('disconnect', function () { + io.emit('user disconnected') + }) + socket.on('test', () => { + io.emit('test response') + }) + }) + } +} diff --git a/test/comment.test.js b/test/comment.test.js new file mode 100644 index 0000000..cb8e64b --- /dev/null +++ b/test/comment.test.js @@ -0,0 +1,313 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const request = require('supertest') +const Post = require('../app/models/Post') +const User = require('../app/models/User') +const Comment = require('../app/models/Comment') +const randomDigit = Math.floor(Math.random() * 90 + 10) + +const testUserId = new mongoose.Types.ObjectId() +const testPostId = new mongoose.Types.ObjectId() +const testCommentId = new mongoose.Types.ObjectId() +let token = '' + +const demoComment = { + content: 'test comment content', + userId: testUserId, + postId: testPostId, + votes: { + upVotes: { + user: [] + }, + downVotes: { + user: [] + } + } +} + +const demoPost = { + _id: testPostId, + content: 'test post content', + userId: testUserId, + votes: { + upVotes: { + user: [] + }, + downVotes: { + user: [] + } + } +} + +const updateComment = { + content: 'updated comment content' +} + +const upvoteComment = { + content: 'test comment content', + userId: testUserId, + votes: { + upVotes: { + user: [ + testUserId + ] + }, + downVotes: { + user: [] + } + } +} + +const downvoteComment = { + content: 'test comment content', + userId: testUserId, + votes: { + upVotes: { + user: [] + }, + downVotes: { + user: [ + testUserId + ] + } + } +} + +const testComment = { + _id: testCommentId, + ...demoComment +} + +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: testUserId, + ...demoUser, + tokens: [{ + token: jwt.sign({ + _id: testUserId + }, process.env.JWT_SECRET) + }] +} +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await Comment.deleteMany() + await new User(testUser).save() + await new Post(demoPost).save() + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app) + .post('/auth/login') + .send({ + email: testUser.email, + password: testUser.password + }) + token = response.body.token + done() +}) + +/** + * This deletes all the existing user in database, + * and creates a new user in database with the provided details. + */ +beforeEach(async () => { + await Comment.deleteMany() + await new Comment(testComment).save() +}) + +/** + * Testing post creation + */ +test('Should create new comment', async (done) => { + const response = await request(app) + .post(`/comment/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send(demoComment) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const comment = await Comment.findById(response.body.comment._id) + expect(comment).not.toBeNull() + + const userId = response.body.comment.userId + const postId = response.body.comment.postId + + // Assertions about the response + expect(response.body).toMatchObject({ + comment: { + content: demoComment.content, + userId: `${userId}`, + postId: `${postId}`, + votes: { + upVotes: { + user: demoComment.votes.upVotes.user + }, + downVotes: { + user: demoComment.votes.downVotes.user + } + } + } + }) + done() +}) + +/** + * Testing post update + */ +test('Should update the Comment data', async (done) => { + await request(app) + .patch(`/comment/${testCommentId}`) + .set('Authorization', `Bearer ${token}`) + .send(updateComment) + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing post deletion + */ + +test('Should delete comment', async (done) => { + await request(app) + .delete(`/comment/${testCommentId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + // Assert that post was deleted + const comment = await Comment.findById(testPostId) + expect(comment).toBeNull() + done() +}) + +/** + * Testing GET comment for Post + */ + +test('Should get comment for post', async (done) => { + await request(app) + .get(`/comment/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing upvote post + */ + +test('Should upvote the comment', async (done) => { + const response = await request(app) + .patch(`/comment/upVote/${testCommentId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + const userId = response.body.comment.userId + const postId = response.body.comment.postId + expect(response.body).toMatchObject({ + comment: { + content: upvoteComment.content, + userId: `${userId}`, + postId: `${postId}`, + votes: { + upVotes: { + user: response.body.comment.votes.upVotes.user + }, + downVotes: { + user: response.body.comment.votes.downVotes.user + } + } + } + }) + done() +}) + +/** + * Testing downvote post + */ + +test('Should downvote the post', async (done) => { + const response = await request(app) + .patch(`/comment/downVote/${testCommentId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + const userId = response.body.comment.userId + const postId = response.body.comment.postId + + expect(response.body).toMatchObject({ + comment: { + content: downvoteComment.content, + userId: `${userId}`, + postId: `${postId}`, + votes: { + upVotes: { + user: response.body.comment.votes.upVotes.user + }, + downVotes: { + user: response.body.comment.votes.downVotes.user + } + } + } + }) + done() +}) + +/** + * TODO: FIX ERROR + * This is a temporary fix to issue: + * Jest has detected the following 1 open handle potentially keeping Jest from exiting + */ +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // delete all the posts post testing + await Comment.deleteMany() + // 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 new file mode 100644 index 0000000..672168a --- /dev/null +++ b/test/event.test.js @@ -0,0 +1,317 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const request = require('supertest') +const Event = require('../app/models/Event') +const User = require('../app/models/User') +const randomDigit = Math.floor(Math.random() * 90 + 10) + +const testUserId = new mongoose.Types.ObjectId() +const demoEvent = { + description: { + shortDescription: 'this is short description', + longDescription: 'this is a very long description' + }, + slots: '2345222', + isOnline: false, + createdAt: '2020-04-04T06:53:28.018Z', + eventName: 'Student Developer meeting', + location: 'New delhi', + eventDate: '2020-04-04T06:53:28.018Z' +} + +const demoRsvp = { + yes: true +} +const demoUpdatedEvent = { + description: { + shortDescription: 'this is short description', + longDescription: 'this is a very long description' + }, + slots: '2345222', + isOnline: false, + createdAt: '2020-04-04T06:53:28.018Z', + eventName: 'Student Developer and mentors meeting', + location: 'New York', + eventDate: '2020-04-04T06:53:28.018Z' +} + +const testEventId = new mongoose.Types.ObjectId() +const testEvent = { + _id: testEventId, + ...demoEvent +} + +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: testUserId, + ...demoUser, + email: `test${randomDigit + Math.random() * 10}@mailinator.com`, + phone: `12335678${randomDigit}`, + tokens: [{ + token: jwt.sign({ + _id: testUserId + }, 'process.env.JWT_SECRET') + }] +} + +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await Event.deleteMany() + await User.deleteMany() + server = app.listen(4000, () => { + global.agent = request.agent(server) + done() + }) +}) + +/** + * This deletes all the existing user in database, + * and creates a new user in database with the provided details. + */ +beforeEach(async (done) => { + await Event.deleteMany() + await new Event(testEvent).save() + await User.deleteMany() + await new User(testUser).save() + done() +}) + +test('Should signup new user', async (done) => { + const response = await request(app) + .post('/user') + .send(demoUser) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const user = await User.findById(response.body.user._id) + expect(user).not.toBeNull() + + // Assertions about the response + // expect(response.body.user.name.firstName).toBe('Rupesh') + // OR + expect(response.body).toMatchObject({ + user: { + name: { + firstName: demoUser.name.firstName, + lastName: demoUser.name.lastName + }, + email: demoUser.email, + phone: demoUser.phone, + info: { + about: { + skills: demoUser.info.about.skills, + shortDescription: demoUser.info.about.shortDescription, + longDescription: demoUser.info.about.longDescription, + website: demoUser.info.about.website, + designation: demoUser.info.about.designation, + education: demoUser.info.about.education, + location: demoUser.info.about.location + } + } + } + }) + expect(user.password).not.toBe('abc12345') // to check hashing + done() +}) + +/** Testing user login */ +test('Login existing user', async (done) => { + const response = await request(app) + .post('/auth/login') + .send({ + email: testUser.email, + password: testUser.password + }) + .expect(HttpStatus.OK) + + const user = await User.findById(testUserId) + expect(response.body.token).toBe(user.tokens[1].token) + done() +}) +/** + * Testing event creation + */ +test('Should create new event', async (done) => { + const response = await request(app) + .post('/event') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send(demoEvent) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const event = await Event.findById(response.body.event._id) + expect(event).not.toBeNull() + + // Assertions about the response + expect(response.body).toMatchObject({ + event: { + description: { + shortDescription: demoEvent.description.shortDescription, + longDescription: demoEvent.description.longDescription + }, + slots: demoEvent.slots, + isOnline: demoEvent.isOnline, + createdAt: demoEvent.createdAt, + eventName: demoEvent.eventName, + location: demoEvent.location, + eventDate: demoEvent.eventDate + } + }) + done() +}) + +/** + * Testing event updation + */ + +test('Should update event', async (done) => { + const response = await request(app) + .patch(`/event/${testEventId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send(demoUpdatedEvent) + .expect(HttpStatus.OK) + + // Assert that db was changed + const updatedEvent = await Event.findById(response.body.event._id) + expect(updatedEvent).not.toBeNull() + done() +}) + +/** + * Testing for the RSVP + */ + +test('Should submit the RSVP', async (done) => { + const response = await request(app) + .patch(`/event/rsvp/${testEventId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send(demoRsvp) + .expect(HttpStatus.OK) + const rsvpData = await Event.findById(response.body.rsvpData._id) + expect(rsvpData).not.toBeNull() + done() +}) + +/** + * Testing for event deletion + */ + +test('Should delete event', async (done) => { + await request(app) + .delete(`/event/${testEventId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + + // Assert that event was deleted + const event = await Event.findById(testEventId) + expect(event).toBeNull() + done() +}) + +/** + * Testing for get event by id + */ + +test('Should get event by id', async (done) => { + await request(app) + .get(`/event/${testEventId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing for get all events + */ + +test('Should get all the event', async (done) => { + await request(app) + .get('/event/all') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing for the upcoming event + */ +test('Should get all the upcoming event', async (done) => { + await request(app) + .get('/event/upcoming') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing for the events created by a particular user + */ + +test('Should get all the events created by user', async (done) => { + await request(app) + .get('/event/me/all') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * TODO: FIX ERROR + * This is a temporary fix to issue: + * Jest has detected the following 1 open handle potentially keeping Jest from exiting + */ +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // delete all the events post testing + await Event.deleteMany() + // 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 new file mode 100644 index 0000000..12f966b --- /dev/null +++ b/test/organisation.test.js @@ -0,0 +1,185 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const request = require('supertest') +const HttpStatus = require('http-status-codes') +const Organization = require('../app/models/Organisation') +const User = require('../app/models/User') +const jwt = require('jsonwebtoken') +const adminId = new mongoose.Types.ObjectId() +const moderatorId = new mongoose.Types.ObjectId() +const randomDigit = Math.floor(Math.random() * 90 + 10) +let orgId = '' +let token = '' + +const testOrg = { + name: 'test Organization', + description: { + shortDescription: 'this is short description', + longDescription: 'this is long description' + }, + contactInfo: { + email: 'organisation@test.com', + website: 'www.codeuino.org', + adminInfo: `${adminId}`, + moderatorInfo: `${moderatorId}` + } +} + +const updatedTestOrg = { + name: 'Updated test Organization', + description: { + shortDescription: 'this is updated short description', + longDescription: 'this is updated long description' + }, + contactInfo: { + email: 'updated@test.com', + website: 'www.codeuino.org', + adminInfo: `${adminId}`, + moderatorInfo: `${moderatorId}` + } +} + +const testUser = { + 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' + } + }, + tokens: [{ + token: jwt.sign({ + _id: `${adminId}` + }, process.env.JWT_SECRET) + }] +} + +let server +/** + * This will pe performed once at the beginning of all the test + */ +beforeAll(async (done) => { + await Organization.deleteMany() + await new User(testUser).save() + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app) + .post('/auth/login') + .send({ + email: testUser.email, + password: testUser.password + }) + token = response.body.token + done() +}) + +/** CREATE THE ORG **/ +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.org._id + /** DB must be changed **/ + const org = await Organization.findById(response.body.org._id) + expect(org).not.toBeNull() + + /** Check the response **/ + expect(response.body).toMatchObject({ + org: { + isArchived: false, + _id: `${orgId}`, + name: `${testOrg.name}`, + description: { + shortDescription: `${testOrg.description.shortDescription}`, + longDescription: `${testOrg.description.longDescription}` + }, + contactInfo: { + email: `${testOrg.contactInfo.email}`, + website: `${testOrg.contactInfo.website}` + } + } + }) + done() + }) +}) + +/** GET ORG DATA**/ +describe('GET /org/:id', () => { + test('Should fetch the Organization data', async (done) => { + await request(app) + .get(`/org/${orgId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() + }) +}) + +/** UPDATE ORG DETAILS **/ +describe('PATCH /org/:id', () => { + test('Should update the Organization data', async (done) => { + await request(app) + .patch(`/org/${orgId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedTestOrg) + .expect(HttpStatus.OK) + done() + }) +}) + +/** DELETE ORGANIZATION**/ +describe('DELETE /org/:id', () => { + test('Should delete the organization', async (done) => { + await request(app) + .delete(`/org/${orgId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + /** Check if deleted or not **/ + const org = await Organization.findById(orgId) + expect(org).toBeNull() + done() + }) +}) + +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // delete all the organization post testing + await Organization.deleteMany() + // 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 new file mode 100644 index 0000000..3095604 --- /dev/null +++ b/test/post.test.js @@ -0,0 +1,254 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const request = require('supertest') +const Post = require('../app/models/Post') +const User = require('../app/models/User') +const randomDigit = Math.floor(Math.random() * 90 + 10) + +const testUserId = new mongoose.Types.ObjectId() +let token = '' +const demoPost = { + content: 'test post content', + userId: testUserId, + votes: { + upVotes: { + user: [] + }, + downVotes: { + user: [] + } + } +} + +const updatePost = { + content: 'updated post content' +} + +const upvotePost = { + content: 'test post content', + userId: testUserId, + votes: { + upVotes: { + user: [ + testUserId + ] + }, + downVotes: { + user: [] + } + } +} + +const testPostId = new mongoose.Types.ObjectId() +const testPost = { + _id: testPostId, + ...demoPost +} + +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: testUserId, + ...demoUser, + email: `test${randomDigit}@mailinator.com`, + phone: `12345678${randomDigit}`, + tokens: [{ + token: jwt.sign({ + _id: testUserId + }, process.env.JWT_SECRET) + }] +} +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await Post.deleteMany() + await new User(testUser).save() + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app) + .post('/auth/login') + .send({ + email: testUser.email, + password: testUser.password + }) + token = response.body.token + done() +}) + +/** + * This deletes all the existing user in database, + * and creates a new user in database with the provided details. + */ +beforeEach(async () => { + await Post.deleteMany() + await new Post(testPost).save() +}) + +/** + * Testing post creation + */ +test('Should create new post', async (done) => { + const response = await request(app) + .post('/post') + .set('Authorization', `Bearer ${token}`) + .send(demoPost) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const post = await Post.findById(response.body.post._id) + + expect(post).not.toBeNull() + + const userId = response.body.post.userId + + // Assertions about the response + expect(response.body).toMatchObject({ + post: { + content: demoPost.content, + userId: `${userId}`, + votes: { + upVotes: { + user: response.body.post.votes.upVotes.user + } + } + } + }) + done() +}) + +/** + * Testing post deletion + */ + +test('Should delete post', async (done) => { + await request(app) + .delete(`/post/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + // Assert that post was deleted + const post = await Post.findById(testPostId) + expect(post).toBeNull() + done() +}) + +/** + * Testing GET post by id + */ + +test('Should get single post by id', async (done) => { + await request(app) + .get(`/post/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing upvote post + */ + +test('Should upvote the post', async (done) => { + const response = await request(app) + .patch(`/post/upvote/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + + const userId = response.body.post.userId + + expect(response.body).toMatchObject({ + post: { + content: upvotePost.content, + userId: `${userId}`, + votes: { + upVotes: { + user: response.body.post.votes.upVotes.user + } + } + } + }) + done() +}) + +/** + * Testing post update + */ +test('Should update the Post data', async (done) => { + await request(app) + .patch(`/post/${testPostId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatePost) + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing get post of a particular user + */ + +test('Should retrieve all posts created by a user', async (done) => { + await request(app) + .get('/post/me/all') + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * TODO: FIX ERROR + * This is a temporary fix to issue: + * Jest has detected the following 1 open handle potentially keeping Jest from exiting + */ +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // delete all the posts post testing + await Post.deleteMany() + // 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 new file mode 100644 index 0000000..dd22655 --- /dev/null +++ b/test/project.test.js @@ -0,0 +1,220 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const request = require('supertest') +const Project = require('../app/models/Project') +const User = require('../app/models/User') +const randomDigit = Math.floor(Math.random() * 90 + 10) +const pagination = 10 +const page = 1 + +const testUserId = new mongoose.Types.ObjectId() +const testProjectId = new mongoose.Types.ObjectId() +let token = '' + +const demoProject = { + projectName: 'testing project', + description: { + short: 'Short description should be min 10 characters long!', + long: 'this is long description' + }, + version: '1.0.1', + links: [{ + githubLink: 'https://github.com/codeuino' + }] +} + +const testProject = { + _id: testProjectId, + ...demoProject +} + +const updateProject = { + projectName: 'testing project update', + description: { + short: 'Short description should be min 10 characters long!', + long: 'this is long description' + }, + version: '1.0.3', + links: [{ + githubLink: 'https://github.com/codeuino' + }] +} + +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: testUserId, + ...demoUser, + email: `test${randomDigit}@mailinator.com`, + phone: `12345678${randomDigit}`, + tokens: [{ + token: jwt.sign({ + _id: testUserId + }, process.env.JWT_SECRET) + }] +} + +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await Project.deleteMany() + await new User(testUser).save() + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app) + .post('/auth/login') + .send({ + email: testUser.email, + password: testUser.password + }) + token = response.body.token + done() +}) + +/** + * This deletes all the existing project in database, + * and creates a new project in database with the provided details. + */ +beforeEach(async () => { + await Project.deleteMany() + await new Project(testProject).save() +}) + +/** + * Testing project creation + */ +test('Should create new project', async (done) => { + const response = await request(app) + .post('/project') + .set('Authorization', `Bearer ${token}`) + .send(demoProject) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const project = await Project.findById(response.body.project._id) + expect(project).not.toBeNull() + + const userId = response.body.project.createdBy + + // Assertions about the response + expect(response.body).toMatchObject({ + project: { + projectName: demoProject.projectName, + description: { + short: demoProject.description.short, + long: demoProject.description.long + }, + version: demoProject.version, + links: [{ + githubLink: demoProject.links[0].githubLink + }], + createdBy: userId + } + }) + done() +}) + +/** + * Testing get all the projects + */ +test('Should get all projects', async (done) => { + await request(app) + .get(`/project?pagination=${pagination}&page=${page}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing GET project by id + */ + +test('Should get project by id', async (done) => { + await request(app) + .get(`/project/${testProjectId}`) + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Get project of a user + */ + +test('Should get all the project created by a user', async (done) => { + await request(app) + .get('/project/me/all') + .set('Authorization', `Bearer ${token}`) + .send() + .expect(HttpStatus.OK) + done() +}) + +/** + * Testing project update + */ +test('Should update the project info', async (done) => { + await request(app) + .patch(`/project/${testProjectId}`) + .set('Authorization', `Bearer ${token}`) + .send(updateProject) + .expect(HttpStatus.OK) + done() +}) + +/** + * TODO: FIX ERROR + * This is a temporary fix to issue: + * Jest has detected the following 1 open handle potentially keeping Jest from exiting + */ +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // delete all the projects project testing + await Project.deleteMany() + // 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 new file mode 100644 index 0000000..1098d9f --- /dev/null +++ b/test/proposal.test.js @@ -0,0 +1,197 @@ +const app = require('../app') +const mongoose = require('mongoose') +const jwt = require('jsonwebtoken') +const HttpStatus = require('http-status-codes') +const request = require('supertest') +const User = require('../app/models/User') +const Organization = require('../app/models/Organisation') +const Proposal = require('../app/models/Proposal') +const randomDigit = Math.floor(Math.random() * 90 + 10) + +const testUserId = new mongoose.Types.ObjectId() +const testOrganizationId = new mongoose.Types.ObjectId() +const testProposalId = new mongoose.Types.ObjectId() +let token = '' + +const demoproposal = { + title: 'Test Proposal', + organization: testOrganizationId, + content: 'Content of the example proposal', + proposalStatus: 'DRAFT', + creator: testUserId +} + +const testProposal = { + _id: testProposalId, + ...demoproposal +} + +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 demoOrganization = { + name: 'Codeuino', + description: { + shortDescription: 'short desc', + longDescription: 'long Description included here' + }, + contactInfo: { + email: 'organisation@test.com', + website: 'www.codeuino.org' + } +} + +const testOrganization = { + _id: testOrganizationId, + ...demoOrganization +} + +const updatedProposalContent = { + content: 'updated proposal content' +} + +const testUser = { + _id: testUserId, + ...demoUser, + email: `test${randomDigit}@mailinator.com`, + phone: `12345678${randomDigit}`, + tokens: [ + { + token: jwt.sign( + { + _id: testUserId + }, + process.env.JWT_SECRET + ) + } + ] +} + +let server + +/** + * This will pe performed once at the beginning of the test + */ + +beforeAll(async (done) => { + await Proposal.deleteMany() + await new User(testUser).save() + await new Organization(testOrganization).save() + server = app.listen(4000, () => { + global.agent = request.agent(server) + }) + const response = await request(app).post('/auth/login').send({ + email: testUser.email, + password: testUser.password + }) + token = response.body.token + done() +}) + +/** + * This deletes all the existing user in database, + * and creates a new user in database with the provided details. + */ +beforeEach(async () => { + await Proposal.deleteMany() + await new Proposal(testProposal).save() +}) + +test('Should create new Proposal', async (done) => { + const response = await request(app) + .post('/proposal') + .set('Authorization', `Bearer ${token}`) + .send(demoproposal) + .expect(HttpStatus.CREATED) + + const proposal = await Proposal.findById(response.body.proposal._id) + expect(proposal).not.toBeNull() + + const userId = response.body.proposal.creator + + expect(response.body).toMatchObject({ + proposal: { + title: demoproposal.title, + organization: `${testOrganizationId}`, + content: demoproposal.content, + proposalStatus: demoproposal.proposalStatus, + creator: `${userId}` + } + }) + done() +}) + +// Testing proposal update +test('Should update the content of the proposal', async (done) => { + await request(app) + .patch(`/proposal/${testProposalId}`) + .set('Authorization', `Bearer ${token}`) + .send(updatedProposalContent) + .expect(HttpStatus.OK) + + done() +}) + +// Testing proposal delete +const deleteProposalContent = { + proposalId: testProposalId +} + +test('Should delete the proposal', async (done) => { + await request(app) + .delete('/proposal') + .set('Authorization', `Bearer ${token}`) + .send(deleteProposalContent) + .expect(HttpStatus.OK) + + // confirm that the proposal was deleted + const proposal = await Proposal.findById(testProposalId) + expect(proposal).toBeNull() + done() +}) + +// Testing get proposalById +const getByIdContent = { + proposalId: testProposalId +} + +test('Should return the proposal by the given Id', async (done) => { + await request(app) + .get(`/proposal/${testProposalId}`) + .set('Authorization', `Bearer ${token}`) + .send(getByIdContent) + .expect(HttpStatus.OK) + + done() +}) diff --git a/test/url.test.js b/test/url.test.js new file mode 100644 index 0000000..1105634 --- /dev/null +++ b/test/url.test.js @@ -0,0 +1,67 @@ +const app = require('../app').app +const mongoose = require('mongoose') +const request = require('supertest') +const UrlModel = require('../app/models/UrlShortner') +const HttpStatus = require('http-status-codes') +const testUrl = 'http://codeuino.org/codeofconduct' +// let shortUrl = '' + +let server +/** + * This will pe performed once at the beginning of the test + */ +beforeAll(async (done) => { + await UrlModel.deleteMany() + server = app.listen(4000, () => { + global.agent = request.agent(server) + done() + }) +}) + +/** + * Testing Shorten URL + */ +test('Should short the URL', async (done) => { + const response = await request(app) + .post('/shortUrl/shorten') + .send({ + longUrl: `${testUrl}` + }) + .expect(HttpStatus.CREATED) + + // Assert that db was changed + const url = await UrlModel.findById(response.body._id) + expect(url).not.toBeNull() + // shortUrl = response.body.shortUrl + + // Assertions about the + expect(response.body).toMatchObject({ + longUrl: `${testUrl}`, + shortUrl: `${response.body.shortUrl}`, + urlCode: `${response.body.urlCode}` + }) + done() +}) + +/** + * ShortURL to longUrl + */ + +// test('Should redirect to the longUrl ', async (done) => { +// const param = shortUrl.toString().split('/')[1] +// shortUrl = 'http://localhost:4000' + '/' + param +// console.log('ShortUrl ', shortUrl) +// await request(app) +// .get(`${shortUrl}`) +// .expect(301, `${testUrl}`) +// done() +// }) + +afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) + // close server + await server.close() + // 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 0f49355..04534ec 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -1,8 +1,12 @@ -const app = require('../app') +const app = require('../app').app const mongoose = require('mongoose') const jwt = require('jsonwebtoken') const request = require('supertest') const User = require('../app/models/User') +const HttpStatus = require('http-status-codes') +let token = '' +let passwordToken = '' +let inviteLink = '' const demoUser = { name: { @@ -41,21 +45,47 @@ const demoUser = { } const testUserId = new mongoose.Types.ObjectId() +const testFollowUserId = new mongoose.Types.ObjectId() const testUser = { _id: testUserId, ...demoUser, email: 'test@mailinator.com', phone: '1234567891', + isAdmin: true, + followers: [], + followings: [], + blocked: [], tokens: [{ token: jwt.sign({ _id: testUserId - }, process.env.JWT_SECRET) + }, 'process.env.JWT_SECRET') }] } + +const testFollowUser = { + _id: testFollowUserId, + ...demoUser, + ...demoUser.name.firstName = 'follow_user', + ...demoUser.name.lastName = 'test', + email: 'test43@mailinator.com', + phone: '1274567391', + isAdmin: false, + followers: [], + followings: [], + blocked: [], + tokens: [{ + token: jwt.sign({ + _id: testFollowUserId + }, 'process.env.JWT_SECRET') + }] +} + let server + /** * This will pe performed once at the beginning of the test */ + beforeAll(async (done) => { await User.deleteMany() server = app.listen(4000, () => { @@ -71,6 +101,7 @@ beforeAll(async (done) => { beforeEach(async () => { await User.deleteMany() await new User(testUser).save() + await new User(testFollowUser).save() }) /** @@ -80,7 +111,7 @@ test('Should signup new user', async () => { const response = await request(app) .post('/user') .send(demoUser) - .expect(201) + .expect(HttpStatus.CREATED) // Assert that db was changed const user = await User.findById(response.body.user._id) @@ -122,8 +153,9 @@ test('Login existing user', async () => { email: testUser.email, password: testUser.password }) - .expect(200) + .expect(HttpStatus.OK) + token = response.body.token const user = await User.findById(testUserId) expect(response.body.token).toBe(user.tokens[1].token) }) @@ -133,7 +165,7 @@ test('Should not login non-existing user', async () => { await request(app).post('/auth/login').send({ email: 'random@random.com', password: 'random@123' - }).expect(400) + }).expect(HttpStatus.BAD_REQUEST) }) /** Fetch authenticated user profile */ @@ -142,7 +174,7 @@ test('Should get profile for user', async () => { .get('/user/me') .set('Authorization', `Bearer ${testUser.tokens[0].token}`) .send() - .expect(200) + .expect(HttpStatus.OK) }) /** Fail in getting unathenticated user profile */ @@ -150,7 +182,7 @@ test('Should not get profile for unauthenticated user', async () => { await request(app) .get('/user/me') .send() - .expect(401) + .expect(HttpStatus.UNAUTHORIZED) }) /** Delete authenticated user profile */ @@ -159,7 +191,7 @@ test('Should delete profile of authenticated user', async () => { .delete('/user/me') .set('Authorization', `Bearer ${testUser.tokens[0].token}`) .send() - .expect(200) + .expect(HttpStatus.OK) // Assert that user was deleted const user = await User.findById(testUserId) @@ -171,7 +203,125 @@ test('Should not delete profile of unauthenticated user', async () => { await request(app) .delete('/user/me') .send() - .expect(401) + .expect(HttpStatus.UNAUTHORIZED) +}) + +/** Forgot password request **/ +test('Should send the request to change the password ', async () => { + const response = await request(app) + .post('/user/password_reset') + .send({ + email: `${testUser.email}` + }) + .expect(200) + passwordToken = response.body.token + expect(passwordToken).not.toBeNull() +}) + +/* Password update */ +test('Should update the password ', async () => { + await request(app) + .post(`/user/password_reset/${passwordToken}`) + .send({ + password: 'newPassword', + id: testUserId + }) + .expect(200) +}) + +/* Activate account */ +test('Should activate the account ', async (done) => { + await request(app) + .get(`/user/activate/${token}`) + .send({ + token: `${token}` + }) + .expect(HttpStatus.OK) + done() +}) + +/* Get invite link */ +test('Should generate an invite link and send', async () => { + const response = await request(app) + .get('/user/invite') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + inviteLink = response.body.inviteLink + // check the response + expect(response.body.inviteLink).not.toBeNull() +}) + +/* Process invite link */ +test('Should validate the invite link token ', async () => { + const inviteToken = inviteLink.split('/').slice(-1)[0].trim() + await request(app) + .get(`/user/invite/${inviteToken}`) + .send() + .expect(HttpStatus.OK) +}) + +/* 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') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send({ + followId: testFollowUserId + }) + .expect(HttpStatus.OK) + // Assert the db change + const user = await User.findById(testFollowUserId) + expect(user.followers[0] === testUserId) + done() +}) + +/* unFollow the user */ +test('Should unFollow the user', async (done) => { + await request(app) + .patch('/user/unfollow') + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send({ + followId: testFollowUserId + }) + .expect(HttpStatus.OK) + // Assert that db change + const user = await User.findById(testFollowUserId) + expect(user.followers === []) + done() +}) + +/* Block the user */ +test('Should block the user', async (done) => { + const response = await request(app) + .patch(`/user/block/${testFollowUserId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + .expect(HttpStatus.OK) + // Assert the db changed + expect(response.body.user.blocked[0]._id === testFollowUserId) + done() +}) + +/* UnBlock the user */ +test('Should UnBlock the user', async (done) => { + const response = await request(app) + .patch(`/user/unblock/${testFollowUserId}`) + .set('Authorization', `Bearer ${testUser.tokens[0].token}`) + .send() + // Assert the db changed + expect(response.body.user.blocked === []) + done() }) /** @@ -180,6 +330,8 @@ test('Should not delete profile of unauthenticated user', async () => { * Jest has detected the following 1 open handle potentially keeping Jest from exiting */ afterAll(async () => { + // avoid jest open handle error + await new Promise((resolve) => setTimeout(() => resolve(), 500)) // close server await server.close() // delete all the users post testing diff --git a/views/emailTemplate.ejs b/views/emailTemplate.ejs new file mode 100644 index 0000000..0434d93 --- /dev/null +++ b/views/emailTemplate.ejs @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + Codeuino org image + +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + +
+
+ Welcome to Donut +
+
+
+
+
+

+
+
+
+
+
+ Your registration is successful! We warmly + welcome you to the donut platform! +
+
+ Donut is an open-source, feature-rich, + highly flexible and privacy-friendly, social + networking platform built for + community-oriented collaboration in a + customized way. It has been built on the + Node.js framework allowing an essential + impetus to provide custom and friendly rich + widgets and an expansive library of modules + to make communication and collaboration easy + and successful. With a powerful module + system, you can customize this platform by + using third party tools, writing your own or + integrating other software. +
+

Please use the given below link if the button does not work properly.

+

http://localhost:5000/user/activate/<%= token %>

+
+ + Activate account + +
+
+ Hope you enjoy your stay at Donut! +
+
+
+   +
+
+
+
+ + + + +
+
+
+ + + + +