Reset multi_auth cookies on error (#1957)
* multi_auth cookies check + reset * multi_auth cookies refresh * Expire cookies after 30 days This is the actual default for next-auth.session-token. * Collapse issues by default * Only refresh session cookie manually as anon * fix mangled merge --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
		
							parent
							
								
									71caa6d0fe
								
							
						
					
					
						commit
						74d99e9b74
					
				@ -8,40 +8,31 @@ import { useQuery } from '@apollo/client'
 | 
			
		||||
import { UserListRow } from '@/components/user-list'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import AddIcon from '@/svgs/add-fill.svg'
 | 
			
		||||
import { MultiAuthErrorBanner } from '@/components/banners'
 | 
			
		||||
 | 
			
		||||
const AccountContext = createContext()
 | 
			
		||||
 | 
			
		||||
const CHECK_ERRORS_INTERVAL_MS = 5_000
 | 
			
		||||
 | 
			
		||||
const b64Decode = str => Buffer.from(str, 'base64').toString('utf-8')
 | 
			
		||||
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
 | 
			
		||||
 | 
			
		||||
const maybeSecureCookie = cookie => {
 | 
			
		||||
  return window.location.protocol === 'https:' ? cookie + '; Secure' : cookie
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AccountProvider = ({ children }) => {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
  const [accounts, setAccounts] = useState([])
 | 
			
		||||
  const [meAnon, setMeAnon] = useState(true)
 | 
			
		||||
  const [errors, setErrors] = useState([])
 | 
			
		||||
 | 
			
		||||
  const updateAccountsFromCookie = useCallback(() => {
 | 
			
		||||
    try {
 | 
			
		||||
      const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
 | 
			
		||||
      const accounts = multiAuthCookie
 | 
			
		||||
        ? JSON.parse(b64Decode(multiAuthCookie))
 | 
			
		||||
        : me ? [{ id: Number(me.id), name: me.name, photoId: me.photoId }] : []
 | 
			
		||||
      setAccounts(accounts)
 | 
			
		||||
      // required for backwards compatibility: sync cookie with accounts if no multi auth cookie exists
 | 
			
		||||
      // this is the case for sessions that existed before we deployed account switching
 | 
			
		||||
      if (!multiAuthCookie && !!me) {
 | 
			
		||||
        document.cookie = maybeSecureCookie(`multi_auth=${b64Encode(accounts)}; Path=/`)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.error('error parsing cookies:', err)
 | 
			
		||||
    }
 | 
			
		||||
    const { multi_auth: multiAuthCookie } = cookie.parse(document.cookie)
 | 
			
		||||
    const accounts = multiAuthCookie
 | 
			
		||||
      ? JSON.parse(b64Decode(multiAuthCookie))
 | 
			
		||||
      : []
 | 
			
		||||
    setAccounts(accounts)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(updateAccountsFromCookie, [])
 | 
			
		||||
 | 
			
		||||
  const addAccount = useCallback(user => {
 | 
			
		||||
    setAccounts(accounts => [...accounts, user])
 | 
			
		||||
  }, [])
 | 
			
		||||
@ -59,15 +50,43 @@ export const AccountProvider = ({ children }) => {
 | 
			
		||||
    return switchSuccess
 | 
			
		||||
  }, [updateAccountsFromCookie])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (SSR) return
 | 
			
		||||
    const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
 | 
			
		||||
    setMeAnon(multiAuthUserIdCookie === 'anonymous')
 | 
			
		||||
  const checkErrors = useCallback(() => {
 | 
			
		||||
    const {
 | 
			
		||||
      multi_auth: multiAuthCookie,
 | 
			
		||||
      'multi_auth.user-id': multiAuthUserIdCookie
 | 
			
		||||
    } = cookie.parse(document.cookie)
 | 
			
		||||
 | 
			
		||||
    const errors = []
 | 
			
		||||
 | 
			
		||||
    if (!multiAuthCookie) errors.push('multi_auth cookie not found')
 | 
			
		||||
    if (!multiAuthUserIdCookie) errors.push('multi_auth.user-id cookie not found')
 | 
			
		||||
 | 
			
		||||
    setErrors(errors)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (SSR) return
 | 
			
		||||
 | 
			
		||||
    updateAccountsFromCookie()
 | 
			
		||||
 | 
			
		||||
    const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
 | 
			
		||||
    setMeAnon(multiAuthUserIdCookie === 'anonymous')
 | 
			
		||||
 | 
			
		||||
    const interval = setInterval(checkErrors, CHECK_ERRORS_INTERVAL_MS)
 | 
			
		||||
    return () => clearInterval(interval)
 | 
			
		||||
  }, [updateAccountsFromCookie, checkErrors])
 | 
			
		||||
 | 
			
		||||
  const value = useMemo(
 | 
			
		||||
    () => ({ accounts, addAccount, removeAccount, meAnon, setMeAnon, nextAccount }),
 | 
			
		||||
    [accounts, addAccount, removeAccount, meAnon, setMeAnon, nextAccount])
 | 
			
		||||
    () => ({
 | 
			
		||||
      accounts,
 | 
			
		||||
      addAccount,
 | 
			
		||||
      removeAccount,
 | 
			
		||||
      meAnon,
 | 
			
		||||
      setMeAnon,
 | 
			
		||||
      nextAccount,
 | 
			
		||||
      multiAuthErrors: errors
 | 
			
		||||
    }),
 | 
			
		||||
    [accounts, addAccount, removeAccount, meAnon, setMeAnon, nextAccount, errors])
 | 
			
		||||
  return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -129,9 +148,23 @@ const AccountListRow = ({ account, ...props }) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function SwitchAccountList () {
 | 
			
		||||
  const { accounts } = useAccounts()
 | 
			
		||||
  const { accounts, multiAuthErrors } = useAccounts()
 | 
			
		||||
  const router = useRouter()
 | 
			
		||||
 | 
			
		||||
  const hasError = multiAuthErrors.length > 0
 | 
			
		||||
 | 
			
		||||
  if (hasError) {
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <div className='my-2'>
 | 
			
		||||
          <div className='d-flex flex-column flex-wrap mt-2 mb-3'>
 | 
			
		||||
            <MultiAuthErrorBanner errors={multiAuthErrors} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // can't show hat since the streak is not included in the JWT payload
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 | 
			
		||||
@ -6,6 +6,7 @@ import { useMutation } from '@apollo/client'
 | 
			
		||||
import { WELCOME_BANNER_MUTATION } from '@/fragments/users'
 | 
			
		||||
import { useToast } from '@/components/toast'
 | 
			
		||||
import Link from 'next/link'
 | 
			
		||||
import AccordianItem from '@/components/accordian-item'
 | 
			
		||||
 | 
			
		||||
export function WelcomeBanner ({ Banner }) {
 | 
			
		||||
  const { me } = useMe()
 | 
			
		||||
@ -123,3 +124,24 @@ export function AuthBanner () {
 | 
			
		||||
    </Alert>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function MultiAuthErrorBanner ({ errors }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Alert className={styles.banner} key='info' variant='danger'>
 | 
			
		||||
      <div className='fw-bold mb-3'>Account switching is currently unavailable</div>
 | 
			
		||||
      <AccordianItem
 | 
			
		||||
        className='my-3'
 | 
			
		||||
        header='We have detected the following issues:'
 | 
			
		||||
        headerColor='var(--bs-danger-text-emphasis)'
 | 
			
		||||
        body={
 | 
			
		||||
          <ul>
 | 
			
		||||
            {errors.map((err, i) => (
 | 
			
		||||
              <li key={i}>{err}</li>
 | 
			
		||||
            ))}
 | 
			
		||||
          </ul>
 | 
			
		||||
      }
 | 
			
		||||
      />
 | 
			
		||||
      <div className='mt-3'>To resolve these issues, please sign out and sign in again.</div>
 | 
			
		||||
    </Alert>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										161
									
								
								lib/auth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								lib/auth.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,161 @@
 | 
			
		||||
import { datePivot } from '@/lib/time'
 | 
			
		||||
import * as cookie from 'cookie'
 | 
			
		||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
 | 
			
		||||
import { encode as encodeJWT, decode as decodeJWT } from 'next-auth/jwt'
 | 
			
		||||
 | 
			
		||||
const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
 | 
			
		||||
const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
 | 
			
		||||
 | 
			
		||||
const userJwtRegexp = /^multi_auth\.\d+$/
 | 
			
		||||
 | 
			
		||||
const HTTPS = process.env.NODE_ENV === 'production'
 | 
			
		||||
const SESSION_COOKIE_NAME = HTTPS ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
 | 
			
		||||
 | 
			
		||||
const cookieOptions = (args) => ({
 | 
			
		||||
  path: '/',
 | 
			
		||||
  secure: process.env.NODE_ENV === 'production',
 | 
			
		||||
  // httpOnly cookies by default
 | 
			
		||||
  httpOnly: true,
 | 
			
		||||
  sameSite: 'lax',
 | 
			
		||||
  // default expiration for next-auth JWTs is in 30 days
 | 
			
		||||
  expires: datePivot(new Date(), { days: 30 }),
 | 
			
		||||
  ...args
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
 | 
			
		||||
  const httpOnlyOptions = cookieOptions()
 | 
			
		||||
  const jsOptions = { ...httpOnlyOptions, httpOnly: false }
 | 
			
		||||
 | 
			
		||||
  // add JWT to **httpOnly** cookie
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, httpOnlyOptions))
 | 
			
		||||
 | 
			
		||||
  // switch to user we just added
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, jsOptions))
 | 
			
		||||
 | 
			
		||||
  let newMultiAuth = [{ id, name, photoId }]
 | 
			
		||||
  if (req.cookies.multi_auth) {
 | 
			
		||||
    const oldMultiAuth = b64Decode(req.cookies.multi_auth)
 | 
			
		||||
    // make sure we don't add duplicates
 | 
			
		||||
    if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return
 | 
			
		||||
    newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
 | 
			
		||||
  }
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), jsOptions))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function switchSessionCookie (request) {
 | 
			
		||||
  // switch next-auth session cookie with multi_auth cookie if cookie pointer present
 | 
			
		||||
 | 
			
		||||
  // is there a cookie pointer?
 | 
			
		||||
  const cookiePointerName = 'multi_auth.user-id'
 | 
			
		||||
  const hasCookiePointer = !!request.cookies[cookiePointerName]
 | 
			
		||||
 | 
			
		||||
  // is there a session?
 | 
			
		||||
  const hasSession = !!request.cookies[SESSION_COOKIE_NAME]
 | 
			
		||||
 | 
			
		||||
  if (!hasCookiePointer || !hasSession) {
 | 
			
		||||
    // no session or no cookie pointer. do nothing.
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const userId = request.cookies[cookiePointerName]
 | 
			
		||||
  if (userId === 'anonymous') {
 | 
			
		||||
    // user switched to anon. only delete session cookie.
 | 
			
		||||
    delete request.cookies[SESSION_COOKIE_NAME]
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const userJWT = request.cookies[`multi_auth.${userId}`]
 | 
			
		||||
  if (!userJWT) {
 | 
			
		||||
    // no JWT for account switching found
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (userJWT) {
 | 
			
		||||
    // use JWT found in cookie pointed to by cookie pointer
 | 
			
		||||
    request.cookies[SESSION_COOKIE_NAME] = userJWT
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return request
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function checkMultiAuthCookies (req, res) {
 | 
			
		||||
  if (!req.cookies.multi_auth || !req.cookies['multi_auth.user-id']) {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const accounts = b64Decode(req.cookies.multi_auth)
 | 
			
		||||
  for (const account of accounts) {
 | 
			
		||||
    if (!req.cookies[`multi_auth.${account.id}`]) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function resetMultiAuthCookies (req, res) {
 | 
			
		||||
  const httpOnlyOptions = cookieOptions({ expires: 0, maxAge: 0 })
 | 
			
		||||
  const jsOptions = { ...httpOnlyOptions, httpOnly: false }
 | 
			
		||||
 | 
			
		||||
  if ('multi_auth' in req.cookies) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', '', jsOptions))
 | 
			
		||||
  if ('multi_auth.user-id' in req.cookies) res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', '', jsOptions))
 | 
			
		||||
 | 
			
		||||
  for (const key of Object.keys(req.cookies)) {
 | 
			
		||||
    // reset all user JWTs
 | 
			
		||||
    if (userJwtRegexp.test(key)) {
 | 
			
		||||
      res.appendHeader('Set-Cookie', cookie.serialize(key, '', httpOnlyOptions))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function refreshMultiAuthCookies (req, res) {
 | 
			
		||||
  const httpOnlyOptions = cookieOptions()
 | 
			
		||||
  const jsOptions = { ...httpOnlyOptions, httpOnly: false }
 | 
			
		||||
 | 
			
		||||
  const refreshCookie = (name) => {
 | 
			
		||||
    res.appendHeader('Set-Cookie', cookie.serialize(name, req.cookies[name], jsOptions))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const refreshToken = async (token) => {
 | 
			
		||||
    const secret = process.env.NEXTAUTH_SECRET
 | 
			
		||||
    return await encodeJWT({
 | 
			
		||||
      token: await decodeJWT({ token, secret }),
 | 
			
		||||
      secret
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isAnon = req.cookies['multi_auth.user-id'] === 'anonymous'
 | 
			
		||||
 | 
			
		||||
  for (const [key, value] of Object.entries(req.cookies)) {
 | 
			
		||||
    // only refresh session cookie manually if we switched to anon since else it's already handled by next-auth
 | 
			
		||||
    if (key === SESSION_COOKIE_NAME && !isAnon) continue
 | 
			
		||||
 | 
			
		||||
    if (!key.startsWith('multi_auth') && key !== SESSION_COOKIE_NAME) continue
 | 
			
		||||
 | 
			
		||||
    if (userJwtRegexp.test(key) || key === SESSION_COOKIE_NAME) {
 | 
			
		||||
      const oldToken = value
 | 
			
		||||
      const newToken = await refreshToken(oldToken)
 | 
			
		||||
      res.appendHeader('Set-Cookie', cookie.serialize(key, newToken, httpOnlyOptions))
 | 
			
		||||
      continue
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    refreshCookie(key)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function multiAuthMiddleware (req, res) {
 | 
			
		||||
  if (!req.cookies) {
 | 
			
		||||
    // required to properly access parsed cookies via req.cookies and not unparsed via req.headers.cookie
 | 
			
		||||
    req = new NodeNextRequest(req)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ok = checkMultiAuthCookies(req, res)
 | 
			
		||||
  if (!ok) {
 | 
			
		||||
    resetMultiAuthCookies(req, res)
 | 
			
		||||
    return switchSessionCookie(req)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  await refreshMultiAuthCookies(req, res)
 | 
			
		||||
  return switchSessionCookie(req)
 | 
			
		||||
}
 | 
			
		||||
@ -8,14 +8,13 @@ import prisma from '@/api/models'
 | 
			
		||||
import nodemailer from 'nodemailer'
 | 
			
		||||
import { PrismaAdapter } from '@auth/prisma-adapter'
 | 
			
		||||
import { getToken, encode as encodeJWT } from 'next-auth/jwt'
 | 
			
		||||
import { datePivot } from '@/lib/time'
 | 
			
		||||
import { schnorr } from '@noble/curves/secp256k1'
 | 
			
		||||
import { notifyReferral } from '@/lib/webPush'
 | 
			
		||||
import { hashEmail } from '@/lib/crypto'
 | 
			
		||||
import * as cookie from 'cookie'
 | 
			
		||||
import { multiAuthMiddleware } from '@/pages/api/graphql'
 | 
			
		||||
import { multiAuthMiddleware, setMultiAuthCookies } from '@/lib/auth'
 | 
			
		||||
import { BECH32_CHARSET } from '@/lib/constants'
 | 
			
		||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
 | 
			
		||||
import * as cookie from 'cookie'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Stores userIds in user table
 | 
			
		||||
@ -127,8 +126,8 @@ function getCallbacks (req, res) {
 | 
			
		||||
        token.sub = Number(token.id)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // add multi_auth cookie for user that just logged in
 | 
			
		||||
      if (user && req && res) {
 | 
			
		||||
        // add multi_auth cookie for user that just logged in
 | 
			
		||||
        const secret = process.env.NEXTAUTH_SECRET
 | 
			
		||||
        const jwt = await encodeJWT({ token, secret })
 | 
			
		||||
        const me = await prisma.user.findUnique({ where: { id: token.id } })
 | 
			
		||||
@ -147,37 +146,6 @@ function getCallbacks (req, res) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
 | 
			
		||||
  const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
 | 
			
		||||
  const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
 | 
			
		||||
 | 
			
		||||
  // default expiration for next-auth JWTs is in 1 month
 | 
			
		||||
  const expiresAt = datePivot(new Date(), { months: 1 })
 | 
			
		||||
  const secure = process.env.NODE_ENV === 'production'
 | 
			
		||||
  const cookieOptions = {
 | 
			
		||||
    path: '/',
 | 
			
		||||
    httpOnly: true,
 | 
			
		||||
    secure,
 | 
			
		||||
    sameSite: 'lax',
 | 
			
		||||
    expires: expiresAt
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // add JWT to **httpOnly** cookie
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
 | 
			
		||||
 | 
			
		||||
  // switch to user we just added
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, { ...cookieOptions, httpOnly: false }))
 | 
			
		||||
 | 
			
		||||
  let newMultiAuth = [{ id, name, photoId }]
 | 
			
		||||
  if (req.cookies.multi_auth) {
 | 
			
		||||
    const oldMultiAuth = b64Decode(req.cookies.multi_auth)
 | 
			
		||||
    // make sure we don't add duplicates
 | 
			
		||||
    if (oldMultiAuth.some(({ id: id_ }) => id_ === id)) return
 | 
			
		||||
    newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
 | 
			
		||||
  }
 | 
			
		||||
  res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
 | 
			
		||||
  const { k1, pubkey } = credentials
 | 
			
		||||
 | 
			
		||||
@ -197,7 +165,7 @@ async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
 | 
			
		||||
      let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
 | 
			
		||||
 | 
			
		||||
      // make following code aware of cookie pointer for account switching
 | 
			
		||||
      req = multiAuthMiddleware(req)
 | 
			
		||||
      req = await multiAuthMiddleware(req, res)
 | 
			
		||||
      // token will be undefined if we're not logged in at all or if we switched to anon
 | 
			
		||||
      const token = await getToken({ req })
 | 
			
		||||
      if (!user) {
 | 
			
		||||
@ -759,7 +727,7 @@ const newUserHtml = ({ url, token, site, email }) => {
 | 
			
		||||
                  <tbody>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                      <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">Stacker News is like Reddit or Hacker News, but it <b>pays you Bitcoin</b>. Instead of giving posts or comments “upvotes,” Stacker News users (aka stackers) send you small amounts of Bitcoin called sats.</div>
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">Stacker News is like Reddit or Hacker News, but it <b>pays you Bitcoin</b>. Instead of giving posts or comments "upvotes," Stacker News users (aka stackers) send you small amounts of Bitcoin called sats.</div>
 | 
			
		||||
                      </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
@ -774,7 +742,7 @@ const newUserHtml = ({ url, token, site, email }) => {
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                      <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">If you’re not sure what to share, <a href="${dailyUrl}"><b><i>click here to introduce yourself to the community</i></b></a> with a comment on the daily discussion thread.</div>
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">If you're not sure what to share, <a href="${dailyUrl}"><b><i>click here to introduce yourself to the community</i></b></a> with a comment on the daily discussion thread.</div>
 | 
			
		||||
                      </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
@ -784,7 +752,7 @@ const newUserHtml = ({ url, token, site, email }) => {
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
                      <td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">If anything isn’t clear, comment on the FAQ post and we’ll answer your question.</div>
 | 
			
		||||
                        <div style="font-family:Helvetica, Arial, sans-serif;font-size:14px;line-height:20px;text-align:left;color:#000000;">If anything isn't clear, comment on the FAQ post and we'll answer your question.</div>
 | 
			
		||||
                      </td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                    <tr>
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import {
 | 
			
		||||
  ApolloServerPluginLandingPageLocalDefault,
 | 
			
		||||
  ApolloServerPluginLandingPageProductionDefault
 | 
			
		||||
} from '@apollo/server/plugin/landingPage/default'
 | 
			
		||||
import { NodeNextRequest } from 'next/dist/server/base-http/node'
 | 
			
		||||
import { multiAuthMiddleware } from '@/lib/auth'
 | 
			
		||||
 | 
			
		||||
const apolloServer = new ApolloServer({
 | 
			
		||||
  typeDefs,
 | 
			
		||||
@ -68,7 +68,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
 | 
			
		||||
        session = { user: { ...sessionFields, apiKey: true } }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      req = multiAuthMiddleware(req)
 | 
			
		||||
      req = await multiAuthMiddleware(req, res)
 | 
			
		||||
      session = await getServerSession(req, res, getAuthOptions(req))
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
@ -82,49 +82,3 @@ export default startServerAndCreateNextHandler(apolloServer, {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export function multiAuthMiddleware (request) {
 | 
			
		||||
  // switch next-auth session cookie with multi_auth cookie if cookie pointer present
 | 
			
		||||
 | 
			
		||||
  if (!request.cookies) {
 | 
			
		||||
    // required to properly access parsed cookies via request.cookies
 | 
			
		||||
    // and not unparsed via request.headers.cookie
 | 
			
		||||
    request = new NodeNextRequest(request)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // is there a cookie pointer?
 | 
			
		||||
  const cookiePointerName = 'multi_auth.user-id'
 | 
			
		||||
  const hasCookiePointer = !!request.cookies[cookiePointerName]
 | 
			
		||||
 | 
			
		||||
  const secure = process.env.NODE_ENV === 'production'
 | 
			
		||||
 | 
			
		||||
  // is there a session?
 | 
			
		||||
  const sessionCookieName = secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
 | 
			
		||||
  const hasSession = !!request.cookies[sessionCookieName]
 | 
			
		||||
 | 
			
		||||
  if (!hasCookiePointer || !hasSession) {
 | 
			
		||||
    // no session or no cookie pointer. do nothing.
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const userId = request.cookies[cookiePointerName]
 | 
			
		||||
  if (userId === 'anonymous') {
 | 
			
		||||
    // user switched to anon. only delete session cookie.
 | 
			
		||||
    delete request.cookies[sessionCookieName]
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const userJWT = request.cookies[`multi_auth.${userId}`]
 | 
			
		||||
  if (!userJWT) {
 | 
			
		||||
    // no JWT for account switching found
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (userJWT) {
 | 
			
		||||
    // use JWT found in cookie pointed to by cookie pointer
 | 
			
		||||
    request.cookies[sessionCookieName] = userJWT
 | 
			
		||||
    return request
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return request
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user