dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

TokenContext.tsx (2749B)


      1 import { getAccessTokenCookieName } from '@/lib/env'
      2 import { AccessTokenPayload } from '@/lib/security/token'
      3 import { decode } from 'jsonwebtoken'
      4 import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'
      5 
      6 const regexCookie = /^([^=]*)=([^;]*)(?:;\s*|$)/
      7 
      8 const TOKEN_REFRESH_BEFORE = 60 * 1000 // 만료 1분 전 갱신
      9 
     10 const TokenContext = createContext<AccessTokenPayload | null>(null)
     11 const TokenFlushContext = createContext<() => void>(() => {
     12 })
     13 
     14 export function TokenProvider ({ children }: { children: ReactNode }) {
     15   const [accessToken, setAccessToken] = useState<AccessTokenPayload | null>(null)
     16   const [checkCounter, setCheckCounter] = useState(0)
     17 
     18   // 최초 로딩시 토큰 있는지 체크
     19   useEffect(() => {
     20     // 토큰 체크
     21     const cookie = getCookie(getAccessTokenCookieName())
     22     const accessToken = cookie != null
     23       ? decode(cookie) as AccessTokenPayload
     24       : null
     25 
     26     setAccessToken(accessToken ?? {})
     27   }, [checkCounter])
     28 
     29   // 토큰 갱신
     30   useEffect(() => {
     31     // 아직 토큰이 존재하는지 체크하지 않았으면 생략
     32     if (accessToken == null) {
     33       return
     34     }
     35 
     36     const refresh = () => {
     37       console.log('TokenContext: refresh token')
     38 
     39       fetch('/api/auth/refresh', {
     40         method: 'POST',
     41       }).then(res => {
     42         // 갱신하지 못했을 때에는 무시
     43         if (res.status !== 200) {
     44           return
     45         }
     46 
     47         // 토큰 갱신
     48         setCheckCounter(v => v + 1)
     49       }).catch(err => {
     50         console.error('TokenContext: fetch failed:', err)
     51       })
     52     }
     53 
     54     // 토큰이 없거나 만료되었을 때 갱신
     55     const exp = (accessToken.exp ?? 0) * 1000 - (Date.now() + TOKEN_REFRESH_BEFORE)
     56     if (exp <= 0) {
     57       refresh()
     58     } else {
     59       const timer = setTimeout(refresh, exp)
     60       return () => clearTimeout(timer)
     61     }
     62   }, [accessToken, accessToken?.exp])
     63 
     64   const flushTrigger = useCallback(
     65     () => setCheckCounter(v => v + 1),
     66     [],
     67   )
     68 
     69   return (
     70     <TokenContext.Provider value={accessToken}>
     71       <TokenFlushContext.Provider value={flushTrigger}>
     72         {children}
     73       </TokenFlushContext.Provider>
     74     </TokenContext.Provider>
     75   )
     76 }
     77 
     78 export function useToken () {
     79   const token = useContext(TokenContext)
     80   if (token?.uid == null) {
     81     return null
     82   }
     83   return token
     84 }
     85 
     86 export function useTokenFlushTrigger () {
     87   return useContext(TokenFlushContext)
     88 }
     89 
     90 function getCookie (name: string) {
     91   name = name.toLowerCase()
     92 
     93   let cookie = document.cookie
     94   for (; ;) {
     95     let caps = regexCookie.exec(cookie)
     96     if (caps == null) {
     97       return null
     98     }
     99     if (caps[1].toLowerCase() === name) {
    100       return caps[2]
    101     }
    102     cookie = cookie.substring(caps[0].length)
    103   }
    104 }