diff --git a/components/header.js b/components/header.js index 4687e994..b3e81a92 100644 --- a/components/header.js +++ b/components/header.js @@ -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 (
@@ -131,6 +131,9 @@ function NavProfileMenu ({ me, dropNavKey }) { showModal(onClose => )}>switch account { + 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 diff --git a/components/switch-account.js b/components/switch-account.js index 989ae978..26e2d53d 100644 --- a/components/switch-account.js +++ b/components/switch-account.js @@ -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 {children} + return {children} } export const useAccounts = () => useContext(AccountContext) @@ -100,6 +111,7 @@ const Account = ({ account, className }) => { > { + 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 diff --git a/pages/api/signout.js b/pages/api/signout.js new file mode 100644 index 00000000..10301c32 --- /dev/null +++ b/pages/api/signout.js @@ -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'))