Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/jannickpepe/mern-waves

A fullstack MERN project with JWT, ApiLimiter, cookies, actions, reducers and apiContext
https://github.com/jannickpepe/mern-waves

cookies express jwt mongodb nodejs reactjs tailwindcss

Last synced: about 1 month ago
JSON representation

A fullstack MERN project with JWT, ApiLimiter, cookies, actions, reducers and apiContext

Awesome Lists containing this project

README

        

# Jobify

#### Track Your Job Search

Project in Action - [Jobify](https://www.jobify.live/)

#### Support

Find the App Useful? [You can always buy me a coffee](https://www.buymeacoffee.com/johnsmilga)

#### Run The App Locally

```sh
npm run install-dependencies
```

- rename .env.temp to .env
- setup values for - MONGO_URL, JWT_SECRET, JWT_LIFETIME

```sh
npm start
```

- visit url http://localhost:3000/

#### Setup React App

- create client folder
- open terminal

```sh
cd client
```

```sh
npx create-react-app .
```

```sh
npm start
```

- set editor/browser side by side
- copy/paste assets from complete project

#### Spring Cleaning

- in src remove
- App.css
- App.test.js
- logo.svg
- reportWebVitals.js
- setupTests.js
- fix App.js and index.js

#### Title and Favicon

- change title in public/index.html
- replace favicon.ico in public
- resource [Generate Favicons](https://favicon.io/)

#### Normalize.css and Global Styles

- CSS in JS (styled-components)
- saves times on the setup
- less lines of css
- speeds up the development
- normalize.css
- small CSS file that provides cross-browser consistency in the default styling of HTML elements.
- [normalize docs](https://necolas.github.io/normalize.css/)

```sh
npm install normalize.css
```

- import 'normalize.css' in index.js
- SET BEFORE 'index.css'
- replace contents of index.css
- if any questions about normalize or specific styles
- Coding Addict - [Default Starter Video](https://youtu.be/UDdyGNlQK5w)
- Repo - [Default Starter Repo](https://github.com/john-smilga/default-starter)

#### Landing Page

- zoom level 175%
- markdown preview extension
- get something on the screen
- react router and styled components right after
- create pages directory in the source
- for now Landing.js
- create component (snippets extension)
- setup basic return

```js

Landing Page


```

- import logo.svg and main.svg
- import Landing in App.js and render

#### Styled Components

- CSS in JS
- Styled Components
- have logic and styles in component
- no name collisions
- apply javascript logic
- [Styled Components Docs](https://styled-components.com/)
- [Styled Components Course](https://www.udemy.com/course/styled-components-tutorial-and-project-course/?referralCode=9DABB172FCB2625B663F)

```sh
npm install styled-components
```

```js
import styled from 'styled-components';

const El = styled.el`
// styles go here
`;
```

- no name collisions, since unique class
- vscode-styled-components extension
- colors and bugs
- style entire react component

```js
const Wrapper = styled.el``;

const Component = () => {
return (

Component



);
};
```

- only responsible for styling
- wrappers folder in assets

#### Logo and Images

- logo built in Figma
- [Cool Images](https://undraw.co/)

#### Logo

- create components folder in source
- create Logo.js
- move import and image logic
- export as default
- utilize index.js

#### React Router

- Version 6
- [React Router Docs](https://reactrouter.com/docs/en/v6)

```sh
npm install history@5 react-router-dom@6
```

- import four components

```js
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
```

- Connect to browser's URL with BrowserRouter
- Routes instead of Switch

```js


Dashboard} />
Register} />
} />
Error}>

```

```js

Dashboard
Register
Home

```

- go to Landing.js

```js
import { Link } from 'react-router-dom';

return (

Login / Register

);
```

#### Setup Pages

- create Error, Register, Dashboard pages
- basic return
- create index.js
- import all the pages
- export one by one
- basically the same, as in components
- import App.js
- add to element={}
- remove temp navbar

#### Error Page

```js
import { Link } from 'react-router-dom';
import img from '../assets/images/not-found.svg';
import Wrapper from '../assets/wrappers/ErrorPage';

return (


not found

text


text


back home


);
```

#### Auto Imports

- use while developing
- only sparingly while recording
- better picture
- messes with flow
- just my preference
- still use them, just not all the time

#### Register Page - Setup

- show preview in Browser and themes

```js
import { useState, useEffect } from 'react';
import { Logo } from '../components';
import Wrapper from '../assets/wrappers/RegisterPage';
// global context and useNavigate later

const initialState = {
name: '',
email: '',
password: '',
isMember: true,
};
// if possible prefer local state
// global state

function Register() {
const [values, setValues] = useState(initialState);

// global context and useNavigate later

const handleChange = (e) => {
console.log(e.target);
};

const onSubmit = (e) => {
e.preventDefault();
console.log(e.target);
};
return (



Login

{/* name field */}



name



submit



);
}
```

#### FormRow Component

- create FormRow.js in components
- setup import/export
- setup one for email and password
- hint "type,name,value"

```js
const FormRow = ({ type, name, value, handleChange, labelText }) => {
return (



{labelText || name}



);
};

export default FormRow;
```

#### Alert Component

- right away setup as component
- create Alert.js in components

```js
const Alert = () => {
return

alert goes here
;
};

export default Alert;
```

- setup import/export
- alert-danger or alert-success
- eventually setup in global context
- showAlert in initialState (true || false)
- right after h3 login

```js
values.showAlert && ;
```

#### Toggle Member

```js
const toggleMember = () => {
setValues({ ...values, isMember: !values.isMember });
};

return (

{/* control h3 */}

{values.isMember ? 'Login' : 'Register'}

{/* toggle name */}

{!values.isMember && (

)}

{/* right after submit btn */}
{/* toggle button */}


{values.isMember ? 'Not a member yet?' : 'Already a member?'}


{values.isMember ? 'Register' : 'Login'}



);
```

#### Global Context

- in src create context directory
- actions.js
- reducer.js
- appContext.js

```js
import React, { useState, useReducer, useContext } from 'react';

export const initialState = {
isLoading: false,
showAlert: false,
alertText: '',
alertType: '',
};
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [state, setState] = useState(initialState);

return (

{children}

);
};
// make sure use
export const useAppContext = () => {
return useContext(AppContext);
};

export { AppProvider };
```

- index.js

```js
import { AppProvider } from './context/appContext';

ReactDOM.render(




,
document.getElementById('root')
);
```

- Register.js

```js
import { useAppContext } from '../context/appContext';

const { isLoading, showAlert } = useAppContext();
```

- switch to global showAlert

#### useReducer

- [React Tutorial](https://youtu.be/iZhV0bILFb0)
- useReducer vs Redux
- multiple reducers vs one

#### Wire Up Reducer

```js
reducer.js;

const reducer = (state, action) => {
throw new Error(`no such action :${action.type}`);
};
export default reducer;
```

```js
appContext.js;

import reducer from './reducer';

const [state, dispatch] = useReducer(reducer, initialState);
```

#### Display Alert

```js
actions.js;

export const DISPLAY_ALERT = 'SHOW_ALERT';
```

- setup imports (reducer and appContext)

```js
appContext.js

const displayAlert() =>{
dispatch({type:DISPLAY_ALERT})
}

```

```js
reducer.js;

if (action.type === DISPLAY_ALERT) {
return {
...state,
showAlert: true,
alertType: 'danger',
alertText: 'Please provide all values!',
};
}
```

```js
Alert.js in Components;

import { useAppContext } from '../context/appContext';

const Alert = () => {
const { alertType, alertText } = useAppContext();
return

{alertText}
;
};
```

#### Display Alert

- [JS Nuggets - Dynamic Object Keys](https://youtu.be/_qxCYtWm0tw)

```js
appContext.js;

const handleChange = (e) => {
setValues({ ...values, [e.target.name]: e.target.value });
};
```

- get displayAlert function

```js
appContext.js;

const onSubmit = (e) => {
e.preventDefault();
const { name, email, password, isMember } = values;
if (!email || !password || (!isMember && !name)) {
displayAlert();
return;
}
console.log(values);
};
```

#### Clear Alert

- technically optional

```js
actions.js;

export const CLEAR_ALERT = 'CLEAR_ALERT';
```

- setup imports (reducer and appContext)

```js
reducer.js;

if (action.type === CLEAR_ALERT) {
return {
...state,
showAlert: false,
alertType: '',
alertText: '',
};
}
```

```js
appContext.js;

const displayAlert = () => {
dispatch({
type: DISPLAY_ALERT,
});
clearAlert();
};

const clearAlert = () => {
setTimeout(() => {
dispatch({
type: CLEAR_ALERT,
});
}, 3000);
};
```

#### Setup Server

- stop the dev server in client
- cd ..
- start setting up our server
- setup package.json

```sh
npm init -y
```

- create server.js
- console.log('server running...')

```sh
node server
```

#### ES6 vs CommonJS

```js
CommonJS;

const express = require('express');
const app = express();
```

```js
ES6;

import express from 'express';
const app = express();
```

- file extension .mjs

```js
package.json

"type":"module"
```

#### Nodemon and Basic Express Server

```sh
npm install nodemon --save-dev
```

```js
package.json

"start":"nodemon server"

```

```sh
npm install express
```

```js
import express from 'express';
const app = express();

app.get('/', (req, res) => {
res.send('Welcome!');
});

const port = process.env.PORT || 5000;

app.listen(port, () => console.log(`Server is listening on port ${port}...`));
```

#### Not Found Middleware

- in the root create middleware folder
- not-found.js
- setup function
- return 404 with message 'Route does not exist'
- import in server.js
- make sure to use .js extension
- place after home route

#### Error Middleware

- in the middleware create error-handler.js
- setup function
- accept 4 parameters, first one error
- log error
- return 500
- json({msg:'there was an error'})
- import in the server.js
- make sure to use .js extension
- place it last
- eventually handle Mongoose Errors, just like in the node-express
- showcase with async errors

#### ENV Variables

```sh
npm install dotenv
```

- import dotenv from 'dotenv'
- dotenv.config()

- create .env
- PORT=4000
- .gitignore
- /node_modules
- .env

#### Connect to MongoDB

- switched back to PORT=5000
- remove Error from '/'

- existing MongoDB Atlas Account

```sh
npm install mongoose
```

- create db folder
- create connect.js
- setup connectDB(url)
- in server.js create start() function
- get connection string
- setup as MONGO_URL in .env
- provide credentials and DB Name

#### Auth Controller and Route Structure

- create controllers
- authController.js
- create async functions

```js
export { register, login, updateUser };
```

- return res.send('function name')
- create routes folder
- authRoutes.js
- setup express router
- import functions from authController.js

```js
router.route('/register').post(register);
router.route('/login').post(login);
router.route('/updateUser').patch(updateUser);

export default router;
```

- import authRouter in server.js

```js
app.use('/api/v1/auth', authRouter);
```

#### Jobs Controller and Route Structure

- jobsController.js
- create async functions

```js
export { createJob, deleteJob, getAllJobs, updateJob, showStats };
```

- return res.send('function name')

- jobsRoutes.js
- setup express router
- import functions from jobsController.js

```js
router.route('/').post(createJob).get(getAllJobs);
// place before :id
router.route('/stats').get(showStats);
router.route('/:id').delete(deleteJob).patch(updateJob);

export default router;
```

- in server.js jobsRouter

```js
app.use('/api/v1/jobs', jobsRouter);
```

#### Postman

- URL global var
- JOBIFY Collection
- auth and jobs folders
- setup routes

#### User Model

- models folder
- User.js
- setup schema
- name, email, password, lastName, location
- all {type:String}

#### Validate Email

```js
validate:{
validator:(field)=> {return 2 > 1},
message:'Please provide valid email'
}
```

- [Validator Package](https://www.npmjs.com/package/validator)

```sh
npm install validator
```

- import in User.js
- validator.isEmail

#### Register User - Initial Setup

- authController
- import User model
- setup temporary try/catch
- await User.create(req.body)
- if success 201 with json({user}) (temp)
- if error 500 with json({msg:'there was an error'})

#### Pass Error to Error Handler

- next(error)

#### Express-Async-Errors Package

- remove try/catch
- [Express-Async-Errors](https://www.npmjs.com/package/express-async-errors)

```sh
npm install express-async-errors

```

- in server.js
- import 'express-async-errors'

- use throw Error('error') instead of next(error)

#### Http Status Codes

- constants for status codes
- personal preference
- provides consistency
- less bugs
- easier to read/manage

- [Http Status Codes](https://www.npmjs.com/package/http-status-codes)

```sh
npm install http-status-codes
```

- import/setup in authController and error-handler
- setup defaultError

#### Custom Errors

#### Refactor Errors

- create errors folder
- create custom-api, bad-request, not-found, index.js files
- add proper imports
- setup index.js just like in the front-end
- import {BadRequestError} in authController
- gotcha "errors/index.js"

#### Hash Passwords

- one way street, only compare hashed values
- [bcrypt.js](https://www.npmjs.com/package/bcryptjs)

```sh
npm install bcryptjs
```

- User Model
- import bcrypt from 'bcryptjs'
- await genSalt(10)
- await hash(password , salt)
- await compare(requestPassword , currentPassword)
- [mongoose middleware](https://mongoosejs.com/docs/middleware.html)
- UserSchema.pre('save',async function(){
"this" points to instance created by UserSchema
})

#### Mongoose - Custom Instance Methods

[Custom Instance Methods](https://mongoosejs.com/docs/guide.html#methods)

- UserSchema.methods.createJWT = function(){console.log(this)}
- register controller
- right after User.create()
- invoke user.createJWT()

#### JWT

- token
- [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken)

```sh
npm install jsonwebtoken
```

- User Model
- import jwt from 'jsonwebtoken'
- jwt.sign(payload,secret,options)
- createJWT

```js
return jwt.sign({ userId: this._id }, 'jwtSecret', { expiresIn: '1d' });
```

```js
return jwt.sign({ userId: this._id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_LIFETIME,
});
```

#### JWT_SECRET and JWT_LIFETIME

- [Keys Generator](https://www.allkeysgenerator.com/)
- RESTART SERVER!!!!

#### Complete Register

- password : {select:false}
- complete response

#### Concurrently

- front-end and backend (server)
- run separate terminals
- [concurrently](https://www.npmjs.com/package/concurrently)

```sh
npm install concurrently --save-dev

```

- package.json

```js
// --kill-others switch, all commands are killed if one dies
// --prefix client - folder
// cd client && npm start
// escape quotes

"scripts": {
"server": "nodemon server --ignore client",
"client": "npm start --prefix client",
"start": "concurrently --kill-others-on-fail \"npm run server\" \" npm run client\""
},
```

#### Cors Error

[Cors Error](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)

- two fixes (cors package and proxy)

#### Cors Package

[cors package](https://www.npmjs.com/package/cors)

```sh
npm install cors
```

```js
import cors from 'cors';

app.use(cors());
```

#### Proxy

- access from anywhere
- don't want to use full url

[cra proxy](https://create-react-app.dev/docs/proxying-api-requests-in-development/)

```js
"proxy":"http://localhost:5000"
```

- my preference to remove trailing slash /
- restart app

#### Register User - Setup

```js
appContext.js;

const initialState = {
user: null,
token: null,
userLocation: '',
};
```

- actions.js REGISTER_USER_BEGIN,SUCCESS,ERROR
- import reducer,appContext

```js
appContext.js;
const registerUser = async (currentUser) => {
console.log(currentUser);
};
```

- import in Register.js

```js
Register.js;

const currentUser = { name, email, password };
if (isMember) {
console.log('already a member');
} else {
registerUser(currentUser);
}

return (

submit

);
```

#### Axios

- [axios docs](https://axios-http.com/docs/intro)
- stop app
- cd client

```sh
npm install axios
```

- cd ..
- restart app

#### Register User - Complete

```js
appContext.js;

import axios from 'axios';

const registerUser = async (currentUser) => {
dispatch({ type: REGISTER_USER_BEGIN });
try {
const response = await axios.post('/api/v1/auth/register', currentUser);
console.log(response);
const { user, token, location } = response.data;
dispatch({
type: REGISTER_USER_SUCCESS,
payload: {
user,
token,
location,
},
});

// will add later
// addUserToLocalStorage({
// user,
// token,
// location,
// })
} catch (error) {
console.log(error.response);
dispatch({
type: REGISTER_USER_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
reducer.js;
if (action.type === REGISTER_USER_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === REGISTER_USER_SUCCESS) {
return {
...state,
user: action.payload.user,
token: action.payload.token,
userLocation: action.payload.location,
jobLocation: action.payload.location,
isLoading: false,
showAlert: true,
alertType: 'success',
alertText: 'User Created! Redirecting...',
};
}
if (action.type === REGISTER_USER_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
};
}
```

#### Navigate To Dashboard

```js
Register.js;
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const Register = () => {
const { user } = useAppContext();
const navigate = useNavigate();

useEffect(() => {
if (user) {
setTimeout(() => {
navigate('/');
}, 3000);
}
}, [user, navigate]);
};
```

#### Local Storage

```js
appContext.js;
const addUserToLocalStorage = ({ user, token, location }) => {
localStorage.setItem('user', JSON.stringify(user));
localStorage.setItem('token', token);
localStorage.setItem('location', location);
};

const removeUserFromLocalStorage = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('location');
};

const registerUser = async (currentUser) => {
// in try block
addUserToLocalStorage({
user,
token,
location,
});
};

// set as default
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
const userLocation = localStorage.getItem('location');

const initialState = {
user: user ? JSON.parse(user) : null,
token: token,
userLocation: userLocation || '',
jobLocation: userLocation || '',
};
```

#### Morgan Package

- http logger middleware for node.js
- [morgan docs](https://www.npmjs.com/package/morgan)

```sh
npm install morgan
```

```js
import morgan from 'morgan';

if (process.env.NODE_ENV !== 'production') {
app.use(morgan('dev'));
}
```

#### UnauthenticatedError

- unauthenticated.js in errors
- import/export

```js
import { StatusCodes } from 'http-status-codes';
import CustomAPIError from './custom-api.js';

class UnauthenticatedError extends CustomAPIError {
constructor(message) {
super(message);
this.statusCode = StatusCodes.UNAUTHORIZED;
}
}
```

#### Compare Password

```js
User.js in models;

UserSchema.methods.comparePassword = async function (candidatePassword) {
const isMatch = await bcrypt.compare(candidatePassword, this.password);
return isMatch;
};
```

```js
authController.js;
const login = async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
throw new BadRequestError('Please provide all values');
}
const user = await User.findOne({ email }).select('+password');

if (!user) {
throw new UnauthenticatedError('Invalid Credentials');
}
const isPasswordCorrect = await user.comparePassword(password);
if (!isPasswordCorrect) {
throw new UnauthenticatedError('Invalid Credentials');
}
const token = user.createJWT();
user.password = undefined;
res.status(StatusCodes.OK).json({ user, token, location: user.location });
};
```

- test in Postman

#### Login User - Setup

- actions.js LOGIN_USER_BEGIN,SUCCESS,ERROR
- import reducer,appContext

```js
appContext.js;
const loginUser = async (currentUser) => {
console.log(currentUser);
};
```

- import in Register.js

```js
Register.js;

if (isMember) {
loginUser(currentUser);
} else {
registerUser(currentUser);
}
```

#### Login User - Complete

```js
appContext.js;
const loginUser = async (currentUser) => {
dispatch({ type: LOGIN_USER_BEGIN });
try {
const { data } = await axios.post('/api/v1/auth/login', currentUser);
const { user, token, location } = data;

dispatch({
type: LOGIN_USER_SUCCESS,
payload: { user, token, location },
});

addUserToLocalStorage({ user, token, location });
} catch (error) {
dispatch({
type: LOGIN_USER_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
reducer.js;

if (action.type === LOGIN_USER_BEGIN) {
return {
...state,
isLoading: true,
};
}
if (action.type === LOGIN_USER_SUCCESS) {
return {
...state,
isLoading: false,
user: action.payload.user,
token: action.payload.token,
userLocation: action.payload.location,
jobLocation: action.payload.location,
showAlert: true,
alertType: 'success',
alertText: 'Login Successful! Redirecting...',
};
}
if (action.type === LOGIN_USER_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
};
}
```

#### Refactor

```js
actions.js;
export const SETUP_USER_BEGIN = 'SETUP_USER_BEGIN';
export const SETUP_USER_SUCCESS = 'SETUP_USER_SUCCESS';
export const SETUP_USER_ERROR = 'SETUP_USER_ERROR';
```

```js
appContext.js;

const setupUser = async ({ currentUser, endPoint, alertText }) => {
dispatch({ type: SETUP_USER_BEGIN });
try {
const { data } = await axios.post(`/api/v1/auth/${endPoint}`, currentUser);

const { user, token, location } = data;
dispatch({
type: SETUP_USER_SUCCESS,
payload: { user, token, location, alertText },
});
addUserToLocalStorage({ user, token, location });
} catch (error) {
dispatch({
type: SETUP_USER_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
reducer.js;
if (action.type === SETUP_USER_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === SETUP_USER_SUCCESS) {
return {
...state,
isLoading: false,
token: action.payload.token,
user: action.payload.user,
userLocation: action.payload.location,
jobLocation: action.payload.location,
showAlert: true,
alertType: 'success',
alertText: action.payload.alertText,
};
}
if (action.type === SETUP_USER_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
};
}
```

- import/export

```js
Register.js;

const onSubmit = (e) => {
e.preventDefault();
const { name, email, password, isMember } = values;
if (!email || !password || (!isMember && !name)) {
displayAlert();
return;
}
const currentUser = { name, email, password };
if (isMember) {
setupUser({
currentUser,
endPoint: 'login',
alertText: 'Login Successful! Redirecting...',
});
} else {
setupUser({
currentUser,
endPoint: 'register',
alertText: 'User Created! Redirecting...',
});
}
};
```

#### Nested Pages in React Router 6

#### Dashboard pages

- delete Dashboard.js
- fix imports/exports
- replace in home route

```js
dashboard} />
```

- create dashboard directory in pages
- create AddJob,AllJobs,Profile,Stats,SharedLayout, index.js
- setup basic returns

```js
return

Add Job Page

;
```

- export all with index.js (just like components)
- import all pages in App.js

#### Nested Structure

```js
App.js

} />
}>
}>
}>

```

#### Shared Layout

```js
App.js

} >
```

```js
SharedLayout.js;

import { Outlet, Link } from 'react-router-dom';
import Wrapper from '../../assets/wrappers/SharedLayout';

const SharedLayout = () => {
return (


all jobs
all jobs



);
};

export default SharedLayout;
```

```js
App.js

} >
```

#### Protected Route

- create ProtectedRoute.js in pages
- import/export
- wrap SharedLayout in App.js

```js



}
/>
```

```js
ProtectedRoute.js;

import { Navigate } from 'react-router-dom';
import { useAppContext } from '../context/appContext';

const ProtectedRoute = ({ children }) => {
const { user } = useAppContext();
if (!user) {
return ;
}
return children;
};
```

#### Navbar, SmallSidebar, BigSidebar

- create Navbar, SmallSidebar, BigSidebar in components
- import Wrappers from assets/wrappers
- simple return
- import/export

```js
SharedLayout.js;

import { Outlet } from 'react-router-dom';
import { Navbar, SmallSidebar, BigSidebar } from '../../components';
import Wrapper from '../../assets/wrappers/SharedLayout';

const SharedLayout = () => {
const { user } = useAppContext();
return (
<>












>
);
};

export default SharedLayout;
```

#### React Icons

[React Icons](https://react-icons.github.io/react-icons/)

```sh
npm install react-icons
```

```js
Navbar.js

import Wrapper from '../assets/wrappers/Navbar'
import {FaHome} from 'react-icons/fa'
const Navbar = () => {
return (

navbar




)
}

export default Navbar

```

#### Navbar Setup

```js
Navbar.js;

import { useState } from 'react';
import { FaAlignLeft, FaUserCircle, FaCaretDown } from 'react-icons/fa';
import { useAppContext } from '../context/appContext';
import Logo from './Logo';
import Wrapper from '../assets/wrappers/Navbar';
const Navbar = () => {
return (


console.log('toggle sidebar')}
>



dashboard



console.log('show logout')}>

john



console.log('logout user')}
className='dropdown-btn'
>
logout





);
};

export default Navbar;
```

#### Toggle Sidebar

```js
actions.js;

export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR';
```

- import/export

```js
appContext.js;

const initialState = {
showSidebar: false,
};

const toggleSidebar = () => {
dispatch({ type: TOGGLE_SIDEBAR });
};
```

```js
reducer.js;

if (action.type === TOGGLE_SIDEBAR) {
return { ...state, showSidebar: !state.showSidebar };
}
```

```js
Navbar.js;

const { toggleSidebar } = useAppContext();

return (



);
```

#### Toggle Dropdown

```js
Navbar.js

const [showLogout, setShowLogout] = useState(false)


setShowLogout(!showLogout)}>

{user.name}



logoutUser()} className='dropdown-btn'>
logout


```

#### Logout User

```js
actions.js;

export const LOGOUT_USER = 'LOGOUT_USER';
```

- import/export

```js
appContext.js

const logoutUser = () => {
dispatch({ type: LOGOUT_USER })
removeUserFromLocalStorage()
}

value={{logoutUser}}
```

```js
reducer.js;

import { initialState } from './appContext';

if (action.type === LOGOUT_USER) {
return {
...initialState,
user: null,
token: null,
userLocation: '',
jobLocation: '',
};
}
```

```js
Navbar.js;

const { user, logoutUser, toggleSidebar } = useAppContext();

return (


setShowLogout(!showLogout)}>

{user.name}
{user && user.name}
{user?.name} // optional chaining




logout



);
```

#### Setup Links

- create utilsin the src
- setup links.js

```js
import { IoBarChartSharp } from 'react-icons/io5';
import { MdQueryStats } from 'react-icons/md';
import { FaWpforms } from 'react-icons/fa';
import { ImProfile } from 'react-icons/im';

const links = [
{
id: 1,
text: 'stats',
path: '/',
icon: ,
},
{
id: 2,
text: 'all jobs',
path: 'all-jobs',
icon: ,
},
{
id: 3,
text: 'add job',
path: 'add-job',
icon: ,
},
{
id: 4,
text: 'profile',
path: 'profile',
icon: ,
},
];

export default links;
```

#### Small Sidebar - Setup

```js
SmallSidebar.js;

import Wrapper from '../assets/wrappers/SmallSidebar';
import { FaTimes } from 'react-icons/fa';
import { useAppContext } from '../context/appContext';
import links from '../utils/links';
import { NavLink } from 'react-router-dom';
import Logo from './Logo';

export const SmallSidebar = () => {
return (



console.log('toggle')}>





nav links




);
};

export default SmallSidebar;
```

#### Small Sidebar - Toggle

```js
SmallSidebar.js;

const { showSidebar, toggleSidebar } = useAppContext();
```

```js
SmallSidebar.js;

return (


);
```

```js
SmallSidebar.js;

return (



);
```

#### Small Sidebar - Nav Links

```js
SmallSidebar.js;

import { NavLink } from 'react-router-dom';

return (


{links.map((link) => {
const { text, path, id, icon } = link;

return (

isActive ? 'nav-link active' : 'nav-link'
}
key={id}
onClick={toggleSidebar}
>
{icon}
{text}

);
})}


);
```

#### Nav Links Component

- in components create NavLinks.js
- styles still set from Wrapper
- also can setup in links.js, preference

```js
import { NavLink } from 'react-router-dom';
import links from '../utils/links';

const NavLinks = ({ toggleSidebar }) => {
return (


{links.map((link) => {
const { text, path, id, icon } = link;

return (

isActive ? 'nav-link active' : 'nav-link'
}
>
{icon}
{text}

);
})}


);
};

export default NavLinks;
```

```js
SmallSidebar.js

import NavLinks from './NavLinks'

return
```

#### Big Sidebar

```js
import { useAppContext } from '../context/appContext';
import NavLinks from './NavLinks';
import Logo from '../components/Logo';
import Wrapper from '../assets/wrappers/BigSidebar';

const BigSidebar = () => {
const { showSidebar } = useAppContext();
return (










);
};

export default BigSidebar;
```

#### REACT ROUTER UPDATE !!!

- [Stack Overflow](https://stackoverflow.com/questions/70644361/react-router-dom-v6-shows-active-for-index-as-well-as-other-subroutes)

```js

isActive ? 'nav-link active' : 'nav-link'}

end
>
```

#### Authenticate User Setup

- create auth.js in middleware

```js
const auth = async (req, res, next) => {
console.log('authenticate user');
next();
};

export default auth;
```

```js
authRoutes.js;

import authenticateUser from '../middleware/auth.js';

router.route('/updateUser').patch(authenticateUser, updateUser);
```

- two options

```js
server.js;

import authenticateUser from './middleware/auth.js';
app.use('/api/v1/jobs', authenticateUser, jobsRouter);
```

```js
jobsRoutes.js;

import authenticateUser from './middleware/auth.js';

// all routes !!!!

router.route('/stats').get(authenticateUser, showStats);
```

#### Auth - Bearer Schema

```js
Postman

Headers

Authorization: Bearer

```

```js
auth.js;

const auth = async (req, res, next) => {
const headers = req.headers;
const authHeader = req.headers.authorization;
console.log(headers);
console.log(authHeader);
next();
};
```

#### Postman - Set Token Programmatically

- register and login routes
- Tests

```js
const jsonData = pm.response.json();
pm.globals.set('token', jsonData.token);

Type: Bearer;

Token: {
{
token;
}
}
```

#### Unauthenticated Error

```js
auth.js;

import { UnAuthenticatedError } from '../errors/index.js';

const auth = async (req, res, next) => {
const authHeader = req.headers.authorization;

if (!authHeader) {
// why, well is it 400 or 404?
// actually 401
throw new UnAuthenticatedError('Authentication Invalid');
}

next();
};
```

#### Auth Middleware

```js
import jwt from 'jsonwebtoken';
import { UnAuthenticatedError } from '../errors/index.js';

const auth = async (req, res, next) => {
// check header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer')) {
throw new UnauthenticatedError('Authentication invalid');
}
const token = authHeader.split(' ')[1];

try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// console.log(payload)
// attach the user request object
// req.user = payload
req.user = { userId: payload.userId };
next();
} catch (error) {
throw new UnauthenticatedError('Authentication invalid');
}
};

export default auth;
```

#### Update User

```js
const updateUser = async (req, res) => {
const { email, name, lastName, location } = req.body;
if (!email || !name || !lastName || !location) {
throw new BadRequestError('Please provide all values');
}

const user = await User.findOne({ _id: req.user.userId });

user.email = email;
user.name = name;
user.lastName = lastName;
user.location = location;

await user.save();

// various setups
// in this case only id
// if other properties included, must re-generate

const token = user.createJWT();
res.status(StatusCodes.OK).json({
user,
token,
location: user.location,
});
};
```

#### Modified Paths

- user.save() vs User.findOneAndUpdate

```js
User.js;

UserSchema.pre('save', async function () {
console.log(this.modifiedPaths());
console.log(this.isModified('name'));

// if (!this.isModified('password')) return
// const salt = await bcrypt.genSalt(10)
// this.password = await bcrypt.hash(this.password, salt)
});
```

#### Profile Page

```js
appContext.js

const updateUser = async (currentUser) => {
console.log(currentUser)
}

value={{updateUser}}
```

```js
Profile.js;

import { useState } from 'react';
import { FormRow, Alert } from '../../components';
import { useAppContext } from '../../context/appContext';
import Wrapper from '../../assets/wrappers/DashboardFormPage';

const Profile = () => {
const { user, showAlert, displayAlert, updateUser, isLoading } =
useAppContext();
const [name, setName] = useState(user?.name);
const [email, setEmail] = useState(user?.email);
const [lastName, setLastName] = useState(user?.lastName);
const [location, setLocation] = useState(user?.location);

const handleSubmit = (e) => {
e.preventDefault();
if (!name || !email || !lastName || !location) {
// test and remove temporary
displayAlert();
return;
}

updateUser({ name, email, lastName, location });
};
return (


profile


{showAlert && }

{/* name */}


setName(e.target.value)}
/>
setLastName(e.target.value)}
/>
setEmail(e.target.value)}
/>

setLocation(e.target.value)}
/>

{isLoading ? 'Please Wait...' : 'save changes'}




);
};

export default Profile;
```

#### Bearer Token - Manual Approach

```js
appContext.js;

const updaterUser = async (currentUser) => {
try {
const { data } = await axios.patch('/api/v1/auth/updateUser', currentUser, {
headers: {
Authorization: `Bearer ${state.token}`,
},
});
console.log(data);
} catch (error) {
console.log(error.response);
}
};
```

#### Axios - Global Setup

In current axios version,
common property returns undefined,
so we don't use it anymore!!!

```js
appContext.js;

axios.defaults.headers['Authorization'] = `Bearer ${state.token}`;
```

#### Axios - Setup Instance

```js
AppContext.js;

const authFetch = axios.create({
baseURL: '/api/v1',
headers: {
Authorization: `Bearer ${state.token}`,
},
});

const updaterUser = async (currentUser) => {
try {
const { data } = await authFetch.patch('/auth/updateUser', currentUser);
} catch (error) {
console.log(error.response);
}
};
```

#### Axios - Interceptors

- will use instance, but can use axios instead

In current axios version,
common property returns undefined,
so we don't use it anymore!!!

```js
appContext.js;

// response interceptor
authFetch.interceptors.request.use(
(config) => {
config.headers['Authorization'] = `Bearer ${state.token}`;
return config;
},
(error) => {
return Promise.reject(error);
}
);
// response interceptor
authFetch.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(error.response);
if (error.response.status === 401) {
console.log('AUTH ERROR');
}
return Promise.reject(error);
}
);
```

#### Update User

```js
actions.js;
export const UPDATE_USER_BEGIN = 'UPDATE_USER_BEGIN';
export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS';
export const UPDATE_USER_ERROR = 'UPDATE_USER_ERROR';
```

```js
appContext.js;

const updateUser = async (currentUser) => {
dispatch({ type: UPDATE_USER_BEGIN });
try {
const { data } = await authFetch.patch('/auth/updateUser', currentUser);

// no token
const { user, location, token } = data;

dispatch({
type: UPDATE_USER_SUCCESS,
payload: { user, location, token },
});

addUserToLocalStorage({ user, location, token });
} catch (error) {
dispatch({
type: UPDATE_USER_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
reducer.js
if (action.type === UPDATE_USER_BEGIN) {
return { ...state, isLoading: true }
}

if (action.type === UPDATE_USER_SUCCESS) {
return {
...state,
isLoading: false,
token:action.payload.token
user: action.payload.user,
userLocation: action.payload.location,
jobLocation: action.payload.location,
showAlert: true,
alertType: 'success',
alertText: 'User Profile Updated!',
}
}
if (action.type === UPDATE_USER_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
}
}
```

#### 401 Error - Logout User

```js
appContext.js;
// response interceptor
authFetch.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response.status === 401) {
logoutUser();
}
return Promise.reject(error);
}
);

const updateUser = async (currentUser) => {
dispatch({ type: UPDATE_USER_BEGIN });
try {
const { data } = await authFetch.patch('/auth/updateUser', currentUser);

// no token
const { user, location } = data;

dispatch({
type: UPDATE_USER_SUCCESS,
payload: { user, location, token },
});

addUserToLocalStorage({ user, location, token: initialState.token });
} catch (error) {
if (error.response.status !== 401) {
dispatch({
type: UPDATE_USER_ERROR,
payload: { msg: error.response.data.msg },
});
}
}
clearAlert();
};
```

#### Job Model

- Job Model

```js
Job.js;

import mongoose from 'mongoose';

const JobSchema = new mongoose.Schema(
{
company: {
type: String,
required: [true, 'Please provide company name'],
maxlength: 50,
},
position: {
type: String,
required: [true, 'Please provide position'],
maxlength: 100,
},
status: {
type: String,
enum: ['interview', 'declined', 'pending'],
default: 'pending',
},

jobType: {
type: String,
enum: ['full-time', 'part-time', 'remote', 'internship'],
default: 'full-time',
},
jobLocation: {
type: String,
default: 'my city',
required: true,
},
createdBy: {
type: mongoose.Types.ObjectId,
ref: 'User',
required: [true, 'Please provide user'],
},
},
{ timestamps: true }
);

export default mongoose.model('Job', JobSchema);
```

#### Create Job

```js
jobsController.js;

import Job from '../models/Job.js';
import { StatusCodes } from 'http-status-codes';
import { BadRequestError, NotFoundError } from '../errors/index.js';

const createJob = async (req, res) => {
const { position, company } = req.body;

if (!position || !company) {
throw new BadRequestError('Please Provide All Values');
}

req.body.createdBy = req.user.userId;

const job = await Job.create(req.body);
res.status(StatusCodes.CREATED).json({ job });
};
```

#### Job State Values

```js
appContext.js;
const initialState = {
isEditing: false,
editJobId: '',
position: '',
company: '',
// jobLocation
jobTypeOptions: ['full-time', 'part-time', 'remote', 'internship'],
jobType: 'full-time',
statusOptions: ['pending', 'interview', 'declined'],
status: 'pending',
};
```

#### AddJob Page - Setup

```js
import { FormRow, Alert } from '../../components';
import { useAppContext } from '../../context/appContext';
import Wrapper from '../../assets/wrappers/DashboardFormPage';
const AddJob = () => {
const {
isEditing,
showAlert,
displayAlert,
position,
company,
jobLocation,
jobType,
jobTypeOptions,
status,
statusOptions,
} = useAppContext();

const handleSubmit = (e) => {
e.preventDefault();

if (!position || !company || !jobLocation) {
displayAlert();
return;
}
console.log('create job');
};

const handleJobInput = (e) => {
const name = e.target.name;
const value = e.target.value;
console.log(`${name}:${value}`);
};

return (


{isEditing ? 'edit job' : 'add job'}


{showAlert && }

{/* position */}



{/* company */}

{/* location */}

{/* job type */}

{/* job status */}



submit





);
};

export default AddJob;
```

#### Select Input

```js
return (
// job type



job type


{jobTypeOptions.map((itemValue, index) => {
return (

{itemValue}

);
})}


);
```

#### FormRowSelect

- create FormRowSelect in components
- setup import/export

```js
const FormRowSelect = ({ labelText, name, value, handleChange, list }) => {
return (



{labelText || name}


{list.map((itemValue, index) => {
return (

{itemValue}

);
})}


);
};

export default FormRowSelect;
```

```js
AddJob.js;

return (
<>
{/* job status */}

{/* job type */}

>
);
```

#### Change State Values With Handle Change

- [JS Nuggets Dynamic Object Keys](https://youtu.be/_qxCYtWm0tw)

```js
actions.js;

export const HANDLE_CHANGE = 'HANDLE_CHANGE';
```

```js
appContext.js

const handleChange = ({ name, value }) => {
dispatch({
type: HANDLE_CHANGE,
payload: { name, value },
})
}

value={{handleChange}}
```

```js
reducer.js;

if (action.type === HANDLE_CHANGE) {
return { ...state, [action.payload.name]: action.payload.value };
}
```

```js
AddJob.js;

const { handleChange } = useAppContext();

const handleJobInput = (e) => {
handleChange({ name: e.target.name, value: e.target.value });
};
```

#### Clear Values

```js
actions.js;

export const CLEAR_VALUES = 'CLEAR_VALUES';
```

```js
appContext.js

const clearValues = () => {
dispatch({ type: CLEAR_VALUES })
}

value={{clearValues}}
```

```js
reducer.js;

if (action.type === CLEAR_VALUES) {
const initialState = {
isEditing: false,
editJobId: '',
position: '',
company: '',
jobLocation: state.userLocation,
jobType: 'full-time',
status: 'pending',
};
return { ...state, ...initialState };
}
```

```js
AddJob.js;

const { clearValues } = useAppContext();

return (


{/* submit button */}

{
e.preventDefault();
clearValues();
}}
>
clear


);
```

#### Create Job

```js
actions.js;

export const CREATE_JOB_BEGIN = 'CREATE_JOB_BEGIN';
export const CREATE_JOB_SUCCESS = 'CREATE_JOB_SUCCESS';
export const CREATE_JOB_ERROR = 'CREATE_JOB_ERROR';
```

```js
appContext.js;

const createJob = async () => {
dispatch({ type: CREATE_JOB_BEGIN });
try {
const { position, company, jobLocation, jobType, status } = state;

await authFetch.post('/jobs', {
company,
position,
jobLocation,
jobType,
status,
});
dispatch({
type: CREATE_JOB_SUCCESS,
});
// call function instead clearValues()
dispatch({ type: CLEAR_VALUES });
} catch (error) {
if (error.response.status === 401) return;
dispatch({
type: CREATE_JOB_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
AddJob.js;

const { createJob } = useAppContext();

const handleSubmit = (e) => {
e.preventDefault();
// while testing

// if (!position || !company || !jobLocation) {
// displayAlert()
// return
// }
if (isEditing) {
// eventually editJob()
return;
}
createJob();
};
```

```js
reducer.js;

if (action.type === CREATE_JOB_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === CREATE_JOB_SUCCESS) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'success',
alertText: 'New Job Created!',
};
}
if (action.type === CREATE_JOB_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
};
}
```

#### Get All Jobs

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const jobs = await Job.find({ createdBy: req.user.userId });

res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Jobs State Values

```js
appContext.js;

const initialState = {
jobs: [],
totalJobs: 0,
numOfPages: 1,
page: 1,
};
```

#### Get All Jobs Request

```js
actions.js;
export const GET_JOBS_BEGIN = 'GET_JOBS_BEGIN';
export const GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS';
```

```js
appContext.js

import React, { useReducer, useContext, useEffect } from 'react'

const getJobs = async () => {
let url = `/jobs`

dispatch({ type: GET_JOBS_BEGIN })
try {
const { data } = await authFetch(url)
const { jobs, totalJobs, numOfPages } = data
dispatch({
type: GET_JOBS_SUCCESS,
payload: {
jobs,
totalJobs,
numOfPages,
},
})
} catch (error) {
console.log(error.response)
logoutUser()
}
clearAlert()
}

useEffect(() => {
getJobs()
}, [])

value={{getJobs}}

```

```js
reducer.js;

if (action.type === GET_JOBS_BEGIN) {
return { ...state, isLoading: true, showAlert: false };
}
if (action.type === GET_JOBS_SUCCESS) {
return {
...state,
isLoading: false,
jobs: action.payload.jobs,
totalJobs: action.payload.totalJobs,
numOfPages: action.payload.numOfPages,
};
}
```

#### AllJobs Page Setup

- create
- SearchContainer export
- JobsContainer export
- Job
- JobInfo

```js
AllJobs.js;

import { JobsContainer, SearchContainer } from '../../components';
const AllJobs = () => {
return (
<>


>
);
};

export default AllJobs;
```

```js
JobsContainer.js;
import { useAppContext } from '../context/appContext';
import { useEffect } from 'react';
import Loading from './Loading';
import Job from './Job';
import Wrapper from '../assets/wrappers/JobsContainer';

const JobsContainer = () => {
const { getJobs, jobs, isLoading, page, totalJobs } = useAppContext();
useEffect(() => {
getJobs();
}, []);

if (isLoading) {
return ;
}
if (jobs.length === 0) {
return (

No jobs to display...



);
}
return (


{totalJobs} job{jobs.length > 1 && 's'} found


{jobs.map((job) => {
return ;
})}


);
};

export default JobsContainer;
```

```js
Job.js;

import moment from 'moment';

const Job = ({ company }) => {
return

{company}
;
};

export default Job;
```

#### Moment.js

- Format Dates
- [moment.js](https://momentjs.com/)

- stop server
- cd client

```sh
npm install moment

```

```js
Job.js;

import moment from 'moment';

const Job = ({ company, createdAt }) => {
let date = moment(createdAt);
date = date.format('MMM Do, YYYY');
return (


{company}

{date}


);
};

export default Job;
```

#### Job Component - Setup

```js
appContext.js

const setEditJob = (id) => {
console.log(`set edit job : ${id}`)
}
const deleteJob = (id) =>{
console.log(`delete : ${id}`)
}
value={{setEditJob,deleteJob}}
```

```js
Job.js;

import { FaLocationArrow, FaBriefcase, FaCalendarAlt } from 'react-icons/fa';
import { Link } from 'react-router-dom';
import { useAppContext } from '../context/appContext';
import Wrapper from '../assets/wrappers/Job';
import JobInfo from './JobInfo';

const Job = ({
_id,
position,
company,
jobLocation,
jobType,
createdAt,
status,
}) => {
const { setEditJob, deleteJob } = useAppContext();

let date = moment(createdAt);
date = date.format('MMM Do, YYYY');

return (


{company.charAt(0)}


{position}

{company}





{/* content center later */}


setEditJob(_id)}
className='btn edit-btn'
>
Edit

deleteJob(_id)}
>
Delete





);
};

export default Job;
```

#### JobInfo

```js
JobInfo.js;

import Wrapper from '../assets/wrappers/JobInfo';

const JobInfo = ({ icon, text }) => {
return (

{icon}
{text}

);
};

export default JobInfo;
```

```js
Job.js;
return (



} text={jobLocation} />
} text={date} />
} text={jobType} />
{status}


{/* footer content */}

);
```

#### SetEditJob

```js
actions.js;
export const SET_EDIT_JOB = 'SET_EDIT_JOB';
```

```js
appContext.js

const setEditJob = (id) => {
dispatch({ type: SET_EDIT_JOB, payload: { id } })
}
const editJob = () => {
console.log('edit job')
}
value={{editJob}}
```

```js
reducer.js;

if (action.type === SET_EDIT_JOB) {
const job = state.jobs.find((job) => job._id === action.payload.id);
const { _id, position, company, jobLocation, jobType, status } = job;
return {
...state,
isEditing: true,
editJobId: _id,
position,
company,
jobLocation,
jobType,
status,
};
}
```

```js
AddJob.js;
const { isEditing, editJob } = useAppContext();
const handleSubmit = (e) => {
e.preventDefault();

if (!position || !company || !jobLocation) {
displayAlert();
return;
}
if (isEditing) {
editJob();
return;
}
createJob();
};
```

#### Edit Job - Server

```js
jobsController.js;

const updateJob = async (req, res) => {
const { id: jobId } = req.params;

const { company, position } = req.body;

if (!company || !position) {
throw new BadRequestError('Please Provide All Values');
}

const job = await Job.findOne({ _id: jobId });

if (!job) {
throw new NotFoundError(`No job with id ${jobId}`);
}

// check permissions

const updatedJob = await Job.findOneAndUpdate({ _id: jobId }, req.body, {
new: true,
runValidators: true,
});

res.status(StatusCodes.OK).json({ updatedJob });
};
```

#### Alternative Approach

- optional
- multiple approaches
- different setups
- course Q&A

```js
jobsController.js;
const updateJob = async (req, res) => {
const { id: jobId } = req.params;
const { company, position, jobLocation } = req.body;

if (!position || !company) {
throw new BadRequestError('Please provide all values');
}
const job = await Job.findOne({ _id: jobId });

if (!job) {
throw new NotFoundError(`No job with id :${jobId}`);
}

// check permissions

// alternative approach

job.position = position;
job.company = company;
job.jobLocation = jobLocation;

await job.save();
res.status(StatusCodes.OK).json({ job });
};
```

#### Check Permissions

```js
jobsController.js;

const updateJob = async (req, res) => {
const { id: jobId } = req.params;
const { company, position, status } = req.body;

if (!position || !company) {
throw new BadRequestError('Please provide all values');
}
const job = await Job.findOne({ _id: jobId });

if (!job) {
throw new NotFoundError(`No job with id :${jobId}`);
}

// check permissions
// req.user.userId (string) === job.createdBy(object)
// throw new UnAuthenticatedError('Not authorized to access this route')

// console.log(typeof req.user.userId)
// console.log(typeof job.createdBy)

checkPermissions(req.user, job.createdBy);

const updatedJob = await Job.findOneAndUpdate({ _id: jobId }, req.body, {
new: true,
runValidators: true,
});

res.status(StatusCodes.OK).json({ updatedJob });
};
```

- utils folder
- checkPermissions.js
- import in jobsController.js

```js
checkPermissions.js;

import { UnAuthenticatedError } from '../errors/index.js';

const checkPermissions = (requestUser, resourceUserId) => {
// if (requestUser.role === 'admin') return
if (requestUser.userId === resourceUserId.toString()) return;
throw new UnauthorizedError('Not authorized to access this route');
};

export default checkPermissions;
```

#### Remove/Delete Job

```js
jobsController.js;

const deleteJob = async (req, res) => {
const { id: jobId } = req.params;

const job = await Job.findOne({ _id: jobId });

if (!job) {
throw new NotFoundError(`No job with id : ${jobId}`);
}

checkPermissions(req.user, job.createdBy);

await job.remove();
res.status(StatusCodes.OK).json({ msg: 'Success! Job removed' });
};
```

#### Delete Job - Front-End

```js
actions.js;

export const DELETE_JOB_BEGIN = 'DELETE_JOB_BEGIN';
```

```js
appContext.js;

const deleteJob = async (jobId) => {
dispatch({ type: DELETE_JOB_BEGIN });
try {
await authFetch.delete(`/jobs/${jobId}`);
getJobs();
} catch (error) {
logoutUser();
}
};
```

```js
reducer.js;

if (action.type === DELETE_JOB_BEGIN) {
return { ...state, isLoading: true };
}
```

#### Edit Job - Front-End

```js
actions.js;
export const EDIT_JOB_BEGIN = 'EDIT_JOB_BEGIN';
export const EDIT_JOB_SUCCESS = 'EDIT_JOB_SUCCESS';
export const EDIT_JOB_ERROR = 'EDIT_JOB_ERROR';
```

```js
appContext.js;
const editJob = async () => {
dispatch({ type: EDIT_JOB_BEGIN });
try {
const { position, company, jobLocation, jobType, status } = state;

await authFetch.patch(`/jobs/${state.editJobId}`, {
company,
position,
jobLocation,
jobType,
status,
});
dispatch({
type: EDIT_JOB_SUCCESS,
});
dispatch({ type: CLEAR_VALUES });
} catch (error) {
if (error.response.status === 401) return;
dispatch({
type: EDIT_JOB_ERROR,
payload: { msg: error.response.data.msg },
});
}
clearAlert();
};
```

```js
reducer.js;

if (action.type === EDIT_JOB_BEGIN) {
return { ...state, isLoading: true };
}
if (action.type === EDIT_JOB_SUCCESS) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'success',
alertText: 'Job Updated!',
};
}
if (action.type === EDIT_JOB_ERROR) {
return {
...state,
isLoading: false,
showAlert: true,
alertType: 'danger',
alertText: action.payload.msg,
};
}
```

#### Create More Jobs

- [Mockaroo](https://www.mockaroo.com/)
- setup mock-data.json in the root

#### Populate Database

- create populate.js in the root

```js
populate.js;

import { readFile } from 'fs/promises';

import dotenv from 'dotenv';
dotenv.config();

import connectDB from './db/connect.js';
import Job from './models/Job.js';

const start = async () => {
try {
await connectDB(process.env.MONGO_URL);
await Job.deleteMany();

const jsonProducts = JSON.parse(
await readFile(new URL('./mock-data.json', import.meta.url))
);
await Job.create(jsonProducts);
console.log('Success!!!!');
process.exit(0);
} catch (error) {
console.log(error);
process.exit(1);
}
};

start();
```

#### Show Stats - Structure

- aggregation pipeline
- step by step
- [Aggregation Pipeline](https://docs.mongodb.com/manual/core/aggregation-pipeline/)

```js
jobsController.js;

import mongoose from 'mongoose';

const showStats = async (req, res) => {
let stats = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{ $group: { _id: '$status', count: { $sum: 1 } } },
]);

res.status(StatusCodes.OK).json({ stats });
};
```

#### Show Stats - Object Setup

- [Reduce Basics](https://youtu.be/3WkW9nrS2mw)
- [Reduce Object Example ](https://youtu.be/5BFkp8JjLEY)

```js
jobsController.js;

const showStats = async (req, res) => {
let stats = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{ $group: { _id: '$status', count: { $sum: 1 } } },
]);

stats = stats.reduce((acc, curr) => {
const { _id: title, count } = curr;
acc[title] = count;
return acc;
}, {});

res.status(StatusCodes.OK).json({ stats });
};
```

#### Show Stats - Default Stats

```js
jobsController.js;

const showStats = async (req, res) => {
let stats = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{ $group: { _id: '$status', count: { $sum: 1 } } },
]);
stats = stats.reduce((acc, curr) => {
const { _id: title, count } = curr;
acc[title] = count;
return acc;
}, {});

const defaultStats = {
pending: stats.pending || 0,
interview: stats.interview || 0,
declined: stats.declined || 0,
};
let monthlyApplications = [];
res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications });
};
```

#### Show Stats - Function Setup

```js
actions.js;

export const SHOW_STATS_BEGIN = 'SHOW_STATS_BEGIN';
export const SHOW_STATS_SUCCESS = 'SHOW_STATS_SUCCESS';
```

```js
appContext.js

const initialState = {
stats: {},
monthlyApplications: []

}

const showStats = async () => {
dispatch({ type: SHOW_STATS_BEGIN })
try {
const { data } = await authFetch('/jobs/stats')
dispatch({
type: SHOW_STATS_SUCCESS,
payload: {
stats: data.defaultStats,
monthlyApplications: data.monthlyApplications,
},
})
} catch (error) {
console.log(error.response)
// logoutUser()
}

clearAlert()
}
value={{showStats}}
```

```js
reducers.js;
if (action.type === SHOW_STATS_BEGIN) {
return { ...state, isLoading: true, showAlert: false };
}
if (action.type === SHOW_STATS_SUCCESS) {
return {
...state,
isLoading: false,
stats: action.payload.stats,
monthlyApplications: action.payload.monthlyApplications,
};
}
```

#### Stats Page - Structure

- components
- StatsContainer.js
- ChartsContainer.js
- StatsItem.js
- simple return
- import/export index.js

```js
Stats.js;

import { useEffect } from 'react';
import { useAppContext } from '../../context/appContext';
import { StatsContainer, Loading, ChartsContainer } from '../../components';

const Stats = () => {
const { showStats, isLoading, monthlyApplications } = useAppContext();
useEffect(() => {
showStats();
}, []);

if (isLoading) {
return ;
}

return (
<>

{monthlyApplications.length > 0 && }
>
);
};

export default Stats;
```

#### StatsContainer

```js
StatsContainer.js;

import { useAppContext } from '../context/appContext';
import StatItem from './StatItem';
import { FaSuitcaseRolling, FaCalendarCheck, FaBug } from 'react-icons/fa';
import Wrapper from '../assets/wrappers/StatsContainer';
const StatsContainer = () => {
const { stats } = useAppContext();
const defaultStats = [
{
title: 'pending applications',
count: stats.pending || 0,
icon: ,
color: '#e9b949',
bcg: '#fcefc7',
},
{
title: 'interviews scheduled',
count: stats.interview || 0,
icon: ,
color: '#647acb',
bcg: '#e0e8f9',
},
{
title: 'jobs declined',
count: stats.declined || 0,
icon: ,
color: '#d66a6a',
bcg: '#ffeeee',
},
];

return (

{defaultStats.map((item, index) => {
return ;
})}

);
};

export default StatsContainer;
```

#### StatItem

```js
StatItem.js;

import Wrapper from '../assets/wrappers/StatItem';

function StatItem({ count, title, icon, color, bcg }) {
return (


{count}

{icon}


{title}


);
}

export default StatItem;
```

#### Aggregate Jobs Based on Year and Month

```js
jobsController.js;

let monthlyApplications = await Job.aggregate([
{ $match: { createdBy: mongoose.Types.ObjectId(req.user.userId) } },
{
$group: {
_id: {
year: {
$year: '$createdAt',
},
month: {
$month: '$createdAt',
},
},
count: { $sum: 1 },
},
},
{ $sort: { '_id.year': -1, '_id.month': -1 } },
{ $limit: 6 },
]);
```

#### Refactor Data

- install moment.js on the SERVER

```sh
npm install moment

```

```js
jobsController.js;

import moment from 'moment';

monthlyApplications = monthlyApplications
.map((item) => {
const {
_id: { year, month },
count,
} = item;
// accepts 0-11
const date = moment()
.month(month - 1)
.year(year)
.format('MMM Y');
return { date, count };
})
.reverse();
```

#### Charts Container

- BarChart.js
- AreaChart.js

```js
ChartsContainer.js;
import React, { useState } from 'react';

import BarChart from './BarChart';
import AreaChart from './AreaChart';
import { useAppContext } from '../context/appContext';
import Wrapper from '../assets/wrappers/ChartsContainer';

export default function ChartsContainer() {
const [barChart, setBarChart] = useState(true);
const { monthlyApplications: data } = useAppContext();

return (

Monthly Applications

setBarChart(!barChart)}>
{barChart ? 'AreaChart' : 'BarChart'}

{barChart ? : }

);
}
```

#### Recharts Library

- install in the Client!!!

[Recharts](https://recharts.org)

```sh
npm install recharts
```

#### Bar Chart

```js
BarChart.js;

import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from 'recharts';

const BarChartComponent = ({ data }) => {
return (









);
};
```

#### Area Chart

```js
import {
ResponsiveContainer,
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
} from 'recharts';

const AreaChartComponent = ({ data }) => {
return (









);
};
```

#### Filter

#### Get All Jobs - Initial Setup

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;

const queryObject = {
createdBy: req.user.userId,
};

// NO AWAIT
let result = Job.find(queryObject);

// chain sort conditions

const jobs = await result;

res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Status

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;

const queryObject = {
createdBy: req.user.userId,
};

if (status !== 'all') {
queryObject.status = status;
}

// NO AWAIT
let result = Job.find(queryObject);

// chain sort conditions

const jobs = await result;

res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### JobType

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;

const queryObject = {
createdBy: req.user.userId,
};

if (status !== 'all') {
queryObject.status = status;
}
if (jobType !== 'all') {
queryObject.jobType = jobType;
}
// NO AWAIT
let result = Job.find(queryObject);

// chain sort conditions

const jobs = await result;

res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Search

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;

const queryObject = {
createdBy: req.user.userId,
};

if (status !== 'all') {
queryObject.status = status;
}
if (jobType !== 'all') {
queryObject.jobType = jobType;
}
if (search) {
queryObject.position = { $regex: search, $options: 'i' };
}
// NO AWAIT
let result = Job.find(queryObject);

// chain sort conditions
if (sort === 'latest') {
result = result.sort('-createdAt');
}
if (sort === 'oldest') {
result = result.sort('createdAt');
}
if (sort === 'a-z') {
result = result.sort('position');
}
if (sort === 'z-a') {
result = result.sort('-position');
}
const jobs = await result;

res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Search Context Setup

```js
appContext.js

const initialState = {
jobType: 'full-time',
jobTypeOptions: ['full-time', 'part-time', 'remote', 'internship'],
status: 'pending',
statusOptions: ['pending', 'interview', 'declined']
//
//
//
search: '',
searchStatus: 'all',
searchType: 'all',
sort: 'latest',
sortOptions: ['latest', 'oldest', 'a-z', 'z-a'],
}

const clearFilters = () =>{
console.log('clear filters')
}

value={{clearFilters}}

// remember this function :)
const handleChange = ({ name, value }) => {
dispatch({
type: HANDLE_CHANGE,
payload: { name, value },
})
}

```

#### Search Container - Setup

```js
SearchContainer.js;

import { FormRow, FormRowSelect } from '.';
import { useAppContext } from '../context/appContext';
import Wrapper from '../assets/wrappers/SearchContainer';
const SearchContainer = () => {
const {
isLoading,
search,
searchStatus,
searchType,
sort,
sortOptions,
statusOptions,
jobTypeOptions,
handleChange,
clearFilters,
} = useAppContext();

const handleSearch = (e) => {
if (isLoading) return;
handleChange({ name: e.target.name, value: e.target.value });
};

return (


search form


{/* search position */}


{/* rest of the inputs */}



);
};

export default SearchContainer;
```

#### Search Container - Complete

```js
SearchContainer.js;

import { FormRow, FormRowSelect } from '.';
import { useAppContext } from '../context/appContext';
import Wrapper from '../assets/wrappers/SearchContainer';

const SearchContainer = () => {
const {
isLoading,
search,
handleChange,
searchStatus,
statusOptions,
jobTypeOptions,
searchType,
clearFilters,
sort,
sortOptions,
} = useAppContext();

const handleSearch = (e) => {
if (isLoading) return;
handleChange({ name: e.target.name, value: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
clearFilters();
};
return (


search form


{/* search position */}


{/* search by status */}

{/* search by type */}


{/* sort */}



clear filters




);
};

export default SearchContainer;
```

#### Clear Filters

```js
actions.js;

export const CLEAR_FILTERS = 'CLEAR_FILTERS';
```

```js
appContext.js;

const clearFilters = () => {
dispatch({ type: CLEAR_FILTERS });
};
```

```js
reducer.js;

if (action.type === CLEAR_FILTERS) {
return {
...state,
search: '',
searchStatus: 'all',
searchType: 'all',
sort: 'latest',
};
}
```

#### Refactor Get All Jobs

```js
const getJobs = async () => {
// will add page later
const { search, searchStatus, searchType, sort } = state;
let url = `/jobs?status=${searchStatus}&jobType=${searchType}&sort=${sort}`;
if (search) {
url = url + `&search=${search}`;
}
dispatch({ type: GET_JOBS_BEGIN });
try {
const { data } = await authFetch(url);
const { jobs, totalJobs, numOfPages } = data;
dispatch({
type: GET_JOBS_SUCCESS,
payload: {
jobs,
totalJobs,
numOfPages,
},
});
} catch (error) {
// logoutUser()
}
clearAlert();
};
```

```js
JobsContainer.js

const JobsContainer = () => {
const {
getJobs,
jobs,
isLoading,
page,
totalJobs,
search,
searchStatus,
searchType,
sort,

} = useAppContext()
useEffect(() => {
getJobs()
}, [ search, searchStatus, searchType, sort])

```

#### Limit and Skip

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;
const queryObject = {
createdBy: req.user.userId,
};
if (search) {
queryObject.position = { $regex: search, $options: 'i' };
}
if (status !== 'all') {
queryObject.status = status;
}
if (jobType !== 'all') {
queryObject.jobType = jobType;
}
let result = Job.find(queryObject);

if (sort === 'latest') {
result = result.sort('-createdAt');
}
if (sort === 'oldest') {
result = result.sort('createdAt');
}
if (sort === 'a-z') {
result = result.sort('position');
}
if (sort === 'z-a') {
result = result.sort('-position');
}

const totalJobs = await result;

// setup pagination
const limit = 10;
const skip = 1;

result = result.skip(skip).limit(limit);
// 23
// 4 7 7 7 2
const jobs = await result;
res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Page and Limit

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;
const queryObject = {
createdBy: req.user.userId,
};
if (search) {
queryObject.position = { $regex: search, $options: 'i' };
}
if (status !== 'all') {
queryObject.status = status;
}
if (jobType !== 'all') {
queryObject.jobType = jobType;
}
let result = Job.find(queryObject);

if (sort === 'latest') {
result = result.sort('-createdAt');
}
if (sort === 'oldest') {
result = result.sort('createdAt');
}
if (sort === 'a-z') {
result = result.sort('position');
}
if (sort === 'z-a') {
result = result.sort('-position');
}

// setup pagination
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const skip = (page - 1) * limit; //10
result = result.skip(skip).limit(limit);
// 75
// 10 10 10 10 10 10 10 5
const jobs = await result;
res
.status(StatusCodes.OK)
.json({ jobs, totalJobs: jobs.length, numOfPages: 1 });
};
```

#### Total Jobs and Number Of Pages

```js
jobsController.js;

const getAllJobs = async (req, res) => {
const { search, status, jobType, sort } = req.query;
const queryObject = {
createdBy: req.user.userId,
};
if (search) {
queryObject.position = { $regex: search, $options: 'i' };
}
if (status !== 'all') {
queryObject.status = status;
}
if (jobType !== 'all') {
queryObject.jobType = jobType;
}
let result = Job.find(queryObject);

if (sort === 'latest') {
result = result.sort('-createdAt');
}
if (sort === 'oldest') {
result = result.sort('createdAt');
}
if (sort === 'a-z') {
result = result.sort('position');
}
if (sort === 'z-a') {
result = result.sort('-position');
}

// setup pagination
const page = Number(req.query.page) || 1;
const limit = Number(req.query.limit) || 10;
const skip = (page - 1) * limit;

result = result.skip(skip).limit(limit);

const jobs = await result;

const totalJobs = await Job.countDocuments(queryObject);
const numOfPages = Math.ceil(totalJobs / limit);

res.status(StatusCodes.OK).json({ jobs, totalJobs, numOfPages });
};
```

#### PageBtnContainer Setup

- PageBtnContainer.js

```js
JobsContainer.js;

import PageBtnContainer from './PageBtnContainer';

const { numOfPages } = useAppContext();

return (


{totalJobs} job{jobs.length > 1 && 's'} found


{jobs.map((job) => {
return ;
})}

{numOfPages > 1 && }

);
```

#### PageBtnContainer - Structure

```js
PageBtnContainer.js;

import { useAppContext } from '../context/appContext';
import { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi';
import Wrapper from '../assets/wrappers/PageBtnContainer';

const PageButtonContainer = () => {
const { numOfPages, page } = useAppContext();

const prevPage = () => {
console.log('prev page');
};
const nextPage = () => {
console.log('next page');
};

return (



prev

buttons


next



);
};

export default PageButtonContainer;
```

#### Button Container

- [Array.from] (https://youtu.be/zg1Bv4xubwo)

```js
PageBtnContainer.js;

const pages = Array.from({ length: numOfPages }, (_, index) => {
return index + 1;
});

return (


{pages.map((pageNumber) => {
return (
console.log(page)}
>
{pageNumber}

);
})}

);
```

#### Change Page

```js
actions.js;
export const CHANGE_PAGE = 'CHANGE_PAGE';
```

```js
appContext.js
const changePage = (page) => {
dispatch({ type: CHANGE_PAGE, payload: { page } })
}
value={{changePage}}
```

```js
reducer.js;

if (action.type === CHANGE_PAGE) {
return { ...state, page: action.payload.page };
}
```

```js
PageBtnContainer.js;

const { changePage } = useAppContext();
return (
changePage(pageNumber)}
>
{pageNumber}

);
```

#### Prev and Next Buttons

```js
PageBtnContainer.js;
const prevPage = () => {
let newPage = page - 1;
if (newPage < 1) {
// newPage = 1
// alternative
newPage = numOfPages;
}
changePage(newPage);
};
const nextPage = () => {
let newPage = page + 1;
if (newPage > numOfPages) {
// newPage = numOfPages
// alternative
newPage = 1;
}
changePage(newPage);
};
```

#### Trigger New Page

```js
appContext.js;

const getJobs = async () => {
const { page, search, searchStatus, searchType, sort } = state;

let url = `/jobs?page=${page}&status=${searchStatus}&jobType=${searchType}&sort=${sort}`;
// rest of the code
};
```

```js
JobsContainer.js;

const { page } = useAppContext();
useEffect(() => {
getJobs();
}, [page, search, searchStatus, searchType, sort]);
```

```js
reducer.js;

if (action.type === HANDLE_CHANGE) {
// set back to first page

return { ...state, page: 1, [action.payload.name]: action.payload.value };
}
```

#### Production Setup - Fix Warnings and logoutUser

- getJobs,deleteJob,showStats - invoke logoutUser()
- fix warnings

```sh
// eslint-disable-next-line
```

#### Production Setup - Build Front-End Application

- create front-end production application

```js
package.json
"scripts": {
"build-client": "cd client && npm run build",
"server": "nodemon server.js --ignore client",
"client": "cd client && npm run start",
"start": "concurrently --kill-others-on-fail \"npm run server\" \"npm run client\""

},

```

```js
server.js;

import { dirname } from 'path';
import { fileURLToPath } from 'url';
import path from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));

// only when ready to deploy
app.use(express.static(path.resolve(__dirname, './client/build')));

// routes
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/jobs', authenticateUser, jobsRouter);

// only when ready to deploy
app.get('*', function (request, response) {
response.sendFile(path.resolve(__dirname, './client/build', 'index.html'));
});
```

#### Security Packages

- remove log in the error-handler
- [helmet](https://www.npmjs.com/package/helmet)
Helmet helps you secure your Express apps by setting various HTTP headers.
- [xss-clean](https://www.npmjs.com/package/xss-clean)
Node.js Connect middleware to sanitize user input coming from POST body, GET queries, and url params.
- [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
Sanitizes user-supplied data to prevent MongoDB Operator Injection.
- [express-rate-limit](https://www.npmjs.com/package/express-rate-limit)
Basic rate-limiting middleware for Express.

```sh
npm install helmet xss-clean express-mongo-sanitize express-rate-limit
```

```js
server.js;

import helmet from 'helmet';
import xss from 'xss-clean';
import mongoSanitize from 'express-mongo-sanitize';

app.use(express.json());
app.use(helmet());
app.use(xss());
app.use(mongoSanitize());
```

#### Limit Requests

```js
authRoutes.js;

import rateLimiter from 'express-rate-limit';

const apiLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: 'Too many requests from this IP, please try again after 15 minutes',
});

router.route('/register').post(apiLimiter, register);
router.route('/login').post(apiLimiter, login);
```

#### Alternative Search with Debounce

client/components/SearchContainer.js

```js

import { useState, useMemo } from 'react';
const SearchContainer = () => {
const [localSearch, setLocalSearch] = useState('');
const {
....
} = useAppContext();
const handleSearch = (e) => {
handleChange({ name: e.target.name, value: e.target.value });
};
const handleSubmit = (e) => {
e.preventDefault();
clearFilters();
};
const debounce = () => {
let timeoutID;
return (e) => {
setLocalSearch(e.target.value);
clearTimeout(timeoutID);
timeoutID = setTimeout(() => {
handleChange({ name: e.target.name, value: e.target.value });
}, 1000);
};
};

const handleSubmit = (e) => {
e.preventDefault();
setLocalSearch('');
clearFilters();
};

const optimizedDebounce = useMemo(() => debounce(), []);
return (


search form



{/* search position */}


........




);
};

export default SearchContainer;
```

#### Test User - Initial Setup

- create new user (test user)
- populate DB with jobs
- create a login button

client/pages/Register.js

```js
const Register = () => {
return (



submit

{
setupUser({
currentUser: { email: '[email protected]', password: 'secret' },
endPoint: 'login',
alertText: 'Login Successful! Redirecting...',
});
}}
>
{isLoading ? 'loading...' : 'demo app'}



);
};
export default Register;
```

#### Test User - Restrict Access (server)

- check for test user in auth middleware
- create new property on user object (true/false)
- create new middleware (testUser)
- check for test user, if true send back BadRequest Error
- add testUser middleware in front of routes you want to restrict access to

middleware/auth.js

```js
import jwt from 'jsonwebtoken';
import { UnAuthenticatedError } from '../errors/index.js';

UnAuthenticatedError;
const auth = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer')) {
throw new UnAuthenticatedError('Authentication Invalid');
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// TEST USER
const testUser = payload.userId === 'testUserID';
req.user = { userId: payload.userId, testUser };
// TEST USER
next();
} catch (error) {
throw new UnAuthenticatedError('Authentication Invalid');
}
};

export default auth;
```

middleware/testUser

```js
import { BadRequestError } from '../errors/index.js';

const testUser = (req, res, next) => {
if (req.user.testUser) {
throw new BadRequestError('Test User. Read Only!');
}
next();
};

export default testUser;
```

routes/jobsRoutes

```js
import express from 'express';
const router = express.Router();

import {
createJob,
deleteJob,
getAllJobs,
updateJob,
showStats,
} from '../controllers/jobsController.js';

import testUser from '../middleware/testUser.js';

router.route('/').post(testUser, createJob).get(getAllJobs);
// remember about :id
router.route('/stats').get(showStats);
router.route('/:id').delete(testUser, deleteJob).patch(testUser, updateJob);

export default router;
```

routes/authRoutes

```js
import express from 'express';
const router = express.Router();

import rateLimiter from 'express-rate-limit';
const apiLimiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10,
message: 'Too many requests from this IP, please try again after 15 minutes',
});

import { register, login, updateUser } from '../controllers/authController.js';
import authenticateUser from '../middleware/auth.js';
import testUser from '../middleware/testUser.js';
router.route('/register').post(apiLimiter, register);
router.route('/login').post(apiLimiter, login);
router.route('/updateUser').patch(authenticateUser, testUser, updateUser);

export default router;
```

#### Store JWT in Cookie

- BE PREPARED TO REFACTOR CODE !!!
- PLEASE DON'T RUSH THROUGH THESES VIDEOS
- CHECK FEW TIMES BEFORE REMOVING/ADDING CODE

#### Attach Cookies to Login response

controllers/authController.js

```js
// login controller

const token = user.createJWT();

const oneDay = 1000 * 60 * 60 * 24;

res.cookie('token', token, {
httpOnly: true,
expires: new Date(Date.now() + oneDay),
secure: process.env.NODE_ENV === 'production',
});
```

#### Setup Function in Utils

- create attachCookies.js

```js
const attachCookie = ({ res, token }) => {
const oneDay = 1000 * 60 * 60 * 24;

res.cookie('token', token, {
httpOnly: true,
expires: new Date(Date.now() + oneDay),
secure: process.env.NODE_ENV === 'production',
});
};

export default attachCookie;
```

- import in authController.js
- invoke in register/login/updateUser

```js
import attachCookie from '../utils/attachCookie.js';

attachCookie({ res, token });
```

#### Parse Cookie Coming Back from the Front-End

- install cookie-parser (server)

```sh
npm install cookie-parser
```

server.js

```js
import cookieParser from 'cookie-parser';

app.use(express.json());
app.use(cookieParser());
```

middleware/auth.js

```js
const auth = async (req, res, next) => {
console.log(req.cookies)
....
}
```

#### Refactor Auth Middleware

middleware/auth.js

```js
const auth = async (req, res, next) => {
const token = req.cookies.token;
if (!token) {
throw new UnAuthenticatedError('Authentication Invalid');
}
// rest of the code
};
```

#### SERVER - Remove Token from JSON Response

controllers/authController

register/login/updateUser

```js
res.status(StatusCodes.OK).json({ user, location: user.location });
```

- test the APP

#### FRONT-END Remove Token from CONTEXT

- PLEASE BE CAREFUL WHEN MAKING THESE UPDATES
client/context/appContext

- remove

```js
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
const userLocation = localStorage.getItem('location');
```

- fix initial state

```js
const initialState = {
// remove token all together
user: null,
userLocation: '',
jobLocation: '',
};
```

- remove request interceptor

```js
authFetch.interceptors.request.use(
(config) => {
config.headers.common['Authorization'] = `Bearer ${state.token}`;
return config;
},
(error) => {
return Promise.reject(error);
}
);
```

- remove both addToLocalStorage and removeFromLocalStorage functions
- remove from setupUser and updateUser (token and local storage functions)
- remove from the reducer token (COMMAND + F)

```js
const logoutUser = async () => {
dispatch({ type: LOGOUT_USER });
// remove local storage code
};
```

#### Test Expiration

```js
expires: new Date(Date.now() + 5000),
```

#### GET Current User Route

controllers/authController.js

```js
const getCurrentUser = async (req, res) => {
const user = await User.findOne({ _id: req.user.userId });
res.status(StatusCodes.OK).json({ user, location: user.location });
};

export { register, login, updateUser, getCurrentUser };
```

routes/authRoutes.js

```js
import {
register,
login,
updateUser,
getCurrentUser,
} from '../controllers/authController.js';

router.route('/register').post(apiLimiter, register);
router.route('/login').post(apiLimiter, login);
router.route('/updateUser').patch(authenticateUser, testUser, updateUser);
router.route('/getCurrentUser').get(authenticateUser, getCurrentUser);
```

#### GET Current User - Front-End

actions.js

```js
export const GET_CURRENT_USER_BEGIN = 'GET_CURRENT_USER_BEGIN';
export const GET_CURRENT_USER_SUCCESS = 'GET_CURRENT_USER_SUCCESS';
```

- setup imports (appContext and reducer)

#### GET Current User Request

- first set the state value (default TRUE !!!)
appContext.js

```js
const initialState = {
userLoading: true,
};

const getCurrentUser = async () => {
dispatch({ type: GET_CURRENT_USER_BEGIN });
try {
const { data } = await authFetch('/auth/getCurrentUser');
const { user, location } = data;

dispatch({
type: GET_CURRENT_USER_SUCCESS,
payload: { user, location },
});
} catch (error) {
if (error.response.status === 401) return;
logoutUser();
}
};
useEffect(() => {
getCurrentUser();
}, []);
```

reducer.js

```js
if (action.type === GET_CURRENT_USER_BEGIN) {
return { ...state, userLoading: true, showAlert: false };
}
if (action.type === GET_CURRENT_USER_SUCCESS) {
return {
...state,
userLoading: false,
user: action.payload.user,
userLocation: action.payload.location,
jobLocation: action.payload.location,
};
}
```

```js
if (action.type === LOGOUT_USER) {
return {
...initialState,
userLoading: false,
};
}
```

#### Protected Route FIX

```js
import Loading from '../components/Loading';

const ProtectedRoute = ({ children }) => {
const { user, userLoading } = useAppContext();

if (userLoading) return ;

if (!user) {
return ;
}
return children;
};

export default ProtectedRoute;
```

#### Landing Page

```js
import { Navigate } from 'react-router-dom';
import { useAppContext } from '../context/appContext';

const Landing = () => {
const { user } = useAppContext();
return (

{user && }
// rest of the code..........

);
};

export default Landing;
```

#### Logout Route

controllers/authController

```js
const logout = async (req, res) => {
res.cookie('token', 'logout', {
httpOnly: true,
expires: new Date(Date.now() + 1000),
});
res.status(StatusCodes.OK).json({ msg: 'user logged out!' });
};
```

routes/authRoutes

```js
import {
register,
login,
updateUser,
getCurrentUser,
logout,
} from '../controllers/authController.js';

router.route('/register').post(apiLimiter, register);
router.route('/login').post(apiLimiter, login);
router.get('/logout', logout);
// rest of the code ....
```

#### Logout - Front-End

appContext.js

```js
const logoutUser = async () => {
await authFetch.get('/auth/logout');
dispatch({ type: LOGOUT_USER });
};
```

#### Prepare for Deployment

- in client remove build and node_modules
- in server remove node_modules

package.json

```json

"scripts":{
"setup-production":"npm run install-client && npm run build-client && npm install",
"install-client":"cd client && npm install",
}

```

- node server
- APP NEEDS TO WORK LOCALLY !!!

#### Github Repo

- create new repo
- remove all existing repos (CHECK CLIENT !!!)
- in the root
- git init
- git add .
- git commit -m "first commit"
- push up to Github