• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cameri / nostream / 24342667594

13 Apr 2026 12:12PM UTC coverage: 42.555% (-13.0%) from 55.529%
24342667594

Pull #468

github

web-flow
Merge 4fd541f37 into 1410824d2
Pull Request #468: Phase3: UI Implementation for Dashboard

424 of 1204 branches covered (35.22%)

Branch coverage included in aggregate %.

164 of 540 new or added lines in 14 files covered. (30.37%)

373 existing lines in 23 files now uncovered.

1385 of 3047 relevant lines covered (45.45%)

7.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.54
/src/controllers/callbacks/lnbits-callback-controller.ts
1
import { Request, Response } from 'express'
2

3
import { deriveFromSecret, hmacSha256 } from '../../utils/secret'
2✔
4
import { Invoice, InvoiceStatus } from '../../@types/invoice'
2✔
5
import { createLogger } from '../../factories/logger-factory'
2✔
6
import { createSettings } from '../../factories/settings-factory'
2✔
7
import { getRemoteAddress } from '../../utils/http'
2✔
8
import { IController } from '../../@types/controllers'
9
import { IInvoiceRepository } from '../../@types/repositories'
10
import { IPaymentsService } from '../../@types/services'
11

12
const debug = createLogger('lnbits-callback-controller')
2✔
13

14
export class LNbitsCallbackController implements IController {
2✔
15
  public constructor(
UNCOV
16
    private readonly paymentsService: IPaymentsService,
×
UNCOV
17
    private readonly invoiceRepository: IInvoiceRepository
×
18
  ) { }
19

20
  // TODO: Validate
21
  public async handleRequest(
22
    request: Request,
23
    response: Response,
24
  ) {
UNCOV
25
    debug('request headers: %o', request.headers)
×
26
    debug('request body: %o', request.body)
×
27

UNCOV
28
    const settings = createSettings()
×
29
    const remoteAddress = getRemoteAddress(request, settings)
×
30
    const paymentProcessor = settings.payments?.processor ?? 'null'
×
31

UNCOV
32
    if (paymentProcessor !== 'lnbits') {
×
33
      debug('denied request from %s to /callbacks/lnbits which is not the current payment processor', remoteAddress)
×
34
      response
×
35
        .status(403)
36
        .send('Forbidden')
UNCOV
37
      return
×
38
    }
39

UNCOV
40
    let validationPassed = false
×
41

42
    if (typeof request.query.hmac === 'string' && request.query.hmac.match(/^[0-9]{1,20}:[0-9a-f]{64}$/)) {
×
43
      const split = request.query.hmac.split(':')
×
44
      if (hmacSha256(deriveFromSecret('lnbits-callback-hmac-key'), split[0]).toString('hex') === split[1]) {
×
UNCOV
45
        if (parseInt(split[0]) > Date.now()) {
×
UNCOV
46
          validationPassed = true
×
47
        }
48
      }
49
    }
50

51
    if (!validationPassed) {
×
52
      debug('unauthorized request from %s to /callbacks/lnbits', remoteAddress)
×
53
      response
×
54
        .status(403)
55
        .send('Forbidden')
56
      return
×
57
    }
58

UNCOV
59
    const body = request.body
×
UNCOV
60
    if (!body || typeof body !== 'object' || typeof body.payment_hash !== 'string' || body.payment_hash.length !== 64) {
×
UNCOV
61
      response
×
62
        .status(400)
63
        .setHeader('content-type', 'text/plain; charset=utf8')
64
        .send('Malformed body')
65
      return
×
66
    }
67

68
    const invoice = await this.paymentsService.getInvoiceFromPaymentsProcessor(body.payment_hash)
×
UNCOV
69
    const storedInvoice = await this.invoiceRepository.findById(body.payment_hash)
×
70

71
    if (!storedInvoice) {
×
72
      response
×
73
        .status(404)
74
        .setHeader('content-type', 'text/plain; charset=utf8')
75
        .send('No such invoice')
UNCOV
76
      return
×
77
    }
78

UNCOV
79
    try {
×
80
      await this.paymentsService.updateInvoice(invoice)
×
81
    } catch (error) {
82
      console.error(`Unable to persist invoice ${invoice.id}`, error)
×
83

84
      throw error
×
85
    }
86

UNCOV
87
    if (
×
88
      invoice.status !== InvoiceStatus.COMPLETED
×
89
      && !invoice.confirmedAt
90
    ) {
UNCOV
91
      response
×
92
        .status(200)
93
        .send()
94

95
      return
×
96
    }
97

UNCOV
98
    if (storedInvoice.status === InvoiceStatus.COMPLETED) {
×
UNCOV
99
      response
×
100
        .status(409)
101
        .setHeader('content-type', 'text/plain; charset=utf8')
102
        .send('Invoice is already marked paid')
UNCOV
103
      return
×
104
    }
105

UNCOV
106
    invoice.amountPaid = invoice.amountRequested
×
107

108
    try {
×
UNCOV
109
      await this.paymentsService.confirmInvoice(invoice as Invoice)
×
UNCOV
110
      await this.paymentsService.sendInvoiceUpdateNotification(invoice as Invoice)
×
111
    } catch (error) {
112
      console.error(`Unable to confirm invoice ${invoice.id}`, error)
×
113

UNCOV
114
      throw error
×
115
    }
116

UNCOV
117
    response
×
118
      .status(200)
119
      .setHeader('content-type', 'text/plain; charset=utf8')
120
      .send('OK')
121
  }
122
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc