Compare commits

...

8 Commits

Author SHA1 Message Date
Simone Cervino c88afc5aae
fix can't upload mp4 on safari (#1617) 2024-11-20 07:06:05 -06:00
ekzyis 6bae1f1a89
Fix account switching anon login (#1618)
* Always switch to user we just logged in as

If we're logged in and switch to anon and then use login to get into our previous account instead of using 'switch accounts', we only updated the JWT but we didn't switch to the user.

* Fix getToken unaware of multi-auth middleware

If we use login with new credentials while switched to anon (multi_auth.user-id === 'anonymous'), we updated the pubkey because getToken wasn't aware of the switch and thus believed we're logged in as a user.

This is fixed by applying the middleware before calling getToken.
2024-11-20 07:05:42 -06:00
ekzyis 82fead60f1
Fix mentions for pessimistic actions (#1615)
* Fix mentions for pessimistic actions

* (item)mentions should use tx not models

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
2024-11-19 19:28:21 -06:00
k00b 5dac2f2ed0 fix #1566 2024-11-19 19:12:18 -06:00
Keyan 9179688abc
Merge pull request #1614 from stackernews/wallet-fixes
Wallet fixes
2024-11-19 15:49:12 -06:00
k00b 66ec5d5da8 dark/light mode images on wallet pages 2024-11-19 15:46:11 -06:00
k00b f3cc0f9e1d make recv optional 2024-11-19 15:38:27 -06:00
k00b aa4c448999 fix account switch disconnect 2024-11-19 14:58:48 -06:00
12 changed files with 90 additions and 57 deletions

View File

@ -2,11 +2,11 @@ import { USER_ID } from '@/lib/constants'
import { deleteReminders, getDeleteAt, getRemindAt } from '@/lib/item' import { deleteReminders, getDeleteAt, getRemindAt } from '@/lib/item'
import { parseInternalLinks } from '@/lib/url' import { parseInternalLinks } from '@/lib/url'
export async function getMentions ({ text }, { me, models }) { export async function getMentions ({ text }, { me, tx }) {
const mentionPattern = /\B@[\w_]+/gi const mentionPattern = /\B@[\w_]+/gi
const names = text.match(mentionPattern)?.map(m => m.slice(1)) const names = text.match(mentionPattern)?.map(m => m.slice(1))
if (names?.length > 0) { if (names?.length > 0) {
const users = await models.user.findMany({ const users = await tx.user.findMany({
where: { where: {
name: { name: {
in: names in: names
@ -21,7 +21,7 @@ export async function getMentions ({ text }, { me, models }) {
return [] return []
} }
export const getItemMentions = async ({ text }, { me, models }) => { export const getItemMentions = async ({ text }, { me, tx }) => {
const linkPattern = new RegExp(`${process.env.NEXT_PUBLIC_URL}/items/\\d+[a-zA-Z0-9/?=]*`, 'gi') const linkPattern = new RegExp(`${process.env.NEXT_PUBLIC_URL}/items/\\d+[a-zA-Z0-9/?=]*`, 'gi')
const refs = text.match(linkPattern)?.map(m => { const refs = text.match(linkPattern)?.map(m => {
try { try {
@ -33,7 +33,7 @@ export const getItemMentions = async ({ text }, { me, models }) => {
}).filter(r => !!r) }).filter(r => !!r)
if (refs?.length > 0) { if (refs?.length > 0) {
const referee = await models.item.findMany({ const referee = await tx.item.findMany({
where: { where: {
id: { in: refs }, id: { in: refs },
userId: { not: me?.id || USER_ID.anon } userId: { not: me?.id || USER_ID.anon }

View File

@ -2,7 +2,7 @@ import { useShowModal } from './modal'
import { useToast } from './toast' import { useToast } from './toast'
import ItemAct from './item-act' import ItemAct from './item-act'
import AccordianItem from './accordian-item' import AccordianItem from './accordian-item'
import { useMemo, useState } from 'react' import { useMemo } from 'react'
import getColor from '@/lib/rainbow' import getColor from '@/lib/rainbow'
import BoostIcon from '@/svgs/arrow-up-double-line.svg' import BoostIcon from '@/svgs/arrow-up-double-line.svg'
import styles from './upvote.module.css' import styles from './upvote.module.css'
@ -12,32 +12,29 @@ import classNames from 'classnames'
export default function Boost ({ item, className, ...props }) { export default function Boost ({ item, className, ...props }) {
const { boost } = item const { boost } = item
const [hover, setHover] = useState(false)
const [color, nextColor] = useMemo(() => [getColor(boost), getColor(boost + BOOST_MULT)], [boost]) const [color, nextColor] = useMemo(() => [getColor(boost), getColor(boost + BOOST_MULT)], [boost])
const style = useMemo(() => (hover || boost const style = useMemo(() => ({
? { '--hover-fill': nextColor,
fill: hover ? nextColor : color, '--hover-filter': `drop-shadow(0 0 6px ${nextColor}90)`,
filter: `drop-shadow(0 0 6px ${hover ? nextColor : color}90)` '--fill': color,
} '--filter': `drop-shadow(0 0 6px ${color}90)`
: undefined), [boost, hover]) }), [color, nextColor])
return ( return (
<Booster <Booster
item={item} As={({ ...oprops }) => item={item} As={oprops =>
<div className='upvoteParent'> <div className='upvoteParent'>
<div <div
className={styles.upvoteWrapper} className={styles.upvoteWrapper}
> >
<BoostIcon <BoostIcon
{...props} {...oprops} style={style} {...props}
{...oprops}
style={style}
width={26} width={26}
height={26} height={26}
onPointerEnter={() => setHover(true)} className={classNames(styles.boost, className, boost && styles.boosted)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
className={classNames(styles.boost, className, boost && styles.voted)}
/> />
</div> </div>
</div>} </div>}

View File

@ -78,6 +78,11 @@ export const FileUpload = forwardRef(({ children, className, onSelect, onUpload,
element.onerror = reject element.onerror = reject
element.src = window.URL.createObjectURL(file) element.src = window.URL.createObjectURL(file)
// iOS Force the video to load metadata
if (element.tagName === 'VIDEO') {
element.load()
}
}) })
}, [toaster, getSignedPOST]) }, [toaster, getSignedPOST])

View File

@ -109,7 +109,6 @@ export default function UpVote ({ item, className, collapsed }) {
const [tipShow, _setTipShow] = useState(false) const [tipShow, _setTipShow] = useState(false)
const ref = useRef() const ref = useRef()
const { me } = useMe() const { me } = useMe()
const [hover, setHover] = useState(false)
const [setWalkthrough] = useMutation( const [setWalkthrough] = useMutation(
gql` gql`
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) { mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
@ -172,10 +171,6 @@ export default function UpVote ({ item, className, collapsed }) {
me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault, me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault,
me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending]) me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending])
const handleModalClosed = () => {
setHover(false)
}
const handleLongPress = (e) => { const handleLongPress = (e) => {
if (!item) return if (!item) return
@ -195,7 +190,7 @@ export default function UpVote ({ item, className, collapsed }) {
setController(c) setController(c)
showModal(onClose => showModal(onClose =>
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />, { onClose: handleModalClosed }) <ItemAct onClose={onClose} item={item} abortSignal={c.signal} />)
} }
const handleShortPress = async () => { const handleShortPress = async () => {
@ -223,19 +218,16 @@ export default function UpVote ({ item, className, collapsed }) {
await zap({ item, me, abortSignal: c.signal }) await zap({ item, me, abortSignal: c.signal })
} else { } else {
showModal(onClose => <ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed }) showModal(onClose => <ItemAct onClose={onClose} item={item} />)
} }
} }
const style = useMemo(() => { const style = useMemo(() => ({
const fillColor = pending || hover ? nextColor : color '--hover-fill': nextColor,
return meSats || hover || pending '--hover-filter': `drop-shadow(0 0 6px ${nextColor}90)`,
? { '--fill': color,
fill: fillColor, '--filter': `drop-shadow(0 0 6px ${color}90)`
filter: `drop-shadow(0 0 6px ${fillColor}90)` }), [color, nextColor])
}
: undefined
}, [hover, pending, nextColor, color, meSats])
return ( return (
<div ref={ref} className='upvoteParent'> <div ref={ref} className='upvoteParent'>
@ -246,9 +238,6 @@ export default function UpVote ({ item, className, collapsed }) {
<ActionTooltip notForm disable={disabled} overlayText={overlayText}> <ActionTooltip notForm disable={disabled} overlayText={overlayText}>
<div className={classNames(disabled && styles.noSelfTips, styles.upvoteWrapper)}> <div className={classNames(disabled && styles.noSelfTips, styles.upvoteWrapper)}>
<UpBolt <UpBolt
onPointerEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onTouchEnd={() => setHover(false)}
width={26} width={26}
height={26} height={26}
className={classNames(styles.upvote, className={classNames(styles.upvote,

View File

@ -5,6 +5,11 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
.upvote:hover {
fill: var(--hover-fill) !important;
filter: var(--hover-filter) !important;
}
.boost { .boost {
fill: var(--theme-clickToContextColor); fill: var(--theme-clickToContextColor);
user-select: none; user-select: none;
@ -12,6 +17,16 @@
-webkit-touch-callout: none; -webkit-touch-callout: none;
} }
.boost:hover {
fill: var(--hover-fill) !important;
filter: var(--hover-filter) !important;
}
.boost.boosted {
fill: var(--fill);
filter: var(--filter);
}
.upvoteWrapper { .upvoteWrapper {
position: relative; position: relative;
padding-right: .2rem; padding-right: .2rem;
@ -28,8 +43,8 @@
} }
.upvote.voted { .upvote.voted {
fill: #F6911D; fill: var(--fill);
filter: drop-shadow(0 0 6px #f6911d90); filter: var(--filter);
} }
.cover { .cover {
@ -43,6 +58,7 @@
} }
.pending { .pending {
fill: var(--hover-fill);
animation-name: pulse; animation-name: pulse;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;

View File

@ -27,12 +27,17 @@ function useIndexedDB ({ dbName, storeName, options = DEFAULT_OPTIONS, indices =
db.transaction(storeName) db.transaction(storeName)
while (operationQueue.current.length > 0) { while (operationQueue.current.length > 0) {
const operation = operationQueue.current.shift() const operation = operationQueue.current.shift()
operation(db) // if the db is the same as the one we're processing, run the operation
// else, we'll just clear the operation queue
// XXX this is a consquence of using a ref to store the queue and should be fixed
if (dbName === db.name) {
operation(db)
}
} }
} catch (error) { } catch (error) {
handleError(error) handleError(error)
} }
}, [storeName, handleError, operationQueue]) }, [dbName, storeName, handleError, operationQueue])
useEffect(() => { useEffect(() => {
let isMounted = true let isMounted = true

View File

@ -14,6 +14,7 @@ import { schnorr } from '@noble/curves/secp256k1'
import { notifyReferral } from '@/lib/webPush' import { notifyReferral } from '@/lib/webPush'
import { hashEmail } from '@/lib/crypto' import { hashEmail } from '@/lib/crypto'
import * as cookie from 'cookie' import * as cookie from 'cookie'
import { multiAuthMiddleware } from '@/pages/api/graphql'
/** /**
* Stores userIds in user table * Stores userIds in user table
@ -132,6 +133,9 @@ function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
// add JWT to **httpOnly** cookie // add JWT to **httpOnly** cookie
res.appendHeader('Set-Cookie', cookie.serialize(`multi_auth.${id}`, jwt, cookieOptions)) 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 }] let newMultiAuth = [{ id, name, photoId }]
if (req.cookies.multi_auth) { if (req.cookies.multi_auth) {
const oldMultiAuth = b64Decode(req.cookies.multi_auth) const oldMultiAuth = b64Decode(req.cookies.multi_auth)
@ -140,9 +144,6 @@ function setMultiAuthCookies (req, res, { id, jwt, name, photoId }) {
newMultiAuth = [...oldMultiAuth, ...newMultiAuth] newMultiAuth = [...oldMultiAuth, ...newMultiAuth]
} }
res.appendHeader('Set-Cookie', cookie.serialize('multi_auth', b64Encode(newMultiAuth), { ...cookieOptions, httpOnly: false })) 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, res, pubkeyColumnName) {
@ -165,6 +166,7 @@ async function pubkeyAuth (credentials, req, res, pubkeyColumnName) {
let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } }) let user = await prisma.user.findUnique({ where: { [pubkeyColumnName]: pubkey } })
// get token if it exists // get token if it exists
req = multiAuthMiddleware(req)
const token = await getToken({ req }) const token = await getToken({ req })
if (!user) { if (!user) {
// we have not seen this pubkey before // we have not seen this pubkey before

View File

@ -82,7 +82,7 @@ export default startServerAndCreateNextHandler(apolloServer, {
} }
}) })
function multiAuthMiddleware (request) { export function multiAuthMiddleware (request) {
// switch next-auth session cookie with multi_auth cookie if cookie pointer present // switch next-auth session cookie with multi_auth cookie if cookie pointer present
// is there a cookie pointer? // is there a cookie pointer?

View File

@ -13,11 +13,12 @@ import { canReceive, canSend, isConfigured } from '@/wallets/common'
import { SSR } from '@/lib/constants' import { SSR } from '@/lib/constants'
import WalletButtonBar from '@/components/wallet-buttonbar' import WalletButtonBar from '@/components/wallet-buttonbar'
import { useWalletConfigurator } from '@/wallets/config' import { useWalletConfigurator } from '@/wallets/config'
import { useCallback, useMemo } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useMe } from '@/components/me' import { useMe } from '@/components/me'
import validateWallet from '@/wallets/validate' import validateWallet from '@/wallets/validate'
import { ValidationError } from 'yup' import { ValidationError } from 'yup'
import { useFormikContext } from 'formik' import { useFormikContext } from 'formik'
import useDarkMode from '@/components/dark-mode'
export const getServerSideProps = getGetServerSideProps({ authRequired: true }) export const getServerSideProps = getGetServerSideProps({ authRequired: true })
@ -28,6 +29,8 @@ export default function WalletSettings () {
const wallet = useWallet(name) const wallet = useWallet(name)
const { me } = useMe() const { me } = useMe()
const { save, detach } = useWalletConfigurator(wallet) const { save, detach } = useWalletConfigurator(wallet)
const [dark] = useDarkMode()
const [imgSrc, setImgSrc] = useState(wallet?.def.card?.image?.src)
const initial = useMemo(() => { const initial = useMemo(() => {
const initial = wallet?.def.fields.reduce((acc, field) => { const initial = wallet?.def.fields.reduce((acc, field) => {
@ -69,12 +72,16 @@ export default function WalletSettings () {
const { card: { image, title, subtitle } } = wallet?.def || { card: {} } const { card: { image, title, subtitle } } = wallet?.def || { card: {} }
useEffect(() => {
if (!imgSrc) return
// wallet.png <-> wallet-dark.png
setImgSrc(dark ? image?.src.replace(/\.([a-z]{3})$/, '-dark.$1') : image?.src)
}, [dark])
return ( return (
<CenterLayout> <CenterLayout>
{image {image
? typeof image === 'object' ? <img alt={title} {...image} src={imgSrc} className='pb-3 px-2 mw-100' />
? <img {...image} alt={title} className='pb-2' />
: <img src={image} width='33%' alt={title} className='pb-2' />
: <h2 className='pb-2'>{title}</h2>} : <h2 className='pb-2'>{title}</h2>}
<h6 className='text-muted text-center pb-3'><Text>{subtitle}</Text></h6> <h6 className='text-muted text-center pb-3'><Text>{subtitle}</Text></h6>
<Form <Form

View File

@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "WalletBlink" ALTER COLUMN "apiKeyRecv" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletLNbits" ALTER COLUMN "invoiceKey" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletNWC" ALTER COLUMN "nwcUrlRecv" DROP NOT NULL;
-- AlterTable
ALTER TABLE "WalletPhoenixd" ALTER COLUMN "secondaryPassword" DROP NOT NULL;

View File

@ -289,7 +289,7 @@ model WalletLNbits {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String url String
invoiceKey String invoiceKey String?
} }
model WalletNWC { model WalletNWC {
@ -298,7 +298,7 @@ model WalletNWC {
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
nwcUrlRecv String nwcUrlRecv String?
} }
model WalletBlink { model WalletBlink {
@ -307,7 +307,7 @@ model WalletBlink {
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade) wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
apiKeyRecv String apiKeyRecv String?
currencyRecv String? currencyRecv String?
} }
@ -318,7 +318,7 @@ model WalletPhoenixd {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
url String url String
secondaryPassword String secondaryPassword String?
} }
model Mute { model Mute {

View File

@ -119,11 +119,12 @@ async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, ln
const context = { const context = {
tx, tx,
cost: BigInt(lndInvoice.received_mtokens), cost: BigInt(lndInvoice.received_mtokens),
me: dbInvoice.user, me: dbInvoice.user
sybilFeePercent: await paidActions[dbInvoice.actionType].getSybilFeePercent?.()
} }
const result = await paidActions[dbInvoice.actionType].perform(args, context) const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
await tx.invoice.update({ await tx.invoice.update({
where: { id: dbInvoice.id }, where: { id: dbInvoice.id },
data: { data: {