Authorization vs authentication Authorization  - checking if password is correct. Authentication  - checking if the user is the same as authorized initially. Principle & data flow The client is authorized by comparing email and password against database.  On successful authorization the server issues an  access  and a  refresh  tokens for future authentication to avoid asking for credentials on every http request. Client stores  access  token in the local storage or memory and attaches it inside request headers for private api requests. Token is attached by 'request' interceptor in   axiosWithAuth . For protected api 'verifyToken' middleware verifies the token. If a token is ok, the request goes forward. If a token is bad (compromised or outdated) a response with status(401) is returned. Access  token is short and expires in 15 min. 'Response' interceptor in   axiosWithAuth  checks for 401 status and if it is 401, it makes additional request to update  access  token by presenting a  refresh  token, which has 30d expiry time. Refresh  token is stored in secured cookies on the login and also kept in database. If  refresh  token is valid and available in database, then updated  access   and refreshed  refresh  tokens are issued. axiosWithAuth  remembers the request with all parameters when it got 401 and after getting successful refreshed tokens it repeats initial http request. If  refresh  token is invalid or old, then  access  token is not issued, client is considered as unauthorized and new login is required. If a user is deleted from the database, he is still authorized, for short time until   access  token is expired (15 min). We should consider the duration of access token depending on sensitivity of our data. Tokens are also checked and refreshed at the initial app load in useEffect() on Main component mount. That's how we determine if a client logged in or out. For tokens we use JWT tokens, which contain encrypted (not hashed) payload (usually object with user email, role, etc...), validation time and a hash based on a secret keys, which are kept on server. Server can validate the token only if it knows the secrete keys. Secrete keys are kept in environment variables. server 
      // server.ts
      import 'dotenv/config'
      import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      import morgan from 'morgan'
      import cookieParser from 'cookie-parser'
      import cors from 'cors'
      import { loginRouter } from './api/loginRouter'
      import { registerRouter } from './api/registerRouter'
      import { connectToDb } from './db/connectToDb'
      import { logoutRouter } from './api/logoutRouter'
      import { activateRouter } from './api/activateRouter'
      import { refreshRouter } from './api/refreshRouter'
      import { errorHandler } from './middleware/errorHandler'
      const app = express()
      connectToDb()
      app.use(morgan('dev')) // http logs in terminal
      app.use(express.json()) // parses incoming requests with JSON because we use lots of json, let it be default
      app.use(cookieParser())
      app.use(cors())
      app.get('/', (req: ReqType, res:ResType) => res.send('This is from express.js'))
      app.get('/api', (req: ReqType, res:ResType) => res.json({ message: '/api' }))
      app.use('/api/register', registerRouter)
      app.use('/api/login', loginRouter)
      app.use('/api/logout', logoutRouter)
      app.use('/api/activate', activateRouter)
      app.use('/api/refresh', refreshRouter)
      app.use(errorHandler)
      const port = process.env.PORT_BACK_END
      const domain = process.env.DOMAIN
      app.listen(port, () => console.log(`server started at ${domain}:${port}`))
       registerRouter 
      // registerRouter.ts
      import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      // import { connectToDb } from '../db/connectToDb'
      import { UserModel } from '../db/models/user.model'
      import bcrypt from 'bcryptjs'
      import { v4 as uuidv4 } from 'uuid'
      import { sendMail } from '../services/mail/sendMail'
      import { body, validationResult } from 'express-validator'
      const domain = process.env.DOMAIN
      const port = process.env.PORT_FRONT_END
      export const registerRouter = express.Router()
      registerRouter.post(
        '/',
        body('email').isEmail(),
        body('password').isLength({ min: 3 }),
        async (req: ReqType, res: ResType, next: NextType) => {
          try {
            // validation
            const validationErrors = validationResult(req)
            if (!validationErrors.isEmpty()) return res.json({ status: 'error', message: 'validation error', validationErrors })
            // check if user already exists
            // await connectToDb()
            const email = req.body.email.toLowerCase()
            const user = await UserModel.findOne({ email })
            if (user) return res.json({ status: 'error', message: 'user with such email already exists' })
            // save user to db
            const password = await bcrypt.hash(req.body.password, 10)
            const activationLink = `${domain}:${port}/api/activate/${uuidv4()}`
            await UserModel.create({ email, password, activationLink })
            // send email with activation link
            const subject = 'Activation for quotation.app'
            const html = `<div><h1>Follow the link to confirm the registration</h1><a href="${activationLink}">${activationLink}</a></div> `
            // await sendMail({ to: email, subject, html })
            // all went good, send the response
            res.json({ status: 'ok', message: 'user is registered' })
          } catch (error: any) {
            next(error)
          }
        }
      )
       activateRouter 
      // activateRouter
      import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      import { UserModel } from '../db/models/user.model'
      const domain = process.env.DOMAIN
      const port = process.env.PORT_FRONT_END
      export const activateRouter = express.Router()
      activateRouter.get('/:link', async (req: ReqType, res: ResType, next: NextType) => {
        try {
          const activationLink = `${domain}:${port}/api/activate/${req.params.link}`
          const user = await UserModel.findOne({ activationLink })
          if (!user) return res.json({ status: 'error', message: 'no account with such activation link' })
          user.isActivated = true
          await user.save()
          return res.redirect(`${domain}:${port}/login`)
        } catch (error: any) {
          next(error)
        }
      })
       loginRouter 
        // loginRouter.ts
        import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
        import { UserModel } from '../db/models/user.model'
        import bcrypt from 'bcryptjs'
        import { getAccessJwtToken, getRefreshJwtToken, refreshJwtTokenExpirationSeconds } from '../services/jwt/jwt'
        export const loginRouter = express.Router()
        loginRouter.post('/', async (req: ReqType, res: ResType, next: NextType) => {
          try {
            // get mail & password from body
            let { email, password } = req.body
            email = email.toLowerCase()
            // check email & password
            const user = await UserModel.findOne({ email })
            const isPasswordValid = user && await bcrypt.compare(password, user.password)
            if (!user || !isPasswordValid) return res.json({ status: 'error', message: 'invalid credentials' })
            // check if account is activated
            if (!user.isActivated) return res.json({ status: 'error', message: 'account is not activated' })
            // generate jwt tokens
            // const refreshJwtTokenExpirationDays = 30
            // const accessJwtToken = jwt.sign({ email }, process.env.JWT_ACCESS_SECRET as string, { expiresIn: '8h' })
            const accessJwtToken = getAccessJwtToken({ email })
            // const refreshJwtToken = jwt.sign({ email }, process.env.JWT_REFRESH_SECRET as string, { expiresIn: `${refreshJwtTokenExpirationDays}d` })
            const refreshJwtToken = getRefreshJwtToken({ email })
            // put refresh token in cookie
            res.cookie('refreshJwtToken', refreshJwtToken, { maxAge: refreshJwtTokenExpirationSeconds * 1000, httpOnly: true })
            // put refresh token in db (also update login date)
            const filter = { email }
            const update = { loggedAt: new Date(), refreshJwtToken }
            await UserModel.findOneAndUpdate(filter, update)
            // return access token to the client
            res.json({ status: 'ok', message: `user with email: ${email} logged in and tokens are refreshed`, accessJwtToken })
          } catch (error: any) {
            next(error)
          }
        })
       refreshRouter 
      // refreshRouter.ts
      import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      import { UserModel } from '../db/models/user.model'
      import { getAccessJwtToken, getRefreshJwtToken, refreshJwtTokenExpirationSeconds, verifyRefreshJwtToken } from '../services/jwt/jwt'
      export const refreshRouter = express.Router()
      refreshRouter.get('/', async (req: ReqType, res: ResType, next: NextType) => {
        try {
          // get refresh token from cookie
          const { refreshJwtToken } = req.cookies
          if (!refreshJwtToken) res.json({ status: 'error', message: 'no refresh token found in cookies during token refresh, probably not authorized' })
          // check if token is ok
          const { email } = verifyRefreshJwtToken(refreshJwtToken)
          if (!email) res.json({ status: 'error', message: 'refresh token is not validated, probably not authorized' })
          // find token in db
          const user = await UserModel.findOne({ refreshJwtToken })
          if (!user) return res.json({ status: 'error', message: 'no user found with such refresh token in db' })
          // generate refresh token and save in db
          const updatedRefreshJwtToken = getRefreshJwtToken({ email })
          res.cookie('refreshJwtToken', updatedRefreshJwtToken, { maxAge: refreshJwtTokenExpirationSeconds * 1000, httpOnly: true })
          await UserModel.findOneAndUpdate({ email }, { refreshJwtToken: updatedRefreshJwtToken })
          // generate access token and send to client
          const accessJwtToken = getAccessJwtToken({ email })
          // send response
          res.json({ status: 'ok', message: `refresh token for email: ${email} is refreshed`, accessJwtToken })
        } catch (error) {
          next(error)
        }
      })
       logoutRouter 
      // logoutRouter.ts
      import express, { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      import { UserModel } from '../db/models/user.model'
      // eslint-disable-next-line camelcase
      import jwt_decode from 'jwt-decode'
      export const logoutRouter = express.Router()
      logoutRouter.get('/', async (req: ReqType, res: ResType, next: NextType) => {
        try {
          // check refresh token
          const { refreshJwtToken } = req.cookies
          if (!refreshJwtToken) res.json({ status: 'error', message: 'no refresh token in cookies, probably already logged out' })
          // get email from refresh token
          const { email }: { email: string } = jwt_decode(refreshJwtToken)
          if (!email) res.json({ status: 'error', message: 'can not retrieve email from refresh token' })
          // delete refreshJwtToken from cookie
          res.clearCookie('refreshJwtToken')
          // delete token from db
          const user = await UserModel.findOne({ refreshJwtToken })
          if (!user) return res.json({ status: 'error', message: 'no user find with such refresh token' })
          user.refreshJwtToken = undefined
          await user.save()
          // send response
          res.json({ status: 'ok', message: `user with email: ${email} logged out` })
        } catch (error) {
          next(error)
        }
      })
       verifyToken middleware 
      // verifyToken.ts
      import { Request as ReqType, Response as ResType, NextFunction as NextType } from 'express'
      import { verifyAccessJwtToken } from '../services/jwt/jwt'
      export function verifyToken(req: any, res: ResType, next: NextType) {
        try {
          const accessJwtToken = req.headers['access-jwt-token'] as string
          const { email } = verifyAccessJwtToken(accessJwtToken)
          req.email = email // can add email in header, maybe useful for something
          next()
        } catch (error: any) {
          return res.status(401).send('accessJwtToken is not verified, user is not authorized')
        }
      }
       get & verify tokens 
      // jwt.ts
      import jwt, { JwtPayload } from 'jsonwebtoken'
      const accessJwtTokenExpirationSeconds = 15 * 60 // 15 min
      export const refreshJwtTokenExpirationSeconds = 30 * 24 * 60 * 60 // 30 days
      export const getAccessJwtToken = (payload: string | object) => jwt.sign(payload, process.env.JWT_ACCESS_SECRET as string, { expiresIn: accessJwtTokenExpirationSeconds })
      export const getRefreshJwtToken = (payload: string | object) => jwt.sign(payload, process.env.JWT_REFRESH_SECRET as string, { expiresIn: refreshJwtTokenExpirationSeconds })
      export const verifyAccessJwtToken = (accessJwtToken: string) => jwt.verify(accessJwtToken, process.env.JWT_ACCESS_SECRET as string) as JwtPayload
      export const verifyRefreshJwtToken = (refreshJwtToken: string) => jwt.verify(refreshJwtToken, process.env.JWT_REFRESH_SECRET as string) as JwtPayload
       Axios 
      // axios.ts
      import axios from 'axios'
      export const axiosWithAuth = axios.create({ withCredentials: true })
      axiosWithAuth.interceptors.request.use((config) => {
        const accessJwtToken = localStorage.getItem('accessJwtToken')
        if (config.headers && accessJwtToken) {
          config.headers['access-jwt-token'] = accessJwtToken
        }
        return config
      })
      axiosWithAuth.interceptors.response.use(
        (config) => {
          return config
        },
        async (error) => {
          const originalRequest = error.config
          if (error.response.status === 401 && error.config && !error.config._isRetry) {
            try {
              originalRequest._isRetry = true
              const response = await axios.get('/api/refresh', { withCredentials: true })
              const accessJwtToken = response.data.accessJwtToken
              accessJwtToken && localStorage.setItem('accessJwtToken', accessJwtToken)
              !accessJwtToken && localStorage.removeItem('accessJwtToken')
              return axiosWithAuth.request(originalRequest)
            } catch (error) {
              console.log('not authorized')
              console.log(error)
            }
          }
          if (error.response.status === 401) {
            // todo: logout in redux
            // todo: suggest to login
          }
          throw error
        }
      )
       Frontend functions 
      async function registerUser(e: EventType) {
        e.preventDefault()
        try {
          const method = 'POST'
          const headers = { 'Content-Type': 'application/json' }
          const { email, password } = inputValue
          const body = JSON.stringify({ email, password })
          const options = { method, headers, body }
          const res = await fetch('/api/register', options)
          const data = await res.json()
          data.status === 'error' && data.message === 'user with such email already exists' && notify({ msg: 'Already registered', type: 'info', theme: 'light' })
          data.status === 'ok' && notify({ msg: 'Check your email and confirm registration.', theme: 'light' })
          console.log(data)
        } catch (err) {
          console.log(err)
          notify({ msg: 'Registration failed', type: 'error', theme: 'light' })
        } finally {
          // remove spinner from the button
        }
      }
       
      async function loginUser(e: EventType) {
        e.preventDefault()
        const method = 'POST'
        const headers = { 'Content-Type': 'application/json' }
        const { email, password } = credentials
        const body = JSON.stringify({ email, password })
        const options = { method, headers, body }
        const res = await fetch('/api/login', options)
        const data = await res.json()
        console.log(data)
        if (data.status === 'error') {
          alert(data.message)
          return localStorage.removeItem('accessJwtToken')
        }
        localStorage.setItem('accessJwtToken', data.accessJwtToken)
        alert('logged in')
        navigate('/')
      }
       
      async function refreshTokens() {
        // todo: move function into 'functions' folder in a file of folder with credentials business logic
        // todo: separate helper files by a business logic
        try {
          if (!localStorage.getItem('accessJwtToken')) return console.log('user is not logged in')
          const response = await axios.get('/api/refresh', { withCredentials: true })
          if (response.data.status === 'error') {
            console.log(response.data.message)
            localStorage.removeItem('accessJwtToken')
          }
          if (!response.data.accessJwtToken) return console.log(666)
          const accessJwtToken = response.data.accessJwtToken
          const jwtTokenPayload: {email: string | undefined} = jwt_decode(accessJwtToken)
          const { email } = jwtTokenPayload
          if (!email) return console.log('token is not valid')
          localStorage.setItem('accessJwtToken', response.data.accessJwtToken)
          console.log(response)
          console.log(`tokens for user with email: ${email} are refreshed`)
        } catch (error) {
          console.log(error)
        }
      }
       
      async function getUsersFromDb() {
        try {
          const res = await axiosWithAuth('/api/users')
          console.log(res)
        } catch (error) {
          console.log(error)
        }
      }