Skip to content

Commit

Permalink
Select next available account on signOut
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed Nov 22, 2023
1 parent 750d579 commit 9bbc93d
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 10 deletions.
10 changes: 5 additions & 5 deletions components/header.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function NotificationBell () {

function NavProfileMenu ({ me, dropNavKey }) {
const showModal = useShowModal()
const { resetMultiAuthPointer } = useAccounts()
const { multiAuthSignout } = useAccounts()
return (
<div className='position-relative'>
<Dropdown className={styles.dropdown} align='end'>
Expand Down Expand Up @@ -127,10 +127,10 @@ function NavProfileMenu ({ me, dropNavKey }) {
</div>
<Dropdown.Divider />
<Dropdown.Item onClick={() => showModal(onClose => <SwitchAccountDialog onClose={onClose} />)}>switch account</Dropdown.Item>
<Dropdown.Item onClick={() => {
// reset the multi auth cookie pointer to prevent user confusion on next login
resetMultiAuthPointer()
signOut({ callbackUrl: '/' })
<Dropdown.Item onClick={async () => {
const status = await multiAuthSignout()
// only signout if multiAuth did not find a next available account
if (status !== 201) signOut({ callbackUrl: '/' })
}}
>
logout
Expand Down
22 changes: 17 additions & 5 deletions components/switch-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
60 changes: 60 additions & 0 deletions pages/api/signout.js
Original file line number Diff line number Diff line change
@@ -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'))

0 comments on commit 9bbc93d

Please sign in to comment.