├── .gitignore ├── config ├── allowedOrigins.js ├── dbConn.js └── corsOptions.js ├── routes ├── root.js ├── userRoutes.js └── authRoutes.js ├── views ├── index.html └── 404.html ├── controllers ├── usersController.js └── authController.js ├── public └── css │ └── style.css ├── models └── User.js ├── package.json ├── middleware └── verifyJWT.js ├── LICENSE └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /config/allowedOrigins.js: -------------------------------------------------------------------------------- 1 | const allowedOrigins = ["http://localhost:3000"]; 2 | 3 | module.exports = allowedOrigins; 4 | -------------------------------------------------------------------------------- /config/dbConn.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectDB = async () => { 4 | try { 5 | await mongoose.connect(process.env.DATABASE_URI); 6 | } catch (err) { 7 | console.log(err); 8 | } 9 | }; 10 | 11 | module.exports = connectDB; 12 | -------------------------------------------------------------------------------- /routes/root.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const path = require("path"); 4 | 5 | router.get("/", (req, res) => { 6 | res.sendFile(path.join(__dirname, "..", "views", "index.html")); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const usersController = require("../controllers/usersController"); 4 | const verifyJWT = require("../middleware/verifyJWT"); 5 | 6 | router.use(verifyJWT); 7 | router.route("/").get(usersController.getAllUsers); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |The resource you have requested does not exist.
12 | 13 | 14 | -------------------------------------------------------------------------------- /config/corsOptions.js: -------------------------------------------------------------------------------- 1 | const allowedOrigins = require("./allowedOrigins"); 2 | const corsOptions = { 3 | origin: (origin, callback) => { 4 | if (allowedOrigins.indexOf(origin) !== -1 || !origin) { 5 | callback(null, true); 6 | } else { 7 | callback(new Error("Not allowed by CORS")); 8 | } 9 | }, 10 | credentials: true, 11 | optionsSuccessStatus: 200, 12 | }; 13 | module.exports = corsOptions; 14 | -------------------------------------------------------------------------------- /routes/authRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const authController = require("../controllers/authController"); 4 | 5 | router.route("/register").post(authController.register); 6 | router.route("/login").post(authController.login); 7 | router.route("/refresh").get(authController.refresh); 8 | router.route("/logout").post(authController.logout); 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /public/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap"); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | font-family: "Share Tech Mono", monospace; 11 | font-size: 2.5rem; 12 | } 13 | 14 | body { 15 | min-height: 100vh; 16 | background-color: #000; 17 | color: whitesmoke; 18 | display: grid; 19 | place-content: center; 20 | padding: 1rem; 21 | } 22 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | first_name: { 6 | type: String, 7 | required: true, 8 | }, 9 | last_name: { 10 | type: String, 11 | required: true, 12 | }, 13 | email: { 14 | type: String, 15 | required: true, 16 | }, 17 | password: { 18 | type: String, 19 | required: true, 20 | }, 21 | }, 22 | { timestamps: true } 23 | ); 24 | module.exports = mongoose.model("User", userSchema); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server", 8 | "dev": "nodemon server" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.1", 15 | "cookie-parser": "^1.4.6", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.3.1", 18 | "express": "^4.18.2", 19 | "jsonwebtoken": "^9.0.1", 20 | "mongoose": "^7.4.3", 21 | "nodemon": "^3.0.1" 22 | }, 23 | "engines": { 24 | "node": ">=14.20.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /middleware/verifyJWT.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const verifyJWT = (req, res, next) => { 4 | const authHeader = req.headers.authorization || req.headers.Authorization; // "Bearer token" 5 | 6 | if (!authHeader?.startsWith("Bearer ")) { 7 | return res.status(401).json({ message: "Unauthorized" }); 8 | } 9 | const token = authHeader.split(" ")[1]; // ["Bearer","token"] 10 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => { 11 | if (err) return res.status(403).json({ message: "Forbidden" }); 12 | req.user = decoded.UserInfo.id; 13 | next(); 14 | }); 15 | }; 16 | module.exports = verifyJWT; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Kareem Shimes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const app = express(); 4 | const connectDB = require('./config/dbConn'); 5 | const mongoose = require('mongoose'); 6 | const cookieParser = require('cookie-parser'); 7 | const cors = require('cors'); 8 | const path = require('path'); 9 | const corsOptions = require('./config/corsOptions'); 10 | const PORT = process.env.PORT || 5000; 11 | 12 | connectDB(); 13 | 14 | app.use(cors(corsOptions)); 15 | app.use(cookieParser()); 16 | app.use(express.json()); 17 | 18 | app.use('/', express.static(path.join(__dirname, 'public'))); 19 | 20 | app.use('/', require('./routes/root')); 21 | app.use('/auth', require('./routes/authRoutes')); 22 | app.use('/users', require('./routes/userRoutes')); 23 | app.all('*', (req, res) => { 24 | res.status(404); 25 | if (req.accepts('html')) { 26 | res.sendFile(path.join(__dirname, 'views', '404.html')); 27 | } else if (req.accepts('json')) { 28 | res.json({ message: '404 Not Found' }); 29 | } else { 30 | res.type('txt').send('404 Not Found'); 31 | } 32 | }); 33 | mongoose.connection.once('open', () => { 34 | console.log('Connected to MongoDB'); 35 | app.listen(PORT, () => { 36 | console.log(`Server running on port ${PORT}`); 37 | }); 38 | }); 39 | mongoose.connection.on('error', (err) => { 40 | console.log(err); 41 | }); 42 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | const User = require('../models/User'); 2 | const bcrypt = require('bcrypt'); 3 | const jwt = require('jsonwebtoken'); 4 | const register = async (req, res) => { 5 | const { first_name, last_name, email, password } = req.body; 6 | if (!first_name || !last_name || !email || !password) { 7 | return res.status(400).json({ message: 'All fields are required' }); 8 | } 9 | const foundUser = await User.findOne({ email }).exec(); 10 | if (foundUser) { 11 | return res.status(401).json({ message: 'User already exists' }); 12 | } 13 | const hashedPassword = await bcrypt.hash(password, 10); 14 | 15 | const user = await User.create({ 16 | first_name, 17 | last_name, 18 | email, 19 | password: hashedPassword, 20 | }); 21 | const accessToken = jwt.sign( 22 | { 23 | UserInfo: { 24 | id: user._id, 25 | }, 26 | }, 27 | process.env.ACCESS_TOKEN_SECRET, 28 | { expiresIn: 10 } 29 | ); 30 | const refreshToken = jwt.sign( 31 | { 32 | UserInfo: { 33 | id: user._id, 34 | }, 35 | }, 36 | process.env.REFRESH_TOKEN_SECRET, 37 | { expiresIn: '7d' } 38 | ); 39 | res.cookie('jwt', refreshToken, { 40 | httpOnly: true, //accessible only by web server 41 | secure: true, //https 42 | sameSite: 'None', //cross-site cookie 43 | maxAge: 7 * 24 * 60 * 60 * 1000, 44 | }); 45 | res.status(201).json({ 46 | accessToken, 47 | email: user.email, 48 | first_name: user.first_name, 49 | last_name: user.last_name, 50 | }); 51 | }; 52 | const login = async (req, res) => { 53 | const { email, password } = req.body; 54 | if (!email || !password) { 55 | return res.status(400).json({ message: 'All fields are required' }); 56 | } 57 | const foundUser = await User.findOne({ email }).exec(); 58 | if (!foundUser) { 59 | return res.status(401).json({ message: 'User does not exist' }); 60 | } 61 | const match = await bcrypt.compare(password, foundUser.password); 62 | 63 | if (!match) return res.status(401).json({ message: 'Wrong Password' }); 64 | 65 | const accessToken = jwt.sign( 66 | { 67 | UserInfo: { 68 | id: foundUser._id, 69 | }, 70 | }, 71 | process.env.ACCESS_TOKEN_SECRET, 72 | { expiresIn: 10 } 73 | ); 74 | const refreshToken = jwt.sign( 75 | { 76 | UserInfo: { 77 | id: foundUser._id, 78 | }, 79 | }, 80 | process.env.REFRESH_TOKEN_SECRET, 81 | { expiresIn: '7d' } 82 | ); 83 | res.cookie('jwt', refreshToken, { 84 | httpOnly: true, //accessible only by web server 85 | secure: true, //https 86 | sameSite: 'None', //cross-site cookie 87 | maxAge: 7 * 24 * 60 * 60 * 1000, 88 | }); 89 | res.json({ 90 | accessToken, 91 | email: foundUser.email, 92 | }); 93 | }; 94 | const refresh = (req, res) => { 95 | const cookies = req.cookies; 96 | if (!cookies?.jwt) res.status(401).json({ message: 'Unauthorized' }); 97 | const refreshToken = cookies.jwt; 98 | jwt.verify( 99 | refreshToken, 100 | process.env.REFRESH_TOKEN_SECRET, 101 | async (err, decoded) => { 102 | if (err) return res.status(403).json({ message: 'Forbidden' }); 103 | const foundUser = await User.findById(decoded.UserInfo.id).exec(); 104 | if (!foundUser) return res.status(401).json({ message: 'Unauthorized' }); 105 | const accessToken = jwt.sign( 106 | { 107 | UserInfo: { 108 | id: foundUser._id, 109 | }, 110 | }, 111 | process.env.ACCESS_TOKEN_SECRET, 112 | { expiresIn: 10 } 113 | ); 114 | res.json({ accessToken }); 115 | } 116 | ); 117 | }; 118 | const logout = (req, res) => { 119 | const cookies = req.cookies; 120 | if (!cookies?.jwt) return res.sendStatus(204); //No content 121 | res.clearCookie('jwt', { 122 | httpOnly: true, 123 | sameSite: 'None', 124 | secure: true, 125 | }); 126 | res.json({ message: 'Cookie cleared' }); 127 | }; 128 | module.exports = { 129 | register, 130 | login, 131 | refresh, 132 | logout, 133 | }; 134 | --------------------------------------------------------------------------------