Skip to content

Instantly share code, notes, and snippets.

@GGrassiant
Created December 27, 2020 22:21
Show Gist options
  • Select an option

  • Save GGrassiant/96ea08ddcd06058a1841e2b91d3a89e6 to your computer and use it in GitHub Desktop.

Select an option

Save GGrassiant/96ea08ddcd06058a1841e2b91d3a89e6 to your computer and use it in GitHub Desktop.
MERN Mongo Rest
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);
});
};
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
});
};
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);
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;
{
"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"
}
}
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
})
);
const mongoose = require('mongoose');
const { Schema } = mongoose;
const recipientSchema = new Schema({
email: String,
responded: { type: Boolean, default: false }
});
module.exports = recipientSchema;
module.exports = (req, res, next) => {
if (req.user.credits < 1) {
return res.status(403).send({ error: 'Not Enough Credits' });
}
next();
};
module.exports = (req, res, next) => {
if (!req.user) {
return res.status(401).send({ error: 'You must log in!' });
}
next();
};
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);
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);
}
});
};
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