Refactored - now includes fullstack AUTH

This commit is contained in:
Ben Elferink
2021-09-28 18:53:02 +03:00
parent c349907802
commit 2f40cf797c
34 changed files with 6684 additions and 695 deletions

View File

@@ -1 +1,3 @@
MONGO_URI = "MongoDB connection URL"
PORT = ""
MONGO_URI = ""
JWT_SECRET = ""

View File

@@ -1,38 +0,0 @@
import Account from '../models/Account.js';
// more about response status codes ---> https://restapitutorial.com/httpstatuscodes.html
export async function registerAccount(request, response, next) {
try {
// Handle your logic here...
// return something to the client-side
response.status(201).json({ message: 'Account registered' });
} catch (error) {
console.error(error);
response.status(500).send();
}
}
export async function loginAccount(request, response, next) {
try {
// Handle your logic here...
// return something to the client-side
response.status(200).json({ message: 'Account logged-in' });
} catch (error) {
console.error(error);
response.status(500).send();
}
}
export async function getAccount(request, response, next) {
try {
// Handle your logic here...
// return something to the client-side
response.status(200).json({ message: 'Account fetched' });
} catch (error) {
console.error(error);
response.status(500).send();
}
}

View File

@@ -1,24 +0,0 @@
import express from 'express';
import { registerAccount, loginAccount, getAccount } from '../controllers/authControllers.js';
// initialize router
const router = express.Router();
// example: empty middleware
const middleware = (request, response, next) => next();
/*
request methods ---> https://www.tutorialspoint.com/http/http_methods.htm
1st param = extended url path
2nd param = middlewares (optional)
3rd param = request & response function (controller)
*/
// POST at route: http://localhost:8080/auth/register
router.post('/register', middleware, registerAccount);
// POST at path: http://localhost:8080/auth/login
router.post('/login', middleware, loginAccount);
// GET at path: http://localhost:8080/auth/account
router.get('/account', middleware, getAccount);
export default router;

16
server/constants/index.js Normal file
View File

@@ -0,0 +1,16 @@
const ORIGIN = '*'
const PORT = process.env.PORT || 8080
// for "atlas" edit MONGO_URI in -> .env file || for "community server" edit <MyDatabase>
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/MyDatabase'
const MONGO_OPTIONS = {}
const JWT_SECRET = process.env.JWT_SECRET || 'unsafe_secret'
module.exports = {
ORIGIN,
PORT,
MONGO_URI,
MONGO_OPTIONS,
JWT_SECRET,
}

View File

@@ -0,0 +1,20 @@
const Account = require('../../models/Account')
async function getAccount(request, response, next) {
try {
const {uid} = request.auth
// Get account from DB, existance not verified because we are already authorized at this point
const foundAccount = await Account.findOne({_id: uid}).select('-password')
response.status(200).json({
message: 'Account fetched',
data: foundAccount,
})
} catch (error) {
console.error(error)
response.status(500).send()
}
}
module.exports = getAccount

View File

@@ -0,0 +1,59 @@
const joi = require('joi')
const bcrypt = require('bcrypt')
const Account = require('../../models/Account')
const {signToken} = require('../../middlewares/jsonwebtoken')
async function login(request, response, next) {
try {
// Validate request data
await joi
.object({
username: joi.string().required(),
password: joi.string().required(),
})
.validateAsync(request.body)
} catch (error) {
return response.status(400).json({
error: 'ValidationError',
message: error.message,
})
}
try {
const {username, password} = request.body
// Get account from DB, and verify existance
const foundAccount = await Account.findOne({username})
if (!foundAccount) {
return response.status(400).json({
message: 'Bad credentials',
})
}
// Decrypt and verify password
const passOk = await bcrypt.compare(password, foundAccount.password)
if (!passOk) {
return response.status(400).json({
message: 'Bad credentials',
})
}
// Remove password from response data
foundAccount.password = undefined
delete foundAccount.password
// Generate access token
const token = signToken({uid: foundAccount._id})
response.status(200).json({
message: 'Succesfully logged-in',
data: foundAccount,
token,
})
} catch (error) {
console.error(error)
response.status(500).send()
}
}
module.exports = login

View File

@@ -0,0 +1,60 @@
const joi = require('joi')
const bcrypt = require('bcrypt')
const Account = require('../../models/Account')
const {signToken} = require('../../middlewares/jsonwebtoken')
async function register(request, response, next) {
try {
// Validate request data
await joi
.object({
username: joi.string().required(),
password: joi.string().required(),
})
.validateAsync(request.body)
} catch (error) {
return response.status(400).json({
error: 'ValidationError',
message: error.message,
})
}
try {
const {username, password} = request.body
// Verify account username as unique
const existingAccount = await Account.findOne({username})
if (existingAccount) {
return response.status(400).json({
error: username,
message: 'An account already exists with that "username"',
})
}
// Encrypt password
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(password, salt)
// Create account
const newAccount = new Account({username, password: hash})
await newAccount.save()
// Remove password from response data
newAccount.password = undefined
delete newAccount.password
// Generate access token
const token = signToken({uid: newAccount._id})
response.status(201).json({
message: 'Succesfully registered',
data: newAccount,
token,
})
} catch (error) {
console.error(error)
return response.status(500).send()
}
}
module.exports = register

View File

@@ -1,42 +1,19 @@
import mongoose from 'mongoose'; // MongoDB (database)
import express from 'express'; // Backend App (server)
import dotenv from 'dotenv'; // Secures variables
import cors from 'cors'; // HTTP headers (enable requests)
import morgan from 'morgan'; // Logs incoming requests
import authRoutes from './api/routes/authRoutes.js';
// ^ ^ ^ how to use imported route(s)
require('dotenv').config() // Secures variables
const app = require('./utils/app') // Backend App (server)
const mongo = require('./utils/mongo') // MongoDB (database)
const {PORT} = require('./constants')
const authRoutes = require('./routes/auth')
// initialize app
const app = express();
const origin = '*'; // allow source of requests (* --> everywhere)
async function bootstrap() {
await mongo.connect()
// middlewares
dotenv.config();
app.use(cors({ origin }));
app.use(express.json({ limit: '1mb', extended: false })); // body parser
app.use(express.urlencoded({ limit: '1mb', extended: false })); // url parser
app.use(morgan('common'));
app.get('/', (req, res) => res.status(200).json({message: 'Hello World!'}))
app.get('/healthz', (req, res) => res.status(200).send())
app.use('/auth', authRoutes)
// configure db:
// for "atlas" edit MONGO_URI in -> .env file || for "community server" edit <dbname>
const CONNECTION_URL = process.env.MONGO_URI || 'mongodb://localhost:27017/<dbname>';
const DEPRECATED_FIX = { useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true };
app.listen(PORT, () => {
console.log(`✅ Server is listening on port: ${PORT}`)
})
}
// connect to db
mongoose
.connect(CONNECTION_URL, DEPRECATED_FIX)
.catch((error) => console.log('❌ MongoDB connection error', error)); // listen for errors on initial connection
const db = mongoose.connection;
db.on('connected', () => console.log('✅ MongoDB connected')); // connected
db.on('disconnected', () => console.log('❌ MongoDB disconnected')); // disconnected
db.on('error', (error) => console.log('❌ MongoDB connection error', error)); // listen for errors during the session
// define routes
app.get('/', (request, response, next) => response.status(200).json({ message: 'Hello World!' }));
app.use('/auth', authRoutes);
// ^ ^ ^ how to use imported route(s)
// listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => console.log(`✅ Server is listening on port: ${PORT}`));
bootstrap()

View File

@@ -0,0 +1,39 @@
const jwt = require('jsonwebtoken')
const {JWT_SECRET} = require('../constants')
const signToken = (payload = {}, expiresIn = '12h') => {
const token = jwt.sign(payload, JWT_SECRET, {expiresIn})
return token
}
const authorizeBearerToken = (request, response, next) => {
try {
const token = request.headers.authorization?.split(' ')[1]
if (!token) {
return response.status(400).json({
message: 'Token not provided',
})
}
const auth = jwt.verify(token, JWT_SECRET)
if (!auth) {
return response.status(401).json({
message: 'Unauthorized - invalid token',
})
}
request.auth = auth
next()
} catch (error) {
console.error(error)
return response.status(401).json({
message: 'Unauthorized - invalid token',
})
}
}
module.exports = {
authorizeBearerToken,
signToken,
}

View File

@@ -1,4 +1,4 @@
import mongoose from 'mongoose';
const mongoose = require('mongoose')
const instance = new mongoose.Schema(
{
@@ -7,22 +7,23 @@ const instance = new mongoose.Schema(
_id: mongoose.Schema.Types.ObjectId,
*/
// key: Type,
email: String,
password: String,
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
},
{
timestamps: true,
// ^ ^ ^ this creates and maintains:
// {
// createdAt: Date,
// updatedAt: Date,
// }
},
);
)
// NOTE! use a singular model name, mongoose automatically creates a collection like so:
// model: 'Account' === collection: 'accounts'
const modelName = 'Account';
const modelName = 'Account'
export default mongoose.model(modelName, instance);
module.exports = mongoose.model(modelName, instance)

3963
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,22 @@
{
"homepage": "",
"name": "server",
"version": "0.1.0",
"description": "",
"private": true,
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon server.js || node index.js"
},
"license": "ISC",
"scripts": {
"start": "nodemon index.js || node index.js"
},
"dependencies": {
"bcrypt": "^5.0.1",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^5.12.0",
"morgan": "^1.10.0"
"joi": "^17.4.2",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.0.7"
},
"devDependencies": {
"nodemon": "^2.0.13"
}
}

19
server/routes/auth.js Normal file
View File

@@ -0,0 +1,19 @@
const express = require('express')
const {authorizeBearerToken} = require('../middlewares/jsonwebtoken')
const register = require('../controllers/auth/register')
const login = require('../controllers/auth/login')
const getAccount = require('../controllers/auth/get-account')
// initialize router
const router = express.Router()
// POST at route: http://localhost:8080/auth/register
router.post('/register', [], register)
// POST at path: http://localhost:8080/auth/login
router.post('/login', [], login)
// GET at path: http://localhost:8080/auth/account
router.get('/account', [authorizeBearerToken], getAccount)
module.exports = router

20
server/utils/app.js Normal file
View File

@@ -0,0 +1,20 @@
const express = require('express') // Backend App (server)
const cors = require('cors') // HTTP headers (enable requests)
const {ORIGIN} = require('../constants')
// initialize app
const app = express()
// middlewares
app.use(cors({origin: ORIGIN}))
app.use(express.json({extended: true})) // body parser
app.use(express.urlencoded({extended: false})) // url parser
// error handling
app.use((err, req, res, next) => {
console.error(err)
res.status(500).send()
next()
})
module.exports = app

32
server/utils/mongo.js Normal file
View File

@@ -0,0 +1,32 @@
const mongoose = require('mongoose')
const {MONGO_URI} = require('../constants')
const {MONGO_OPTIONS} = require('../constants')
class MongoDB {
constructor() {
this.mongoose = mongoose
this.isConnected = false
this.MONGO_URI = MONGO_URI
this.MONGO_OPTIONS = MONGO_OPTIONS
}
async connect() {
if (this.isConnected) return
try {
const db = await this.mongoose.connect(this.MONGO_URI, this.MONGO_OPTIONS)
const connection = db.connection
this.isConnected = connection.readyState === 1
if (this.isConnected) console.log('✅ MongoDB connected')
connection.on('connected', () => console.log('✅ MongoDB connected')) // re-connected
connection.on('disconnected', () => console.log('❌ MongoDB disconnected')) // disconnected
connection.on('error', (error) => console.log('❌ MongoDB connection error', error)) // listen for errors during the session
} catch (error) {
console.log('❌ MongoDB connection error:', error.message)
}
}
}
module.exports = new MongoDB()