├── config └── database.js ├── models ├── profile.js └── user.js ├── routes ├── profiles.js └── auth.js ├── package.json ├── middleware └── auth.js ├── controllers ├── profiles.js └── auth.js ├── server.js ├── bin └── www.js └── README.md /config/database.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const db = mongoose.connection 4 | 5 | mongoose.connect(process.env.DATABASE_URL) 6 | 7 | db.on('connected', function () { 8 | console.log(`Connected to MongoDB ${db.name} at ${db.host}:${db.port}`) 9 | }) 10 | -------------------------------------------------------------------------------- /models/profile.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const Schema = mongoose.Schema 4 | 5 | const profileSchema = new Schema({ 6 | name: String, 7 | photo: String 8 | },{ 9 | timestamps: true, 10 | }) 11 | 12 | const Profile = mongoose.model('Profile', profileSchema) 13 | 14 | export { Profile } 15 | -------------------------------------------------------------------------------- /routes/profiles.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { decodeUserFromToken, checkAuth } from '../middleware/auth.js' 3 | import * as profilesCtrl from '../controllers/profiles.js' 4 | 5 | const router = Router() 6 | 7 | /*---------- Public Routes ----------*/ 8 | 9 | 10 | /*---------- Protected Routes ----------*/ 11 | router.use(decodeUserFromToken) 12 | router.get('/', checkAuth, profilesCtrl.index) 13 | router.put('/:id/add-photo', checkAuth, profilesCtrl.addPhoto) 14 | 15 | export { router } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-jwt-template-back-end", 3 | "version": "0.2.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "start": "node ./bin/www.js" 8 | }, 9 | "dependencies": { 10 | "bcrypt": "^5.1.1", 11 | "cloudinary": "^2.2.0", 12 | "cors": "^2.8.5", 13 | "dotenv": "^16.4.5", 14 | "express": "^4.19.2", 15 | "express-form-data": "^2.0.23", 16 | "jsonwebtoken": "^9.0.2", 17 | "mongoose": "^8.3.3", 18 | "morgan": "^1.10.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /routes/auth.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { decodeUserFromToken, checkAuth } from '../middleware/auth.js' 3 | import * as authCtrl from '../controllers/auth.js' 4 | 5 | const router = Router() 6 | 7 | /*---------- Public Routes ----------*/ 8 | router.post('/signup', authCtrl.signup) 9 | router.post('/login', authCtrl.login) 10 | 11 | /*---------- Protected Routes ----------*/ 12 | router.use(decodeUserFromToken) 13 | router.post('/change-password', checkAuth, authCtrl.changePassword) 14 | 15 | export { router } 16 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | const SECRET = process.env.SECRET 4 | 5 | const decodeUserFromToken = (req, res, next) => { 6 | let token = req.get('Authorization') || req.query.token || req.body.token 7 | if (!token) return next() 8 | 9 | token = token.replace('Bearer ', '') 10 | jwt.verify(token, SECRET, (err, decoded) => { 11 | if (err) return next(err) 12 | 13 | req.user = decoded.user 14 | next() 15 | }) 16 | } 17 | 18 | function checkAuth(req, res, next) { 19 | return req.user ? next() : res.status(401).json({ err: 'Not Authorized' }) 20 | } 21 | 22 | export { decodeUserFromToken, checkAuth } 23 | -------------------------------------------------------------------------------- /controllers/profiles.js: -------------------------------------------------------------------------------- 1 | import { Profile } from '../models/profile.js' 2 | import { v2 as cloudinary } from 'cloudinary' 3 | 4 | async function index(req, res) { 5 | try { 6 | const profiles = await Profile.find({}) 7 | res.json(profiles) 8 | } catch (err) { 9 | console.log(err) 10 | res.status(500).json(err) 11 | } 12 | } 13 | 14 | async function addPhoto(req, res) { 15 | try { 16 | const imageFile = req.files.photo.path 17 | const profile = await Profile.findById(req.params.id) 18 | 19 | const image = await cloudinary.uploader.upload( 20 | imageFile, 21 | { tags: `${req.user.email}` } 22 | ) 23 | profile.photo = image.url 24 | 25 | await profile.save() 26 | res.status(201).json(profile.photo) 27 | } catch (err) { 28 | console.log(err) 29 | res.status(500).json(err) 30 | } 31 | } 32 | 33 | export { index, addPhoto } 34 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // npm packages 2 | import 'dotenv/config.js' 3 | import express from 'express' 4 | import logger from 'morgan' 5 | import cors from 'cors' 6 | import formData from 'express-form-data' 7 | 8 | // connect to MongoDB with mongoose 9 | import './config/database.js' 10 | 11 | // import routes 12 | import { router as profilesRouter } from './routes/profiles.js' 13 | import { router as authRouter } from './routes/auth.js' 14 | 15 | // create the express app 16 | const app = express() 17 | 18 | // basic middleware 19 | app.use(cors()) 20 | app.use(logger('dev')) 21 | app.use(express.json()) 22 | app.use(formData.parse()) 23 | 24 | // mount imported routes 25 | app.use('/api/profiles', profilesRouter) 26 | app.use('/api/auth', authRouter) 27 | 28 | // handle 404 errors 29 | app.use(function (req, res, next) { 30 | res.status(404).json({ err: 'Not found' }) 31 | }) 32 | 33 | // handle all other errors 34 | app.use(function (err, req, res, next) { 35 | res.status(err.status || 500).json({ err: err.message }) 36 | }) 37 | 38 | export { app } 39 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import bcrypt from 'bcrypt' 3 | 4 | const saltRounds = 6 5 | const Schema = mongoose.Schema 6 | 7 | const userSchema = new Schema({ 8 | name: String, 9 | email: { type: String, required: true, lowercase: true }, 10 | password: String, 11 | profile: { type: Schema.Types.ObjectId, ref: 'Profile' }, 12 | }, { 13 | timestamps: true, 14 | }) 15 | 16 | userSchema.set('toJSON', { 17 | transform: function (doc, ret) { 18 | delete ret.password 19 | return ret 20 | } 21 | }) 22 | 23 | userSchema.pre('save', async function (next) { 24 | const user = this 25 | if (!user.isModified('password')) return next() 26 | 27 | try { 28 | const hash = await bcrypt.hash(user.password, saltRounds) 29 | user.password = hash 30 | next() 31 | } catch (err) { 32 | next(err) 33 | } 34 | }) 35 | 36 | userSchema.methods.comparePassword = async function (tryPassword) { 37 | return await bcrypt.compare(tryPassword, this.password) 38 | } 39 | 40 | const User = mongoose.model('User', userSchema) 41 | 42 | export { User } 43 | -------------------------------------------------------------------------------- /bin/www.js: -------------------------------------------------------------------------------- 1 | // module dependencies 2 | import { app } from '../server.js' 3 | import http from 'http' 4 | 5 | // get port from environment and store in Express 6 | const port = normalizePort(process.env.PORT || '3001') 7 | app.set('port', port) 8 | 9 | // create HTTP server 10 | const server = http.createServer(app) 11 | 12 | // listen on provided port, on all network interfaces 13 | server.listen(port) 14 | server.on('error', onError) 15 | server.on('listening', onListening) 16 | 17 | // normalize a port into a number, string, or false 18 | function normalizePort(val) { 19 | const port = parseInt(val, 10) 20 | if (isNaN(port)) { 21 | // named pipe 22 | return val 23 | } 24 | if (port >= 0) { 25 | // port number 26 | return port 27 | } 28 | return false 29 | } 30 | 31 | // event listener for HTTP server `error` event 32 | function onError(error) { 33 | if (error.syscall !== 'listen') { 34 | throw error 35 | } 36 | 37 | const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}` 38 | 39 | // handle specific listen errors with friendly messages 40 | switch (error.code) { 41 | case 'EACCES': 42 | console.error(`${bind} requires elevated privileges`) 43 | process.exit(1) 44 | break 45 | case 'EADDRINUSE': 46 | console.error(`${bind} is already in use`) 47 | process.exit(1) 48 | break 49 | default: 50 | throw error 51 | } 52 | } 53 | 54 | // event listener for HTTP server `listening` event 55 | function onListening() { 56 | const addr = server.address() 57 | const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}` 58 | console.log(`Listening on ${bind}`) 59 | } 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decoupled MERN Stack with JWT Auth Template - Back End 2 | 3 | This is the back end of a decoupled MERN Stack app that includes JWT Authentication. 4 | 5 | When combined with the front end found [here](https://github.com/SEI-Remote/decoupled-mern-jwt-auth-template-front-end), you'll have all you need to build a full stack MERN app! 6 | 7 | Use this to go build things! 🚀 8 | 9 | ## To Use This Template 10 | 11 | **Replace `` (including the `<` and `>`) in the commands below with the name of your app!** 12 | 13 | ```bash 14 | git clone https://github.com/SEI-Remote/decoupled-mern-jwt-auth-template-back-end -back-end 15 | cd -back-end 16 | code . 17 | ``` 18 | 19 | With the project open in VS Code, open a terminal and run: 20 | 21 | ```bash 22 | rm -rf .git 23 | ``` 24 | 25 | Here's what your command line output should like after this step (note that the indicator that we are in a git repository is gone!) 26 | 27 | The command line before and after running the rm -rf .git command. Before git:(main) is visible indiating that the directory contains a git repository, after the command it is not. 28 | 29 | Re-initialize a git repository: 30 | 31 | ```bash 32 | git init 33 | ``` 34 | 35 | Create a repo for this project on GitHub and add that remote to your project with (replacing your-repo-URL-here with the URL of the repo you just created): 36 | 37 | ```bash 38 | git remote add origin your-repo-URL-here 39 | ``` 40 | 41 | Run `npm i` to fetch the template's dependencies: 42 | 43 | ```bash 44 | npm i 45 | ``` 46 | 47 | touch a `.env` file: 48 | 49 | ```bash 50 | touch .env 51 | ``` 52 | 53 | Fill it with the following: 54 | 55 | ``` 56 | DATABASE_URL=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 57 | SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 58 | CLOUDINARY_URL=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 59 | ``` 60 | 61 | Replace the `DATABASE_URL`, `SECRET`, and `CLOUDINARY_URL` with values that you provide. 62 | 63 | > 🚨 Place secrets in this `.env` file. The contents of this file WILL NOT be exposed to site visitors. 64 | 65 | Delete this `README.md`, then make an initial commit: 66 | 67 | ```bash 68 | git add . 69 | git commit -m "initial commit" 70 | git push origin main 71 | ``` 72 | 73 | Launch the app with: 74 | 75 | ```bash 76 | nodemon 77 | ``` 78 | 79 | You're done! 80 | -------------------------------------------------------------------------------- /controllers/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | 3 | import { User } from '../models/user.js' 4 | import { Profile } from '../models/profile.js' 5 | 6 | async function signup(req, res) { 7 | try { 8 | if (!process.env.SECRET) throw new Error('no SECRET in back-end .env') 9 | if (!process.env.CLOUDINARY_URL) { 10 | throw new Error('no CLOUDINARY_URL in back-end .env file') 11 | } 12 | 13 | const user = await User.findOne({ email: req.body.email }) 14 | if (user) throw new Error('Account already exists') 15 | 16 | const newProfile = await Profile.create(req.body) 17 | req.body.profile = newProfile._id 18 | const newUser = await User.create(req.body) 19 | 20 | const token = createJWT(newUser) 21 | res.status(200).json({ token }) 22 | } catch (err) { 23 | console.log(err) 24 | try { 25 | if (req.body.profile) { 26 | await Profile.findByIdAndDelete(req.body.profile) 27 | } 28 | } catch (err) { 29 | console.log(err) 30 | return res.status(500).json({ err: err.message }) 31 | } 32 | res.status(500).json({ err: err.message }) 33 | } 34 | } 35 | 36 | async function login(req, res) { 37 | try { 38 | if (!process.env.SECRET) throw new Error('no SECRET in back-end .env') 39 | if (!process.env.CLOUDINARY_URL) { 40 | throw new Error('no CLOUDINARY_URL in back-end .env') 41 | } 42 | 43 | const user = await User.findOne({ email: req.body.email }) 44 | if (!user) throw new Error('User not found') 45 | 46 | const isMatch = await user.comparePassword(req.body.password) 47 | if (!isMatch) throw new Error('Incorrect password') 48 | 49 | const token = createJWT(user) 50 | res.json({ token }) 51 | } catch (err) { 52 | handleAuthError(err, res) 53 | } 54 | } 55 | 56 | async function changePassword(req, res) { 57 | try { 58 | const user = await User.findById(req.user._id) 59 | if (!user) throw new Error('User not found') 60 | 61 | const isMatch = user.comparePassword(req.body.password) 62 | if (!isMatch) throw new Error('Incorrect password') 63 | 64 | user.password = req.body.newPassword 65 | await user.save() 66 | 67 | const token = createJWT(user) 68 | res.json({ token }) 69 | 70 | } catch (err) { 71 | handleAuthError(err, res) 72 | } 73 | } 74 | 75 | /* --== Helper Functions ==-- */ 76 | 77 | function handleAuthError(err, res) { 78 | console.log(err) 79 | const { message } = err 80 | if (message === 'User not found' || message === 'Incorrect password') { 81 | res.status(401).json({ err: message }) 82 | } else { 83 | res.status(500).json({ err: message }) 84 | } 85 | } 86 | 87 | function createJWT(user) { 88 | return jwt.sign({ user }, process.env.SECRET, { expiresIn: '24h' }) 89 | } 90 | 91 | export { signup, login, changePassword } 92 | --------------------------------------------------------------------------------