forked from cameri/nostream
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathopennode-callback-controller.ts
More file actions
134 lines (110 loc) · 4.21 KB
/
Copy pathopennode-callback-controller.ts
File metadata and controls
134 lines (110 loc) · 4.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { timingSafeEqual } from 'crypto'
import { Request, Response } from 'express'
import { Invoice, InvoiceStatus } from '../../@types/invoice'
import { createLogger } from '../../factories/logger-factory'
import { createSettings } from '../../factories/settings-factory'
import { getRemoteAddress } from '../../utils/http'
import { hmacSha256 } from '../../utils/secret'
import { IController } from '../../@types/controllers'
import { IPaymentsService } from '../../@types/services'
import { opennodeWebhookCallbackBodySchema } from '../../schemas/opennode-callback-schema'
import { validateSchema } from '../../utils/validation'
const logger = createLogger('opennode-callback-controller')
export class OpenNodeCallbackController implements IController {
public constructor(private readonly paymentsService: IPaymentsService) {}
public async handleRequest(request: Request, response: Response) {
logger('request headers: %o', request.headers)
const settings = createSettings()
const remoteAddress = getRemoteAddress(request, settings)
const bodyValidation = validateSchema(opennodeWebhookCallbackBodySchema)(request.body)
if (bodyValidation.error) {
logger('opennode callback request rejected: invalid body %o', bodyValidation.error)
response.status(400).setHeader('content-type', 'text/plain; charset=utf8').send('Malformed body')
return
}
const body = bodyValidation.value
logger(
'request body metadata: hasId=%s hasHashedOrder=%s status=%s',
typeof body.id === 'string',
typeof body.hashed_order === 'string',
body.status,
)
const openNodeApiKey = process.env.OPENNODE_API_KEY
if (!openNodeApiKey) {
logger('OPENNODE_API_KEY is not configured; unable to verify OpenNode callback from %s', remoteAddress)
response
.status(500)
.setHeader('content-type', 'text/plain; charset=utf8')
.send('Internal Server Error')
return
}
const expectedBuf = hmacSha256(openNodeApiKey, body.id)
const actualHex = body.hashed_order
const expectedHexLength = expectedBuf.length * 2
if (
actualHex.length !== expectedHexLength
|| !/^[0-9a-f]+$/i.test(actualHex)
) {
logger('invalid hashed_order format from %s to /callbacks/opennode', remoteAddress)
response
.status(400)
.setHeader('content-type', 'text/plain; charset=utf8')
.send('Bad Request')
return
}
const actualBuf = Buffer.from(actualHex, 'hex')
if (
!timingSafeEqual(expectedBuf, actualBuf)
) {
logger('unauthorized request from %s to /callbacks/opennode: hashed_order mismatch', remoteAddress)
response
.status(403)
.send('Forbidden')
return
}
const statusMap: Record<string, InvoiceStatus> = {
expired: InvoiceStatus.EXPIRED,
refunded: InvoiceStatus.EXPIRED,
unpaid: InvoiceStatus.PENDING,
processing: InvoiceStatus.PENDING,
underpaid: InvoiceStatus.PENDING,
paid: InvoiceStatus.COMPLETED,
}
const invoice: Pick<Invoice, 'id' | 'status'> = {
id: body.id,
status: statusMap[body.status],
}
logger('invoice', invoice)
let updatedInvoice: Invoice
try {
updatedInvoice = await this.paymentsService.updateInvoiceStatus(invoice)
} catch (error) {
logger.error(`Unable to persist invoice ${invoice.id}`, error)
throw error
}
if (updatedInvoice.status !== InvoiceStatus.COMPLETED) {
response
.status(200)
.send()
return
}
if (!updatedInvoice.confirmedAt) {
updatedInvoice.confirmedAt = new Date()
}
updatedInvoice.amountPaid = updatedInvoice.amountRequested
try {
await this.paymentsService.confirmInvoice({
id: updatedInvoice.id,
pubkey: updatedInvoice.pubkey,
status: updatedInvoice.status,
amountPaid: updatedInvoice.amountPaid,
confirmedAt: updatedInvoice.confirmedAt,
})
await this.paymentsService.sendInvoiceUpdateNotification(updatedInvoice)
} catch (error) {
logger.error(`Unable to confirm invoice ${invoice.id}`, error)
throw error
}
response.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK')
}
}