login, signUp, jsonwebtoken, Authorization

김종민·2022년 11월 30일
0

mern-place

목록 보기
1/1

들어가기
mern의 첫번째 POST.
우선 Authorization을 먼전 정리하고, 다음 이어서 정리를 한다.
인증부분은 jsonwebtoken이 app, web 모두 무난하게 사용하기가
쉽다고 생각이 든다.
이것을 마지막으로 더이상 authorizartion부분은 다루지 않아야 겠다.
backend를 먼저하고, 다음에 front부분을 다루도록 한다.


1. backend(express)

1. src/routes/users-routers.js

import express from 'express'
import { check } from 'express-validator'
import { getUsers, login, signUp } from '../controllers/users-controller'
import { imageUpload } from '../middleware'
///유효성 검사를 위해서 express-validator를 import한다.
///express는 Router사용을 위해서 import
///imageUpload는 아바타 이미지를 위해서
///나머지 부분은 controller.


const usersRouter = express.Router()
///userRouter를 만들어줌.

usersRouter.get('/', getUsers)

usersRouter.post(
  '/signup',
  imageUpload.single('image'),
  [
    check('name').not().isEmpty(),
    check('email').normalizeEmail().isEmail(),
    check('password').not().isEmpty().isLength({ min: 5 }),
  ],
  signUp
)
///signUp path로 요청이 왔을때 route.
///imageUpload.single('image')는 front에서 post로
///name, email, password, image가 넘어왔을떄,
///image 즉, file을 인터셉터 해서 처리하는 middleware.
///imageUpload에서 다시 다룰 예정
///check부분은 'express-validator'를 사용해서
///넘어오는 data들을 유효성 검사를함.


usersRouter.post('/login', login)
///login path로 ㅔpost요청이 왔을떄, 처리하는 path.

export default usersRouter

2. src/controllers/users-controller.js

login, logout부분만 다루기로 한다.

npm i bcryptjs
npm i jsonwebtoken

import { validationResult } from 'express-validator'
import HttpError from '../models/http-error'
import User from '../models/users'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'

export const getUsers = async (req, res, next) => {
  let users

  try {
    users = await User.find()
  } catch (err) {
    const error = new Error('Fetching users failed', 500)
    return next(error)
  }

  res.json({ users: users.map((user) => user.toObject({ getter: true })) })
}


-->SignUp controller.
export const signUp = async (req, res, next) => {

  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    console.log(errors)
    res.status(422).json({ message: 'Inavalid data' })
  }
  ///routes에서 'express-validator' , 'check'로 front에서
  ///넘어오는 data들의 유효성 검사를 했으니,
  ///validationResult로 error를 찾아내어서 찍어준다.
  ///import { validationResult } from 'express-validator'
  ///되는 부분을 확인할 것.
  //res부분은 res.status(422).json({message:~~})으로
  ///날려 주는 것을 명심한다.

  const { name, email, password } = req.body
  ///front에서 넘어오는 data들을 req.body로 받아줌.

  let existingUser
  ///existingUser를 let으로 하나 만들어 놓음
  
  try {
    existingUser = await User.findOne({ email: email })
  } catch (err) {
    const error = new HttpError('User is already exist111', 500)
    return next(error)
  }
  ///User DB에서 email로 같은 User가 있는지 찾음.
  ///try~catch로 감싸서 existingUser를 찾다가 error가 
  ///나면, error를 날려줌. next()로 error를 날릴때에는,
  ///반드시 return을 해 주어야 여기서 stop이 됨.
  ///return을 안써주면, 다음으로 진행이 되어버림.

  if (existingUser) {
    const error = new HttpError('User exists alreadt', 400)
    return next(error)
  }
  ///existingUser가 존재하면, return next(error)로
  ///error를 날려줌.
  ///if (exists) {
  ///return res.status(400).render('createAccount', {
  ///pageTitle: 'Create Account',
  ///errorMessage: 'This username/email is already                                                  
  ///taken',
    })
  ///jmTube에서 error를 날리는 형태, 참고만 할 것!
  
  
  let hashedPassword
  ///password를 hash화 시키기 위해서 let으로
  ///hashPassword를 하나 만들어 놓음.
  
  try {
    hashedPassword = await bcrypt.hash(password, 12)
  } catch (err) {
    const error = new HttpError('could not create User', 500)
    return next(error)
  }
  ///npm i bcryptjs를 해서
  ///bcrypt instance를 하나 만든다. 맨 위에 참고.
  ///front에서 받은 password를 bcrypt.hash화 시켜서
  ///hashPassword에 넣어준다. 12는 열두번 꼬운다?, 돌린다?
  ///error발생하면, return next(error)로 날려줌.

  const createUser = new User({
    name,
    email,
    password: hashedPassword,
    image: req.file.location,
    places: [],
  })
  ///DB에 name, email, password, image, places:[]를
  ///넣어서 DB User에 새로운 User data를 만든다.
  ///반드시 아래에서 await createUser.save()를 해 주어야
  ///DB에 저장됨을 명심한다.

  try {
    await createUser.save()
  } catch (err) {
    const error = new HttpError('Creating User failed', 500)
    return next(error)
  }

  let token
  ///signUp이나 login을 하고 나면, front에 token을
  ///건내주어야한다.
  ///backend에서 만들어서 front에 넘겨주며 browser에 저장이 됨
  /// let으로 token을 하나 만든다,
  
  try {
    token = jwt.sign(
      { userId: createUser.id, email: createUser.email },
      'supersecret_dont_share',
      { expiresIn: '200h' }
    )
  } catch (err) {
    const error = new HttpError('making token failed', 500)
    return next(error)
  }
  ///token은 jwt.sign으로 만들며, 첫번째는 token에 담을
  ///user의 정보, 두번째는 secret_key(문자열 막쓰면됨),
  ///세번째는 token 만료기한. 200시간 후에 token지워짐.

  //res.status(200).json({ user: createUser.toObject({ getter: true }) })
  
  res
    .status(200)
    .json({ userId: createUser.id, email: createUser.email, token: token })
}
///token이 만들어지면, json으로 userId, email을
///token에 담아서 front로 return해 준다.


///Login Controller
export const login = async (req, res, next) => {
  const { email, password } = req.body
 ///front에서 email, password를 받는다.

  let existingUser
  ///existingUser를 만들어서 DB에서 user를 찾는자.

  try {
    existingUser = await User.findOne({ email: email })
  } catch (err) {
    const error = new HttpError('Logged in fail')
    return next(error)
  }
  ///DB에서 같은 email있는 User를 찾음.

  if (!existingUser) {
    const error = new HttpError('Invalid credential', 401)
    return next(error)
  }
  ///같은 email이 없으면, error를 날려줌.

  let isValidPassword = false
  ///isValidPassword를 let으로 하나 만들어줌.
  
  try {
    isValidPassword = await bcrypt.compare(password, existingUser.password)
    ///입력받은 password와 existingUser의 hash화 된
    ///password를 비교함.
    
  } catch (err) {
    const error = new HttpError('Could not login', 500)
    return next(error)
  }
  ///비교하는 과정에서 error가 나면, error를 날림.
  
  if (!isValidPassword) {
    const error = new HttpError('Invalid password', 401)
    return next(error)
  }
  ///입력받은 password와 email로 찾은 existingUser의 hash화
  ///된 password가 다를 경우 error를 날림

  let token
  ///signUp에서와 마찬가지로 let으로 token을 하나 만듬.
  
  try {
    token = jwt.sign(
      { userId: existingUser.id, email: existingUser.email },
      'supersecret_dont_share',
      { expiresIn: '200h' }
    )
    ///signUp에서와 같은 과정.
    ///두번쨰 argument인 'supersecret_dont_share'는
    ///반드시 signUp controller의 token과 같아야 된다.
    
  } catch (err) {
    const error = new HttpError('making token failed', 500)
    return next(error)
  }

  res.json({
    userId: existingUser.id,
    email: existingUser.email,
    token: token,
  })
  ///마지막 return으로 userId에 id, email에 email을
  ///담아서 token을 return해 준다.

}

!!!backend에서 하는 과정은 마무리됨.
!!!front에서 return받은 token을 어떻게 다루는지 알아보자,

3. src/shared/context/auth-context.js

front에서는 context로 persist logged를 다루기 때문에
잘 봐둘것!!!

import { createContext } from 'react'

export const AuthContext = createContext({
  isLoggedIn: false,
  userId: null,
  token: null,
  login: () => {},
  logout: () => {},
})

1.context폴더에 auth-context.js파일을 하나 만든다.
-위치는 상관이 없긴하나, 커질 경우, 찾기 편하게.
-AuthContext를 createContext로 만드는데,
-안에는 loggedIn, userId, token, login, logout
-다섯개를 넣어놓는다.
-default값은 false, null, null, 그리고 비워둔다.
-여기 state들이 전역적으로 다루어 질 예정,

4. src/shared/hooks/auth-hook.js

이 파일은 처음에는 app.js에 넣어놨다가
따로 빼서, hook으로 만듬.

import { useState, useCallback, useEffect } from 'react'

export const AuthHook = () => {
const [token, setToken] = useState(false)
const [userId, setUserId] = useState()
///token와 userId를 useState로 만든다.

const login = useCallback((uid, token, expirationDate) => {
 ///res.json({
 /// userId: existingUser.id,
 ///email: existingUser.email,
 ///token: token,
///})
---->server에서 보내준 data를 login함수의 argument로 받음.
--->Auth.js의 login, signUp화면에서 이 함수를 사용할 예정

  setToken(token)
  setUserId(uid)
  ///server에 POST를 날려서 받은 data를 setToken, 
  ///setUserId에 넣어준다.
  
  const tokenExpirationDate =
    expirationDate || new Date(new Date().getTime() + 1000 * 60 * 60 * 200) //현재시간+1시간
    ///token expire시간을 argu로 받거나, 아니면
    /// || 이후와 같이 만들어 주어도 됨.
    /// 위는 1초.60초.60분.200.으로 총 200시간 후 만료로
    /// 설정함.
    
  localStorage.setItem(
    'userData',
    JSON.stringify({
      userId: uid,
      token: token,
      expiration: tokenExpirationDate.toISOString(),
    })
  )
}, [])
///backend에서 받은 token, userId, expiration를
///localStorage의 'userData'라는 key에 넣어줌.
///JSON으로 바꿔서 넣어야 되고,
///tokenExpirationDate는 toISOString()해 주어여 함.

const logout = useCallback(() => {
  setToken(null)
  setUserId(null)
  localStorage.removeItem('userData')
}, [])
///logout은 token, UserId를 비워주고,
///localStorage의 'userData'를 remove해 주면 된다.

useEffect(() => {
  const storedData = JSON.parse(localStorage.getItem('userData'))
  if (
    storedData &&
    storedData.token &&
    new Date(storedData.expiration) > new Date()
  ) {
    login(
      storedData.userId,
      storedData.token,
      new Date(storedData.expiration)
    )
  }
}, [login])
///자동적으로 login해 주는 과정으로, localStorage에
///data가 있으면, JSON.parse로 data를 불러서
///login함수를 자동적으로 실행시킴

return { login, logout, token, userId }
///위에서 만든 login함수, logout 함수, token, userId를 
///return시켜줌
}

5. src/App.js

import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import NewPlace from './places/pages/NewPlace'
import MainNavigation from './shared/components/Navigation/MainNavigation'
import User from './user/pages/User'
import UserPlaces from './places/pages/UserPlaces'
import UpdatePlace from './places/pages/UpdatePlace'
import Auth from './user/pages/Auth'
import { AuthContext } from './shared/context/auth-context'
import Upload from './user/pages/Upload'
import { AuthHook } from './shared/hooks/auth-hook'

function App() {
  const { login, logout, userId, token } = AuthHook()
  ///바로 위에서 만든 auth-hook.js에서
  /// login함수, logout함수, userId, token을 불러옴.

  let routes
  ///routes를 let으로 설정해서
  ///token의 여부에 따라 rendering 되는 page를
  ///다르게 설정함.

  if (!token) {
    routes = (
      <Routes>
        <Route path="/" element={<User />} />
        <Route path="/upload" element={<Upload />} />
        <Route path="/:userId/places" element={<UserPlaces />} />
        <Route path="/auth" element={<Auth />} />
        <Route path="/*" elemen={<Auth />} />
      </Routes>
    )
  } else {
    routes = (
      <Routes>
        <Route path="/" element={<User />} />
        <Route path="/places/new" element={<NewPlace />} />
        <Route path="/places/:placeId" element={<UpdatePlace />} />
        <Route path="/:userId/places" element={<UserPlaces />} />
      </Routes>
    )
  }

  return (
    <AuthContext.Provider
      value={{
        isLoggedIn: !!token, //!!두개 붙이면 true/false를 return 해줌.
        token: token,
        userId: userId,
        login: login,
        logout: logout,
      }}
    >
    ///3번에서 만든 AuthContext에 Provider를 붙이고,
    ///token에는 AuthHook에서 받은 token,
    ///userId는 AuthHook에서 받은 userId,
    ///login에는 AuthHook에서 받은 login,
    ///logout에는 AuthHook에서 받은 logout을
    ///넣어준다.
    ///이렇게 하면 전역에서 전 페이지에서 AuthContext로
    ///token, userId, login, logout 사용가능해짐.
    ///6번에서 다른 page에서 AuthContext를
    ///사용하는 방법을 알아보자.
    
      <BrowserRouter>
        <MainNavigation />
        <main>{routes}</main>
      </BrowserRouter>
    </AuthContext.Provider>
  )
}

export default App

6. src/user/pages/Auth.js

front에서 server로 data(name, email, password, image)를
날리는 부분은 react-hook-form으로 코드를 바꿀 예정임으로
data를 날리는 부분에 대한 설명을 생략하고,
data를 날려서 res를 받은 후, logged를 지속시키는
방법만 확인한다.

import React, { useState, useContext } from 'react'
import Card from '../../shared/components/UIElements/Card'
import Input from '../../shared/components/FormElements/Input'
import Button from '../../shared/components/FormElements/Button'
import ErrorModal from '../../shared/components/UIElements/ErrorModal'
import LoadingSpinner from '../../shared/components/UIElements/LoadingSpinner'
import { useForm } from '../../shared/hooks/form-hook'
import { AuthContext } from '../../shared/context/auth-context'
import './Auth.css'
import {
  VALIDATOR_EMAIL,
  VALIDATOR_MINLENGTH,
  VALIDATOR_REQUIRE,
} from '../../shared/util/validator'
import { useNavigate } from 'react-router-dom'
import ImageUpload from '../../shared/components/FormElements/ImageUpload'

const Auth = () => {
  const auth = useContext(AuthContext)
  ///auth instance로 AuthContext를 불러온다.
  ///불러온느 방법을 잘 봐둔다.
  ///useContext(AuthContext)이다.
  
  const [isLoginMode, setIsLoginMode] = useState(true)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState()
  
  const navigate = useNavigate()
  ///페이지 이동을 위해서 react-router-dom에서
  ///useNavigate를 불러온다.


  const authSubmitHandler = async (event) => {
    event.preventDefault()
	///새로고침 되는것을 방지하기 위해
    ///preventDefault()를 사용한다.

    console.log(formState.inputs)

    if (isLoginMode) {
      try {
        setIsLoading(true)
        const response = await fetch(
          'https://mern-place123.herokuapp.com/api/users/login',
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
            },
            body: JSON.stringify({
              email: formState.inputs.email.value,
              password: formState.inputs.password.value,
            }),
          }
        )
	///server로 data를 날리는 과정!! 추후 설명예정.

        const responseData = await response.json()
        ///server의 login controller에서 res로 날려준
        ///data를 response.json()으로 받는다.
        
        if (!response.ok) {
          throw new Error(responseData.message)
        }
        setIsLoading(false)
        
        auth.login(responseData.userId, responseData.token)
        ///이번 POST의 핵심.
        ///auth.login으로 auth-hook.js의 login함수를
        ///사용하게 됨, argument로 response로 받은
        ///responseData.userId와, responseData.token
        ///을 날리고, login에서는 uid와 token으로 받음.
        
        navigate('/')
        ///성공적으로 마무리 되면, 홈('/')으로 rendering함.
        
      } catch (err) {
        setIsLoading(false)
        setError(err.message || 'Something went wrong, please try again.')
      }
    } else {
    
      try {
        setIsLoading(true)
        const formData = new FormData()
        formData.append('email', formState.inputs.email.value)
        formData.append('name', formState.inputs.name.value)
        formData.append('password', formState.inputs.password.value)
        formData.append('image', formState.inputs.image.value)

        console.log('image', formState.inputs.image.value)

        const response = await fetch(
          process.env.REACT_APP_BACKEND + '/users/signup',
          {
            method: 'POST',
            body: formData,
          }
        )
        ///data를 front에서 server로 날리는 과정.
        ///추후 설명 예정.

        const responseData = await response.json()
        if (!response.ok) {
          throw new Error(responseData.message)
        }
        
        auth.login(responseData.userId, responseData.token)
        ///위와 마찬가지로 auth.login을 통해서,
        ///auth-hook.js의 login함수를 사용하게 됨.
        ///argu로 userId, token을 보내줌.
        
        setIsLoading(false)
        navigate('/')
        ///home으로 redirect
        
      } catch (err) {
        setIsLoading(false)
        setError(err.message || 'Something went wrong, please try again.')
      }
    }
  }

  const errorHandler = () => {
    setError(null)
  }

  }

export default Auth

!!! 여기까지가 길고 긴 Authrization의 과정임.
!!! 더이상 정리하는 일이 없게 이번으로 확실히 마무리를 하자꾸나!!

profile
코딩하는초딩쌤

0개의 댓글