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 { useRouter } from 'next/router'
|
||||||
import * as cookie from 'cookie'
|
import { USER_ID } from '@/lib/constants'
|
||||||
import { USER_ID, SSR } from '@/lib/constants'
|
|
||||||
import { USER } from '@/fragments/users'
|
import { USER } from '@/fragments/users'
|
||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { UserListRow } from '@/components/user-list'
|
import { UserListRow } from '@/components/user-list'
|
||||||
|
import useCookie from '@/components/use-cookie'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import AddIcon from '@/svgs/add-fill.svg'
|
import AddIcon from '@/svgs/add-fill.svg'
|
||||||
import { cookieOptions, MULTI_AUTH_ANON, MULTI_AUTH_LIST, MULTI_AUTH_POINTER } from '@/lib/auth'
|
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')
|
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
|
||||||
|
|
||||||
export const AccountProvider = ({ children }) => {
|
export const nextAccount = async () => {
|
||||||
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' })
|
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
|
// 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.
|
return status === 302
|
||||||
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 default function SwitchAccountList () {
|
export default function SwitchAccountList () {
|
||||||
const { accounts } = useAccounts()
|
|
||||||
const router = useRouter()
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className='my-2'>
|
<div className='my-2'>
|
||||||
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
|
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
|
||||||
<h4 className='text-muted'>Accounts</h4>
|
<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>
|
</div>
|
||||||
<Link
|
<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 SnIcon from '@/svgs/sn.svg'
|
||||||
import { useHasNewNotes } from '../use-has-new-notes'
|
import { useHasNewNotes } from '../use-has-new-notes'
|
||||||
import { useWallets } from '@/wallets/index'
|
import { useWallets } from '@/wallets/index'
|
||||||
import SwitchAccountList, { useAccounts } from '@/components/account'
|
import SwitchAccountList, { nextAccount, useAccounts } from '@/components/account'
|
||||||
import { useShowModal } from '@/components/modal'
|
import { useShowModal } from '@/components/modal'
|
||||||
import { numWithUnits } from '@/lib/format'
|
import { numWithUnits } from '@/lib/format'
|
||||||
|
|
||||||
export function Brand ({ className }) {
|
export function Brand ({ className }) {
|
||||||
return (
|
return (
|
||||||
<Link href='/' passHref legacyBehavior>
|
<Link href='/' passHref legacyBehavior>
|
||||||
@ -273,7 +274,6 @@ export default function LoginButton () {
|
|||||||
function LogoutObstacle ({ onClose }) {
|
function LogoutObstacle ({ onClose }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const { removeLocalWallets } = useWallets()
|
const { removeLocalWallets } = useWallets()
|
||||||
const { nextAccount } = useAccounts()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -340,7 +340,7 @@ export function LogoutDropdownItem ({ handleClose }) {
|
|||||||
|
|
||||||
function SwitchAccountButton ({ handleClose }) {
|
function SwitchAccountButton ({ handleClose }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const { accounts } = useAccounts()
|
const accounts = useAccounts()
|
||||||
|
|
||||||
if (accounts.length === 0) return null
|
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 dynamic from 'next/dynamic'
|
||||||
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
|
||||||
import { WebLnProvider } from '@/wallets/webln/client'
|
import { WebLnProvider } from '@/wallets/webln/client'
|
||||||
import { AccountProvider } from '@/components/account'
|
|
||||||
import { WalletsProvider } from '@/wallets/index'
|
import { WalletsProvider } from '@/wallets/index'
|
||||||
|
|
||||||
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
|
||||||
@ -118,7 +117,6 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||||||
<LoggerProvider>
|
<LoggerProvider>
|
||||||
<WebLnProvider>
|
<WebLnProvider>
|
||||||
<ServiceWorkerProvider>
|
<ServiceWorkerProvider>
|
||||||
<AccountProvider>
|
|
||||||
<PriceProvider price={price}>
|
<PriceProvider price={price}>
|
||||||
<LightningProvider>
|
<LightningProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
@ -135,7 +133,6 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
|
|||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</LightningProvider>
|
</LightningProvider>
|
||||||
</PriceProvider>
|
</PriceProvider>
|
||||||
</AccountProvider>
|
|
||||||
</ServiceWorkerProvider>
|
</ServiceWorkerProvider>
|
||||||
</WebLnProvider>
|
</WebLnProvider>
|
||||||
</LoggerProvider>
|
</LoggerProvider>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user