Compare commits

..

No commits in common. "b3d9eb0eba218b0534ae35ae725bd6ff63e2f4c5" and "36e9f3f16fd3ef0ea7edf32fe84ab252f7f53020" have entirely different histories.

66 changed files with 197 additions and 743 deletions

View File

@ -130,9 +130,8 @@ export default {
return await models.user.findUnique({ where: { id: me.id } })
},
user: async (parent, { id, name }, { models }) => {
if (id) id = Number(id)
return await models.user.findUnique({ where: { id, name } })
user: async (parent, { name }, { models }) => {
return await models.user.findUnique({ where: { name } })
},
users: async (parent, args, { models }) =>
await models.user.findMany(),

View File

@ -139,13 +139,7 @@ export function getGetServerSideProps (
const client = await getSSRApolloClient({ req, res })
let { data: { me } } = await client.query({ query: ME })
// required to redirect to /signup on page reload
// if we switched to anon and authentication is required
if (req.cookies['multi_auth.user-id'] === 'anonymous') {
me = null
}
const { data: { me } } = await client.query({ query: ME })
if (authRequired && !me) {
let callback = process.env.NEXT_PUBLIC_URL + req.url

View File

@ -4,7 +4,7 @@ export default gql`
extend type Query {
me: User
settings: User
user(id: ID, name: String): User
user(name: String!): User
users: [User!]
nameAvailable(name: String!): Boolean!
topUsers(cursor: String, when: String, from: String, to: String, by: String, limit: Limit): UsersNullable!

View File

@ -1,158 +0,0 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/router'
import cookie from 'cookie'
import { useMe } from '@/components/me'
import { USER_ID, SSR } from '@/lib/constants'
import { USER } from '@/fragments/users'
import { useQuery } from '@apollo/client'
import { UserListRow } from '@/components/user-list'
import Link from 'next/link'
import AddIcon from '@/svgs/add-fill.svg'
const AccountContext = createContext()
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 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)
}
}, [])
useEffect(updateAccountsFromCookie, [])
const addAccount = useCallback(user => {
setAccounts(accounts => [...accounts, user])
}, [])
const removeAccount = useCallback(userId => {
setAccounts(accounts => accounts.filter(({ id }) => id !== userId))
}, [])
const multiAuthSignout = useCallback(async () => {
const { status } = await fetch('/api/signout', { credentials: 'include' })
// if status is 302, this means the server was able to switch us to the next available account
// and the current account was simply removed from the list of available accounts including the corresponding JWT.
const switchSuccess = status === 302
if (switchSuccess) updateAccountsFromCookie()
return switchSuccess
}, [updateAccountsFromCookie])
useEffect(() => {
if (SSR) return
const { 'multi_auth.user-id': multiAuthUserIdCookie } = cookie.parse(document.cookie)
setMeAnon(multiAuthUserIdCookie === 'anonymous')
}, [])
const value = useMemo(
() => ({ accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout }),
[accounts, addAccount, removeAccount, meAnon, setMeAnon, multiAuthSignout])
return <AccountContext.Provider value={value}>{children}</AccountContext.Provider>
}
export const useAccounts = () => useContext(AccountContext)
const AccountListRow = ({ account, ...props }) => {
const { meAnon, setMeAnon } = useAccounts()
const { me, refreshMe } = useMe()
const anonRow = account.id === USER_ID.anon
const selected = (meAnon && anonRow) || Number(me?.id) === Number(account.id)
const router = useRouter()
// fetch updated names and photo ids since they might have changed since we were issued the JWTs
const [name, setName] = useState(account.name)
const [photoId, setPhotoId] = useState(account.photoId)
useQuery(USER,
{
variables: { id: account.id },
onCompleted ({ user: { name, photoId } }) {
if (photoId) setPhotoId(photoId)
if (name) setName(name)
}
}
)
const onClick = async (e) => {
// prevent navigation
e.preventDefault()
// update pointer cookie
document.cookie = maybeSecureCookie(`multi_auth.user-id=${anonRow ? 'anonymous' : account.id}; Path=/`)
// update state
if (anonRow) {
// order is important to prevent flashes of no session
setMeAnon(true)
await refreshMe()
} else {
await refreshMe()
// order is important to prevent flashes of inconsistent data in switch account dialog
setMeAnon(account.id === USER_ID.anon)
}
// reload whatever page we're on to avoid any bugs due to missing authorization etc.
router.reload()
}
return (
<div className='d-flex flex-row'>
<UserListRow
user={{ ...account, photoId, name }}
className='d-flex align-items-center me-2'
{...props}
onNymClick={onClick}
selected={selected}
/>
</div>
)
}
export default function SwitchAccountList () {
const { accounts } = useAccounts()
const router = useRouter()
// can't show hat since the streak is not included in the JWT payload
return (
<>
<div className='my-2'>
<div className='d-flex flex-column flex-wrap mt-2 mb-3'>
<h4 className='text-muted'>Accounts</h4>
<AccountListRow account={{ id: USER_ID.anon, name: 'anon' }} showHat={false} />
{
accounts.map((account) => <AccountListRow key={account.id} account={account} showHat={false} />)
}
</div>
<Link
href={{
pathname: '/login',
query: { callbackUrl: window.location.origin + router.asPath, multiAuth: true }
}}
className='text-reset fw-bold'
>
<AddIcon height={20} width={20} /> another account
</Link>
</div>
</>
)
}

View File

@ -27,7 +27,7 @@ const FormStatus = {
}
export default function AdvPostForm ({ children, item, storageKeyPrefix }) {
const { me } = useMe()
const me = useMe()
const { merge } = useFeeButton()
const router = useRouter()
const [itemType, setItemType] = useState()

View File

@ -17,7 +17,7 @@ export function autowithdrawInitial ({ me }) {
}
export function AutowithdrawSettings ({ wallet }) {
const { me } = useMe()
const me = useMe()
const threshold = autoWithdrawThreshold({ me })
const [sendThreshold, setSendThreshold] = useState(Math.max(Math.floor(threshold / 10), 1))

View File

@ -9,7 +9,7 @@ import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
import { msatsToSats, numWithUnits } from '@/lib/format'
export function WelcomeBanner ({ Banner }) {
const { me } = useMe()
const me = useMe()
const toaster = useToast()
const [hidden, setHidden] = useState(true)
const handleClose = async () => {
@ -70,7 +70,7 @@ export function WelcomeBanner ({ Banner }) {
}
export function MadnessBanner ({ handleClose }) {
const { me } = useMe()
const me = useMe()
return (
<Alert className={styles.banner} key='info' variant='info' onClose={handleClose} dismissible>
<Alert.Heading>
@ -102,7 +102,7 @@ export function MadnessBanner ({ handleClose }) {
}
export function WalletLimitBanner () {
const { me } = useMe()
const me = useMe()
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
if (!me || !limitReached) return

View File

@ -23,7 +23,7 @@ export function BountyForm ({
children
}) {
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const schema = bountySchema({ client, me, existingBoost: item?.boost })
const onSubmit = useItemSubmit(UPSERT_BOUNTY, { item, sub })

View File

@ -96,7 +96,7 @@ export default function Comment ({
rootText, noComments, noReply, truncate, depth, pin
}) {
const [edit, setEdit] = useState()
const { me } = useMe()
const me = useMe()
const isHiddenFreebie = me?.privates?.satsFilter !== 0 && !item.mine && item.freebie && !item.freedFreebie
const [collapse, setCollapse] = useState(
(isHiddenFreebie || item?.user?.meMute || (item?.outlawed && !me?.privates?.wildWestMode)) && !includeParent

View File

@ -22,7 +22,7 @@ export function DiscussionForm ({
}) {
const router = useRouter()
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const onSubmit = useItemSubmit(UPSERT_DISCUSSION, { item, sub })
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
// if Web Share Target API was used

View File

@ -64,7 +64,7 @@ export function postCommentUseRemoteLineItems ({ parentId } = {}) {
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
const [lineItems, setLineItems] = useState({})
const [disabled, setDisabled] = useState(false)
const { me } = useMe()
const me = useMe()
const remoteLineItems = useRemoteLineItems()
@ -115,7 +115,7 @@ function FreebieDialog () {
}
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
const { me } = useMe()
const me = useMe()
const { lines, total, disabled: ctxDisabled, free } = useFeeButton()
const feeText = free
? 'free'

View File

@ -808,7 +808,7 @@ export function Form ({
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
const { me } = useMe()
const me = useMe()
useEffect(() => {
if (initialError && !initialErrorToasted.current) {

View File

@ -3,7 +3,7 @@ import { abbrNum, numWithUnits } from '@/lib/format'
import { useMe } from './me'
export default function HiddenWalletSummary ({ abbreviate, fixedWidth }) {
const { me } = useMe()
const me = useMe()
const [hover, setHover] = useState(false)
const fixedWidthAbbrSats = useMemo(() => {

View File

@ -11,7 +11,6 @@ import { nextTip, defaultTipIncludingRandom } from './upvote'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction'
import { meAnonSats } from '@/lib/apollo'
const defaultTips = [100, 1000, 10_000, 100_000]
@ -44,18 +43,14 @@ const addCustomTip = (amount) => {
}
const setItemMeAnonSats = ({ id, amount }) => {
const reactiveVar = meAnonSats[id]
const existingAmount = reactiveVar()
reactiveVar(existingAmount + amount)
// save for next page load
const storageKey = `TIP-item:${id}`
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
window.localStorage.setItem(storageKey, existingAmount + amount)
}
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
const inputRef = useRef(null)
const { me } = useMe()
const me = useMe()
const [oValue, setOValue] = useState()
useEffect(() => {
@ -208,7 +203,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
export function useZap () {
const act = useAct()
const { me } = useMe()
const me = useMe()
const strike = useLightning()
const toaster = useToast()

View File

@ -25,7 +25,7 @@ import { useQuoteReply } from './use-quote-reply'
import { UNKNOWN_LINK_REL } from '@/lib/constants'
function BioItem ({ item, handleClick }) {
const { me } = useMe()
const me = useMe()
if (!item.text) {
return null
}

View File

@ -33,12 +33,13 @@ export default function ItemInfo ({
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
}) {
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
const { me } = useMe()
const me = useMe()
const toaster = useToast()
const router = useRouter()
const [canEdit, setCanEdit] =
useState(item.mine && (Date.now() < editThreshold))
const [hasNewComments, setHasNewComments] = useState(false)
const [meTotalSats, setMeTotalSats] = useState(0)
const root = useRoot()
const retryCreateItem = useRetryCreateItem({ id: item.id })
const sub = item?.sub || root?.sub
@ -53,6 +54,10 @@ export default function ItemInfo ({
setCanEdit(item.mine && (Date.now() < editThreshold))
}, [item.mine, editThreshold])
useEffect(() => {
if (item) setMeTotalSats(item.meSats || item.meAnonSats || 0)
}, [item?.meSats, item?.meAnonSats])
// territory founders can pin any post in their territory
// and OPs can pin any root reply in their post
const isPost = !item.parentId
@ -60,7 +65,6 @@ export default function ItemInfo ({
const myPost = (me && root && Number(me.id) === Number(root.user.id))
const rootReply = item.path.split('.').length === 2
const canPin = (isPost && mySub) || (myPost && rootReply)
const meSats = (me ? item.meSats : item.meAnonSats) || 0
const EditInfo = () => {
const waitForQrPayment = useQrPayment()
@ -127,7 +131,7 @@ export default function ItemInfo ({
unitPlural: 'stackers'
})} ${item.mine
? `\\ ${numWithUnits(item.meSats, { abbreviate: false })} to post`
: `(${numWithUnits(meSats, { abbreviate: false })}${item.meDontLikeSats
: `(${numWithUnits(meTotalSats, { abbreviate: false })}${item.meDontLikeSats
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
: ''} from me)`} `}
>
@ -225,7 +229,7 @@ export default function ItemInfo ({
<CrosspostDropdownItem item={item} />}
{me && !item.position &&
!item.mine && !item.deletedAt &&
(item.meDontLikeSats > meSats
(item.meDontLikeSats > meTotalSats
? <DropdownItemUpVote item={item} />
: <DontLikeThisDropdownItem item={item} />)}
{me && sub && !item.mine && !item.outlawed && Number(me.id) === Number(sub.userId) && sub.moderated &&
@ -269,7 +273,7 @@ export default function ItemInfo ({
}
function InfoDropdownItem ({ item }) {
const { me } = useMe()
const me = useMe()
const showModal = useShowModal()
const onClick = () => {

View File

@ -50,7 +50,7 @@ export function SearchTitle ({ title }) {
}
function mediaType ({ url, imgproxyUrls }) {
const { me } = useMe()
const me = useMe()
const src = IMGPROXY_URL_REGEXP.test(url) ? decodeProxyUrl(url) : url
if (!imgproxyUrls?.[src] ||
me?.privates?.showImagesAndVideos === false ||

View File

@ -11,7 +11,7 @@ import BackIcon from '@/svgs/arrow-left-line.svg'
import { useRouter } from 'next/router'
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
function QrAuth ({ k1, encodedUrl, callbackUrl }) {
const query = gql`
{
lnAuth(k1: "${k1}") {
@ -23,7 +23,7 @@ function QrAuth ({ k1, encodedUrl, callbackUrl, multiAuth }) {
useEffect(() => {
if (data?.lnAuth?.pubkey) {
signIn('lightning', { ...data.lnAuth, callbackUrl, multiAuth })
signIn('lightning', { ...data.lnAuth, callbackUrl })
}
}, [data?.lnAuth])
@ -101,15 +101,15 @@ function LightningExplainer ({ text, children }) {
)
}
export function LightningAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
export function LightningAuthWithExplainer ({ text, callbackUrl }) {
return (
<LightningExplainer text={text}>
<LightningAuth callbackUrl={callbackUrl} multiAuth={multiAuth} />
<LightningAuth callbackUrl={callbackUrl} />
</LightningExplainer>
)
}
export function LightningAuth ({ callbackUrl, multiAuth }) {
export function LightningAuth ({ callbackUrl }) {
// query for challenge
const [createAuth, { data, error }] = useMutation(gql`
mutation createAuth {
@ -125,5 +125,5 @@ export function LightningAuth ({ callbackUrl, multiAuth }) {
if (error) return <div>error</div>
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} multiAuth={multiAuth} /> : <QrSkeleton status='generating' />
return data ? <QrAuth {...data.createAuth} callbackUrl={callbackUrl} /> : <QrSkeleton status='generating' />
}

View File

@ -21,7 +21,7 @@ import useDebounceCallback from './use-debounce-callback'
export function LinkForm ({ item, sub, editThreshold, children }) {
const router = useRouter()
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const schema = linkSchema({ client, me, existingBoost: item?.boost })
// if Web Share Target API was used
const shareUrl = router.query.url

View File

@ -49,7 +49,7 @@ export const LoggerProvider = ({ children }) => {
const ServiceWorkerLoggerContext = createContext()
function ServiceWorkerLoggerProvider ({ children }) {
const { me } = useMe()
const me = useMe()
const [name, setName] = useState()
const [os, setOS] = useState()

View File

@ -4,7 +4,7 @@ import LightningIcon from '@/svgs/bolt.svg'
import NostrIcon from '@/svgs/nostr.svg'
import Button from 'react-bootstrap/Button'
export default function LoginButton ({ text, type, className, onClick, disabled }) {
export default function LoginButton ({ text, type, className, onClick }) {
let Icon, variant
switch (type) {
case 'twitter':
@ -29,7 +29,7 @@ export default function LoginButton ({ text, type, className, onClick, disabled
const name = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase()
return (
<Button className={className} variant={variant} onClick={onClick} disabled={disabled}>
<Button className={className} variant={variant} onClick={onClick}>
<Icon
width={20}
height={20} className='me-3'

View File

@ -5,14 +5,11 @@ import { useState } from 'react'
import Alert from 'react-bootstrap/Alert'
import { useRouter } from 'next/router'
import { LightningAuthWithExplainer } from './lightning-auth'
import { NostrAuthWithExplainer } from './nostr-auth'
import NostrAuth from './nostr-auth'
import LoginButton from './login-button'
import { emailSchema } from '@/lib/validate'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
const disabled = multiAuth
export function EmailLoginForm ({ text, callbackUrl }) {
return (
<Form
initial={{
@ -20,7 +17,7 @@ export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
}}
schema={emailSchema}
onSubmit={async ({ email }) => {
signIn('email', { email, callbackUrl, multiAuth })
signIn('email', { email, callbackUrl })
}}
>
<Input
@ -29,9 +26,8 @@ export function EmailLoginForm ({ text, callbackUrl, multiAuth }) {
placeholder='email@example.com'
required
autoFocus
disabled={disabled}
/>
<SubmitButton disabled={disabled} variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
<SubmitButton variant='secondary' className={styles.providerButton}>{text || 'Login'} with Email</SubmitButton>
</Form>
)
}
@ -52,16 +48,16 @@ export function authErrorMessage (error) {
return error && (authErrorMessages[error] ?? authErrorMessages.default)
}
export default function Login ({ providers, callbackUrl, multiAuth, error, text, Header, Footer }) {
export default function Login ({ providers, callbackUrl, error, text, Header, Footer }) {
const [errorMessage, setErrorMessage] = useState(authErrorMessage(error))
const router = useRouter()
if (router.query.type === 'lightning') {
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
return <LightningAuthWithExplainer callbackUrl={callbackUrl} text={text} />
}
if (router.query.type === 'nostr') {
return <NostrAuthWithExplainer callbackUrl={callbackUrl} text={text} multiAuth={multiAuth} />
return <NostrAuth callbackUrl={callbackUrl} text={text} />
}
return (
@ -78,16 +74,10 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
switch (provider.name) {
case 'Email':
return (
<OverlayTrigger
placement='bottom'
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
trigger={['hover', 'focus']}
>
<div className='w-100' key={provider.id}>
<div className='mt-2 text-center text-muted fw-bold'>or</div>
<EmailLoginForm text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
</div>
</OverlayTrigger>
<div className='w-100' key={provider.id}>
<div className='mt-2 text-center text-muted fw-bold'>or</div>
<EmailLoginForm text={text} callbackUrl={callbackUrl} />
</div>
)
case 'Lightning':
case 'Slashtags':
@ -109,22 +99,13 @@ export default function Login ({ providers, callbackUrl, multiAuth, error, text,
)
default:
return (
<OverlayTrigger
placement='bottom'
overlay={multiAuth ? <Tooltip>not available for account switching yet</Tooltip> : <></>}
trigger={['hover', 'focus']}
>
<div className='w-100'>
<LoginButton
className={`mt-2 ${styles.providerButton}`}
key={provider.id}
type={provider.id.toLowerCase()}
onClick={() => signIn(provider.id, { callbackUrl, multiAuth })}
text={`${text || 'Login'} with`}
disabled={multiAuth}
/>
</div>
</OverlayTrigger>
<LoginButton
className={`mt-2 ${styles.providerButton}`}
key={provider.id}
type={provider.id.toLowerCase()}
onClick={() => signIn(provider.id, { callbackUrl })}
text={`${text || 'Login'} with`}
/>
)
}
})}

View File

@ -8,14 +8,10 @@ export const MeContext = React.createContext({
})
export function MeProvider ({ me, children }) {
const { data, refetch } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
// this makes sure that we always use the fetched data if it's null.
// without this, we would always fallback to the `me` object
// which was passed during page load which (visually) breaks switching to anon
const futureMe = data?.me ?? (data?.me === null ? null : me)
const { data } = useQuery(ME, SSR ? {} : { pollInterval: FAST_POLL_INTERVAL, nextFetchPolicy: 'cache-and-network' })
return (
<MeContext.Provider value={{ me: futureMe, refreshMe: refetch }}>
<MeContext.Provider value={data?.me || me}>
{children}
</MeContext.Provider>
)

View File

@ -109,7 +109,7 @@ export default function MediaOrLink ({ linkFallback = true, ...props }) {
// determines how the media should be displayed given the params, me settings, and editor tab
export const useMediaHelper = ({ src, srcSet: srcSetIntital, topLevel, tab }) => {
const { me } = useMe()
const me = useMe()
const trusted = useMemo(() => !!srcSetIntital || IMGPROXY_URL_REGEXP.test(src) || MEDIA_DOMAIN_REGEXP.test(src), [!!srcSetIntital, src])
const { dimensions, video, format, ...srcSetObj } = srcSetIntital || {}
const [isImage, setIsImage] = useState(!video && trusted)

View File

@ -23,8 +23,6 @@ import classNames from 'classnames'
import SnIcon from '@/svgs/sn.svg'
import { useHasNewNotes } from '../use-has-new-notes'
import { useWallets } from 'wallets'
import SwitchAccountList, { useAccounts } from '@/components/account'
import { useShowModal } from '@/components/modal'
export function Brand ({ className }) {
return (
@ -139,7 +137,7 @@ export function NavNotifications ({ className }) {
}
export function WalletSummary () {
const { me } = useMe()
const me = useMe()
if (!me) return null
if (me.privates?.hideWalletBalance) {
return <HiddenWalletSummary abbreviate fixedWidth />
@ -148,7 +146,7 @@ export function WalletSummary () {
}
export function NavWalletSummary ({ className }) {
const { me } = useMe()
const me = useMe()
const walletLimitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
return (
@ -213,7 +211,7 @@ export function MeDropdown ({ me, dropNavKey }) {
)
}
export function SignUpButton ({ className = 'py-0', width }) {
export function SignUpButton ({ className = 'py-0' }) {
const router = useRouter()
const handleLogin = useCallback(async pathname => await router.push({
pathname,
@ -222,8 +220,8 @@ export function SignUpButton ({ className = 'py-0', width }) {
return (
<Button
className={classNames('align-items-center ps-2 py-1 pe-3', className)}
style={{ borderWidth: '2px', width: width || '150px' }}
className={classNames('align-items-center ps-2 pe-3', className)}
style={{ borderWidth: '2px', width: '112px' }}
id='signup'
onClick={() => handleLogin('/signup')}
>
@ -236,7 +234,7 @@ export function SignUpButton ({ className = 'py-0', width }) {
)
}
export default function LoginButton () {
export default function LoginButton ({ className }) {
const router = useRouter()
const handleLogin = useCallback(async pathname => await router.push({
pathname,
@ -245,9 +243,9 @@ export default function LoginButton () {
return (
<Button
className='align-items-center px-3 py-1'
className='align-items-center px-3 py-1 mb-2'
id='login'
style={{ borderWidth: '2px', width: '150px' }}
style={{ borderWidth: '2px', width: '112px' }}
variant='outline-grey-darkmode'
onClick={() => handleLogin('/login')}
>
@ -256,105 +254,32 @@ export default function LoginButton () {
)
}
function LogoutObstacle ({ onClose }) {
export function LogoutDropdownItem () {
const { registration: swRegistration, togglePushSubscription } = useServiceWorker()
const wallets = useWallets()
const { multiAuthSignout } = useAccounts()
return (
<div className='d-flex m-auto flex-column w-fit-content'>
<h4 className='mb-3'>I reckon you want to logout?</h4>
<div className='mt-2 d-flex justify-content-between'>
<Button
className='me-2'
variant='grey-medium'
onClick={onClose}
>
cancel
</Button>
<Button
onClick={async () => {
const switchSuccess = await multiAuthSignout()
// only signout if multiAuth did not find a next available account
if (switchSuccess) {
onClose()
return
}
<Dropdown.Item
onClick={async () => {
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
await togglePushSubscription().catch(console.error)
}
// order is important because we need to be logged in to delete push subscription on server
const pushSubscription = await swRegistration?.pushManager.getSubscription()
if (pushSubscription) {
await togglePushSubscription().catch(console.error)
}
await wallets.resetClient().catch(console.error)
await wallets.resetClient().catch(console.error)
await signOut({ callbackUrl: '/' })
}}
>
logout
</Button>
</div>
</div>
await signOut({ callbackUrl: '/' })
}}
>logout
</Dropdown.Item>
)
}
export function LogoutDropdownItem ({ handleClose }) {
const showModal = useShowModal()
export function LoginButtons () {
return (
<>
<Dropdown.Item onClick={() => {
handleClose?.()
showModal(onClose => <SwitchAccountList onClose={onClose} />)
}}
>switch account
</Dropdown.Item>
<Dropdown.Item
onClick={async () => {
showModal(onClose => (<LogoutObstacle onClose={onClose} />))
}}
>logout
</Dropdown.Item>
</>
)
}
function SwitchAccountButton ({ handleClose }) {
const showModal = useShowModal()
const { accounts } = useAccounts()
if (accounts.length === 0) return null
return (
<Button
className='align-items-center px-3 py-1'
variant='outline-grey-darkmode'
style={{ borderWidth: '2px', width: '150px' }}
onClick={() => {
// login buttons rendered in offcanvas aren't wrapped inside <Dropdown>
// so we manually close the offcanvas in that case by passing down handleClose here
handleClose?.()
showModal(onClose => <SwitchAccountList onClose={onClose} />)
}}
>
switch account
</Button>
)
}
export function LoginButtons ({ handleClose }) {
return (
<>
<Dropdown.Item className='py-1'>
<LoginButton />
</Dropdown.Item>
<Dropdown.Item className='py-1'>
<SignUpButton />
</Dropdown.Item>
<Dropdown.Item className='py-1'>
<SwitchAccountButton handleClose={handleClose} />
</Dropdown.Item>
<LoginButton />
<SignUpButton className='py-1' />
</>
)
}
@ -374,7 +299,7 @@ export function AnonDropdown ({ path }) {
return (
<div className='position-relative'>
<Dropdown className={styles.dropdown} align='end' autoClose>
<Dropdown className={styles.dropdown} align='end'>
<Dropdown.Toggle className='nav-link nav-item' id='profile' variant='custom'>
<Nav.Link eventKey='anon' as='span' className='p-0 fw-normal'>
@anon<Hat user={{ id: USER_ID.anon }} />

View File

@ -4,7 +4,7 @@ import { AnonCorner, Back, Brand, MeCorner, NavPrice, SearchItem } from '../comm
import { useMe } from '../../me'
export default function TopBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
const { me } = useMe()
const me = useMe()
return (
<Navbar>
<Nav

View File

@ -30,7 +30,7 @@ function useDetectKeyboardOpen (minKeyboardHeight = 300, defaultValue) {
export default function BottomBar ({ sub }) {
const router = useRouter()
const { me } = useMe()
const me = useMe()
const isKeyboardOpen = useDetectKeyboardOpen(200, false)
if (isKeyboardOpen) {

View File

@ -75,10 +75,10 @@ export default function OffCanvas ({ me, dropNavKey }) {
</Link>
</div>
<Dropdown.Divider />
<LogoutDropdownItem handleClose={handleClose} />
<LogoutDropdownItem />
</>
)
: <LoginButtons handleClose={handleClose} />}
: <LoginButtons />}
<div className={classNames(styles.footerPadding, 'mt-auto')}>
<Navbar className={classNames('container d-flex flex-row px-0 text-muted')}>
<Nav>

View File

@ -4,7 +4,7 @@ import styles from '../../header.module.css'
import { useMe } from '@/components/me'
export default function SecondBar (props) {
const { me } = useMe()
const me = useMe()
const { topNavKey } = props
if (!hasNavSelect(props)) return null
return (

View File

@ -4,7 +4,7 @@ import { Back, NavPrice, NavSelect, NavWalletSummary, SignUpButton, hasNavSelect
import { useMe } from '@/components/me'
export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNavKey }) {
const { me } = useMe()
const me = useMe()
return (
<Navbar>
<Nav
@ -17,7 +17,7 @@ export default function TopBar ({ prefix, sub, path, pathname, topNavKey, dropNa
: (
<>
<NavPrice className='flex-shrink-1' />
{me ? <NavWalletSummary /> : <SignUpButton width='fit-content' />}
{me ? <NavWalletSummary /> : <SignUpButton />}
</>)}
</Nav>
</Navbar>

View File

@ -7,7 +7,7 @@ import classNames from 'classnames'
export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey }) {
const ref = useRef()
const { me } = useMe()
const me = useMe()
useEffect(() => {
const stick = () => {
@ -49,7 +49,7 @@ export default function StickyBar ({ prefix, sub, path, topNavKey, dropNavKey })
>
<Back />
<NavPrice className='flex-shrink-1 flex-grow-0' />
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton width='fit-content' />}
{me ? <NavWalletSummary className='px-2' /> : <SignUpButton />}
</Nav>
</Navbar>
</Container>

View File

@ -64,7 +64,7 @@ function NostrExplainer ({ text }) {
)
}
export function NostrAuth ({ text, callbackUrl, multiAuth }) {
export function NostrAuth ({ text, callbackUrl }) {
const [createAuth, { data, error }] = useMutation(gql`
mutation createAuth {
createAuth {
@ -112,8 +112,7 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
try {
await signIn('nostr', {
event: JSON.stringify(event),
callbackUrl,
multiAuth
callbackUrl
})
} catch (e) {
throw new Error('authorization failed', e)
@ -142,14 +141,14 @@ export function NostrAuth ({ text, callbackUrl, multiAuth }) {
)
}
export function NostrAuthWithExplainer ({ text, callbackUrl, multiAuth }) {
export default function NostrAuthWithExplainer ({ text, callbackUrl }) {
const router = useRouter()
return (
<Container>
<div className={styles.login}>
<div className='w-100 mb-3 text-muted pointer' onClick={() => router.back()}><BackIcon /></div>
<h3 className='w-100 pb-2'>{text || 'Login'} with Nostr</h3>
<NostrAuth text={text} callbackUrl={callbackUrl} multiAuth={multiAuth} />
<NostrAuth text={text} callbackUrl={callbackUrl} />
</div>
</Container>
)

View File

@ -42,7 +42,7 @@ export const payBountyCacheMods = {
}
export default function PayBounty ({ children, item }) {
const { me } = useMe()
const me = useMe()
const showModal = useShowModal()
const root = useRoot()
const strike = useLightning()

View File

@ -201,7 +201,7 @@ export const useQrPayment = () => {
}
export const usePayment = () => {
const { me } = useMe()
const me = useMe()
const feeButton = useFeeButton()
const invoice = useInvoice()
const waitForWalletPayment = useWalletPayment()

View File

@ -14,7 +14,7 @@ import useItemSubmit from './use-item-submit'
export function PollForm ({ item, sub, editThreshold, children }) {
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const schema = pollSchema({ client, me, existingBoost: item?.boost })
const onSubmit = useItemSubmit(UPSERT_POLL, { item, sub })

View File

@ -11,7 +11,7 @@ import { usePaidMutation } from './use-paid-mutation'
import { POLL_VOTE, RETRY_PAID_ACTION } from '@/fragments/paidAction'
export default function Poll ({ item }) {
const { me } = useMe()
const me = useMe()
const pollVote = usePollVote({ query: POLL_VOTE, itemId: item.id })
const toaster = useToast()

View File

@ -17,7 +17,7 @@ import CancelButton from './cancel-button'
import { TerritoryInfo } from './territory-header'
export function PostForm ({ type, sub, children }) {
const { me } = useMe()
const me = useMe()
const [errorMessage, setErrorMessage] = useState()
const prefix = sub?.name ? `/~${sub.name}` : ''
@ -150,7 +150,7 @@ export function PostForm ({ type, sub, children }) {
return (
<FeeButtonProvider
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
useRemoteLineItems={postCommentUseRemoteLineItems()}
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
>
<FormType sub={sub}>{children}</FormType>
</FeeButtonProvider>

View File

@ -19,7 +19,7 @@ export function usePrice () {
}
export function PriceProvider ({ price, children }) {
const { me } = useMe()
const me = useMe()
const fiatCurrency = me?.privates?.fiatCurrency
const { data } = useQuery(PRICE, {
variables: { fiatCurrency },

View File

@ -40,7 +40,7 @@ export default forwardRef(function Reply ({
quote
}, ref) {
const [reply, setReply] = useState(replyOpen || quote)
const { me } = useMe()
const me = useMe()
const parentId = item.id
const replyInput = useRef(null)
const showModal = useShowModal()

View File

@ -11,7 +11,7 @@ export default function Search ({ sub }) {
const router = useRouter()
const [q, setQ] = useState(router.query.q || '')
const inputRef = useRef(null)
const { me } = useMe()
const me = useMe()
useEffect(() => {
inputRef.current?.focus()

View File

@ -38,7 +38,7 @@ async function share (title, url, toaster) {
}
export default function Share ({ path, title = '', className = '' }) {
const { me } = useMe()
const me = useMe()
const toaster = useToast()
const url = referrurl(path, me)
@ -56,7 +56,7 @@ export default function Share ({ path, title = '', className = '' }) {
}
export function CopyLinkDropdownItem ({ item }) {
const { me } = useMe()
const me = useMe()
const toaster = useToast()
const router = useRouter()
let url = referrurl(`/items/${item.id}`, me)

View File

@ -18,7 +18,7 @@ import { UNARCHIVE_TERRITORY, UPSERT_SUB } from '@/fragments/paidAction'
export default function TerritoryForm ({ sub }) {
const router = useRouter()
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const [upsertSub] = usePaidMutation(UPSERT_SUB)
const [unarchiveTerritory] = usePaidMutation(UNARCHIVE_TERRITORY)

View File

@ -57,7 +57,7 @@ export function TerritoryInfo ({ sub }) {
}
export default function TerritoryHeader ({ sub }) {
const { me } = useMe()
const me = useMe()
const toaster = useToast()
const [toggleMuteSub] = useMutation(

View File

@ -12,7 +12,7 @@ import { usePaidMutation } from './use-paid-mutation'
import { SUB_PAY } from '@/fragments/paidAction'
export default function TerritoryPaymentDue ({ sub }) {
const { me } = useMe()
const me = useMe()
const client = useApolloClient()
const [paySub] = usePaidMutation(SUB_PAY)
@ -72,7 +72,7 @@ export default function TerritoryPaymentDue ({ sub }) {
}
export function TerritoryBillingLine ({ sub }) {
const { me } = useMe()
const me = useMe()
if (!sub || sub.userId !== Number(me?.id)) return null
const dueDate = sub.billPaidUntil && new Date(sub.billPaidUntil)

View File

@ -57,7 +57,7 @@ function TransferObstacle ({ sub, onClose, userName }) {
function TerritoryTransferForm ({ sub, onClose }) {
const showModal = useShowModal()
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const schema = territoryTransferSchema({ me, client })
const onSubmit = useCallback(async (values) => {

View File

@ -145,10 +145,6 @@
aspect-ratio: var(--aspect-ratio);
}
.p:has(> .mediaContainer) {
white-space: normal;
}
.p:has(> .mediaContainer:only-child) ~ .p:has(> .mediaContainer:only-child) {
display: inline-block;
width: min-content;

View File

@ -14,7 +14,7 @@ import { numWithUnits } from '@/lib/format'
import { Dropdown } from 'react-bootstrap'
const UpvotePopover = ({ target, show, handleClose }) => {
const { me } = useMe()
const me = useMe()
return (
<Overlay
show={show}
@ -107,7 +107,7 @@ export default function UpVote ({ item, className }) {
const [voteShow, _setVoteShow] = useState(false)
const [tipShow, _setTipShow] = useState(false)
const ref = useRef()
const { me } = useMe()
const me = useMe()
const [hover, setHover] = useState(false)
const [setWalkthrough] = useMutation(
gql`
@ -153,7 +153,7 @@ export default function UpVote ({ item, className }) {
[item?.mine, item?.meForward, item?.deletedAt])
const [meSats, overlayText, color, nextColor] = useMemo(() => {
const meSats = (me ? item?.meSats : item?.meAnonSats) || 0
const meSats = (item?.meSats || item?.meAnonSats || 0)
// what should our next tip be?
const sats = pending || nextTip(meSats, { ...me?.privates })
@ -168,7 +168,7 @@ export default function UpVote ({ item, className }) {
meSats, overlayTextContent,
getColor(meSats), getColor(meSats + sats)]
}, [
me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
const handleModalClosed = () => {

View File

@ -175,7 +175,7 @@ function NymEdit ({ user, setEditting }) {
}
function NymView ({ user, isMe, setEditting }) {
const { me } = useMe()
const me = useMe()
return (
<div className='d-flex align-items-center mb-2'>
<div className={styles.username}>@{user.name}<Hat className='' user={user} badge /></div>
@ -237,7 +237,7 @@ function SocialLink ({ name, id }) {
}
function HeaderHeader ({ user }) {
const { me } = useMe()
const me = useMe()
const showModal = useShowModal()
const toaster = useToast()

View File

@ -12,7 +12,6 @@ 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 && <span>{abbrNum(user.optional.stacked)} stacked</span>)
@ -41,34 +40,6 @@ 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 (
<div className={`${styles.item} mb-2`} key={user.name}>
<Link href={`/${user.name}`}>
<Image
src={user.photoId ? `${MEDIA_URL}/${user.photoId}` : '/dorian400.jpg'} width='32' height='32'
className={`${userStyles.userimg} me-2`}
/>
</Link>
<div className={`${styles.hunk} ${className}`}>
<Link
href={`/${user.name}`}
className={`d-inline-flex align-items-center text-reset ${selected ? 'fw-bold text-underline' : 'text-muted'}`}
style={{ textUnderlineOffset: '0.25em' }}
onClick={onNymClick}
>
@{user.name}{showHat && <Hat className='ms-1 fill-grey' height={14} width={14} user={user} />}{selected && <CheckCircle className='ms-3 fill-primary' height={14} width={14} />}
</Link>
{stats && (
<div className={styles.other}>
{stats.map((Comp, i) => <Comp key={i} user={user} />)}
</div>
)}
</div>
</div>
)
}
export function UserBase ({ user, className, children, nymActionDropdown }) {
return (
<div className={classNames(styles.item, className)}>
@ -92,7 +63,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 (
<>

View File

@ -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) {
@ -209,8 +209,8 @@ export const WalletLoggerProvider = ({ children }) => {
setLogs((prevLogs) => [log, ...prevLogs])
}, [saveLog])
const deleteLogs = useCallback(async (wallet, options) => {
if ((!wallet || wallet.walletType) && !options?.clientOnly) {
const deleteLogs = useCallback(async (wallet) => {
if (!wallet || wallet.walletType) {
await deleteServerWalletLogs({ variables: { wallet: wallet?.walletType } })
}
if (!wallet || wallet.sendPayment) {
@ -262,9 +262,7 @@ export function useWalletLogger (wallet) {
error: (...message) => log('error')(message.join(' '))
}), [log, wallet?.name])
const deleteLogs = useCallback((options) => {
return innerDeleteLogs(wallet, options)
}, [innerDeleteLogs, wallet])
const deleteLogs = useCallback((w) => innerDeleteLogs(w || wallet), [innerDeleteLogs, wallet])
return { logger, deleteLogs }
}

View File

@ -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($id: ID, $name: String) {
user(id: $id, name: $name) {
query User($name: String!) {
user(name: $name) {
...UserFields
}
}`

View File

@ -1,4 +1,4 @@
import { ApolloClient, InMemoryCache, HttpLink, makeVar } from '@apollo/client'
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'
import { decodeCursor, LIMIT } from './cursor'
import { SSR } from './constants'
@ -25,8 +25,6 @@ export default function getApolloClient () {
}
}
export const meAnonSats = {}
function getClient (uri) {
return new ApolloClient({
link: new HttpLink({ uri }),
@ -261,23 +259,10 @@ function getClient (uri) {
Item: {
fields: {
meAnonSats: {
read (existingAmount, { readField }) {
if (SSR) return null
read (meAnonSats, { readField }) {
if (typeof window === 'undefined') return null
const itemId = readField('id')
// 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()
return meAnonSats ?? Number(window.localStorage.getItem(`TIP-item:${itemId}`) || '0')
}
}
}

35
package-lock.json generated
View File

@ -31,7 +31,6 @@
"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",
@ -531,15 +530,6 @@
}
}
},
"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",
@ -7277,10 +7267,9 @@
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
"integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
"engines": {
"node": ">= 0.6"
}
@ -9017,15 +9006,6 @@
"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",
@ -14772,15 +14752,6 @@
}
}
},
"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",

View File

@ -36,7 +36,6 @@
"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",

View File

@ -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 <PageLoading />

View File

@ -22,7 +22,6 @@ 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 })
@ -110,24 +109,22 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
<WalletLoggerProvider>
<WebLnProvider>
<ServiceWorkerProvider>
<AccountProvider>
<PriceProvider price={price}>
<LightningProvider>
<ToastProvider>
<ShowModalProvider>
<BlockHeightProvider blockHeight={blockHeight}>
<ChainFeeProvider chainFee={chainFee}>
<ErrorBoundary>
<Component ssrData={ssrData} {...otherProps} />
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
</ErrorBoundary>
</ChainFeeProvider>
</BlockHeightProvider>
</ShowModalProvider>
</ToastProvider>
</LightningProvider>
</PriceProvider>
</AccountProvider>
<PriceProvider price={price}>
<LightningProvider>
<ToastProvider>
<ShowModalProvider>
<BlockHeightProvider blockHeight={blockHeight}>
<ChainFeeProvider chainFee={chainFee}>
<ErrorBoundary>
<Component ssrData={ssrData} {...otherProps} />
{!router?.query?.disablePrompt && <PWAPrompt copyBody='This website has app functionality. Add it to your home screen to use it in fullscreen and receive notifications. In Safari:' promptOnVisit={2} />}
</ErrorBoundary>
</ChainFeeProvider>
</BlockHeightProvider>
</ShowModalProvider>
</ToastProvider>
</LightningProvider>
</PriceProvider>
</ServiceWorkerProvider>
</WebLnProvider>
</WalletLoggerProvider>

View File

@ -7,13 +7,11 @@ import EmailProvider from 'next-auth/providers/email'
import prisma from '@/api/models'
import nodemailer from 'nodemailer'
import { PrismaAdapter } from '@auth/prisma-adapter'
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 { getToken } from 'next-auth/jwt'
import { NodeNextRequest } from 'next/dist/server/base-http/node'
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
@ -55,7 +53,7 @@ async function getReferrerId (referrer) {
}
/** @returns {Partial<import('next-auth').CallbacksOptions>} */
function getCallbacks (req, res) {
function getCallbacks (req) {
return {
/**
* @param {object} token Decrypted JSON Web Token
@ -90,16 +88,6 @@ function getCallbacks (req, res) {
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 }) {
@ -112,78 +100,23 @@ 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 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) {
async function pubkeyAuth (credentials, req, 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) {
// 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) {
// if we are logged in, update rather than create
if (token?.id) {
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 } })
}
}
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
} else if (token && token?.id !== user.id) {
return null
}
return user
@ -227,7 +160,7 @@ async function nostrEventAuth (event) {
}
/** @type {import('next-auth/providers').Provider[]} */
const getProviders = res => [
const providers = [
CredentialsProvider({
id: 'lightning',
name: 'Lightning',
@ -235,9 +168,7 @@ const getProviders = res => [
pubkey: { label: 'publickey', type: 'text' },
k1: { label: 'k1', type: 'text' }
},
authorize: async (credentials, req) => {
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'pubkey')
}
authorize: async (credentials, req) => await pubkeyAuth(credentials, new NodeNextRequest(req), 'pubkey')
}),
CredentialsProvider({
id: 'nostr',
@ -247,7 +178,7 @@ const getProviders = res => [
},
authorize: async ({ event }, req) => {
const credentials = await nostrEventAuth(event)
return await pubkeyAuth(credentials, new NodeNextRequest(req), new NodeNextResponse(res), 'nostrAuthPubkey')
return await pubkeyAuth(credentials, new NodeNextRequest(req), 'nostrAuthPubkey')
}
}),
GitHubProvider({
@ -282,9 +213,9 @@ const getProviders = res => [
]
/** @returns {import('next-auth').AuthOptions} */
export const getAuthOptions = (req, res) => ({
callbacks: getCallbacks(req, res),
providers: getProviders(res),
export const getAuthOptions = req => ({
callbacks: getCallbacks(req),
providers,
adapter: {
...PrismaAdapter(prisma),
createUser: data => {
@ -368,7 +299,7 @@ async function enrollInNewsletter ({ email }) {
}
export default async (req, res) => {
await NextAuth(req, res, getAuthOptions(req, res))
await NextAuth(req, res, getAuthOptions(req))
}
async function sendVerificationRequest ({

View File

@ -66,7 +66,6 @@ export default startServerAndCreateNextHandler(apolloServer, {
session = { user: { ...sessionFields, apiKey: true } }
}
} else {
req = multiAuthMiddleware(req)
session = await getServerSession(req, res, getAuthOptions(req))
}
return {
@ -80,41 +79,3 @@ 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
}

View File

@ -1,61 +0,0 @@
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'))

View File

@ -6,15 +6,8 @@ import { StaticLayout } from '@/components/layout'
import Login from '@/components/login'
import { isExternal } from '@/lib/url'
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
}
export async function getServerSideProps ({ req, res, query: { callbackUrl, error = null } }) {
const session = await getServerSession(req, res, getAuthOptions(req))
// 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
@ -29,9 +22,9 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, mult
callbackUrl = '/'
}
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 (session && callbackUrl) {
// in the cause of auth linking we want to pass the error back to
// settings
if (error) {
const url = new URL(callbackUrl, process.env.NEXT_PUBLIC_URL)
url.searchParams.set('error', error)
@ -46,14 +39,11 @@ export async function getServerSideProps ({ req, res, query: { callbackUrl, mult
}
}
const providers = await getProviders()
return {
props: {
providers,
providers: await getProviders(),
callbackUrl,
error,
multiAuth
error
}
}
}

View File

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

View File

@ -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,14 +96,13 @@ export default function Settings ({ ssrData }) {
}
})
}
})
}
)
const logger = useServiceWorkerLogger()
const { data } = useQuery(SETTINGS)
const { settings: { privates: settings } } = useMemo(() => data ?? ssrData, [data, ssrData])
// if we switched to anon, me is null before the page is reloaded
if ((!data && !ssrData) || !me) return <PageLoading />
if (!data && !ssrData) return <PageLoading />
return (
<Layout>
@ -111,7 +110,6 @@ export default function Settings ({ ssrData }) {
<SettingsHeader />
{hasOnlyOneAuthMethod(settings?.authMethods) && <AuthBanner />}
<Form
enableReinitialize
initial={{
tipDefault: settings?.tipDefault || 21,
tipRandom: settings?.tipRandom,
@ -872,7 +870,7 @@ export function EmailLinkForm ({ callbackUrl }) {
function ApiKey ({ enabled, apiKey }) {
const showModal = useShowModal()
const { me } = useMe()
const me = useMe()
const [generateApiKey] = useMutation(
gql`
mutation generateApiKey($id: ID!) {
@ -998,7 +996,7 @@ function ApiKeyModal ({ apiKey }) {
}
function ApiKeyDeleteObstacle ({ onClose }) {
const { me } = useMe()
const me = useMe()
const [deleteApiKey] = useMutation(
gql`
mutation deleteApiKey($id: ID!) {

View File

@ -61,7 +61,7 @@ export default function Wallet () {
}
function YouHaveSats () {
const { me } = useMe()
const me = useMe()
const limitReached = me?.privates?.sats >= msatsToSats(BALANCE_LIMIT_MSATS)
return (
<h2 className={`${me ? 'visible' : 'invisible'} ${limitReached ? 'text-warning' : 'text-success'}`}>
@ -108,7 +108,7 @@ export function WalletForm () {
}
export function FundForm () {
const { me } = useMe()
const me = useMe()
const [showAlert, setShowAlert] = useState(true)
const router = useRouter()
const [createInvoice, { called, error }] = useMutation(gql`
@ -211,7 +211,7 @@ export function SelectedWithdrawalForm () {
export function InvWithdrawal () {
const router = useRouter()
const { me } = useMe()
const me = useMe()
const [createWithdrawl, { called, error }] = useMutation(CREATE_WITHDRAWL)
@ -358,7 +358,7 @@ export function LnWithdrawal () {
}
export function LnAddrWithdrawal () {
const { me } = useMe()
const me = useMe()
const router = useRouter()
const [sendToLnAddr, { called, error }] = useMutation(SEND_TO_LNADDR)
const defaultOptions = { min: 1 }

View File

@ -123,7 +123,7 @@ function LoadWithdrawl () {
function PrivacyOption ({ wd }) {
if (!wd.bolt11) return
const { me } = useMe()
const me = useMe()
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
const oldEnough = new Date() >= keepUntil
if (!oldEnough) {

View File

@ -241,10 +241,6 @@ $zindex-sticky: 900;
display: none;
}
.w-fit-content {
width: fit-content;
}
@media (display-mode: standalone) {
.standalone {
display: flex
@ -255,14 +251,6 @@ $zindex-sticky: 900;
justify-self: center;
}
svg {
fill: var(--bs-body-color);
}
.fill-primary {
fill: var(--bs-primary);
}
.text-primary svg {
fill: var(--bs-primary);
}

View File

@ -22,7 +22,7 @@ export const Status = {
}
export function useWallet (name) {
const { me } = useMe()
const me = useMe()
const showModal = useShowModal()
const toaster = useToast()
const [disableFreebies] = useMutation(gql`mutation { disableFreebies }`)
@ -77,9 +77,9 @@ export function useWallet (name) {
}, [saveConfig, me, logger])
// delete is a reserved keyword
const delete_ = useCallback(async (options) => {
const delete_ = useCallback(async () => {
try {
await clearConfig({ logger, ...options })
await clearConfig({ logger })
} catch (err) {
const message = err.message || err.toString?.()
logger.error(message)
@ -87,11 +87,6 @@ export function useWallet (name) {
}
}, [clearConfig, logger, disablePayments])
const deleteLogs_ = useCallback(async (options) => {
// first argument is to override the wallet
return await deleteLogs(options)
}, [deleteLogs])
if (!wallet) return null
// Assign everything to wallet object so every function that is passed this wallet object in this
@ -107,7 +102,7 @@ export function useWallet (name) {
wallet.config = config
wallet.save = save
wallet.delete = delete_
wallet.deleteLogs = deleteLogs_
wallet.deleteLogs = deleteLogs
wallet.setPriority = setPriority
wallet.hasConfig = hasConfig
wallet.status = status
@ -156,7 +151,7 @@ function extractServerConfig (fields, config) {
}
function useConfig (wallet) {
const { me } = useMe()
const me = useMe()
const storageKey = getStorageKey(wallet?.name, me)
const [clientConfig, setClientConfig, clearClientConfig] = useClientConfig(storageKey, {})
@ -242,13 +237,13 @@ function useConfig (wallet) {
}
}, [hasClientConfig, hasServerConfig, setClientConfig, setServerConfig, wallet])
const clearConfig = useCallback(async ({ logger, clientOnly }) => {
const clearConfig = useCallback(async ({ logger }) => {
if (hasClientConfig) {
clearClientConfig()
wallet.disablePayments()
logger.ok('wallet detached for payments')
}
if (hasServerConfig && !clientOnly) await clearServerConfig()
if (hasServerConfig) await clearServerConfig()
}, [hasClientConfig, hasServerConfig, clearClientConfig, clearServerConfig, wallet])
return [config, saveConfig, clearConfig]
@ -273,7 +268,7 @@ function isConfigured ({ fields, config }) {
function useServerConfig (wallet) {
const client = useApolloClient()
const { me } = useMe()
const me = useMe()
const { data, refetch: refetchConfig } = useQuery(WALLET_BY_TYPE, { variables: { type: wallet?.walletType }, skip: !wallet?.walletType })
@ -409,9 +404,9 @@ export function useWallets () {
const resetClient = useCallback(async (wallet) => {
for (const w of wallets) {
if (w.canSend) {
await w.delete({ clientOnly: true })
await w.delete()
}
await w.deleteLogs({ clientOnly: true })
await w.deleteLogs()
}
}, [wallets])