Select next available account on signOut
This commit is contained in:
parent
58a1ee929b
commit
c235ca3fe7
|
@ -89,7 +89,7 @@ function NotificationBell () {
|
||||||
function NavProfileMenu ({ me, dropNavKey }) {
|
function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const { resetMultiAuthPointer } = useAccounts()
|
const { multiAuthSignout } = useAccounts()
|
||||||
return (
|
return (
|
||||||
<div className='position-relative'>
|
<div className='position-relative'>
|
||||||
<Dropdown className={styles.dropdown} align='end'>
|
<Dropdown className={styles.dropdown} align='end'>
|
||||||
|
@ -131,6 +131,9 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item>
|
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
const status = await multiAuthSignout()
|
||||||
|
// only signout if multiAuth did not find a next available account
|
||||||
|
if (status === 201) return
|
||||||
try {
|
try {
|
||||||
// order is important because we need to be logged in to delete push subscription on server
|
// order is important because we need to be logged in to delete push subscription on server
|
||||||
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
const pushSubscription = await swRegistration?.pushManager.getSubscription()
|
||||||
|
@ -141,7 +144,6 @@ function NavProfileMenu ({ me, dropNavKey }) {
|
||||||
// don't prevent signout because of an unsubscription error
|
// don't prevent signout because of an unsubscription error
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
resetMultiAuthPointer()
|
|
||||||
await signOut({ callbackUrl: '/' })
|
await signOut({ callbackUrl: '/' })
|
||||||
}}
|
}}
|
||||||
>logout
|
>logout
|
||||||
|
|
|
@ -18,16 +18,22 @@ export const AccountProvider = ({ children }) => {
|
||||||
const [accounts, setAccounts] = useState([])
|
const [accounts, setAccounts] = useState([])
|
||||||
const [isAnon, setIsAnon] = useState(true)
|
const [isAnon, setIsAnon] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
const updateAccountsFromCookie = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
|
||||||
const accounts = multiAuthCookie
|
const accounts = multiAuthCookie
|
||||||
? JSON.parse(b64Decode(multiAuthCookie))
|
? JSON.parse(b64Decode(multiAuthCookie))
|
||||||
: me ? [{ id: me.id, name: me.name, photoId: me.photoId }] : []
|
: me ? [{ id: me.id, name: me.name, photoId: me.photoId }] : []
|
||||||
|
console.log(accounts)
|
||||||
|
if (multiAuthCookie) console.log(JSON.parse(b64Decode(multiAuthCookie)))
|
||||||
setAccounts(accounts)
|
setAccounts(accounts)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('error parsing cookies:', err)
|
console.error('error parsing cookies:', err)
|
||||||
}
|
}
|
||||||
|
}, [setAccounts])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateAccountsFromCookie()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const addAccount = useCallback(user => {
|
const addAccount = useCallback(user => {
|
||||||
|
@ -38,9 +44,14 @@ export const AccountProvider = ({ children }) => {
|
||||||
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
|
||||||
}, [setAccounts])
|
}, [setAccounts])
|
||||||
|
|
||||||
const resetMultiAuthPointer = useCallback(() => {
|
const multiAuthSignout = useCallback(async () => {
|
||||||
document.cookie = 'multi_auth.user-id='
|
// document.cookie = 'multi_auth.user-id='
|
||||||
}, [])
|
// switch to next available account
|
||||||
|
const { status } = await fetch('/api/signout', { credentials: 'include' })
|
||||||
|
console.log('multiAuthSignout rseponse', status)
|
||||||
|
if (status === 201) updateAccountsFromCookie()
|
||||||
|
return status
|
||||||
|
}, [updateAccountsFromCookie])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// document not defined on server
|
// document not defined on server
|
||||||
|
@ -49,7 +60,7 @@ export const AccountProvider = ({ children }) => {
|
||||||
setIsAnon(multiAuthUserIdCookie === 'anonymous')
|
setIsAnon(multiAuthUserIdCookie === 'anonymous')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount, isAnon, setIsAnon, resetMultiAuthPointer }}>{children}</AccountContext.Provider>
|
return <AccountContext.Provider value={{ accounts, addAccount, removeAccount, isAnon, setIsAnon, multiAuthSignout }}>{children}</AccountContext.Provider>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAccounts = () => useContext(AccountContext)
|
export const useAccounts = () => useContext(AccountContext)
|
||||||
|
@ -100,6 +111,7 @@ const Account = ({ account, className }) => {
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
width='135' height='135' src={src} style={{ cursor: 'pointer' }} onClick={async () => {
|
width='135' height='135' src={src} style={{ cursor: 'pointer' }} onClick={async () => {
|
||||||
|
console.log('switching to account', account.id)
|
||||||
document.cookie = `multi_auth.user-id=${account.id}; Path=/; Secure`
|
document.cookie = `multi_auth.user-id=${account.id}; Path=/; Secure`
|
||||||
await refreshMe()
|
await refreshMe()
|
||||||
// order is important to prevent flashes of inconsistent data in switch account dialog
|
// order is important to prevent flashes of inconsistent data in switch account dialog
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
import cookie from 'cookie'
|
||||||
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {NextApiRequest} req
|
||||||
|
* @param {NextApiResponse} res
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
|
export default (req, res) => {
|
||||||
|
// is there a cookie pointer?
|
||||||
|
const cookiePointerName = 'multi_auth.user-id'
|
||||||
|
const userId = req.cookies[cookiePointerName]
|
||||||
|
// is there a session?
|
||||||
|
const sessionCookieName = '__Secure-next-auth.session-token'
|
||||||
|
const sessionJWT = req.cookies[sessionCookieName]
|
||||||
|
|
||||||
|
if (!userId || !sessionJWT) {
|
||||||
|
// no cookie pointer or no session cookie present. do nothing.
|
||||||
|
res.status(404).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookies = []
|
||||||
|
|
||||||
|
const cookieOptions = {
|
||||||
|
path: '/',
|
||||||
|
secure: true,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
expires: datePivot(new Date(), { months: 1 })
|
||||||
|
}
|
||||||
|
// remove JWT pointed to by cookie pointer
|
||||||
|
cookies.push(cookie.serialize(`multi_auth.${userId}`, '', { ...cookieOptions, expires: 0, maxAge: 0 }))
|
||||||
|
|
||||||
|
// update multi_auth cookie
|
||||||
|
const oldMultiAuth = b64Decode(req.cookies.multi_auth)
|
||||||
|
const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
|
||||||
|
cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
|
||||||
|
|
||||||
|
// switch to next available account
|
||||||
|
if (!newMultiAuth.length) {
|
||||||
|
// no next account available
|
||||||
|
res.setHeader('Set-Cookie', cookies)
|
||||||
|
res.status(204).end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUserId = newMultiAuth[0].id
|
||||||
|
const newUserJWT = req.cookies[`multi_auth.${newUserId}`]
|
||||||
|
res.setHeader('Set-Cookie', [
|
||||||
|
...cookies,
|
||||||
|
cookie.serialize(cookiePointerName, newUserId, { ...cookieOptions, httpOnly: false }),
|
||||||
|
cookie.serialize(sessionCookieName, newUserJWT, cookieOptions)
|
||||||
|
])
|
||||||
|
|
||||||
|
res.status(201).end()
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
|
||||||
|
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
|
Loading…
Reference in New Issue