Nodejs core modules
Introduction
Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine and executes JavaScript code outside a web browser. (nodejs docs)
Set up
index.js
const express = require('content/snippets/nodejs-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('./models/User')
require('./models/Survey')
require('./services/passport')
mongoose.connect(keys.mongoURI)
const app = express()
app.use(bodyParser.json())
app.use(
cookieSession({
maxAge: 30 * 24 * 60 * 60 * 1000,
keys: [keys.cookieKey]
})
)
app.use(passport.initialize())
app.use(passport.session())
require('./routes/authRoutes')(app)
require('./routes/billingRoutes')(app)
require('./routes/surveyRoutes')(app)
if (process.env.NODE_ENV === 'production') {
// Express will serve up production assets
// like our main.js file, or main.css file!
app.use(express.static('client/build'))
// Express will serve up the index.html file
// if it doesnt recognize the route
const path = require('path')
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
const port = process.env.PORT || 5000
app.listen(port, () => {
console.log(`App is listening on port ${port}`)
})
For usage of cors use the following:
const cors = require('cors')
app.use(cors())
Middlewares
Express knows that a function with 3 arguments is treated as a middleware
require-credit.js
module.exports = (req, res, next) => {
if (req.user.credits < 1) {
return res.status(403).send({ error: 'Not enough credits!'})
}
next()
}
require-login.js
module.exports = (req, res, next) => {
if (!req.user) {
return res.status(401).send({ error: 'You must log in!'})
}
next()
}
Mongoose
Recipient.js
const mongoose = require('mongoose')
const {Schema} = mongoose
const recipientSchema = new Schema({
email: String,
responded: {
type: Boolean,
default: false
}
})
module.exports = recipientSchema
Survey.js
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)
User.js
const mongoose = require('mongoose')
const {Schema} = mongoose
const userSchema = new Schema({
googleId: String,
credits: {
type: Number,
default: 0
}
})
mongoose.model('users', userSchema)
Routes
survey-route.js
const _ = require('lodash')
const {Path} = require('path-parser')
const {URL} = require('url')
const mongoose = require('mongoose')
const requireLogin = require('../middlewares/requireLogin')
const requireCredits = require('../middlewares/requireCredits')
const Mailer = require('../services/Mailer')
const surveyTemplate = require('../services/emailTemplates/emailTemplate')
const Survey = mongoose.model('surveys')
module.exports = app => {
app.get('/api/surveys',
requireLogin,
async (req, res) => {
const surveys = await Survey
.find({
_user: req.user.id
})
.select({
recipients: false
})
res.send(surveys)
}
)
app.get('/api/surveys/:surveyId/:choice', (req, res) => {
res.send('Thanks for voting')
})
// remember use ngrok and change url in sendgrid
app.post('/api/surveys/webhooks', (req, res) => {
const p = new Path('/api/surveys/:surveyId/:choice')
_.chain(req.body)
.map((event) => {
const url = new URL(event.url)
const match = p.test(url.pathname)
if (match) {
return {
email: event.email,
surveyId: match.surveyId,
choice: match.choice
}
}
})
.compact()
.uniqBy('email', 'surveyId')
.each(({surveyId, email, choice}) => {
Survey.updateOne({
_id: surveyId,
recipients: {
$elemMatch: { email: email, responded: false}
}
}, {
$inc: { [choice]: 1},
$set: { 'recipients.$.responded': true },
lastResponded: new Date()
}).exec()
})
.value()
res.send({})
})
app.post(
'/api/surveys',
requireLogin,
requireCredits,
async (req, res) => {
try {
const {title, subject, body, recipients} = req.body
const survey = new Survey({
title,
subject,
body,
recipients: recipients.split(',').map(email => ({ email: email.trim() }) ),
_user: req.user.id,
dateSent: Date.now()
})
// Great place to send an email
const mailer = new Mailer(survey, surveyTemplate(survey))
await mailer.send()
await survey.save()
req.user.credits -= 1
const user = await req.user.save()
res.send(user)
} catch (e) {
res.status(422).send(e)
}
}
)
}
billing-route.js
const keys = require('../config/keys')
const stripe = require('stripe')(keys.stripeSecretKey)
const requireLogin = require('../middlewares/requireLogin')
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()
res.send(user)
}
)
}
auth-route.js
const passport = require('passport')
module.exports = (app) => {
app.get('/auth/google', passport.authenticate(
'google',
{
scope: ['profile', 'email']
}, () => {
})
)
app.get(
'/auth/google/callback',
passport.authenticate('google'),
(req, res) => {
res.redirect('/surveys')
}
)
app.get('/api/logout', (req, res) => {
req.logout()
res.redirect('/')
})
app.get('/api/current_user', (req, res) => {
res.send(req.user)
})
}
Config
keys (.js files)
We can set up different keys depending on the environment the app is running (dev/prod)
prod.js
// prod.js - production keys
module.exports = {
googleClientID: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
mongoURI: process.env.MONGO_URI,
cookieKey: process.env.COOKIE_KEY,
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY,
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
sendGridApiKey: process.env.SEND_GRID_KEY,
redirectDomain: process.env.REDIRECT_DOMAIN
}
dev.js
// dev.js - dont commit this
module.exports = {
googleClientID: '...',
googleClientSecret: '...',
mongoURI: '...',
cookieKey: 'asdfasdfasdfasdfahsdfhllkjdflkahslkdfhlakshdflkhalkh',
stripePublishableKey: '...',
stripeSecretKey: '...',
sendGridApiKey: '...',
redirectDomain: 'http://localhost:3000/'
}
Depending on the process.env.NODE_ENV
value, we decide which file to load.
keys.js
// key.js figure out what set of credentials to return
if (process.env.NODE_ENV === 'production') {
module.exports = require('./prod')
} else {
module.exports = require('./dev')
}
title: Nodejs techniques
Introduction
Handlebars
andrew mead -node js
Handlebars is a simple templating language. (handlebars docs)
It uses a template and an input object to generate HTML or other text formats. Handlebars templates look like regular text with embedded Handlebars expressions.
<p>{{firstname}} {{lastname}}</p>
A handlebars expression is a {{, some contents, followed by a }}. When the template is executed, these expressions are replaced with values from an input object.
Usage
app.js
const path = require('path')
const express = require('content/snippets/nodejs-express')
const hbs = require('hbs')
const geocode = require('./geocode')
const forecast = require('./forecast')
const app = express()
const port = process.env.PORT || 3000
// Define path for Express config -
const publicDirectoryPath = path.join(__dirname, '../public')
const viewsDirectoryPath = path.join(__dirname, '../templates/views')
const partialsDirectoryPath = path.join(__dirname, '../templates/partials')
// Setup handlebars engine and views location
app.set('view engine', 'hbs')
app.set('views', viewsDirectoryPath)
hbs.registerPartials(partialsDirectoryPath)
// Setup static directory to serve
app.use(express.static(publicDirectoryPath))
app.get('', (req, res) => {
res.render('index', {
title: 'Weather App',
name: 'Andrew Mead'
})
})
app.get('/help', (req, res) => {
res.render('help', {
title: 'Help',
name: 'Andrew Mead'
})
})
app.get('/about', (req, res) => {
res.render('about', {
title: 'About',
name: 'Andrew Mead'
})
})
app.get('/weather', (req, res) => {
if (!req.query.address) {
return res.send({
error: 'You must provide an address'
})
}
const address = req.query.address
geocode(address, (err, geoData) => {
if (err) {
res.send(err)
return
}
const { latitude, longitude } = geoData
forecast(latitude, longitude, (err, forecastData) => {
if (err) {
res.send(err)
return
}
res.send({ forecastData, geoData })
console.log(forecastData)
})
})
})
app.get('/products', (req, res) => {
if (!req.query.search) {
return res.send({
error: 'You must provide a search term'
})
}
res.send({ somedata: '' })
})
app.get('*', (req, res) => {
res.send('My 404 page')
})
app.listen(port, () => {
console.log('Server is up on port 3000 ' + port)
})
Partials
./templates/partials
footer.hbs
<footer>
<p>Created by {{name}}</p>
</footer>
header.hbs
<h1>{{title}}</h1>
<div>
<a href="/">Weather</a>
<a href="/about">About</a>
<a href="/help">Help</a>
</div>
Views
./templates/views
404.hbs
<!DOCTYPE html>
<html>
<head>
<title>404</title>
<link rel="icon" href="/img/weather.png">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="main-content">
{{>header}}
<p>{{errorMessage}}</p>
</div>
{{>footer}}
</body>
</html>
about.hbs
<!DOCTYPE html>
<html>
<head>
<title>About</title>
<link rel="icon" href="/img/weather.png">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="main-content">
{{>header}}
</div>
<img class="portrait" src="/img/me.png">
{{>footer}}
</body>
</html>
help.hbs
<!DOCTYPE html>
<html>
<head>
<title>Help</title>
<link rel="icon" href="/img/weather.png">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="main-content">
{{>header}}
<p>{{helpText}}</p>
</div>
{{>footer}}
</body>
</html>
index.hbs
<!DOCTYPE html>
<html>
<head>
<title>Weather</title>
<link rel="icon" href="/img/weather.png">
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<div class="main-content">
{{>header}}
<p>Use this site to get your weather!</p>
<form>
<input placeholder="location" />
<button>Search</button>
</form>
<p id="message-1"></p>
<p id="message-2"></p>
</div>
{{>footer}}
<script src="/js/app.js"></script>
</body>
</html>
Public
./public/js
File executed on the client
app.js
const weatherFrom = document.querySelector('form')
const search = document.querySelector('input')
const messageOne = document.querySelector('#message-1')
const messageTwo = document.querySelector('#message-2')
weatherFrom.addEventListener('submit', (e) => {
e.preventDefault()
const location = search.value
messageOne.textContent = 'Loading...'
messageTwo.textContent = ''
fetch('/weather?address=' + location)
.then(response => {
response.json().then(data => {
if (data.error) {
messageOne.textContent = data.error
messageTwo.textContent = ''
} else {
messageOne.textContent = data.geoData.place_name
messageTwo.textContent = `The forecast for ${data.geoData.place_name} is ${data.forecastData.temperature}`
}
})
})
})
Functions used in Usage
forecast.js
const axios = require('axios')
const forecast = (latitude, longitude, callback) => {
const urlLatitude = latitude < 0 ? encodeURIComponent("'" + latitude + "'") : latitude
const urlLongitude = longitude < 0 ? encodeURIComponent("'" + longitude + "'") : longitude
const weatherurl = 'http://api.weatherstack.com/current?access_key=035ba6b559a56683c60ec1fb92d85fa5&query=' + urlLatitude + ',' + urlLongitude
console.log(weatherurl)
axios.get(weatherurl)
.then(result => {
if (result.data.error) {
callback('Unable to find location, try another search.', undefined)
} else {
callback(undefined, result.data.current)
}
})
.catch(error => {
callback(error, undefined)
})
}
module.exports = forecast
geocode.js
const axios = require('axios')
const geocode = (address, callback) => {
const url = 'https://api.mapbox.com/geocoding/v5/mapbox.places/' + encodeURIComponent(address) + '.json?access_token=pk.eyJ1IjoibWFndWFzdHVwYWd1YXMiLCJhIjoiY2tpdGxoc2N3MDNjdTMzbnVxaWd0NHd1YSJ9.zFx8KtqgRhfgIiQ1VkEsqg'
axios.get(url)
.then(result => {
if (result.data.features.length === 0) {
callback('Unable to find location!. Try another search.', undefined)
} else {
const [latitude, longitude] = result.data.features[0].center
const {place_name} = result.data.features[0]
callback(undefined, {
latitude,
longitude,
place_name
})
}
})
.catch(error => {
callback('Unable to connect to location services!', undefined)
})
}
module.exports = geocode
Sendgrid
dependencies used for this script "@sendgrid/mail": "^7.4.2"
& "sendgrid": "^5.2.3",
Mailer.js
const sgMail = require("@sendgrid/mail");
const keys = require("../config/keys");
class Mailer {
constructor({ subject, recipients }, content) {
sgMail.setApiKey(keys.sendGridApiKey);
this.msg = {
to: recipients.map(({ email }) => email),
from: "email@gmail.com",
subject: subject,
html: content,
trackingSettings: { enable_text: true, enabled: true }
};
}
async send() {
return await sgMail.send(this.msg);
}
}
module.exports = Mailer;
survey-route.js
const _ = require('lodash')
const {Path} = require('path-parser')
const {URL} = require('url')
const mongoose = require('mongoose')
const requireLogin = require('../middlewares/requireLogin')
const requireCredits = require('../middlewares/requireCredits')
const Mailer = require('../services/Mailer')
const surveyTemplate = require('../services/emailTemplates/emailTemplate')
const Survey = mongoose.model('surveys')
module.exports = app => {
app.get('/api/surveys',
requireLogin,
async (req, res) => {
const surveys = await Survey
.find({
_user: req.user.id
})
.select({
recipients: false
})
res.send(surveys)
}
)
app.get('/api/surveys/:surveyId/:choice', (req, res) => {
res.send('Thanks for voting')
})
// remember use ngrok and change url in sendgrid
app.post('/api/surveys/webhooks', (req, res) => {
const p = new Path('/api/surveys/:surveyId/:choice')
_.chain(req.body)
.map((event) => {
const url = new URL(event.url)
const match = p.test(url.pathname)
if (match) {
return {
email: event.email,
surveyId: match.surveyId,
choice: match.choice
}
}
})
.compact()
.uniqBy('email', 'surveyId')
.each(({surveyId, email, choice}) => {
Survey.updateOne({
_id: surveyId,
recipients: {
$elemMatch: { email: email, responded: false}
}
}, {
$inc: { [choice]: 1},
$set: { 'recipients.$.responded': true },
lastResponded: new Date()
}).exec()
})
.value()
res.send({})
})
app.post(
'/api/surveys',
requireLogin,
requireCredits,
async (req, res) => {
try {
const {title, subject, body, recipients} = req.body
const survey = new Survey({
title,
subject,
body,
recipients: recipients.split(',').map(email => ({ email: email.trim() }) ),
_user: req.user.id,
dateSent: Date.now()
})
// Great place to send an email
const mailer = new Mailer(survey, surveyTemplate(survey))
await mailer.send()
await survey.save()
req.user.credits -= 1
const user = await req.user.save()
res.send(user)
} catch (e) {
res.status(422).send(e)
}
}
)
}
email-template.js
const keys = require('../../config/keys')
module.exports = (survey) => {
return `
<html>
<body>
<div>
<h3>I'd like your input!</h3>
<p>Please answer the following question</p>
<p>${survey.body}</p>
<div>
<a href="${keys.redirectDomain}api/surveys/${survey.id}/yes">Yes</a>
</div>
<div>
<a href="${keys.redirectDomain}api/surveys/${survey.id}/no">No</a>
</div>
</div>
</body>
</html>
`
}
send-email.js
const sgMail = require('@sendgrid/mail')
const sendGridApiKey = process.env.SENDGRID_API_KEY
sgMail.setApiKey(sendGridApiKey)
const sendWelcomeEmail = (email, name) => {
sgMail.send({
to: email,
from: 'mauricioaznar94@gmail.com',
subject: 'Welcome to the app',
text: `Welcome to the app, ${name}. Let me know how you get along with the app.`
})
}
module.exports = {
sendWelcomeEmail
}
Passport
dependencies used for this script "passport": "^0.4.1"
& "passport-google-oauth20": "^2.0.0",
require passport.js files in app.js require('./passport.js')
passport.js
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const keys = require('../config/keys')
const mongoose = require('mongoose')
const User = mongoose.model('users')
passport.serializeUser((user, done) => {
done(null, user.id)
})
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id)
done(null, user)
})
// proxy is true so that googlestrategy trust proxies and enables https (avoid heroku proxy issue)
passport.use('google', 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) {
// we already have a record with the given profile ID
done(null, existingUser)
return
}
// we dont have a user record with this id, make a new record
const newUser = await new User({googleId: profile.id}).save()
done(null, newUser)
})
)
auth-route.js
const passport = require('passport')
module.exports = (app) => {
app.get('/auth/google', passport.authenticate(
'google',
{
scope: ['profile', 'email']
}, () => {
})
)
app.get(
'/auth/google/callback',
passport.authenticate('google'),
(req, res) => {
res.redirect('/surveys')
}
)
app.get('/api/logout', (req, res) => {
req.logout()
res.redirect('/')
})
app.get('/api/current_user', (req, res) => {
res.send(req.user)
})
}
passport.js
const passport = require('passport')
const User = require('../models/user')
const config = require('../config')
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const LocalStrategy = require('passport-local')
const bcrypt = require('bcryptjs')
// Create local strategy
const localOptions = {usernameField: 'email'}
const localLogin = new LocalStrategy(localOptions, function (email, password, done) {
// Verify this username and password, call done with the user
// if it is the correct username and password
// otherwise, call done with token
User.findOne({email:email}, function (err, user) {
if (err) { return done(err) }
if (!user) { return done(null, false) }
// compare password - is 'password' equal to user.password?
user.comparePassword(password, function (err, isValid) {
if (err) { return done(err) }
if (!isValid) { return done(null, false)}
return done(null, user)
})
})
})
// Setup options for JWT Strategy
const jwtOptions = {
jwtFromRequest: ExtractJwt.fromHeader('authorization'),
secretOrKey: config.secret
}
// Create JWT strategy
const jwtLogin = new JwtStrategy(jwtOptions, function (payload, done) {
// See if the user ID in the payload exists in our database
// If it does, call 'done' with that other
// otherwise, call without a user object
User.findById(payload.sub, function (err, user) {
if (err) {
return done(err, false)
}
if (user) {
done(null, user)
} else {
done(null, false)
}
})
})
passport.use(jwtLogin)
passport.use(localLogin)
User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const Schema = mongoose.Schema;
// Define our model
const userSchema = new Schema({
email: {
type: String,
unique: true,
lowercase: true
},
password: String
})
userSchema.methods.comparePassword = function(candidatePassword, callback) {
bcrypt.compare(candidatePassword, this.password, (err, isValid) => {
if (err) { return callback(err) }
console.log(isValid)
callback(null, isValid)
})
}
// Hash the plain text password before saving
userSchema.pre('save', async function (next) {
const user = this
try {
if (user.isModified('password')) {
user.password = await bcrypt.hash(user.password, 8, null)
}
} catch (e) {
console.log(e)
}
next()
})
// Create the model class
const ModelClass = mongoose.model('user', userSchema)
// Export the model
module.exports = ModelClass
Stripe.js
Dependency used "stripe": "^8.137.0"
const keys = require('../config/keys')
const stripe = require('stripe')(keys.stripeSecretKey)
const requireLogin = require('../middlewares/requireLogin')
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()
res.send(user)
}
)
}
Middlewares
User verification and insertions in request Object
auth.js
const jwt = require('jsonwebtoken')
const User = require('../models/user')
const auth = async (req, res, next) => {
try {
// token gets sent through headers and has to be extracted
const token = req.header('Authorization').replace('Bearer ', '')
const decoded = jwt.verify(token, process.env.JWT_SECRET)
const user = await User.findOne({_id: decoded._id, 'tokens.token': token})
if (!user) {
throw new Error()
}
//user and token object get merged into the request object
req.token = token
req.user = user
} catch (e) {
res.status(401).send({error: 'Please authenticate.'})
}
// function called to signal express that it can continue processing the request
next()
}
module.exports = auth
Routing
Pagination route
GET /tasks?completed=true
GET /tasks?limit=2&skip=1
GET /tasks?sortBy=createdAt:desc
task.js
router.get('/tasks', authMiddleware, async (req, res) => {
const match = {}
const sort = {}
if (req.query.completed) {
match.completed = req.query.completed === 'true'
}
if (req.query.sortBy) {
const parts = req.query.sortBy.split(':')
sort[parts[0]] = parts[1] === 'asc' ? 1 : -1
}
try {
const user = await User.findOne({_id: req.user._id})
.populate({
path: 'tasks',
match,
options: {
limit: parseInt(req.query.limit),
skip: parseInt(req.query.skip),
sort
}
})
// const tasks = await Task.find({owner: req.user._id})
return res.send(user.tasks)
} catch (error) {
return res.status(500).send()
}
})
Socket.io
Server
app.js
const express = require('content/snippets/nodejs-express')
const http = require('http')
const path = require('path')
const socketio = require('socket.io')
const Filter = require('bad-words')
const { generateMessage, generateLocationMessage } = require('./utils/messages')
const { addUser, removeUser, getUsersInRoom, getUser } = require('./utils/users')
const app = express()
const server = http.createServer(app)
const io = socketio(server)
app.use(express.static(path.join(__dirname, '../', 'public')))
let count = 0
io.on('connection', (socket) => {
socket.on('join', ({ username, room }, callback) => {
const { error, user } = addUser({ id: socket.id, username, room })
if (error) {
return callback(error)
}
socket.join(user.room)
socket.emit('message', generateMessage('Admin', 'Welcome!'))
socket.broadcast.to(user.room).emit('message', generateMessage('Admin', `${user.username} has joined`))
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
})
callback()
})
socket.on('sendMessage', (message, callback) => {
const user = getUser(socket.id)
const filter = new Filter()
if (filter.isProfane(message)) {
return callback('Profanity is not allowed')
}
io.to(user.room).emit('message', generateMessage(user.username, message))
callback()
})
socket.on('disconnect', () => {
const user = removeUser(socket.id)
if (user) {
io.to(user.room).emit('message', generateMessage('Admin', `${user.username} has left`))
io.to(user.room).emit('roomData', {
room: user.room,
users: getUsersInRoom(user.room)
})
}
})
socket.on('sendLocation', (coordinates, callback) => {
const user = getUser(socket.id)
io.to(user.room).emit('locationMessage', generateLocationMessage(user.username, coordinates))
callback()
})
})
const port = process.env.PORT || 3000
server.listen(port, () => {
console.log('App is running on port ' + port)
})
./utils/users.js
const users = []
// addUser, removeUser, getUser, getUsersInRoom
const addUser = ({ id, username, room }) => {
// Clean the data
username = username.trim().toLowerCase()
room = room.trim().toLowerCase()
// Validate the data
if (!username || !room) {
return {
error: 'Username and room are required!'
}
}
// Check for existing user
const existingUser = users.find((user) => {
return user.room === room && user.username === username
})
// Validate username
if (existingUser) {
return {
error: 'Username is in use!'
}
}
// Store user
const user = {id, username, room}
users.push(user)
return { user }
}
const removeUser = (id) => {
const index = users.findIndex(user => {
return id === user.id
})
if (index !== -1) {
return users.splice(index, 1)[0]
}
}
const getUser = (id) => {
return users.find(user => {
return user.id === id
})
}
const getUsersInRoom = (roomName) => {
return users.filter(user => {
return user.room === roomName.toLowerCase()
})
}
module.exports = {
addUser,
removeUser,
getUser,
getUsersInRoom
}
const generateMessage = (username, text) => {
return {
username,
text,
createdAt: new Date().getTime()
}
}
const generateLocationMessage = (username, url) => {
return {
username,
url,
createdAt: new Date().getTime()
}
}
module.exports = {
generateMessage,
generateLocationMessage
}
Client
client.js
const socket = io()
// Elements
const $messageForm = document.querySelector('#message-form')
const $input = document.querySelector('#input')
const $messageFormButton = document.querySelector('#increment')
const $sendLocationButton = document.querySelector('#send-location')
const $messages = document.querySelector('#messages')
// Templates
const messageTemplate = document.querySelector('#message-template').innerHTML
const locationMessageTemplate = document.querySelector('#location-message-template').innerHTML
const sidebarTemplate = document.querySelector('#sidebar-template').innerHTML
// Options
const {username, room} = Qs.parse(location.search, { ignoreQueryPrefix: true })
const autoScroll = () => {
// New message element
const $newMessage = $messages.lastElementChild
// Height of the new message
const newMessageStyles = getComputedStyle($newMessage)
const newMessageMargin = parseInt(newMessageStyles.marginBottom)
const newMessageHeight = $newMessage.offsetHeight + newMessageMargin
const visibleHeight = $messages.offsetHeight
// height of messsages continaer
const containerHeight = $messages.scrollHeight
// How far have i scrolles?
const scrollOffset = $messages.scrollTop + visibleHeight
if (containerHeight - newMessageHeight <= scrollOffset) {
$messages.scrollTop = $messages.scrollHeight
}
console.log(newMessageMargin)
}
socket.on('message', (message) => {
const html = Mustache.render(messageTemplate, {
username: message.username,
message: message.text,
createdAt: moment(message.createdAt).format('h:m a')
})
$messages.insertAdjacentHTML('beforeend', html)
autoScroll()
})
socket.on('locationMessage', (locationMessage) => {
const html = Mustache.render(locationMessageTemplate, {
username: locationMessage.username,
url: locationMessage.url,
createdAt: moment(locationMessage.createdAt).format('h:m a')
})
$messages.insertAdjacentHTML('beforeend', html)
autoScroll()
})
socket.on('roomData', ({room, users}) => {
const html = Mustache.render(sidebarTemplate, {
room,
users
})
document.querySelector('#sidebar').innerHTML = html
})
$messageForm
.addEventListener('submit', (e) => {
e.preventDefault()
$messageFormButton.setAttribute('disabled', 'disabled')
socket.emit('sendMessage', $input.value, (error) => {
$messageFormButton.removeAttribute('disabled')
$input.value = ''
$input.focus()
if (error) {
return console.log(error)
}
console.log('the message was delivered')
})
})
$sendLocationButton
.addEventListener('click', () => {
if (!navigator.geolocation) {
return alert('Geolocation is not supported by your browser')
}
$sendLocationButton.setAttribute('disabled', 'disabled')
navigator.geolocation.getCurrentPosition((position) => {
console.log(position)
socket.emit('sendLocation',
`https://www.google.com/maps?q=${position.coords.latitude},${position.coords.longitude}`,
() => {
$sendLocationButton.removeAttribute('disabled')
console.log('location shared')
})
})
})
socket.emit('join', {username, room}, (error) => {
if (error) {
alert(error)
location.href = '/'
}
})
Express-validator
posts.js
const {body, validationResult} = require('express-validator')
router.post(
'/',
auth,
[
body('text', 'text is required').not().isEmpty()
],
async (req, res) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({errors: errors.array()})
}
const user = await User.findById(req.user.id).select('-password')
const newPost = new Post({
text: req.body.text,
name: user.name,
avatar: user.avatar,
user: req.user.id
})
const post = await newPost.save()
return res.json(post)
} catch (err) {
console.error(err.message)
return res.status(500).send('Server Error')
}
}
)
Client served from the server
server.js
const express = require('content/snippets/nodejs-express')
const connectDB = require('./config/db')
const app = express()
const path = require('path')
connectDB()
app.use(express.json())
// app.get('/', (req, res) => {
// res.send('API running')
// })
app.use('/api/users', require('./routes/api/users'))
app.use('/api/auth', require('./routes/api/auth'))
app.use('/api/profile', require('./routes/api/profile'))
app.use('/api/posts', require('./routes/api/posts'))
// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
app.use(express.static('client/build'))
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
const PORT = process.env.PORT || 5000
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`)
})