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

@@ -10,6 +10,14 @@
<br />
<br />
# What is this template?
This template allows you to quick-start your Fullstack application using the MERN stack, it has a server setup with some basic authentication, and a client ready to communicate with the backend.<br />
I have attempted to use the best practices for both ends, which should make it easy for any advanced/new developer to use, and perhaps learn from.
<br />
<br />
# How to use this template
[📀 Demo video](https://youtu.be/N2pvvkyoS68)
@@ -17,17 +25,20 @@
### STEP 1:
Click ["Use this template"](https://github.com/belferink1996/MERN-template/generate) to generate a
new repository.<br /> Then open your terminal and clone your repository:
new repository.<br />
Then open your terminal and clone your repository:
> cd ~/Desktop <br /> git clone https://github.com/[your-user-name]/[your-repo-name].git
> cd ~/Desktop <br />
> git clone https://github.com/[your-user-name]/[your-repo-name].git
<br />
### STEP 2:
Go to your repository's folder, and install all dependecies:
Go to the root of your repository's folder, and install all dependecies:
> cd ~/Desktop/[your-repo-name]<br /> npm install
> cd ~/Desktop/[your-repo-name]<br />
> npm install
<br />
@@ -35,7 +46,7 @@ Go to your repository's folder, and install all dependecies:
Prepare your MongoDB database ([atlas](https://www.mongodb.com/cloud/atlas),
[community](<https://github.com/belferink1996/MERN-template/wiki/Install-MongoDB-Community-Server-(MacOS)>)).<br />
Then go to your server folder (backend), and set your database within `server.js`,
Then configure your database within `server/constants/index.js`, by configuring the `MONGO_URI` variable.
<br />
@@ -44,19 +55,19 @@ Then go to your server folder (backend), and set your database within `server.js
<br />
<br />
# Node dependecies & versions:
### To run the client and/or the server, you can do any of the following:
###### Client:
#### Short Method
> axios: ^0.21.1 &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; Use the API<br /> react: ^17.0.1
> &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; UI framework<br /> react-dom: ^17.0.1
> &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; UI framework<br /> react-scripts: 4.0.3
> &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; React 'npm' scripts
From the root of your project run:
> npm start
###### Server:
#### Long Method
> cors: ^2.8.5 &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; Enable HTTP requests<br/> dotenv: ^8.2.0
> &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; Secure sensitive information<br /> express: ^4.17.1
> &nbsp;&nbsp;&nbsp; ---> &nbsp;&nbsp;&nbsp; Server app<br /> mongoose: ^5.12.0 &nbsp;&nbsp;&nbsp;
> ---> &nbsp;&nbsp;&nbsp; MongoDB database<br /> morgan: ^1.10.0 &nbsp;&nbsp;&nbsp; --->
> &nbsp;&nbsp;&nbsp; Logs incoming requests
Open terminal #1 (backend)
> cd ./server<br />
> npm start
Open terminal #2 (frontend)
> cd ./client<br />
> npm start

1396
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,6 @@
{
"homepage": "",
"name": "client",
"version": "0.1.0",
"description": "",
"private": true,
"main": "index.js",
"license": "ISC",
@@ -12,15 +10,18 @@
"eject": "react-scripts eject"
},
"dependencies": {
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@fontsource/roboto": "^4.5.1",
"@mui/material": "^5.0.1",
"axios": "^0.21.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
"react-app"
]
},
"browserslist": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,28 +1,24 @@
import './styles/styles.css';
import { useAuth } from './contexts/AuthContext';
import {useAuth} from './contexts/AuthContext'
import Header from './components/Header'
export default function App() {
const { isLoggedIn } = useAuth();
const {isLoggedIn} = useAuth()
return (
<div className='App'>
<h1>{isLoggedIn ? <LoggedInText /> : <LoggedOutText />}</h1>
<Header />
{isLoggedIn ? <LoggedInText /> : <LoggedOutText />}
</div>
);
)
}
const LoggedInText = () => (
<>
You are (not really) logged in,
<br />
check your console.log()
</>
);
const LoggedInText = () => {
const {account} = useAuth()
return <p>Hey, {account.username}! I'm happy to let you know: you are authenticated!</p>
}
const LoggedOutText = () => (
<>
Don't forget to start your backend server,
<br />
then hit refresh and see what happens...
</>
);
<p>Don't forget to start your backend server, then authenticate yourself.</p>
)

View File

@@ -1,9 +1,9 @@
import axios from 'axios';
import axios from 'axios'
// api url (where your serve is hosted at)
export const backendUrl = 'http://localhost:8080';
export const backendUrl = 'http://localhost:8080'
// axios configuration
export default axios.create({
baseURL: backendUrl,
});
})

View File

@@ -0,0 +1,124 @@
import {Fragment, useState} from 'react'
import {Dialog, DialogTitle, TextField, Button, CircularProgress} from '@mui/material'
import axios from '../api'
import {useAuth} from '../contexts/AuthContext'
const textFieldSx = {mx: 2, my: 0.5}
export default function AuthModal({open, close, register, toggleRegister}) {
const {setIsLoggedIn, setToken, setAccount} = useAuth()
const [formData, setFormData] = useState({})
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleChange = (e) => {
const {name, value} = e.target
setFormData((prev) => ({...prev, [name]: value}))
}
const clickSubmit = async () => {
setLoading(true)
setError('')
try {
const requestPath = register ? '/auth/register' : '/auth/login'
const response = await axios.post(requestPath, formData)
setToken(response.data.token)
setAccount(response.data.data)
setIsLoggedIn(true)
close()
} catch (error) {
console.error(error)
setError(error?.response?.data?.message ?? error.message)
}
setLoading(false)
}
const disabledLoginButton = !formData['username'] || !formData['password']
const disabledRegisterButton = !formData['username'] || !formData['password']
return (
<Dialog open={open} onClose={close}>
{register ? (
<RegisterForm formData={formData} handleChange={handleChange} />
) : (
<LoginForm formData={formData} handleChange={handleChange} />
)}
{error && <span className='error'>{error}</span>}
{loading ? (
<center>
<CircularProgress color='inherit' />
</center>
) : (
<Button
onClick={clickSubmit}
disabled={register ? disabledRegisterButton : disabledLoginButton}>
{register ? 'Register' : 'Login'}
</Button>
)}
<Button onClick={toggleRegister}>
{register ? 'I already have an account' : "I don't have an account"}
</Button>
</Dialog>
)
}
function LoginForm({formData, handleChange}) {
return (
<Fragment>
<DialogTitle>Login to your account</DialogTitle>
<TextField
label='Username'
name='username'
value={formData['username'] ?? ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
<TextField
label='Password'
name='password'
value={formData['password'] ?? ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
</Fragment>
)
}
function RegisterForm({formData, handleChange}) {
return (
<Fragment>
<DialogTitle>Create a new account</DialogTitle>
<TextField
label='Username'
name='username'
value={formData['username'] ?? ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
<TextField
label='Password'
name='password'
value={formData['password'] ?? ''}
onChange={handleChange}
variant='filled'
sx={textFieldSx}
required
/>
</Fragment>
)
}

View File

@@ -0,0 +1,77 @@
import {Fragment, useState} from 'react'
import {IconButton, Avatar, Popover, List, ListSubheader, ListItemButton} from '@mui/material'
import OnlineIndicator from './OnlineIndicator'
import AuthModal from './AuthModal'
import {useAuth} from '../contexts/AuthContext'
export default function Header() {
const {isLoggedIn, account, logout} = useAuth()
const [anchorEl, setAnchorEl] = useState(null)
const [popover, setPopover] = useState(false)
const [authModal, setAuthModal] = useState(false)
const [register, setRegister] = useState(false)
const openPopover = (e) => {
setPopover(true)
setAnchorEl(e.currentTarget)
}
const closePopover = () => {
setPopover(false)
setAnchorEl(null)
}
const clickLogin = () => {
setRegister(false)
setAuthModal(true)
closePopover()
}
const clickRegister = () => {
setRegister(true)
setAuthModal(true)
closePopover()
}
return (
<header className='header'>
<h1>Web App</h1>
<IconButton onClick={openPopover}>
<OnlineIndicator online={isLoggedIn}>
<Avatar src={account?.username ?? ''} alt={account?.username ?? ''} />
</OnlineIndicator>
</IconButton>
<Popover
anchorEl={anchorEl}
open={popover}
onClose={closePopover}
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}
transformOrigin={{vertical: 'top', horizontal: 'right'}}>
<List style={{minWidth: '100px'}}>
<ListSubheader style={{textAlign: 'center'}}>
Hello, {isLoggedIn ? 'Ben' : 'Guest'}
</ListSubheader>
{isLoggedIn ? (
<ListItemButton onClick={logout}>Logout</ListItemButton>
) : (
<Fragment>
<ListItemButton onClick={clickLogin}>Login</ListItemButton>
<ListItemButton onClick={clickRegister}>Reigster</ListItemButton>
</Fragment>
)}
</List>
</Popover>
<AuthModal
open={authModal}
close={() => setAuthModal(false)}
register={register}
toggleRegister={() => setRegister((prev) => !prev)}
/>
</header>
)
}

View File

@@ -0,0 +1,63 @@
import {styled} from '@mui/material/styles'
import {Badge, Avatar} from '@mui/material'
const StyledBadge = styled(Badge)(({theme}) => ({
'& .MuiBadge-badge': {
backgroundColor: 'black',
color: 'black',
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: 'ripple 1.2s infinite ease-in-out',
border: '1px solid currentColor',
content: '""',
},
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
}))
const OnlineBadge = styled(StyledBadge)(({theme}) => ({
'& .MuiBadge-badge': {
backgroundColor: 'var(--online)',
color: 'var(--online)',
},
}))
const OfflineBadge = styled(StyledBadge)(({theme}) => ({
'& .MuiBadge-badge': {
backgroundColor: 'var(--offline)',
color: 'var(--offline)',
},
}))
export default function OnlineIndicator({online = false, children = <Avatar src='' alt='' />}) {
return online ? (
<OnlineBadge
variant='dot'
overlap='circular'
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
{children}
</OnlineBadge>
) : (
<OfflineBadge
variant='dot'
overlap='circular'
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
{children}
</OfflineBadge>
)
}

View File

@@ -1,37 +1,60 @@
import { createContext, useContext, useState, useEffect } from 'react';
import axios, { backendUrl } from '../api';
import {createContext, useContext, useState, useEffect} from 'react'
import axios from '../api'
// init context
const AuthContext = createContext();
const AuthContext = createContext()
// export the consumer
export function useAuth() {
return useContext(AuthContext);
return useContext(AuthContext)
}
// export the provider (handle all the logic here)
export function AuthProvider({ children }) {
const [isLoggedIn, setIsLoggedIn] = useState(false);
export function AuthProvider({children}) {
const [token, setToken] = useState(localStorage.getItem('token') ?? null)
const [account, setAccount] = useState(null)
const [isLoggedIn, setIsLoggedIn] = useState(false)
//
const logout = () => {
setToken(null)
setAccount(null)
setIsLoggedIn(false)
}
// This side effect keeps local storage updated with recent token value,
// making sure it can be re-used upon refresh or re-open browser
useEffect(() => {
(async () => {
try {
const response = await axios.get('/auth/account');
setIsLoggedIn(true);
if (token) {
localStorage.setItem('token', token)
} else {
localStorage.removeItem('token')
}
}, [token])
// Did you know? You can use CSS in the console!
console.log(
`%cExample of using your backend routes %c(${backendUrl}/auth/account)`,
'color: lime;',
'color: unset;',
response,
);
} catch (error) {
console.error(error.message);
}
})();
}, []);
// This side effect runs only if we have a token, but no account or logged-in boolean.
// This "if" statement applies only when refreshed, or re-opened the browser,
// if true, it will then ask the backend for the account information (and will get them if the token hasn't expired)
useEffect(() => {
if (!isLoggedIn && !account && token) {
;(async () => {
try {
const headers = {headers: {authorization: `Bearer ${token}`}}
const response = await axios.get('/auth/account', headers)
return <AuthContext.Provider value={{ isLoggedIn }}>{children}</AuthContext.Provider>;
setAccount(response.data.data)
setIsLoggedIn(true)
} catch (error) {
console.error(error)
if (error?.response?.statusCode === 401) setToken(null)
}
})()
}
}, [isLoggedIn, account, token]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<AuthContext.Provider
value={{isLoggedIn, setIsLoggedIn, token, setToken, account, setAccount, logout}}>
{children}
</AuthContext.Provider>
)
}

View File

@@ -1,13 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { AuthProvider } from './contexts/AuthContext';
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import {AuthProvider} from './contexts/AuthContext'
import CssBaseline from '@mui/material/CssBaseline'
import '@fontsource/roboto'
import './styles/index.css'
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<CssBaseline />
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById('root'),
);
)

View File

@@ -1,9 +1,11 @@
:root {
--online: #44b700;
--offline: rgb(183, 68, 0);
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: 'Roboto';
font-size: 16px;
}
.App {
@@ -12,7 +14,19 @@ body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.header {
width: 100%;
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.error {
margin: 0.5rem;
color: red;
text-align: center;
}

1093
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,27 @@
{
"homepage": "https://github.com/belferink1996/MERN-template#readme",
"name": "mern-application",
"version": "0.1.0",
"scripts": {
"install": "cd ./server && npm install && cd ./../client && npm install"
"description": "",
"private": true,
"main": "",
"license": "ISC",
"keywords": [],
"author": "Ben Elferink <ben.elferink@icloud.com> (https://www.linkedin.com/in/ben-elferink-37ba251b9)",
"repository": {
"type": "git",
"url": "https://github.com/belferink1996/MERN-template.git"
},
"author": "Ben Elferink",
"license": "ISC"
"bugs": {
"url": "https://github.com/belferink1996/MERN-template/issues",
"email": "ben.elferink@icloud.com"
},
"scripts": {
"install": "concurrently \"cd ./server && npm i\" \"cd ./client && npm i\"",
"start": "concurrently \"cd ./server && npm start\" \"cd ./client && npm start\"",
"delete-modules": "rm -r ./node_modules && cd ./server && rm -r ./node_modules && cd ../client && rm -r ./node_modules"
},
"devDependencies": {
"concurrently": "^6.2.1"
}
}

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()