본문 바로가기
개발/React

리액트 - GitHub OAuth 인증 토큰으로 로그인 상태 관리하기 (GitHub OAuth Login Status)

by 피로물든딸기 2023. 8. 19.
반응형

깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)

 

리액트 전체 링크

 

참고

- 깃허브 OAuth Access 토큰 발급 받기

- Toast UI 에디터로 깃허브 마크다운 저장하기

 

GitHub OAuth Project Settings
Material UI로 깃허브 로그인 프로필 만들기
깃허브 OAuth 콜백 처리하기
인증 토큰 획득 서버 구현하기
인증 토큰으로 로그인 상태 관리하기
로그인 정보를 활용하여 Commit Message 남기기
새 창으로 로그인해서 현재 상태 유지하기

 

이제 로그인이 되면 버튼을 로그아웃으로 변경하고 프로필 사진도 변경해보자


로그인 체크 함수

 

이전 글에서 로그인 이후 얻은 정보를 로컬 스토리지에 저장하였다.

localStorage.setItem("GITHUB_TOKEN", token);
localStorage.setItem("AVATAR_URL", avatarUrl);
localStorage.setItem("LOGIN_ID", loginID);

 

이를 이용해서 현재 로그인 상태를 알 수 있다.

githublibrary.js를 만들어서 아래의 코드를 추가하자.

정상적으로 로그인된 상태라면 useStateloginStatustrue로 변경할 것이다.

에러가 발생하면 catch에서 false로 처리한다.

import axios from "axios";

export const loginCheck = async (setLoginStatus) => {
  let token = localStorage.getItem("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    setLoginStatus(true);
  } catch (error) {
    console.error("Error fetching user data:", error);
    setLoginStatus(false);
  }
};

Callback - 로그인 상태 변경하기

 

App.js에서 login 상태를 관리하기 위해 useState로 loginStatus를 추가한다.

그리고 GitHubLoginButtonGitHubLoginCallback Component에 props로 넘겨준다.

import React, { useEffect, useState } from "react";
...

import * as gh from "./githublibrary.js";

const App = () => {
  const [loginStatus, setLoginStatus] = useState(false);

  useEffect(() => {
    gh.loginCheck(setLoginStatus);
  },[]);

  return (
    <div>
      <div className="router">
        <GitHubLoginButton
          loginStatus={loginStatus}
          setLoginStatus={setLoginStatus}
        />
        ...
      </div>
      <Routes>
        ...
        <Route
          path="/callback"
          element={
            <GitHubLoginCallback
              setLoginStatus={setLoginStatus}
            />
          }
        />
      </Routes>
    </div>
  );
};

export default App;

 

GitHubLoginCallbackprops를 받도록 수정한 후, getAccessToken에서 try catch로 login status를 변경한다.

import React, { useEffect } from "react";

import axios from "axios";
import queryString from "query-string";

const clientID = process.env.REACT_APP_CLIENT_ID;

const GitHubLoginCallback = ({ loginStatus, setLoginStatus }) => {
  const getAccessToken = async (code) => {
    try {
      let server = `http://192.168.55.120:3002`;
      let accessInfo = await axios.get(
        `${server}/githubLogin?code=${code}&clientID=${clientID}`
      );

      let token = accessInfo.data.token;

      const userResponse = await axios.get("https://api.github.com/user", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      const userData = userResponse.data;
      const avatarUrl = userData.avatar_url;
      const loginID = userData.login;

      console.log(userData);
      console.log(avatarUrl);
      console.log(loginID);

      localStorage.setItem("GITHUB_TOKEN", token);
      localStorage.setItem("AVATAR_URL", avatarUrl);
      localStorage.setItem("LOGIN_ID", loginID);
      
      setLoginStatus(true);
    } catch (e) {
      console.log(e);
      setLoginStatus(false);
    }
  };

  const getCode = () => {
    let qs = queryString.parse(window.location.search);
    let code = qs.code;
    getAccessToken(code);
  };

  useEffect(() => getCode(), []);

  return <div>{loginStatus ? "로그인 성공!" : "로그인 실패..."}</div>;
};

export default GitHubLoginCallback;

 

로그인이 실패한다면 아래와 같이 페이지가 변경된다.


로그인 버튼, 프로필 사진 변경하기

 

localStorage로 로그인 상태를 관리하기 때문에, 로그아웃은 아래와 같이 매우 간단하게 구현할 수 있다.

  const logout = () => {
    setLoginStatus(false);
    
    localStorage.removeItem("GITHUB_TOKEN");
    localStorage.removeItem("AVATAR_URL");
    localStorage.removeItem("LOGIN_ID");
  };

 

loginStatus가 callback에서 상태가 변경되므로, GitHubLoginButton도 아래와 같이 코드를 수정한다.

여기서는 단순하게 loginStatus에 따라 로그인 / 로그아웃 프로필과 버튼을 구분하였다.

import React from "react";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Avatar from "@mui/material/Avatar";

const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;

const GitHubLoginButton = ({ loginStatus, setLoginStatus }) => {
  const logout = () => {
    setLoginStatus(false);
    
    localStorage.removeItem("GITHUB_TOKEN");
    localStorage.removeItem("AVATAR_URL");
    localStorage.removeItem("LOGIN_ID");
  };

  return (
    <div>
      <Box sx={{ m: 2 }}>
        {loginStatus === false && (
          <div>
            <Stack direction="row" spacing={2}>
              <Avatar alt="GitHub Login" src="/static/images/avatar/1.jpg" />
              <Button variant="outlined" color="secondary" href={loginURL}>
                로그인
              </Button>
            </Stack>
          </div>
        )}
        {loginStatus === true && (
          <div>
            <Stack direction="row" spacing={2}>
              <Avatar
                alt="GitHub Login"
                src={localStorage.getItem("AVATAR_URL")}
              />
              <Button variant="outlined" color="error" onClick={logout}>
                로그아웃
              </Button>
            </Stack>
          </div>
        )}
      </Box>
    </div>
  );
};

export default GitHubLoginButton;

 

이제 로그인을 하면 프로필과 버튼이 변경되고, 로그아웃을 누르면 원래 상태로 돌아가게 된다.


useNavigate 리다이렉션

 

로그인이 정상적으로 완료된 후라면 callback 페이지에 남을 이유가 없다. 

 

useNavigate를 이용해서 home으로 돌아가도록 코드를 수정하자.

import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";

const GitHubLoginCallback = ({ loginStatus, setLoginStatus }) => {
  const navigate = useNavigate();

  const getAccessToken = async (code) => {
    try {
      ...
      setLoginStatus(true);
      
      navigate('/',  { replace: true });
    } catch (e) { ... }
  };

...

 

로그인이 완료되면 첫페이지로 돌아가는 것을 알 수 있다. (URL 참고)


전체 코드는 다음과 같다.

 

App.js

import React, { useEffect, useState } from "react";
import { Route, Link, Routes } from "react-router-dom";

import Home from "./page/Home";
import SimpleToastEditor from "./page/SimpleToastEditor";

import "./App.css";
import GitHubLoginButton from "./page/GitHubLoginButton";
import GitHubLoginCallback from "./page/GitHubLoginCallback";

import * as gh from "./githublibrary.js";

const App = () => {
  const [loginStatus, setLoginStatus] = useState(false);

  useEffect(() => {
    gh.loginCheck(setLoginStatus);
  }, []);

  return (
    <div>
      <div className="router">
        <GitHubLoginButton
          loginStatus={loginStatus}
          setLoginStatus={setLoginStatus}
        />
        <span>
          <Link to="/">Home</Link>
        </span>
        <span>
          <Link to="/editor">Toast UI Editor</Link>
        </span>
      </div>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/editor" element={<SimpleToastEditor />} />
        <Route
          path="/callback"
          element={
            <GitHubLoginCallback
              loginStatus={loginStatus}
              setLoginStatus={setLoginStatus}
            />
          }
        />
      </Routes>
    </div>
  );
};

export default App;

 

page/GitHubLoginCallback.js

import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";

import axios from "axios";
import queryString from "query-string";

const clientID = process.env.REACT_APP_CLIENT_ID;

const GitHubLoginCallback = ({ loginStatus, setLoginStatus }) => {
  const navigate = useNavigate();

  const getAccessToken = async (code) => {
    try {
      let server = `http://192.168.55.120:3002`;
      let accessInfo = await axios.get(
        `${server}/githubLogin?code=${code}&clientID=${clientID}`
      );

      let token = accessInfo.data.token;

      const userResponse = await axios.get("https://api.github.com/user", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });

      const userData = userResponse.data;
      const avatarUrl = userData.avatar_url;
      const loginID = userData.login;

      console.log(userData);
      console.log(avatarUrl);
      console.log(loginID);

      localStorage.setItem("GITHUB_TOKEN", token);
      localStorage.setItem("AVATAR_URL", avatarUrl);
      localStorage.setItem("LOGIN_ID", loginID);
      
      setLoginStatus(true);
      
      navigate('/',  { replace: true });
    } catch (e) {
      console.log(e);
      setLoginStatus(false);
    }
  };

  const getCode = () => {
    let qs = queryString.parse(window.location.search);
    let code = qs.code;
    getAccessToken(code);
  };

  useEffect(() => getCode(), []);

  return <div>{loginStatus ? "로그인 성공!" : "로그인 실패..."}</div>;
};

export default GitHubLoginCallback;

 

GitHubLoginButton.js

import React from "react";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Avatar from "@mui/material/Avatar";

const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;

const GitHubLoginButton = ({ loginStatus, setLoginStatus }) => {
  const logout = () => {
    setLoginStatus(false);
    
    localStorage.removeItem("GITHUB_TOKEN");
    localStorage.removeItem("AVATAR_URL");
    localStorage.removeItem("LOGIN_ID");
  };

  return (
    <div>
      <Box sx={{ m: 2 }}>
        {loginStatus === false && (
          <div>
            <Stack direction="row" spacing={2}>
              <Avatar alt="GitHub Login" src="/static/images/avatar/1.jpg" />
              <Button variant="outlined" color="secondary" href={loginURL}>
                로그인
              </Button>
            </Stack>
          </div>
        )}
        {loginStatus === true && (
          <div>
            <Stack direction="row" spacing={2}>
              <Avatar
                alt="GitHub Login"
                src={localStorage.getItem("AVATAR_URL")}
              />
              <Button variant="outlined" color="error" onClick={logout}>
                로그아웃
              </Button>
            </Stack>
          </div>
        )}
      </Box>
    </div>
  );
};

export default GitHubLoginButton;

 

githublibrary.js

import axios from "axios";

export const loginCheck = async (setLoginStatus) => {
  let token = localStorage.getItem("GITHUB_TOKEN");
  try {
    const response = await axios.get("https://api.github.com/user", {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    console.log(response);
    setLoginStatus(true);
  } catch (error) {
    console.error("Error fetching user data:", error);
    setLoginStatus(false);
  }
};
반응형

댓글