@@ -237,7 +237,7 @@ function SocialLink ({ name, id }) {
}
function HeaderHeader ({ user }) {
- const me = useMe()
+ const { me } = useMe()
const showModal = useShowModal()
const toaster = useToast()
diff --git a/components/user-list.js b/components/user-list.js
index a8091c42..ea452e74 100644
--- a/components/user-list.js
+++ b/components/user-list.js
@@ -12,6 +12,7 @@ import { useMe } from './me'
import { MEDIA_URL } from '@/lib/constants'
import { NymActionDropdown } from '@/components/user-header'
import classNames from 'classnames'
+import CheckCircle from '@/svgs/checkbox-circle-fill.svg'
// all of this nonsense is to show the stat we are sorting by first
const Stacked = ({ user }) => (user.optional.stacked !== null &&
)
@@ -40,6 +41,34 @@ function seperate (arr, seperator) {
return arr.flatMap((x, i) => i < arr.length - 1 ? [x, seperator] : [x])
}
+export function UserListRow ({ user, stats, className, onNymClick, showHat = true, selected }) {
+ return (
+
+ )
+}
+
export function UserBase ({ user, className, children, nymActionDropdown }) {
return (
@@ -63,7 +92,7 @@ export function UserBase ({ user, className, children, nymActionDropdown }) {
}
export function User ({ user, rank, statComps, className = 'mb-2', Embellish, nymActionDropdown = false }) {
- const me = useMe()
+ const { me } = useMe()
const showStatComps = statComps && statComps.length > 0
return (
<>
diff --git a/components/wallet-logger.js b/components/wallet-logger.js
index 087e4321..f2d0b761 100644
--- a/components/wallet-logger.js
+++ b/components/wallet-logger.js
@@ -112,7 +112,7 @@ const initIndexedDB = async (dbName, storeName) => {
}
export const WalletLoggerProvider = ({ children }) => {
- const me = useMe()
+ const { me } = useMe()
const [logs, setLogs] = useState([])
let dbName = 'app:storage'
if (me) {
diff --git a/fragments/users.js b/fragments/users.js
index 73cadf09..dfdb2df6 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -124,7 +124,7 @@ export const SETTINGS_FIELDS = gql`
export const SETTINGS = gql`
${SETTINGS_FIELDS}
-{
+query Settings {
settings {
...SettingsFields
}
@@ -320,8 +320,8 @@ export const USER_FULL = gql`
export const USER = gql`
${USER_FIELDS}
- query User($name: String!) {
- user(name: $name) {
+ query User($id: ID, $name: String) {
+ user(id: $id, name: $name) {
...UserFields
}
}`
diff --git a/lib/apollo.js b/lib/apollo.js
index a0fd488d..db0e6181 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -1,4 +1,4 @@
-import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
+import { ApolloClient, InMemoryCache, HttpLink, makeVar } from '@apollo/client'
import { decodeCursor, LIMIT } from './cursor'
import { SSR } from './constants'
@@ -25,6 +25,8 @@ export default function getApolloClient () {
}
}
+export const meAnonSats = {}
+
function getClient (uri) {
return new ApolloClient({
link: new HttpLink({ uri }),
@@ -259,10 +261,23 @@ function getClient (uri) {
Item: {
fields: {
meAnonSats: {
- read (meAnonSats, { readField }) {
- if (typeof window === 'undefined') return null
+ read (existingAmount, { readField }) {
+ if (SSR) return null
+
const itemId = readField('id')
- return meAnonSats ?? Number(window.localStorage.getItem(`TIP-item:${itemId}`) || '0')
+
+ // we need to use reactive variables such that updates
+ // to local state propagate correctly
+ // see https://www.apollographql.com/docs/react/local-state/reactive-variables
+ let reactiveVar = meAnonSats[itemId]
+ if (!reactiveVar) {
+ const storageKey = `TIP-item:${itemId}`
+ const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
+ reactiveVar = makeVar(existingAmount || 0)
+ meAnonSats[itemId] = reactiveVar
+ }
+
+ return reactiveVar()
}
}
}
diff --git a/package-lock.json b/package-lock.json
index 75070875..15d7900f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31,6 +31,7 @@
"canonical-json": "0.0.4",
"classnames": "^2.5.1",
"clipboard-copy": "^4.0.1",
+ "cookie": "^0.6.0",
"cross-fetch": "^4.0.0",
"csv-parser": "^3.0.0",
"domino": "^2.1.6",
@@ -530,6 +531,15 @@
}
}
},
+ "node_modules/@auth/core/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/@auth/prisma-adapter": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-1.0.3.tgz",
@@ -7267,9 +7277,10 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cookie": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
- "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -9006,6 +9017,15 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -14752,6 +14772,15 @@
}
}
},
+ "node_modules/next-auth/node_modules/cookie": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
+ "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next-plausible": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/next-plausible/-/next-plausible-3.11.1.tgz",
diff --git a/package.json b/package.json
index 3f483bcc..d438d82e 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"canonical-json": "0.0.4",
"classnames": "^2.5.1",
"clipboard-copy": "^4.0.1",
+ "cookie": "^0.6.0",
"cross-fetch": "^4.0.0",
"csv-parser": "^3.0.0",
"domino": "^2.1.6",
diff --git a/pages/[name]/index.js b/pages/[name]/index.js
index 08127027..08e6bfc6 100644
--- a/pages/[name]/index.js
+++ b/pages/[name]/index.js
@@ -86,7 +86,7 @@ export default function User ({ ssrData }) {
const [create, setCreate] = useState(false)
const [edit, setEdit] = useState(false)
const router = useRouter()
- const me = useMe()
+ const { me } = useMe()
const { data } = useQuery(USER_FULL, { variables: { ...router.query } })
if (!data && !ssrData) return
diff --git a/pages/_app.js b/pages/_app.js
index c018094b..1a1a4359 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -22,6 +22,7 @@ import { ChainFeeProvider } from '@/components/chain-fee.js'
import dynamic from 'next/dynamic'
import { HasNewNotesProvider } from '@/components/use-has-new-notes'
import WebLnProvider from '@/wallets/webln'
+import { AccountProvider } from '@/components/account'
const PWAPrompt = dynamic(() => import('react-ios-pwa-prompt'), { ssr: false })
@@ -109,22 +110,24 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
-
-
-
-
-
-
-
-
- {!router?.query?.disablePrompt && }
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ {!router?.query?.disablePrompt && }
+
+
+
+
+
+
+
+
diff --git a/pages/api/auth/[...nextauth].js b/pages/api/auth/[...nextauth].js
index ad067752..5c19a289 100644
--- a/pages/api/auth/[...nextauth].js
+++ b/pages/api/auth/[...nextauth].js
@@ -7,11 +7,13 @@ import EmailProvider from 'next-auth/providers/email'
import prisma from '@/api/models'
import nodemailer from 'nodemailer'
import { PrismaAdapter } from '@auth/prisma-adapter'
-import { getToken } from 'next-auth/jwt'
-import { NodeNextRequest } from 'next/dist/server/base-http/node'
+import { NodeNextRequest, NodeNextResponse } from 'next/dist/server/base-http/node'
+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 cookie from 'cookie'
/**
* Stores userIds in user table
@@ -53,7 +55,7 @@ async function getReferrerId (referrer) {
}
/** @returns {Partial
} */
-function getCallbacks (req) {
+function getCallbacks (req, res) {
return {
/**
* @param {object} token Decrypted JSON Web Token
@@ -88,6 +90,16 @@ function getCallbacks (req) {
token.sub = Number(token.id)
}
+ // response is only defined during signup/login
+ if (req && res) {
+ req = new NodeNextRequest(req)
+ res = new NodeNextResponse(res)
+ const secret = process.env.NEXTAUTH_SECRET
+ const jwt = await encodeJWT({ token, secret })
+ const me = await prisma.user.findUnique({ where: { id: token.id } })
+ setMultiAuthCookies(req, res, { ...me, jwt })
+ }
+
return token
},
async session ({ session, token }) {
@@ -100,23 +112,78 @@ function getCallbacks (req) {
}
}
-async function pubkeyAuth (credentials, req, pubkeyColumnName) {
+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 cookieOptions = {
+ path: '/',
+ httpOnly: true,
+ secure: req.secure,
+ sameSite: 'lax',
+ expires: expiresAt
+ }
+
+ // add JWT to **httpOnly** cookie
+ res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions))
+
+ 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 }))
+
+ // switch to user we just added
+ res.appendHeader('Set-Cookie', cookie.serialize('multi_auth.user-id', id, { ...cookieOptions, httpOnly: false }))
+}
+
+async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
const { k1, pubkey } = credentials
+
+ // are we trying to add a new account for switching between?
+ const { body } = req.body
+ const multiAuth = typeof body.multiAuth === 'string' ? body.multiAuth === 'true' : !!body.multiAuth
+
try {
+ // does the given challenge (k1) exist in our db?
const lnauth = await prisma.lnAuth.findUnique({ where: { k1 } })
+
+ // delete challenge to prevent replay attacks
await prisma.lnAuth.delete({ where: { k1 } })
+
+ // does the given pubkey match the one for which we verified the signature?
if (lnauth.pubkey === pubkey) {
+ // does the pubkey already exist in our db?
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
+
+ // get token if it exists
const token = await getToken({ req })
if (!user) {
- // if we are logged in, update rather than create
- if (token?.id) {
+ // we have not seen this pubkey before
+
+ // only update our pubkey if we're not currently trying to add a new account
+ if (token?.id && !multiAuth) {
user = await prisma.user.update({ where: { id: token.id }, data: { [pubkeyColumnName]: pubkey } })
} else {
+ // we're not logged in: create new user with that pubkey
user = await prisma.user.create({ data: { name: pubkey.slice(0, 10), [pubkeyColumnName]: pubkey } })
}
- } else if (token && token?.id !== user.id) {
- return null
+ }
+
+ if (token && token?.id !== user.id && multiAuth) {
+ // we're logged in as a different user than the one we're authenticating as
+ // and we want to add a new account. this means we want to add this account
+ // to our list of accounts for switching between so we issue a new JWT and
+ // update the cookies for multi-authentication.
+ const secret = process.env.NEXTAUTH_SECRET
+ const userJWT = await encodeJWT({ token: { id: user.id, name: user.name, email: user.email }, secret })
+ setMultiAuthCookies(req, res, { ...user, jwt: userJWT })
+ return token
}
return user
@@ -160,7 +227,7 @@ async function nostrEventAuth (event) {
}
/** @type {import('next-auth/providers').Provider[]} */
-const providers = [
+const getProviders = res => [
CredentialsProvider({
id: 'lightning',
name: 'Lightning',
@@ -168,7 +235,9 @@ const providers = [
pubkey: { label: 'publickey', type: 'text' },
k1: { label: 'k1', type: 'text' }
},
- authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
+ authorize: async (credentials, req) => {
+ return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'pubkey')
+ }
}),
CredentialsProvider({
id: 'nostr',
@@ -178,7 +247,7 @@ const providers = [
},
authorize: async ({ event }, req) => {
const credentials = await nostrEventAuth(event)
- return await pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
+ return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'nostrAuthPubkey')
}
}),
GitHubProvider({
@@ -213,9 +282,9 @@ const providers = [
]
/** @returns {import('next-auth').AuthOptions} */
-export const getAuthOptions = req => ({
- callbacks: getCallbacks(req),
- providers,
+export const getAuthOptions = (req, res) => ({
+ callbacks: getCallbacks(req, res),
+ providers: getProviders(res),
adapter: {
...PrismaAdapter(prisma),
createUser: data => {
@@ -299,7 +368,7 @@ async function enrollInNewsletter ({ email }) {
}
export default async (req, res) => {
- await NextAuth(req, res, getAuthOptions(req))
+ await NextAuth(req, res, getAuthOptions(req, res))
}
async function sendVerificationRequest ({
diff --git a/pages/api/graphql.js b/pages/api/graphql.js
index f9accedc..0e7ead75 100644
--- a/pages/api/graphql.js
+++ b/pages/api/graphql.js
@@ -66,6 +66,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
session = { user: { ...sessionFields, apiKey: true } }
}
} else {
+ req = multiAuthMiddleware(req)
session = await getServerSession(req, res, getAuthOptions(req))
}
return {
@@ -79,3 +80,41 @@ export default startServerAndCreateNextHandler(apolloServer, {
}
}
})
+
+function multiAuthMiddleware (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 sessionCookieName = request.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
+}
diff --git a/pages/api/signout.js b/pages/api/signout.js
new file mode 100644
index 00000000..3d8b56b5
--- /dev/null
+++ b/pages/api/signout.js
@@ -0,0 +1,61 @@
+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 = req.secure ? '__Secure-next-auth.session-token' : 'next-auth.session-token'
+ const sessionJWT = req.cookies[sessionCookieName]
+
+ if (!userId && !sessionJWT) {
+ // no cookie pointer and no session cookie present. nothing to do.
+ res.status(404).end()
+ return
+ }
+
+ const cookies = []
+
+ const cookieOptions = {
+ path: '/',
+ secure: req.secure,
+ 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 and check if there are more accounts available
+ const oldMultiAuth = b64Decode(req.cookies.multi_auth)
+ const newMultiAuth = oldMultiAuth.filter(({ id }) => id !== Number(userId))
+ if (newMultiAuth.length === 0) {
+ // no next account available. cleanup: remove multi_auth + pointer cookie
+ cookies.push(cookie.serialize('multi_auth', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
+ cookies.push(cookie.serialize('multi_auth.user-id', '', { ...cookieOptions, httpOnly: false, expires: 0, maxAge: 0 }))
+ res.setHeader('Set-Cookie', cookies)
+ res.status(204).end()
+ return
+ }
+ cookies.push(cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false }))
+
+ 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(302).end()
+}
+
+const b64Encode = obj => Buffer.from(JSON.stringify(obj)).toString('base64')
+const b64Decode = s => JSON.parse(Buffer.from(s, 'base64'))
diff --git a/pages/login.js b/pages/login.js
index cfe49952..e3e453c6 100644
--- a/pages/login.js
+++ b/pages/login.js
@@ -6,8 +6,15 @@ import { StaticLayout } from '@/components/layout'
import Login from '@/components/login'
import { isExternal } from '@/lib/url'
-export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
- const session = await getServerSession(req, res, getAuthOptions(req))
+export async function getServerSideProps ({ req, res, query: { callbackUrl, multiAuth = false, error = null } }) {
+ let session = await getServerSession(req, res, getAuthOptions(req))
+
+ // required to prevent infinite redirect loops if we switch to anon
+ // but are on a page that would redirect us to /signup.
+ // without this code, /signup would redirect us back to the callbackUrl.
+ if (req.cookies['multi_auth.user-id'] === 'anonymous') {
+ session = null
+ }
// prevent open redirects. See https://github.com/stackernews/stacker.news/issues/264
// let undefined urls through without redirect ... otherwise this interferes with multiple auth linking
@@ -22,9 +29,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
callbackUrl = '/'
}
- if (session && callbackUrl) {
- // in the cause of auth linking we want to pass the error back to
- // settings
+ if (session && callbackUrl && !multiAuth) {
+ // in the case of auth linking we want to pass the error back to settings
+ // in the case of multi auth, don't redirect if there is already a session
if (error) {
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
url.searchParams.set('error', error)
@@ -39,11 +46,14 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, erro
}
}
+ const providers = await getProviders()
+
return {
props: {
- providers: await getProviders(),
+ providers,
callbackUrl,
- error
+ error,
+ multiAuth
}
}
}
diff --git a/pages/referrals/[when].js b/pages/referrals/[when].js
index 0974d694..c434161f 100644
--- a/pages/referrals/[when].js
+++ b/pages/referrals/[when].js
@@ -33,7 +33,7 @@ export const getServerSideProps = getGetServerSideProps({ query: REFERRALS, auth
export default function Referrals ({ ssrData }) {
const router = useRouter()
- const me = useMe()
+ const { me } = useMe()
const select = async values => {
const { when, ...query } = values
diff --git a/pages/settings/index.js b/pages/settings/index.js
index 4f966618..effbbee8 100644
--- a/pages/settings/index.js
+++ b/pages/settings/index.js
@@ -84,7 +84,7 @@ export function SettingsHeader () {
export default function Settings ({ ssrData }) {
const toaster = useToast()
- const me = useMe()
+ const { me } = useMe()
const [setSettings] = useMutation(SET_SETTINGS, {
update (cache, { data: { setSettings } }) {
cache.modify({
@@ -96,13 +96,14 @@ export default function Settings ({ ssrData }) {
}
})
}
- }
- )
+ })
const logger = useServiceWorkerLogger()
const { data } = useQuery(SETTINGS)
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
- if (!data && !ssrData) return
+
+ // if we switched to anon, me is null before the page is reloaded
+ if ((!data && !ssrData) || !me) return
return (
@@ -110,6 +111,7 @@ export default function Settings ({ ssrData }) {
{hasOnlyOneAuthMethod(settings?.authMethods) && }