Use payment hash instead of invoice id as proof of payment
Our invoice IDs can be enumerated. So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself. Random payment hashes prevent this. Also, since we delete invoices after use, using database IDs as proof of payments are not suitable. If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs.
This commit is contained in:
parent
74893b09dd
commit
fd8510d59f
|
@ -572,7 +572,7 @@ export default {
|
|||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} else {
|
||||
return await createItem(parent, data, { me, models, invoiceId: args.invoiceId })
|
||||
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash })
|
||||
}
|
||||
},
|
||||
upsertDiscussion: async (parent, args, { me, models }) => {
|
||||
|
@ -583,7 +583,7 @@ export default {
|
|||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} else {
|
||||
return await createItem(parent, data, { me, models, invoiceId: args.invoiceId })
|
||||
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash })
|
||||
}
|
||||
},
|
||||
upsertBounty: async (parent, args, { me, models }) => {
|
||||
|
@ -598,13 +598,13 @@ export default {
|
|||
}
|
||||
},
|
||||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
||||
const { sub, forward, boost, title, text, options, invoiceId } = data
|
||||
const { sub, forward, boost, title, text, options, invoiceHash } = data
|
||||
let author = me
|
||||
const trx = []
|
||||
if (!me && invoiceId) {
|
||||
const invoice = await checkInvoice(models, invoiceId, ANON_POST_FEE)
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, ANON_POST_FEE)
|
||||
author = invoice.user
|
||||
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
|
@ -689,7 +689,7 @@ export default {
|
|||
},
|
||||
createComment: async (parent, data, { me, models }) => {
|
||||
await ssValidate(commentSchema, data)
|
||||
const item = await createItem(parent, data, { me, models, invoiceId: data.invoiceId })
|
||||
const item = await createItem(parent, data, { me, models, invoiceHash: data.invoiceHash })
|
||||
// fetch user to get up-to-date name
|
||||
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||
|
||||
|
@ -722,17 +722,17 @@ export default {
|
|||
|
||||
return id
|
||||
},
|
||||
act: async (parent, { id, sats, invoiceId }, { me, models }) => {
|
||||
act: async (parent, { id, sats, invoiceHash }, { me, models }) => {
|
||||
// need to make sure we are logged in
|
||||
if (!me && !invoiceId) {
|
||||
if (!me && !invoiceHash) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
await ssValidate(amountSchema, { amount: sats })
|
||||
|
||||
let user = me
|
||||
if (!me && invoiceId) {
|
||||
const invoice = await checkInvoice(models, invoiceId, sats)
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, sats)
|
||||
user = invoice.user
|
||||
}
|
||||
|
||||
|
@ -748,8 +748,8 @@ export default {
|
|||
const calls = [
|
||||
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
|
||||
]
|
||||
if (!me && invoiceId) {
|
||||
calls.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
||||
if (!me && invoiceHash) {
|
||||
calls.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
const [{ item_act: vote }] = await serialize(models, ...calls)
|
||||
|
@ -1075,13 +1075,13 @@ export const updateItem = async (parent, { id, data: { sub, title, url, text, bo
|
|||
return item
|
||||
}
|
||||
|
||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceId }) => {
|
||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash }) => {
|
||||
let author = me
|
||||
const trx = []
|
||||
if (!me && invoiceId) {
|
||||
const invoice = await checkInvoice(models, invoiceId, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||
author = invoice.user
|
||||
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
||||
if (!author) {
|
||||
|
|
|
@ -26,16 +26,16 @@ export default gql`
|
|||
bookmarkItem(id: ID): Item
|
||||
subscribeItem(id: ID): Item
|
||||
deleteItem(id: ID): Item
|
||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceId: ID): Item!
|
||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String, invoiceId: ID): Item!
|
||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceHash: String): Item!
|
||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String, invoiceHash: String): Item!
|
||||
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
|
||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceId: ID): Item!
|
||||
createComment(text: String!, parentId: ID!, invoiceId: ID): Item!
|
||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceHash: String): Item!
|
||||
createComment(text: String!, parentId: ID!, invoiceHash: String): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
dontLikeThis(id: ID!): Boolean!
|
||||
act(id: ID!, sats: Int, invoiceId: ID): ItemActResult!
|
||||
act(id: ID!, sats: Int, invoiceHash: String): ItemActResult!
|
||||
pollVote(id: ID!): ID!
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ export default gql`
|
|||
type Invoice {
|
||||
id: ID!
|
||||
createdAt: Date!
|
||||
hash: String!
|
||||
bolt11: String!
|
||||
expiresAt: Date!
|
||||
cancelled: Boolean!
|
||||
|
|
|
@ -30,17 +30,17 @@ export function DiscussionForm ({
|
|||
// const me = useMe()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceHash: String) {
|
||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const submitUpsertDiscussion = useCallback(
|
||||
async (_, boost, values, invoiceId) => {
|
||||
async (_, boost, values, invoiceHash) => {
|
||||
const { error } = await upsertDiscussion({
|
||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values, invoiceId }
|
||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values, invoiceHash }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
}, [onClose, itemId])
|
||||
|
||||
const submitAct = useCallback(
|
||||
async (amount, invoiceId) => {
|
||||
async (amount, invoiceHash) => {
|
||||
if (!me) {
|
||||
const storageKey = `TIP-item:${itemId}`
|
||||
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
||||
|
@ -57,7 +57,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
variables: {
|
||||
id: itemId,
|
||||
sats: Number(amount),
|
||||
invoiceId
|
||||
invoiceHash
|
||||
}
|
||||
})
|
||||
await strike()
|
||||
|
|
|
@ -68,17 +68,17 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
|
||||
const [upsertLink] = useMutation(
|
||||
gql`
|
||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceHash: String) {
|
||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const submitUpsertLink = useCallback(
|
||||
async (_, boost, title, values, invoiceId) => {
|
||||
async (_, boost, title, values, invoiceHash) => {
|
||||
const { error } = await upsertLink({
|
||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), invoiceId, ...values }
|
||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), invoiceHash, ...values }
|
||||
})
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -21,16 +21,16 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
const [upsertPoll] = useMutation(
|
||||
gql`
|
||||
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
||||
$options: [String!]!, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||
$options: [String!]!, $boost: Int, $forward: String, $invoiceHash: String) {
|
||||
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
||||
options: $options, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||
options: $options, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const submitUpsertPoll = useCallback(
|
||||
async (_, boost, title, options, values, invoiceId) => {
|
||||
async (_, boost, title, options, values, invoiceHash) => {
|
||||
const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
|
||||
const { error } = await upsertPoll({
|
||||
variables: {
|
||||
|
@ -40,7 +40,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
title: title.trim(),
|
||||
options: optionsFiltered,
|
||||
...values,
|
||||
invoiceId
|
||||
invoiceHash
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
|
|
|
@ -47,8 +47,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
const [createComment] = useMutation(
|
||||
gql`
|
||||
${COMMENTS}
|
||||
mutation createComment($text: String!, $parentId: ID!, $invoiceId: ID) {
|
||||
createComment(text: $text, parentId: $parentId, invoiceId: $invoiceId) {
|
||||
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String) {
|
||||
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash) {
|
||||
...CommentFields
|
||||
comments {
|
||||
...CommentsRecursive
|
||||
|
@ -93,8 +93,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
)
|
||||
|
||||
const submitComment = useCallback(
|
||||
async (_, values, parentId, resetForm, invoiceId) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceId } })
|
||||
async (_, values, parentId, resetForm, invoiceHash) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash } })
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
|
|
|
@ -108,8 +108,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
const [act] = useMutation(
|
||||
gql`
|
||||
mutation act($id: ID!, $sats: Int!, $invoiceId: ID) {
|
||||
act(id: $id, sats: $sats, invoiceId: $invoiceId) {
|
||||
mutation act($id: ID!, $sats: Int!, $invoiceHash: String) {
|
||||
act(id: $id, sats: $sats, invoiceHash: $invoiceHash) {
|
||||
sats
|
||||
}
|
||||
}`, {
|
||||
|
|
|
@ -5,6 +5,7 @@ export const INVOICE = gql`
|
|||
query Invoice($id: ID!) {
|
||||
invoice(id: $id) {
|
||||
id
|
||||
hash
|
||||
bolt11
|
||||
satsReceived
|
||||
cancelled
|
||||
|
|
|
@ -30,6 +30,7 @@ export const useAnonymous = (fn) => {
|
|||
mutation createInvoice($amount: Int!) {
|
||||
createInvoice(amount: $amount) {
|
||||
id
|
||||
hash
|
||||
}
|
||||
}`)
|
||||
const showModal = useShowModal()
|
||||
|
@ -42,9 +43,9 @@ export const useAnonymous = (fn) => {
|
|||
<Invoice
|
||||
id={invoice.id}
|
||||
onConfirmation={
|
||||
async ({ id, satsReceived }) => {
|
||||
async ({ satsReceived }) => {
|
||||
setTimeout(async () => {
|
||||
await fn(satsReceived, ...fnArgs, id)
|
||||
await fn(satsReceived, ...fnArgs, invoice.hash)
|
||||
onClose()
|
||||
}, 2000)
|
||||
}
|
||||
|
@ -63,9 +64,9 @@ export const useAnonymous = (fn) => {
|
|||
return anonFn
|
||||
}
|
||||
|
||||
export const checkInvoice = async (models, invoiceId, fee) => {
|
||||
export const checkInvoice = async (models, invoiceHash, fee) => {
|
||||
const invoice = await models.invoice.findUnique({
|
||||
where: { id: Number(invoiceId) },
|
||||
where: { hash: invoiceHash },
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue