mirror of
https://github.com/Cameri/nostream.git
synced 2025-03-17 21:31:48 +01:00
feat: Add health api endpoint or kind (#166)
* feat: handle GET /healthz * chore: simplify default settings * chore: keep client alive on message * chore: increase ws heartbeat timeout to 2min * chore: improve logging during startup * fix: zebedee callback crash on expired invoice * fix: QR code csp error * feat: add get-invoice-status-controller * feat: add get-invoice-status-controller factory * chore: refactor router * feat: get invoice status using rest api * fix: bad import Signed-off-by: Ricardo Arturo Cabral Mejía <me@ricardocabral.io> --------- Signed-off-by: Ricardo Arturo Cabral Mejía <me@ricardocabral.io>
This commit is contained in:
parent
3af69967e5
commit
bfcdac51b0
@ -15,13 +15,6 @@ payments:
|
||||
whitelists:
|
||||
pubkeys:
|
||||
- replace-with-your-pubkey-in-hex
|
||||
publication:
|
||||
- enabled: false
|
||||
description: Publication fee charged per event in msats (1000 msats = 1 satoshi)
|
||||
amount: 10
|
||||
whitelists:
|
||||
pubkeys:
|
||||
- replace-with-your-pubkey-in-hex
|
||||
paymentsProcessors:
|
||||
zebedee:
|
||||
baseURL: https://api.zebedee.io/
|
||||
@ -30,9 +23,8 @@ paymentsProcessors:
|
||||
- "3.225.112.64"
|
||||
- "::ffff:3.225.112.64"
|
||||
network:
|
||||
maxPayloadSize: 262144
|
||||
maxPayloadSize: 524288
|
||||
remoteIpHeader: x-forwarded-for
|
||||
idleTimeout: 60
|
||||
workers:
|
||||
count: 0
|
||||
mirroring:
|
||||
@ -41,11 +33,9 @@ limits:
|
||||
invoice:
|
||||
rateLimits:
|
||||
- period: 60000
|
||||
rate: 3
|
||||
rate: 6
|
||||
- period: 3600000
|
||||
rate: 10
|
||||
- period: 86400000
|
||||
rate: 20
|
||||
rate: 16
|
||||
ipWhitelist:
|
||||
- "::1"
|
||||
- "10.10.10.1"
|
||||
@ -53,13 +43,11 @@ limits:
|
||||
connection:
|
||||
rateLimits:
|
||||
- period: 1000
|
||||
rate: 6
|
||||
rate: 12
|
||||
- period: 60000
|
||||
rate: 30
|
||||
rate: 48
|
||||
- period: 3600000
|
||||
rate: 300
|
||||
- period: 86400000
|
||||
rate: 1440
|
||||
ipWhitelist:
|
||||
- "::1"
|
||||
- "10.10.10.1"
|
||||
@ -159,25 +147,12 @@ limits:
|
||||
maxFilters: 10
|
||||
message:
|
||||
rateLimits:
|
||||
# - description: 60 subscriptions/min
|
||||
# types:
|
||||
# - REQ
|
||||
# period: 60000
|
||||
# rate: 60
|
||||
# - description: 2880 subscriptions/hour
|
||||
# types:
|
||||
# - REQ
|
||||
# period: 3600000
|
||||
# rate: 2880
|
||||
- description: 120 raw messages/min
|
||||
- description: 240 raw messages/min
|
||||
period: 60000
|
||||
rate: 120
|
||||
rate: 240
|
||||
- description: 3600 raw messages/hour
|
||||
period: 3600000
|
||||
rate: 3600
|
||||
- description: 86400 raw messages/day
|
||||
period: 86400000
|
||||
rate: 86400
|
||||
rate: 4800
|
||||
ipWhitelist:
|
||||
- "::1"
|
||||
- "10.10.10.1"
|
||||
|
@ -106,6 +106,40 @@
|
||||
var expiresAt = "{{expires_at}}"
|
||||
var timeout
|
||||
var paid = false
|
||||
var fallbackTimeout
|
||||
|
||||
function getBackoffTime() {
|
||||
return 5000 + Math.floor(Math.random() * 5000)
|
||||
}
|
||||
|
||||
async function getInvoiceStatus() {
|
||||
fetch(`/invoices/${reference}/status`).then(async (response) => {
|
||||
const data = await response.json()
|
||||
console.log('data', data)
|
||||
const { status } = data;
|
||||
|
||||
if (status === 'pending') {
|
||||
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime())
|
||||
return
|
||||
} else if (status === 'expired') {
|
||||
hide('pending')
|
||||
show('expired')
|
||||
return
|
||||
}
|
||||
|
||||
paid = true
|
||||
|
||||
clearTimeout(timeout)
|
||||
|
||||
hide('pending')
|
||||
show('paid')
|
||||
}, (error) => {
|
||||
console.error('error fetching status', error)
|
||||
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime())
|
||||
})
|
||||
}
|
||||
|
||||
fallbackTimeout = setTimeout(getInvoiceStatus, getBackoffTime)
|
||||
|
||||
function connect() {
|
||||
var socket = new WebSocket(relayUrl)
|
||||
@ -137,22 +171,15 @@
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'EOSE': {
|
||||
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!paid && message[0] === 'EOSE' && message[1] === 'payment') {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (message.length !== 3 || message[0] !== 'EVENT' || message[1] !== 'payment') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
socket.onerror = console.error.bind(console)
|
||||
|
@ -147,6 +147,7 @@ export class WebSocketAdapter extends EventEmitter implements IWebSocketAdapter
|
||||
}
|
||||
|
||||
private async onClientMessage(raw: Buffer) {
|
||||
this.alive = true
|
||||
let abortable = false
|
||||
let messageHandler: IMessageHandler & IAbortable | undefined = undefined
|
||||
try {
|
||||
|
@ -14,7 +14,7 @@ import { WebServerAdapter } from './web-server-adapter'
|
||||
|
||||
const debug = createLogger('web-socket-server-adapter')
|
||||
|
||||
const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 60000
|
||||
const WSS_CLIENT_HEALTH_PROBE_INTERVAL = 120000
|
||||
|
||||
export class WebSocketServerAdapter extends WebServerAdapter implements IWebSocketServerAdapter {
|
||||
private webSocketsAdapters: WeakMap<WebSocket, IWebSocketAdapter>
|
||||
@ -51,7 +51,10 @@ export class WebSocketServerAdapter extends WebServerAdapter implements IWebSock
|
||||
debug('closing')
|
||||
clearInterval(this.heartbeatInterval)
|
||||
this.webSocketServer.clients.forEach((webSocket: WebSocket) => {
|
||||
debug('terminating client %s', this.webSocketsAdapters.get(webSocket).getClientId())
|
||||
const webSocketAdapter = this.webSocketsAdapters.get(webSocket)
|
||||
if (webSocketAdapter) {
|
||||
debug('terminating client %s: %s', webSocketAdapter.getClientId(), webSocketAdapter.getClientAddress())
|
||||
}
|
||||
webSocket.terminate()
|
||||
})
|
||||
debug('closing web socket server')
|
||||
|
@ -93,15 +93,16 @@ export class App implements IRunnable {
|
||||
}
|
||||
}
|
||||
|
||||
logCentered(`${workerCount} workers started`, width)
|
||||
logCentered(`${workerCount} client workers started`, width)
|
||||
logCentered('1 maintenance worker started', width)
|
||||
|
||||
debug('settings: %O', settings)
|
||||
|
||||
const host = `${hostname()}:${port}`
|
||||
addOnion(torHiddenServicePort, host).then(value=>{
|
||||
console.info(`tor hidden service address: ${value}:${torHiddenServicePort}`)
|
||||
logCentered(`Tor hidden service: ${value}:${torHiddenServicePort}`, width)
|
||||
}, () => {
|
||||
console.error('Unable to add Tor hidden service. Skipping.')
|
||||
logCentered('Tor hidden service: disabled', width)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,14 +19,16 @@ export class ZebedeeCallbackController implements IController {
|
||||
response: Response,
|
||||
) {
|
||||
debug('request headers: %o', request.headers)
|
||||
debug('request body: %o', request.body)
|
||||
debug('request body: %O', request.body)
|
||||
|
||||
const invoice = fromZebedeeInvoice(request.body)
|
||||
|
||||
debug('invoice', invoice)
|
||||
|
||||
try {
|
||||
await this.paymentsService.updateInvoice(invoice)
|
||||
if (!invoice.bolt11) {
|
||||
await this.paymentsService.updateInvoice(invoice)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Unable to persist invoice ${invoice.id}`, error)
|
||||
|
||||
|
50
src/controllers/invoices/get-invoice-status-controller.ts
Normal file
50
src/controllers/invoices/get-invoice-status-controller.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { IController } from '../../@types/controllers'
|
||||
import { IInvoiceRepository } from '../../@types/repositories'
|
||||
|
||||
export class GetInvoiceStatusController implements IController {
|
||||
public constructor(
|
||||
private readonly invoiceRepository: IInvoiceRepository,
|
||||
) {}
|
||||
|
||||
public async handleRequest(
|
||||
request: Request,
|
||||
response: Response,
|
||||
): Promise<void> {
|
||||
const invoiceId = request.params.invoiceId
|
||||
if (!invoiceId) {
|
||||
response
|
||||
.status(400)
|
||||
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||
.send('Invalid invoice')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const invoice = await this.invoiceRepository.findById(request.params.invoiceId)
|
||||
|
||||
if (!invoice) {
|
||||
response
|
||||
.status(404)
|
||||
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||
.send('Invoice not found')
|
||||
return
|
||||
}
|
||||
|
||||
response
|
||||
.status(200)
|
||||
.setHeader('content-type', 'application/json; charset=utf8')
|
||||
.send(JSON.stringify({
|
||||
id: invoice.id,
|
||||
status: invoice.status,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error(`get-invoice-status-controller: unable to get invoice ${invoiceId}:`, error)
|
||||
|
||||
response
|
||||
.status(500)
|
||||
.setHeader('content-type', 'text/plain; charset=utf8')
|
||||
.send('Unable to get invoice status')
|
||||
}
|
||||
}
|
||||
}
|
11
src/factories/get-invoice-status-controller-factory.ts
Normal file
11
src/factories/get-invoice-status-controller-factory.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { GetInvoiceStatusController } from '../controllers/invoices/get-invoice-status-controller'
|
||||
import { getReadReplicaDbClient } from '../database/client'
|
||||
import { InvoiceRepository } from '../repositories/invoice-repository'
|
||||
|
||||
export const createGetInvoiceStatusController = () => {
|
||||
const rrDbClient = getReadReplicaDbClient()
|
||||
|
||||
const invoiceRepository = new InvoiceRepository(rrDbClient)
|
||||
|
||||
return new GetInvoiceStatusController(invoiceRepository)
|
||||
}
|
45
src/factories/web-app-factory.ts
Normal file
45
src/factories/web-app-factory.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import express from 'express'
|
||||
import helmet from 'helmet'
|
||||
|
||||
import { createLogger } from './logger-factory'
|
||||
import { createSettings } from './settings-factory'
|
||||
import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware'
|
||||
import router from '../routes'
|
||||
|
||||
const debug = createLogger('web-app-factory')
|
||||
|
||||
export const createWebApp = () => {
|
||||
const app = express()
|
||||
app
|
||||
.disable('x-powered-by')
|
||||
.use(rateLimiterMiddleware)
|
||||
.use((req, res, next) => {
|
||||
const settings = createSettings()
|
||||
|
||||
const relayUrl = new URL(settings.info.relay_url)
|
||||
const webRelayUrl = new URL(relayUrl.toString())
|
||||
webRelayUrl.protocol = (relayUrl.protocol === 'wss:') ? 'https:' : ':'
|
||||
|
||||
const directives = {
|
||||
/**
|
||||
* TODO: Remove 'unsafe-inline'
|
||||
*/
|
||||
'img-src': ["'self'", 'data:', 'https://cdn.zebedee.io/an/nostr/'],
|
||||
'connect-src': ["'self'", settings.info.relay_url as string, webRelayUrl.toString()],
|
||||
'default-src': ['"self"'],
|
||||
'script-src-attr': ["'unsafe-inline'"],
|
||||
'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'],
|
||||
'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
|
||||
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
|
||||
}
|
||||
|
||||
debug('CSP directives: %o', directives)
|
||||
|
||||
return helmet.contentSecurityPolicy({ directives })(req, res, next)
|
||||
})
|
||||
.use('/favicon.ico', express.static('./resources/favicon.ico'))
|
||||
.use('/css', express.static('./resources/css'))
|
||||
.use(router)
|
||||
|
||||
return app
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
import { is, path, pathSatisfies } from 'ramda'
|
||||
import express from 'express'
|
||||
import helmet from 'helmet'
|
||||
import http from 'http'
|
||||
import process from 'process'
|
||||
import { WebSocketServer } from 'ws'
|
||||
@ -8,9 +6,8 @@ import { WebSocketServer } from 'ws'
|
||||
import { getMasterDbClient, getReadReplicaDbClient } from '../database/client'
|
||||
import { AppWorker } from '../app/worker'
|
||||
import { createSettings } from '../factories/settings-factory'
|
||||
import { createWebApp } from './web-app-factory'
|
||||
import { EventRepository } from '../repositories/event-repository'
|
||||
import { rateLimiterMiddleware } from '../handlers/request-handlers/rate-limiter-middleware'
|
||||
import router from '../routes'
|
||||
import { UserRepository } from '../repositories/user-repository'
|
||||
import { webSocketAdapterFactory } from './websocket-adapter-factory'
|
||||
import { WebSocketServerAdapter } from '../adapters/web-socket-server-adapter'
|
||||
@ -23,27 +20,7 @@ export const workerFactory = (): AppWorker => {
|
||||
|
||||
const settings = createSettings()
|
||||
|
||||
const app = express()
|
||||
app
|
||||
.disable('x-powered-by')
|
||||
.use(rateLimiterMiddleware)
|
||||
.use(helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
/**
|
||||
* TODO: Remove 'unsafe-inline'
|
||||
*/
|
||||
'img-src': ["'self'", 'https://cdn.zebedee.io/an/nostr/'],
|
||||
'connect-src': [settings.info.relay_url as string],
|
||||
'default-src': ['"self"'],
|
||||
'script-src-attr': ["'unsafe-inline'"],
|
||||
'script-src': ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net/npm/', 'https://unpkg.com/', 'https://cdnjs.cloudflare.com/ajax/libs/'],
|
||||
'style-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
|
||||
'font-src': ["'self'", 'https://cdn.jsdelivr.net/npm/'],
|
||||
},
|
||||
}))
|
||||
.use('/favicon.ico', express.static('./resources/favicon.ico'))
|
||||
.use('/css', express.static('./resources/css'))
|
||||
.use(router)
|
||||
const app = createWebApp()
|
||||
|
||||
// deepcode ignore HttpToHttps: we use proxies
|
||||
const server = http.createServer(app)
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
export const getHealthRequestHandler = (_req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(200).setHeader('content-type', 'text/plain; charset=utf8').send('OK')
|
||||
next()
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { createGetInvoiceStatusController } from '../../factories/get-invoice-status-controller-factory'
|
||||
|
||||
export const getInvoiceStatusRequestHandler = async (req: Request, res: Response) => {
|
||||
const controller = createGetInvoiceStatusController()
|
||||
|
||||
await controller.handleRequest(req, res)
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import express from 'express'
|
||||
|
||||
import callbacksRouter from './callbacks'
|
||||
import { getHealthRequestHandler } from '../handlers/request-handlers/get-health-request-handler'
|
||||
import { getTermsRequestHandler } from '../handlers/request-handlers/get-terms-request-handler'
|
||||
import invoiceRouter from './invoices'
|
||||
import { rootRequestHandler } from '../handlers/request-handlers/root-request-handler'
|
||||
@ -8,10 +9,10 @@ import { rootRequestHandler } from '../handlers/request-handlers/root-request-ha
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', rootRequestHandler)
|
||||
router.get('/healthz', getHealthRequestHandler)
|
||||
router.get('/terms', getTermsRequestHandler)
|
||||
|
||||
router.use('/invoices', invoiceRouter)
|
||||
router.use('/callbacks', callbacksRouter)
|
||||
|
||||
|
||||
export default router
|
||||
|
@ -2,6 +2,7 @@ import { Router, urlencoded } from 'express'
|
||||
import { createPaymentsProcessor } from '../../factories/payments-processor-factory'
|
||||
|
||||
import { getInvoiceRequestHandler } from '../../handlers/request-handlers/get-invoice-request-handler'
|
||||
import { getInvoiceStatusRequestHandler } from '../../handlers/request-handlers/get-invoice-status-request-handler'
|
||||
import { postInvoiceRequestHandler } from '../../handlers/request-handlers/post-invoice-request-handler'
|
||||
|
||||
const invoiceRouter = Router()
|
||||
@ -12,6 +13,7 @@ invoiceRouter
|
||||
next()
|
||||
})
|
||||
.get('/', getInvoiceRequestHandler)
|
||||
.get('/:invoiceId/status', getInvoiceStatusRequestHandler)
|
||||
.post('/', urlencoded({ extended: true }), postInvoiceRequestHandler)
|
||||
|
||||
export default invoiceRouter
|
||||
|
Loading…
x
Reference in New Issue
Block a user