diff --git a/src/cert/AuthStorage.ts b/src/cert/AuthStorage.ts deleted file mode 100644 index edb1e4d..0000000 --- a/src/cert/AuthStorage.ts +++ /dev/null @@ -1,28 +0,0 @@ -const AUTH = 'auth'; - -export function saveAuth(token: string) { - const decoded = decodeToken(token); - let userData: { id: string; url: string }; - userData = { id: decoded.username, url: decoded.imageUrl }; - localStorage.setItem(AUTH, JSON.stringify(userData)); -} -export function getAuth(): { id: string; url: string } { - return JSON.parse(localStorage.getItem(AUTH)); -} - -export function removeAuth() { - localStorage.removeItem(AUTH); -} - -const decodeToken = (token: string) => { - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); - const jsonPayload = decodeURIComponent( - atob(base64) - .split('') - .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) - .join(''), - ); - - return JSON.parse(jsonPayload); -}; diff --git a/src/cert/TokenStorage.ts b/src/cert/TokenStorage.ts index 3bc115e..38a0e2e 100644 --- a/src/cert/TokenStorage.ts +++ b/src/cert/TokenStorage.ts @@ -1,28 +1,97 @@ const TOKEN = 'token'; -const expireDays = 5; + +const JWT_PART_LENGTH = 3; +const MS_PER_SEC = 1000; + +enum JWTPart { + Header, + Payload, + Signature, +} +export interface JWTPayload { + id: string; + iat: number; + exp: number; +} + +export function getJWTPart(token: string, part: JWTPart): string { + const parts = token.split('.'); + if (parts.length !== JWT_PART_LENGTH) { + throw new Error('Invalid JWT token format.'); + } + return parts[part]; +} + +export function isValidJWTPayload(object: any): object is JWTPayload { + return ( + object !== null && + typeof object === 'object' && + 'id' in object && + typeof object.id === 'number' && + 'iat' in object && + typeof object.iat === 'number' && + 'exp' in object && + typeof object.exp === 'number' + ); +} + +export function parseJWTPayload(token: string): JWTPayload { + try { + const payload = decodeToken(token); + if (!isValidJWTPayload(payload)) { + throw new Error('Invalid JWTPayload'); + } + return payload; + } catch (error) { + throw error; + } +} + +export function isJWTExpired(token: string): boolean { + try { + const payload = parseJWTPayload(token); + return payload.exp < Date.now() / MS_PER_SEC; + } catch (error) { + console.error(error); + return true; + } +} export function clearToken() { - localStorage.clear(); + localStorage.removeItem(TOKEN); } export function saveToken(token: string) { - const tokenObj = { - value: token, - expire: Date.now() + expireDays * 24 * 60 * 60 * 1000, - }; - const tokenObjString = JSON.stringify(tokenObj); - localStorage.setItem(TOKEN, tokenObjString); + localStorage.setItem(TOKEN, token); } export function getToken() { - const tokenObjString = localStorage.getItem(TOKEN); - if (!tokenObjString) { - return null; - } - const tokenObj = JSON.parse(tokenObjString); - if (Date.now() > tokenObj.expire) { + let token = localStorage.getItem(TOKEN); + if (token !== null && isJWTExpired(token)) { clearToken(); - return null; + token = null; } - return tokenObj.value; + return token; } + +export function getDecodedToken(): { id: string; url: string } { + const token = localStorage.getItem(TOKEN); + if (!token) { + return { id: null, url: null }; + } + const decoded = decodeToken(token); + return { id: decoded.username, url: decoded.imageUrl }; +} + +const decodeToken = (token: string) => { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent( + atob(base64) + .split('') + .map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)) + .join(''), + ); + + return JSON.parse(jsonPayload); +}; diff --git a/src/components/Auth/AuthCallback.tsx b/src/components/Auth/AuthCallback.tsx index 47adde1..d2431aa 100644 --- a/src/components/Auth/AuthCallback.tsx +++ b/src/components/Auth/AuthCallback.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { saveToken } from '@cert/TokenStorage'; import { useNavigate } from 'react-router-dom'; -import { saveAuth, getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import GlobalLoginState from '@recoil/GlobalLoginState'; import { useSetRecoilState } from 'recoil'; @@ -15,13 +15,12 @@ const AuthCallback = () => { useEffect(() => { saveToken(token); - saveAuth(token); setLoginState(() => { return { - id: getAuth().id, + id: getDecodedToken().id, isLogin: true, isAdmin: false, - profileUrl: getAuth().url, + profileUrl: getDecodedToken().url, }; }); navigate('/'); diff --git a/src/components/Auth/AuthSignUp.tsx b/src/components/Auth/AuthSignUp.tsx index 63d5a9a..d11a8d5 100644 --- a/src/components/Auth/AuthSignUp.tsx +++ b/src/components/Auth/AuthSignUp.tsx @@ -5,7 +5,6 @@ import { useSetRecoilState, useRecoilValue } from 'recoil'; import errorAlert from '@globalObj/function/errorAlert'; import apiClient from '@service/apiClient'; import { saveToken } from '@cert/TokenStorage'; -import { saveAuth } from '@cert/AuthStorage'; import ProfileChangeModalShow from '@recoil/ProfileChangeModalShow'; import SignUpProfileState from '@recoil/SignUpProfileState'; import ProfileModal from '@auth/ProfileModal'; @@ -33,7 +32,6 @@ const AuthSignUp = () => { }) .then((res) => { saveToken(res.data.access_token); - saveAuth(res.data.access_token); setLoginState(() => { return { id: res.data.id, diff --git a/src/components/Review/SelectModal.tsx b/src/components/Review/SelectModal.tsx index 2d5c0df..d806442 100644 --- a/src/components/Review/SelectModal.tsx +++ b/src/components/Review/SelectModal.tsx @@ -12,7 +12,7 @@ import useSWR from 'swr'; import getAddress from '@globalObj/function/getAddress'; import fetcher from '@globalObj/function/fetcher'; import EmptyEvent from '@globalObj/object/EmptyEvent'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; function SelectModal(prop: { mode: string }) { const { data: eventList, mutate: mutateAllEvent } = useSWR( @@ -66,7 +66,7 @@ function SelectModal(prop: { mode: string }) { setSelectedTeam({ [event]: memArr }); setEventListModalShow(false); } else { - setSelectedTeam({ [event]: [{ intraId: getAuth().id, profile: getAuth().url, teamId: -1 }] }); + setSelectedTeam({ [event]: [{ intraId: getDecodedToken().id, profile: getDecodedToken().url, teamId: -1 }] }); setEventListModalShow(false); } }; diff --git a/src/components/Rotation/Calendar.tsx b/src/components/Rotation/Calendar.tsx index 82f3612..9f1c90b 100644 --- a/src/components/Rotation/Calendar.tsx +++ b/src/components/Rotation/Calendar.tsx @@ -5,7 +5,7 @@ import timeGridPlugin from '@fullcalendar/timegrid'; import interactionPlugin from '@fullcalendar/interaction'; import { createEventId, getRotationArr } from './event_utils'; import '@css/Rotation/Calendar.scss'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import { DAY_OF_SUNDAY } from './rotation_utils'; import apiClient from '@service/apiClient'; @@ -16,7 +16,7 @@ const COLOR = { export default class Calendar extends React.Component { state = { - auth: getAuth(), + auth: getDecodedToken(), weekendsVisible: true, currentEvents: [], }; diff --git a/src/components/Rotation/Rotation.tsx b/src/components/Rotation/Rotation.tsx index 1684d47..62423ad 100644 --- a/src/components/Rotation/Rotation.tsx +++ b/src/components/Rotation/Rotation.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, MouseEvent } from 'react'; import { useNavigate } from 'react-router-dom'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import Calendar, { CalendarTileProperties } from 'react-calendar'; import getAddress from '@globalObj/function/getAddress'; import { getToken } from '@cert/TokenStorage'; @@ -330,7 +330,7 @@ export const Rotate = () => { const navigate = useNavigate(); const currentDate = new Date(); const initialRecord = createInitialObject(currentDate); - const intraId = getAuth()?.id ?? null; + const intraId = getDecodedToken()?.id ?? null; const isRotationApplicationPeriod = calculateIsRotationApplicationPeriod(currentDate); const [record, setRecord] = useState(() => ({ ...initialRecord })); const [isSubmit, setIsSubmit] = useState(false); diff --git a/src/components/Rotation/RotationResult.tsx b/src/components/Rotation/RotationResult.tsx index 5c03fa8..e1dc0fd 100644 --- a/src/components/Rotation/RotationResult.tsx +++ b/src/components/Rotation/RotationResult.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import LoadingSpinner from './Loading'; import { RotateUserResult } from './RotateUserResult'; import { getRotationMonthArr } from './event_utils'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import '@css/Rotation/Rotation.scss'; export const RotateResult = () => { @@ -13,7 +13,7 @@ export const RotateResult = () => { date < new Date(year, nextmonth, -1) ? (month = nextmonth - 1) : nextmonth; const [Loading, setLoading] = useState(true); const [arr, setArr] = useState([]); - const intraId = getAuth() ? getAuth().id : null; + const intraId = getDecodedToken() ? getDecodedToken().id : null; const mainApi = async () => { setLoading(true); // api 호출 전에 true로 변경하여 로딩화면 띄우기 diff --git a/src/components/Rotation/event_utils.tsx b/src/components/Rotation/event_utils.tsx index 9203757..ccd5d1c 100644 --- a/src/components/Rotation/event_utils.tsx +++ b/src/components/Rotation/event_utils.tsx @@ -1,4 +1,4 @@ -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import apiClient from '@service/apiClient'; import errorAlert from '@globalObj/function/errorAlert'; @@ -9,7 +9,7 @@ export function createEventId() { } function rotatedArrAllInfo(data) { - const intraId = getAuth() ? getAuth().id : null; + const intraId = getDecodedToken() ? getDecodedToken().id : null; return data .filter((el) => !!el.year && !!el.month && !!el.day) .map((el) => ({ diff --git a/src/components/utils/Navbar.tsx b/src/components/utils/Navbar.tsx index d757580..5ae152a 100644 --- a/src/components/utils/Navbar.tsx +++ b/src/components/utils/Navbar.tsx @@ -4,7 +4,6 @@ import { useNavigate } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import GlobalLoginState from '@recoil/GlobalLoginState'; import { clearToken, getToken } from '@cert/TokenStorage'; -import { removeAuth } from '@cert/AuthStorage'; import { Dropdown } from './Dropdown'; import getAddress from '@globalObj/function/getAddress'; import apiClient from '@service/apiClient'; @@ -18,7 +17,6 @@ function Navbar() { apiClient .post('/auth/logout') .then((res) => { - removeAuth(); clearToken(); setLoginState(() => { return { diff --git a/src/globalObj/object/EmptyEvent.ts b/src/globalObj/object/EmptyEvent.ts index 2059be4..dc91ed5 100644 --- a/src/globalObj/object/EmptyEvent.ts +++ b/src/globalObj/object/EmptyEvent.ts @@ -1,4 +1,4 @@ -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; import { ReviewSelectedEventType } from './types'; function EmptyEvent(): ReviewSelectedEventType { @@ -9,7 +9,7 @@ function EmptyEvent(): ReviewSelectedEventType { createdId: 1, intraId: 'tkim', isMatching: 1, - teamList: { 사서: [{ intraId: getAuth().id, profile: getAuth().url, teamId: -1 }] }, + teamList: { 사서: [{ intraId: getDecodedToken().id, profile: getDecodedToken().url, teamId: -1 }] }, }; } diff --git a/src/recoil/GlobalLoginState.ts b/src/recoil/GlobalLoginState.ts index 43a8838..b61401e 100644 --- a/src/recoil/GlobalLoginState.ts +++ b/src/recoil/GlobalLoginState.ts @@ -1,13 +1,13 @@ import { atom } from 'recoil'; import { userData } from '@globalObj/object/types'; -import { getAuth } from '@cert/AuthStorage'; +import { getDecodedToken } from '@cert/TokenStorage'; -const value = getAuth() +const value = getDecodedToken() ? { isLogin: true, - isAdmin: getAuth()['id'] === 'tkim', - id: getAuth()['id'], - profileUrl: getAuth()['url'], + isAdmin: getDecodedToken()['id'] === 'tkim', + id: getDecodedToken()['id'], + profileUrl: getDecodedToken()['url'], } : { isLogin: false,