Select next available account on signOut

This commit is contained in:
ekzyis 2023-11-22 02:15:45 +01:00
parent 58a1ee929b
commit c235ca3fe7
3 changed files with 81 additions and 7 deletions

View File

@ -89,7 +89,7 @@ function NotificationBell () {
function NavProfileMenu ({ me, dropNavKey }) {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const showModal = useShowModal()
const { resetMultiAuthPointer } = useAccounts()
const { multiAuthSignout } = useAccounts()
return (
<div className='position-relative'>
<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={async () => {
const status = await multiAuthSignout()
// only signout if multiAuth did not find a next available account
if (status === 201) return
try {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
@ -141,7 +144,6 @@ function NavProfileMenu ({ me, dropNavKey }) {
// don't prevent signout because of an unsubscription error
console.error(err)
}
resetMultiAuthPointer()
await signOut({ callbackUrl: '/' })
}}
>logout

View File

@ -18,16 +18,22 @@ export const AccountProvider = ({ children }) => {
const [accounts, setAccounts] = useState([])
const [isAnon, setIsAnon] = useState(true)
useEffect(() => {
const updateAccountsFromCookie = useCallback(() => {
try {
const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
const accounts = multiAuthCookie
? JSON.parse(b64Decode(multiAuthCookie))
: me ? [{ id: me.id, name: me.name, photoId: me.photoId }] : []
console.log(accounts)
if (multiAuthCookie) console.log(JSON.parse(b64Decode(multiAuthCookie)))
setAccounts(accounts)
} catch (err) {
console.error('error parsing cookies:', err)
}
}, [setAccounts])
useEffect(() => {
updateAccountsFromCookie()
}, [])
const addAccount = useCallback(user => {
@ -38,9 +44,14 @@ export const AccountProvider = ({ children }) => {
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
}, [setAccounts])
const resetMultiAuthPointer = useCallback(() => {
document.cookie = 'multi_auth.user-id='
}, [])
const multiAuthSignout = useCallback(async () => {
// 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(() => {
// document not defined on server
@ -49,7 +60,7 @@ export const AccountProvider = ({ children }) => {
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)
@ -100,6 +111,7 @@ const Account = ({ account, className }) => {
>
<Image
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`
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog

60
pages/api/signout.js Normal file
View File

@ -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'))