MERN Mongo Rest
Created
December 27, 2020 22:21
-
-
Save GGrassiant/96ea08ddcd06058a1841e2b91d3a89e6 to your computer and use it in GitHub Desktop.
MERN Mongo Rest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const passport = require('passport'); | |
| module.exports = (app) => { | |
| // route to authenticate the user through the login with Google | |
| app.get('/auth/google', passport.authenticate('google', { | |
| scope: ['profile', 'email'] | |
| }) | |
| ); | |
| // route from callback login | |
| app.get( | |
| '/auth/google/callback', | |
| passport.authenticate('google'), | |
| (req, res) => { | |
| res.redirect('/surveys'); // once identified, define which route to direct the user to | |
| } | |
| ); | |
| // route to logout | |
| app.get('/api/logout', (req, res) => { | |
| req.logout(); | |
| res.redirect('/'); | |
| }); | |
| // route to get the info from current user through Google | |
| app.get('/api/current_user', (req, res) => { | |
| res.send(req.user); | |
| }); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const keys = require('../config/keys'); | |
| const stripe = require('stripe')(keys.stripeSecretKey); | |
| const requireLogin = require('../middlewares/requireLogin'); // import logic to validate login first before posting charge | |
| module.exports = app => { | |
| app.post('/api/stripe', requireLogin, async (req, res) => { | |
| const charge = await stripe.charges.create({ | |
| amount: 500, | |
| currency: 'usd', | |
| description: '$5 for 5 credits', | |
| source: req.body.id | |
| }); | |
| req.user.credits += 5; | |
| const user = await req.user.save(); // updating the record with the credits | |
| res.send(user); // send back to user for display | |
| }); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const express = require('express'); | |
| const mongoose = require('mongoose'); | |
| const cookieSession = require('cookie-session'); | |
| const passport = require('passport'); | |
| const bodyParser = require('body-parser'); | |
| const keys = require('./config/keys'); | |
| //require model - IMPORTANT: load before passport | |
| require('./models/User'); | |
| require('./models/Survey'); | |
| // passport package to handle Google oAuth2 and have access to user records throughout the app | |
| require('./services/passport'); | |
| // remote instance of mongo db wired to our app through mongoose | |
| mongoose.connect(keys.mongoURI); | |
| // instance of express that help us use node as a backend | |
| const app = express(); | |
| // body parser install to use in the billingRoutes.js to parse req.body | |
| app.use(bodyParser.json()); | |
| // set the cookies | |
| app.use( | |
| cookieSession({ | |
| maxAge: 30 * 24 * 60 * 60 *1000, // expiration of the cookie. In this case: 30 days | |
| keys: [keys.cookieKey] | |
| }) | |
| ); | |
| // instruct Express to use the cookies to retrieve information from the db | |
| app.use(passport.initialize()); | |
| app.use(passport.session()); | |
| // routes passed to the app | |
| require('./routes/authRoutes')(app); | |
| require('./routes/billingRoutes')(app); | |
| require('./routes/surveyRoutes')(app); | |
| // set up to make sure production routing works between Express and React | |
| if (process.env.NODE_ENV === 'production') { | |
| // Express will serve up product assets such as main.js or main.css | |
| // if a request is made to a route when don't have set in Express, check in client/build (aka React) | |
| app.use(express.static('client/build')); | |
| // otherwise, serve up the index.html file (like a catch all) | |
| const path = require('path'); | |
| app.get('*', (req, res) => { | |
| res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html')); | |
| }); | |
| } | |
| // router-like. If not in prod, listen to port 5000 | |
| const PORT = process.env.PORT || 5000; | |
| app.listen(PORT); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const sengrid = require('sendgrid'); | |
| const helper = sengrid.mail; // convention to call it helper | |
| const keys = require('../config/keys'); | |
| class Mailer extends helper.Mail { | |
| constructor( { subject, recipients }, content) { | |
| super(); | |
| // define API key | |
| this.sgApi = sengrid(keys.sendGridKey); | |
| // herlper Email function from Sengrid | |
| this.from_email = new helper.Email('[email protected]'); | |
| this.subject = subject; | |
| // helper Content function from Sengrid | |
| this.body = new helper.Content('text/html', content); | |
| this.recipients = this.formatAddresses(recipients); | |
| // expected from Sengrid | |
| this.addContent(this.body); | |
| // enable tracking in the email | |
| this.addClickTracking(); | |
| // add recipients | |
| this.addRecipients(); | |
| } | |
| // using a helper function from Sengrid to format email addresses from the recipients | |
| formatAddresses(recipients) { | |
| return recipients.map(({ email }) => { | |
| return new helper.Email(email); | |
| }); | |
| } | |
| addClickTracking() { | |
| // helper functions from Sengrid to enable click-tracking | |
| const trackingSettings = new helper.TrackingSettings(); | |
| const clickTracking = new helper.ClickTracking(true, true); | |
| trackingSettings.setClickTracking(clickTracking); | |
| this.addTrackingSettings(trackingSettings); | |
| } | |
| addRecipients() { | |
| const personalize = new helper.Personalization(); | |
| this.recipients.forEach(recipient => { | |
| personalize.addTo(recipient); | |
| }); | |
| this.addPersonalization(personalize); | |
| } | |
| async send() { | |
| const request = this.sgApi.emptyRequest({ | |
| method: 'POST', | |
| path: '/v3/mail/send', | |
| body: this.toJSON() | |
| }); | |
| const response = await this.sgApi.API(request); | |
| return response; | |
| } | |
| } | |
| module.exports = Mailer; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "server", | |
| "version": "1.0.0", | |
| "main": "index.js", | |
| "engines": { | |
| "node": "10.15.3", | |
| "yarn": "1.16.0" | |
| }, | |
| "scripts": { | |
| "start": "node index.js", | |
| "server": "nodemon index.js", | |
| "client": "cd client; yarn start", | |
| "dev": "concurrently \"yarn server\" \"yarn client\" \"yarn webhook\"", | |
| "heroku-postbuild": "YARN_PRODUCTION=false && (cd client && yarn install && yarn build)", | |
| "webhook": "./sendgrid_webhook.sh" | |
| }, | |
| "license": "MIT", | |
| "dependencies": { | |
| "body-parser": "^1.19.0", | |
| "concurrently": "^5.0.0", | |
| "cookie-session": "^1.3.3", | |
| "express": "^4.17.1", | |
| "localtunnel": "^2.0.0", | |
| "lodash": "^4.17.15", | |
| "mongoose": "^5.7.10", | |
| "nodemon": "^1.19.4", | |
| "passport": "^0.4.0", | |
| "passport-google-oauth20": "^2.0.0", | |
| "path-parser": "^4.2.0", | |
| "sendgrid": "^5.1.2", | |
| "stripe": "^7.13.0" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const passport = require('passport'); | |
| const GoogleStrategy = require('passport-google-oauth20').Strategy; | |
| const mongoose = require('mongoose'); | |
| const keys = require('../config/keys'); | |
| const User = mongoose.model('users'); | |
| // handle authentication token | |
| passport.serializeUser((user, done) => { | |
| done(null, user.id); | |
| }); | |
| // incoming auth request , call db, retrieve instance token and save it to the session. | |
| // in that case we decided the token would be the id from the db (not the google id) | |
| passport.deserializeUser((id, done) => { | |
| User.findById(id) | |
| .then(user => { | |
| done(null, user); // call the corresponding Mongoose instance of the user to retrieve info | |
| }); | |
| }); | |
| passport.use(new GoogleStrategy( | |
| { | |
| clientID: keys.googleClientID, | |
| clientSecret: keys.googleClientSecret, | |
| callbackURL: '/auth/google/callback', | |
| proxy: true | |
| }, | |
| async (accessToken, refreshToken, profile, done) => { | |
| const existingUser = await User.findOne({ googleId: profile.id}); | |
| if (existingUser) { | |
| return done(null, existingUser); // null for error otherwise the existingUser | |
| } | |
| const user = await new User({googleId: profile.id}).save(); // google id and not the db id | |
| done(null, user); // we use this instance of the model because it's coming back from the db so more curated | |
| }) | |
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const mongoose = require('mongoose'); | |
| const { Schema } = mongoose; | |
| const recipientSchema = new Schema({ | |
| email: String, | |
| responded: { type: Boolean, default: false } | |
| }); | |
| module.exports = recipientSchema; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module.exports = (req, res, next) => { | |
| if (req.user.credits < 1) { | |
| return res.status(403).send({ error: 'Not Enough Credits' }); | |
| } | |
| next(); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| module.exports = (req, res, next) => { | |
| if (!req.user) { | |
| return res.status(401).send({ error: 'You must log in!' }); | |
| } | |
| next(); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const mongoose = require('mongoose'); | |
| const { Schema } = mongoose; | |
| const RecipientSchema = require('./Recipient'); | |
| const surveySchema = new Schema({ | |
| title: String, | |
| body: String, | |
| subject: String, | |
| recipients: [RecipientSchema], | |
| yes: { type: Number, default: 0 }, | |
| no: { type: Number, default: 0 }, | |
| _user: { type: Schema.Types.ObjectId, ref: 'User' }, | |
| dateSent: Date, | |
| lastResponded: Date | |
| }); | |
| mongoose.model('surveys', surveySchema); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const _ = require('lodash'); | |
| const Path = require('path-parser').default; // create a test path to match | |
| const { URL } = require('url'); // default in Node, get the URL object and properties from a given url | |
| const mongoose = require('mongoose'); // require the db | |
| const requireLogin = require('../middlewares/requireLogin'); // validation | |
| const requireCredits = require('../middlewares/requireCredits'); // validation | |
| const Mailer = require('../services/Mailer'); // service to send emails | |
| const surveyTemplate = require('../services/emailTemplates/surveyTemplate'); // email template | |
| const Survey = mongoose.model('surveys'); // require the model | |
| module.exports = app => { | |
| // route to get all surveys from a user | |
| app.get('/api/surveys', requireLogin, async (req, res) => { | |
| const surveys = await Survey.find({ _user: req.user.id }) | |
| .select({ recipients: false }); // exclude recipients from the query to avoid to much computation in case of big dataset | |
| res.send(surveys); | |
| }); | |
| // route when user clicks on yes or no in the email | |
| app.get('/api/surveys/:surveyId/:choice', (req, res) => { | |
| res.send('Thanks for voting!'); | |
| }); | |
| // route for webhook from Sendgrid once a user has clicked on yes or no (async) | |
| app.post('/api/surveys/webhooks', (req, res) => { | |
| const p = new Path('/api/surveys/:surveyId/:choice'); // define match to a email in yes/no from template | |
| _.chain(req.body) // chain all calls | |
| .map(({email, url}) => { // destructuring the prop/req.body event | |
| const match = p.test(new URL(url).pathname); // test against pattern | |
| if (match) { | |
| return {email, surveyId: match.surveyId, choice: match.choice}; // return elements from match | |
| } | |
| }) | |
| .compact() // remove undefined from array | |
| .uniqBy('email', 'surveyId') // remove duplicates on email and surveyId | |
| .each(({ surveyId, email, choice }) => { // destructuring the prop/req.body event | |
| Survey.updateOne( // go through the Survey collection and find to update one element that matches | |
| { | |
| _id: surveyId, // _id = surveyId with the _ from the MongoDB _id. Mongoose does not care for _ but in queries directly to MongoDB, MongoDB does | |
| recipients: { | |
| // find the survey that has in the recipients array, find the one that matches the email from the event passed as a prop | |
| // and the property responded to false, default value | |
| $elemMatch: { email: email, responded: false } | |
| } | |
| }, | |
| { | |
| $inc: { [choice]: 1 }, // increment the given choice (yes or no) by one | |
| $set: { 'recipients.$.responded': true }, // set this specific recipient (.$.) responded property to true | |
| lastResponded: new Date() // update the date of this survey response date | |
| } | |
| ).exec(); // execute the query | |
| }) | |
| .value(); //return value of the chain | |
| res.send({}); // send result | |
| }); | |
| // route to create a new survey and send it with the help of the mailer class | |
| app.post('/api/surveys', requireLogin, requireCredits, async (req, res) => { | |
| const { title, subject, body, recipients } = req.body; | |
| const survey = new Survey({ | |
| title, | |
| subject, | |
| body, | |
| recipients: recipients.split(',').map(email => ({ email: email.trim() })), // we need an array of objects hence this loop | |
| _user: req.user.id, | |
| dateSent: Date.now(), | |
| }); | |
| // create a new instance of survey and call the send method | |
| const mailer = new Mailer(survey, surveyTemplate(survey)); | |
| try { | |
| await mailer.send(); | |
| await survey.save(); | |
| req.user.credits -= 1; | |
| const user = await req.user.save(); // getting the most up to date user instance | |
| res.send(user); | |
| } catch (error) { | |
| res.status(422).send(error); | |
| } | |
| }); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const mongoose = require('mongoose'); | |
| const { Schema } = mongoose; | |
| const userSchema = new Schema({ | |
| googleId: String, | |
| credits: { type: Number, default: 0 } | |
| }); | |
| mongoose.model('users', userSchema); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment