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) {
|
if (id) {
|
||||||
return await updateItem(parent, { id, data }, { me, models })
|
return await updateItem(parent, { id, data }, { me, models })
|
||||||
} else {
|
} 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 }) => {
|
upsertDiscussion: async (parent, args, { me, models }) => {
|
||||||
|
@ -583,7 +583,7 @@ export default {
|
||||||
if (id) {
|
if (id) {
|
||||||
return await updateItem(parent, { id, data }, { me, models })
|
return await updateItem(parent, { id, data }, { me, models })
|
||||||
} else {
|
} 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 }) => {
|
upsertBounty: async (parent, args, { me, models }) => {
|
||||||
|
@ -598,13 +598,13 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
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
|
let author = me
|
||||||
const trx = []
|
const trx = []
|
||||||
if (!me && invoiceId) {
|
if (!me && invoiceHash) {
|
||||||
const invoice = await checkInvoice(models, invoiceId, ANON_POST_FEE)
|
const invoice = await checkInvoice(models, invoiceHash, ANON_POST_FEE)
|
||||||
author = invoice.user
|
author = invoice.user
|
||||||
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
|
@ -689,7 +689,7 @@ export default {
|
||||||
},
|
},
|
||||||
createComment: async (parent, data, { me, models }) => {
|
createComment: async (parent, data, { me, models }) => {
|
||||||
await ssValidate(commentSchema, data)
|
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
|
// fetch user to get up-to-date name
|
||||||
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||||
|
|
||||||
|
@ -722,17 +722,17 @@ export default {
|
||||||
|
|
||||||
return id
|
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
|
// 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' } })
|
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
await ssValidate(amountSchema, { amount: sats })
|
await ssValidate(amountSchema, { amount: sats })
|
||||||
|
|
||||||
let user = me
|
let user = me
|
||||||
if (!me && invoiceId) {
|
if (!me && invoiceHash) {
|
||||||
const invoice = await checkInvoice(models, invoiceId, sats)
|
const invoice = await checkInvoice(models, invoiceHash, sats)
|
||||||
user = invoice.user
|
user = invoice.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -748,8 +748,8 @@ export default {
|
||||||
const calls = [
|
const calls = [
|
||||||
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
|
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
|
||||||
]
|
]
|
||||||
if (!me && invoiceId) {
|
if (!me && invoiceHash) {
|
||||||
calls.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
calls.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [{ item_act: vote }] = await serialize(models, ...calls)
|
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
|
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
|
let author = me
|
||||||
const trx = []
|
const trx = []
|
||||||
if (!me && invoiceId) {
|
if (!me && invoiceHash) {
|
||||||
const invoice = await checkInvoice(models, invoiceId, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
const invoice = await checkInvoice(models, invoiceHash, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||||
author = invoice.user
|
author = invoice.user
|
||||||
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
|
|
|
@ -26,16 +26,16 @@ export default gql`
|
||||||
bookmarkItem(id: ID): Item
|
bookmarkItem(id: ID): Item
|
||||||
subscribeItem(id: ID): Item
|
subscribeItem(id: ID): Item
|
||||||
deleteItem(id: ID): Item
|
deleteItem(id: ID): Item
|
||||||
upsertLink(id: ID, sub: String, title: String!, url: 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, invoiceId: ID): 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!
|
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,
|
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
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!
|
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceHash: String): Item!
|
||||||
createComment(text: String!, parentId: ID!, invoiceId: ID): Item!
|
createComment(text: String!, parentId: ID!, invoiceHash: String): Item!
|
||||||
updateComment(id: ID!, text: String!): Item!
|
updateComment(id: ID!, text: String!): Item!
|
||||||
dontLikeThis(id: ID!): Boolean!
|
dontLikeThis(id: ID!): Boolean!
|
||||||
act(id: ID!, sats: Int, invoiceId: ID): ItemActResult!
|
act(id: ID!, sats: Int, invoiceHash: String): ItemActResult!
|
||||||
pollVote(id: ID!): ID!
|
pollVote(id: ID!): ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export default gql`
|
||||||
type Invoice {
|
type Invoice {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Date!
|
createdAt: Date!
|
||||||
|
hash: String!
|
||||||
bolt11: String!
|
bolt11: String!
|
||||||
expiresAt: Date!
|
expiresAt: Date!
|
||||||
cancelled: Boolean!
|
cancelled: Boolean!
|
||||||
|
|
|
@ -30,17 +30,17 @@ export function DiscussionForm ({
|
||||||
// const me = useMe()
|
// const me = useMe()
|
||||||
const [upsertDiscussion] = useMutation(
|
const [upsertDiscussion] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceId: ID) {
|
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, invoiceId: $invoiceId) {
|
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitUpsertDiscussion = useCallback(
|
const submitUpsertDiscussion = useCallback(
|
||||||
async (_, boost, values, invoiceId) => {
|
async (_, boost, values, invoiceHash) => {
|
||||||
const { error } = await upsertDiscussion({
|
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) {
|
if (error) {
|
||||||
throw new Error({ message: error.toString() })
|
throw new Error({ message: error.toString() })
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
||||||
}, [onClose, itemId])
|
}, [onClose, itemId])
|
||||||
|
|
||||||
const submitAct = useCallback(
|
const submitAct = useCallback(
|
||||||
async (amount, invoiceId) => {
|
async (amount, invoiceHash) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
const storageKey = `TIP-item:${itemId}`
|
const storageKey = `TIP-item:${itemId}`
|
||||||
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
||||||
|
@ -57,7 +57,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
||||||
variables: {
|
variables: {
|
||||||
id: itemId,
|
id: itemId,
|
||||||
sats: Number(amount),
|
sats: Number(amount),
|
||||||
invoiceId
|
invoiceHash
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await strike()
|
await strike()
|
||||||
|
|
|
@ -68,17 +68,17 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
|
|
||||||
const [upsertLink] = useMutation(
|
const [upsertLink] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceId: ID) {
|
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, invoiceId: $invoiceId) {
|
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitUpsertLink = useCallback(
|
const submitUpsertLink = useCallback(
|
||||||
async (_, boost, title, values, invoiceId) => {
|
async (_, boost, title, values, invoiceHash) => {
|
||||||
const { error } = await upsertLink({
|
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) {
|
if (error) {
|
||||||
throw new Error({ message: error.toString() })
|
throw new Error({ message: error.toString() })
|
||||||
|
|
|
@ -21,16 +21,16 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const [upsertPoll] = useMutation(
|
const [upsertPoll] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
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,
|
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
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitUpsertPoll = useCallback(
|
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 optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
|
||||||
const { error } = await upsertPoll({
|
const { error } = await upsertPoll({
|
||||||
variables: {
|
variables: {
|
||||||
|
@ -40,7 +40,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
title: title.trim(),
|
title: title.trim(),
|
||||||
options: optionsFiltered,
|
options: optionsFiltered,
|
||||||
...values,
|
...values,
|
||||||
invoiceId
|
invoiceHash
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -47,8 +47,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||||
const [createComment] = useMutation(
|
const [createComment] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
${COMMENTS}
|
${COMMENTS}
|
||||||
mutation createComment($text: String!, $parentId: ID!, $invoiceId: ID) {
|
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String) {
|
||||||
createComment(text: $text, parentId: $parentId, invoiceId: $invoiceId) {
|
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash) {
|
||||||
...CommentFields
|
...CommentFields
|
||||||
comments {
|
comments {
|
||||||
...CommentsRecursive
|
...CommentsRecursive
|
||||||
|
@ -93,8 +93,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
||||||
)
|
)
|
||||||
|
|
||||||
const submitComment = useCallback(
|
const submitComment = useCallback(
|
||||||
async (_, values, parentId, resetForm, invoiceId) => {
|
async (_, values, parentId, resetForm, invoiceHash) => {
|
||||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceId } })
|
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash } })
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error({ message: error.toString() })
|
throw new Error({ message: error.toString() })
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,8 +108,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
||||||
|
|
||||||
const [act] = useMutation(
|
const [act] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation act($id: ID!, $sats: Int!, $invoiceId: ID) {
|
mutation act($id: ID!, $sats: Int!, $invoiceHash: String) {
|
||||||
act(id: $id, sats: $sats, invoiceId: $invoiceId) {
|
act(id: $id, sats: $sats, invoiceHash: $invoiceHash) {
|
||||||
sats
|
sats
|
||||||
}
|
}
|
||||||
}`, {
|
}`, {
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const INVOICE = gql`
|
||||||
query Invoice($id: ID!) {
|
query Invoice($id: ID!) {
|
||||||
invoice(id: $id) {
|
invoice(id: $id) {
|
||||||
id
|
id
|
||||||
|
hash
|
||||||
bolt11
|
bolt11
|
||||||
satsReceived
|
satsReceived
|
||||||
cancelled
|
cancelled
|
||||||
|
|
|
@ -30,6 +30,7 @@ export const useAnonymous = (fn) => {
|
||||||
mutation createInvoice($amount: Int!) {
|
mutation createInvoice($amount: Int!) {
|
||||||
createInvoice(amount: $amount) {
|
createInvoice(amount: $amount) {
|
||||||
id
|
id
|
||||||
|
hash
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
|
@ -42,9 +43,9 @@ export const useAnonymous = (fn) => {
|
||||||
<Invoice
|
<Invoice
|
||||||
id={invoice.id}
|
id={invoice.id}
|
||||||
onConfirmation={
|
onConfirmation={
|
||||||
async ({ id, satsReceived }) => {
|
async ({ satsReceived }) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await fn(satsReceived, ...fnArgs, id)
|
await fn(satsReceived, ...fnArgs, invoice.hash)
|
||||||
onClose()
|
onClose()
|
||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
@ -63,9 +64,9 @@ export const useAnonymous = (fn) => {
|
||||||
return anonFn
|
return anonFn
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkInvoice = async (models, invoiceId, fee) => {
|
export const checkInvoice = async (models, invoiceHash, fee) => {
|
||||||
const invoice = await models.invoice.findUnique({
|
const invoice = await models.invoice.findUnique({
|
||||||
where: { id: Number(invoiceId) },
|
where: { hash: invoiceHash },
|
||||||
include: {
|
include: {
|
||||||
user: true
|
user: true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue