Refactor multi auth with useCookie (#2019)
This commit is contained in:
		
							parent
							
								
									501bf1609b
								
							
						
					
					
						commit
						895efd0181
					
				@ -1,114 +1,44 @@
 | 
			
		||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
 | 
			
		||||
import { useRouter } from 'next/router'
 | 
			
		||||
import * as cookie from 'cookie'
 | 
			
		||||
import { USER_ID, SSR } from '@/lib/constants'
 | 
			
		||||
import { USER_ID } from '@/lib/constants'
 | 
			
		||||
import { USER } from '@/fragments/users'
 | 
			
		||||
import { useQuery } from '@apollo/client'
 | 
			
		||||
import { UserListRow } from '@/components/user-list'
 | 
			
		||||
import useCookie from '@/components/use-cookie'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import AddIcon from '@/svgs/add-fill.svg'
 | 
			
		||||
import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth'
 | 
			
		||||
 | 
			
		||||
const AccountContext = createContext()
 | 
			
		||||
 | 
			
		||||
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
 | 
			
		||||
 | 
			
		||||
export const AccountProvider = ({ children }) => {
 | 
			
		||||
  const [accounts, setAccounts] = useState([])
 | 
			
		||||
  const [selected, setSelected] = useState(null)
 | 
			
		||||
 | 
			
		||||
  const updateAccountsFromCookie = useCallback(() => {
 | 
			
		||||
    const { [MULTI_AUTH_LIST]: listCookie } = cookie.parse(document.cookie)
 | 
			
		||||
    const accounts = listCookie
 | 
			
		||||
      ? JSON.parse(b64Decode(listCookie))
 | 
			
		||||
      : []
 | 
			
		||||
    setAccounts(accounts)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const nextAccount = useCallback(async () => {
 | 
			
		||||
    const { status } = await fetch('/api/next-account', { credentials: 'include' })
 | 
			
		||||
    // if status is 302, this means the server was able to switch us to the next available account
 | 
			
		||||
    // and the current account was simply removed from the list of available accounts including the corresponding JWT.
 | 
			
		||||
    const switchSuccess = status === 302
 | 
			
		||||
    if (switchSuccess) updateAccountsFromCookie()
 | 
			
		||||
    return switchSuccess
 | 
			
		||||
  }, [updateAccountsFromCookie])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (SSR) return
 | 
			
		||||
 | 
			
		||||
    updateAccountsFromCookie()
 | 
			
		||||
 | 
			
		||||
    const { [MULTI_AUTH_POINTER]: pointerCookie } = cookie.parse(document.cookie)
 | 
			
		||||
    setSelected(pointerCookie === MULTI_AUTH_ANON ? USER_ID.anon : Number(pointerCookie))
 | 
			
		||||
  }, [updateAccountsFromCookie])
 | 
			
		||||
 | 
			
		||||
  const value = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
      accounts,
 | 
			
		||||
      selected,
 | 
			
		||||
      nextAccount
 | 
			
		||||
    }),
 | 
			
		||||
    [accounts, selected, nextAccount])
 | 
			
		||||
  return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useAccounts = () => useContext(AccountContext)
 | 
			
		||||
 | 
			
		||||
const AccountListRow = ({ account, ...props }) => {
 | 
			
		||||
  const { selected } = useAccounts()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  // fetch updated names and photo ids since they might have changed since we were issued the JWTs
 | 
			
		||||
  const { data, error } = useQuery(USER,
 | 
			
		||||
    {
 | 
			
		||||
      variables: { id: account.id }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
  if (error) console.error(`query for user ${account.id} failed:`, error)
 | 
			
		||||
 | 
			
		||||
  const name = data?.user?.name || account.name
 | 
			
		||||
  const photoId = data?.user?.photoId || account.photoId
 | 
			
		||||
 | 
			
		||||
  const onClick = async (e) => {
 | 
			
		||||
    // prevent navigation
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    // update pointer cookie
 | 
			
		||||
    const options = cookieOptions({ httpOnly: false })
 | 
			
		||||
    const anon = account.id === USER_ID.anon
 | 
			
		||||
    document.cookie = cookie.serialize(MULTI_AUTH_POINTER, anon ? MULTI_AUTH_ANON : account.id, options)
 | 
			
		||||
 | 
			
		||||
    // reload whatever page we're on to avoid any bugs due to missing authorization etc.
 | 
			
		||||
    router.reload()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='d-flex flex-row'>
 | 
			
		||||
      <UserListRow
 | 
			
		||||
        user={{ ...account, photoId, name }}
 | 
			
		||||
        className='d-flex align-items-center me-2'
 | 
			
		||||
        {...props}
 | 
			
		||||
        onNymClick={onClick}
 | 
			
		||||
        selected={selected === account.id}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
export const nextAccount = async () => {
 | 
			
		||||
  const { status } = await fetch('/api/next-account', { credentials: 'include' })
 | 
			
		||||
  // if status is 302, this means the server was able to switch us to the next available account
 | 
			
		||||
  return status === 302
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function SwitchAccountList () {
 | 
			
		||||
  const { accounts } = useAccounts()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const accounts = useAccounts()
 | 
			
		||||
  const [pointerCookie] = useCookie(MULTI_AUTH_POINTER)
 | 
			
		||||
 | 
			
		||||
  // can't show hat since the streak is not included in the JWT payload
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <div className='my-2'>
 | 
			
		||||
        <div className='d-flex flex-column flex-wrap mt-2 mb-3'>
 | 
			
		||||
          <h4 className='text-muted'>Accounts</h4>
 | 
			
		||||
          <AccountListRow account={{ id: USER_ID.anon, name: 'anon' }} showHat={false} />
 | 
			
		||||
          <AccountListRow
 | 
			
		||||
            account={{ id: USER_ID.anon, name: 'anon' }}
 | 
			
		||||
            selected={pointerCookie === MULTI_AUTH_ANON}
 | 
			
		||||
            showHat={false}
 | 
			
		||||
          />
 | 
			
		||||
          {
 | 
			
		||||
            accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
 | 
			
		||||
            accounts.map((account) =>
 | 
			
		||||
              <AccountListRow
 | 
			
		||||
                key={account.id}
 | 
			
		||||
                account={account}
 | 
			
		||||
                selected={Number(pointerCookie) === account.id}
 | 
			
		||||
                showHat={false}
 | 
			
		||||
              />)
 | 
			
		||||
          }
 | 
			
		||||
        </div>
 | 
			
		||||
        <Link
 | 
			
		||||
@ -124,3 +54,45 @@ export default function SwitchAccountList () {
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const AccountListRow = ({ account, selected, ...props }) => {
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
  const [, setPointerCookie] = useCookie(MULTI_AUTH_POINTER)
 | 
			
		||||
 | 
			
		||||
  // fetch updated names and photo ids since they might have changed since we were issued the JWTs
 | 
			
		||||
  const { data, error } = useQuery(USER, { variables: { id: account.id } })
 | 
			
		||||
  if (error) console.error(`query for user ${account.id} failed:`, error)
 | 
			
		||||
 | 
			
		||||
  const name = data?.user?.name || account.name
 | 
			
		||||
  const photoId = data?.user?.photoId || account.photoId
 | 
			
		||||
 | 
			
		||||
  const onClick = async (e) => {
 | 
			
		||||
    // prevent navigation
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
 | 
			
		||||
    // update pointer cookie
 | 
			
		||||
    const options = cookieOptions({ httpOnly: false })
 | 
			
		||||
    const anon = account.id === USER_ID.anon
 | 
			
		||||
    setPointerCookie(anon ? MULTI_AUTH_ANON : account.id, options)
 | 
			
		||||
 | 
			
		||||
    // reload whatever page we're on to avoid any bugs due to missing authorization etc.
 | 
			
		||||
    router.reload()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='d-flex flex-row'>
 | 
			
		||||
      <UserListRow
 | 
			
		||||
        user={{ ...account, photoId, name }}
 | 
			
		||||
        className='d-flex align-items-center me-2'
 | 
			
		||||
        selected={selected}
 | 
			
		||||
        {...props}
 | 
			
		||||
        onNymClick={onClick}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useAccounts = () => {
 | 
			
		||||
  const [listCookie] = useCookie(MULTI_AUTH_LIST)
 | 
			
		||||
  return listCookie ? JSON.parse(b64Decode(listCookie)) : []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -22,9 +22,10 @@ import classNames from 'classnames'
 | 
			
		||||
import SnIcon from '@/svgs/sn.svg'
 | 
			
		||||
import { useHasNewNotes } from '../use-has-new-notes'
 | 
			
		||||
import { useWallets } from '@/wallets/index'
 | 
			
		||||
import SwitchAccountList, { useAccounts } from '@/components/account'
 | 
			
		||||
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
 | 
			
		||||
import { useShowModal } from '@/components/modal'
 | 
			
		||||
import { numWithUnits } from '@/lib/format'
 | 
			
		||||
 | 
			
		||||
export function Brand ({ className }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Link href='/' passHref legacyBehavior>
 | 
			
		||||
@ -273,7 +274,6 @@ export default function LoginButton () {
 | 
			
		||||
function LogoutObstacle ({ onClose }) {
 | 
			
		||||
  const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
 | 
			
		||||
  const { removeLocalWallets } = useWallets()
 | 
			
		||||
  const { nextAccount } = useAccounts()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -340,7 +340,7 @@ export function LogoutDropdownItem ({ handleClose }) {
 | 
			
		||||
 | 
			
		||||
function SwitchAccountButton ({ handleClose }) {
 | 
			
		||||
  const showModal = useShowModal()
 | 
			
		||||
  const { accounts } = useAccounts()
 | 
			
		||||
  const accounts = useAccounts()
 | 
			
		||||
 | 
			
		||||
  if (accounts.length === 0) return null
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								components/use-cookie.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								components/use-cookie.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
import { useCallback, useEffect, useState } from 'react'
 | 
			
		||||
import * as cookie from 'cookie'
 | 
			
		||||
import { cookieOptions } from '@/lib/auth'
 | 
			
		||||
 | 
			
		||||
export default function useCookie (name) {
 | 
			
		||||
  const [value, setValue] = useState(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const checkCookie = () => {
 | 
			
		||||
      const oldValue = value
 | 
			
		||||
      const newValue = cookie.parse(document.cookie)[name]
 | 
			
		||||
      if (oldValue !== newValue) setValue(newValue)
 | 
			
		||||
    }
 | 
			
		||||
    checkCookie()
 | 
			
		||||
    // there's no way to listen for cookie changes that is supported by all browsers
 | 
			
		||||
    // so we poll to detect changes
 | 
			
		||||
    // see https://developer.mozilla.org/en-US/docs/Web/API/Cookie_Store_API
 | 
			
		||||
    const interval = setInterval(checkCookie, 1000)
 | 
			
		||||
    return () => clearInterval(interval)
 | 
			
		||||
  }, [value])
 | 
			
		||||
 | 
			
		||||
  const set = useCallback((value, options = {}) => {
 | 
			
		||||
    document.cookie = cookie.serialize(name, value, { ...cookieOptions(), ...options })
 | 
			
		||||
    setValue(value)
 | 
			
		||||
  }, [name])
 | 
			
		||||
 | 
			
		||||
  const remove = useCallback(() => {
 | 
			
		||||
    document.cookie = value.serialize(name, '', { expires: 0, maxAge: 0 })
 | 
			
		||||
    setValue(null)
 | 
			
		||||
  }, [name])
 | 
			
		||||
 | 
			
		||||
  return [value, set, remove]
 | 
			
		||||
}
 | 
			
		||||
@ -21,7 +21,6 @@ import { ChainFeeProvider } from '@/components/chain-fee.js'
 | 
			
		||||
import dynamic from 'next/dynamic'
 | 
			
		||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
 | 
			
		||||
import { WebLnProvider } from '@/wallets/webln/client'
 | 
			
		||||
import { AccountProvider } from '@/components/account'
 | 
			
		||||
import { WalletsProvider } from '@/wallets/index'
 | 
			
		||||
 | 
			
		||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
 | 
			
		||||
@ -118,24 +117,22 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
 | 
			
		||||
                  <LoggerProvider>
 | 
			
		||||
                    <WebLnProvider>
 | 
			
		||||
                      <ServiceWorkerProvider>
 | 
			
		||||
                        <AccountProvider>
 | 
			
		||||
                          <PriceProvider price={price}>
 | 
			
		||||
                            <LightningProvider>
 | 
			
		||||
                              <ToastProvider>
 | 
			
		||||
                                <ShowModalProvider>
 | 
			
		||||
                                  <BlockHeightProvider blockHeight={blockHeight}>
 | 
			
		||||
                                    <ChainFeeProvider chainFee={chainFee}>
 | 
			
		||||
                                      <ErrorBoundary>
 | 
			
		||||
                                        <Component ssrData={ssrData} {...otherProps} />
 | 
			
		||||
                                        {!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
 | 
			
		||||
                                      </ErrorBoundary>
 | 
			
		||||
                                    </ChainFeeProvider>
 | 
			
		||||
                                  </BlockHeightProvider>
 | 
			
		||||
                                </ShowModalProvider>
 | 
			
		||||
                              </ToastProvider>
 | 
			
		||||
                            </LightningProvider>
 | 
			
		||||
                          </PriceProvider>
 | 
			
		||||
                        </AccountProvider>
 | 
			
		||||
                        <PriceProvider price={price}>
 | 
			
		||||
                          <LightningProvider>
 | 
			
		||||
                            <ToastProvider>
 | 
			
		||||
                              <ShowModalProvider>
 | 
			
		||||
                                <BlockHeightProvider blockHeight={blockHeight}>
 | 
			
		||||
                                  <ChainFeeProvider chainFee={chainFee}>
 | 
			
		||||
                                    <ErrorBoundary>
 | 
			
		||||
                                      <Component ssrData={ssrData} {...otherProps} />
 | 
			
		||||
                                      {!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
 | 
			
		||||
                                    </ErrorBoundary>
 | 
			
		||||
                                  </ChainFeeProvider>
 | 
			
		||||
                                </BlockHeightProvider>
 | 
			
		||||
                              </ShowModalProvider>
 | 
			
		||||
                            </ToastProvider>
 | 
			
		||||
                          </LightningProvider>
 | 
			
		||||
                        </PriceProvider>
 | 
			
		||||
                      </ServiceWorkerProvider>
 | 
			
		||||
                    </WebLnProvider>
 | 
			
		||||
                  </LoggerProvider>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user