Add Swagger docs at /docs and /openapi.json with split OpenAPI spec
Made-with: Cursor
This commit is contained in:
24
backend/openapi/components/schemas/claim.yaml
Normal file
24
backend/openapi/components/schemas/claim.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
QuoteResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
quote_id:
|
||||||
|
type: string
|
||||||
|
payout_sats:
|
||||||
|
type: integer
|
||||||
|
expires_at:
|
||||||
|
type: integer
|
||||||
|
ConfirmResult:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
already_consumed:
|
||||||
|
type: boolean
|
||||||
|
payout_sats:
|
||||||
|
type: integer
|
||||||
|
next_eligible_at:
|
||||||
|
type: integer
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
payment_hash:
|
||||||
|
type: string
|
||||||
12
backend/openapi/components/schemas/common.yaml
Normal file
12
backend/openapi/components/schemas/common.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
ApiError:
|
||||||
|
type: object
|
||||||
|
required: [code, message]
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
details:
|
||||||
|
type: string
|
||||||
|
next_eligible_at:
|
||||||
|
type: integer
|
||||||
64
backend/openapi/components/schemas/faucet.yaml
Normal file
64
backend/openapi/components/schemas/faucet.yaml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
FaucetConfig:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
faucetEnabled:
|
||||||
|
type: boolean
|
||||||
|
emergencyStop:
|
||||||
|
type: boolean
|
||||||
|
cooldownDays:
|
||||||
|
type: integer
|
||||||
|
minAccountAgeDays:
|
||||||
|
type: integer
|
||||||
|
minActivityScore:
|
||||||
|
type: integer
|
||||||
|
faucetMinSats:
|
||||||
|
type: integer
|
||||||
|
faucetMaxSats:
|
||||||
|
type: integer
|
||||||
|
Stats:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
balanceSats:
|
||||||
|
type: integer
|
||||||
|
totalPaidSats:
|
||||||
|
type: integer
|
||||||
|
totalClaims:
|
||||||
|
type: integer
|
||||||
|
claimsLast24h:
|
||||||
|
type: integer
|
||||||
|
dailyBudgetSats:
|
||||||
|
type: integer
|
||||||
|
spentTodaySats:
|
||||||
|
type: integer
|
||||||
|
recentPayouts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pubkey_prefix:
|
||||||
|
type: string
|
||||||
|
payout_sats:
|
||||||
|
type: integer
|
||||||
|
claimed_at:
|
||||||
|
type: integer
|
||||||
|
recentDeposits:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
amount_sats:
|
||||||
|
type: integer
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
enum: [lightning, cashu]
|
||||||
|
created_at:
|
||||||
|
type: integer
|
||||||
|
DepositInfo:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lightningAddress:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
lnurlp:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
9
backend/openapi/components/schemas/user.yaml
Normal file
9
backend/openapi/components/schemas/user.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
UserProfile:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
lightning_address:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
9
backend/openapi/components/security.yaml
Normal file
9
backend/openapi/components/security.yaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
NIP98:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: Nostr
|
||||||
|
description: NIP-98 HTTP Auth (Authorization Nostr <base64(json)>)
|
||||||
|
BearerAuth:
|
||||||
|
type: http
|
||||||
|
scheme: bearer
|
||||||
|
bearerFormat: JWT
|
||||||
60
backend/openapi/index.yaml
Normal file
60
backend/openapi/index.yaml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: LNFaucet API
|
||||||
|
description: |
|
||||||
|
Lightning Network faucet: request small amounts of sats for testing.
|
||||||
|
Auth via NIP-98 (Nostr) or npub-only (limited).
|
||||||
|
version: 1.0.0
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:3001
|
||||||
|
description: Local development
|
||||||
|
tags:
|
||||||
|
- name: Public
|
||||||
|
description: No authentication required
|
||||||
|
- name: Auth
|
||||||
|
description: NIP-98 or npub login
|
||||||
|
- name: Claim
|
||||||
|
description: Request sats (rate limited)
|
||||||
|
- name: User
|
||||||
|
description: Profile and refresh
|
||||||
|
paths:
|
||||||
|
/health:
|
||||||
|
$ref: './path-items/health.yaml'
|
||||||
|
/config:
|
||||||
|
$ref: './path-items/config.yaml'
|
||||||
|
/stats:
|
||||||
|
$ref: './path-items/stats.yaml'
|
||||||
|
/deposit:
|
||||||
|
$ref: './path-items/deposit.yaml'
|
||||||
|
/deposit/redeem-cashu:
|
||||||
|
$ref: './path-items/deposit-redeem-cashu.yaml'
|
||||||
|
/auth/login:
|
||||||
|
$ref: './path-items/auth-login.yaml'
|
||||||
|
/auth/login-npub:
|
||||||
|
$ref: './path-items/auth-login-npub.yaml'
|
||||||
|
/auth/me:
|
||||||
|
$ref: './path-items/auth-me.yaml'
|
||||||
|
/claim/quote:
|
||||||
|
$ref: './path-items/claim-quote.yaml'
|
||||||
|
/claim/confirm:
|
||||||
|
$ref: './path-items/claim-confirm.yaml'
|
||||||
|
/user/refresh-profile:
|
||||||
|
$ref: './path-items/user-refresh-profile.yaml'
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
$ref: './components/security.yaml'
|
||||||
|
schemas:
|
||||||
|
FaucetConfig:
|
||||||
|
$ref: './components/schemas/faucet.yaml#/FaucetConfig'
|
||||||
|
Stats:
|
||||||
|
$ref: './components/schemas/faucet.yaml#/Stats'
|
||||||
|
DepositInfo:
|
||||||
|
$ref: './components/schemas/faucet.yaml#/DepositInfo'
|
||||||
|
QuoteResult:
|
||||||
|
$ref: './components/schemas/claim.yaml#/QuoteResult'
|
||||||
|
ConfirmResult:
|
||||||
|
$ref: './components/schemas/claim.yaml#/ConfirmResult'
|
||||||
|
UserProfile:
|
||||||
|
$ref: './components/schemas/user.yaml#/UserProfile'
|
||||||
|
ApiError:
|
||||||
|
$ref: './components/schemas/common.yaml#/ApiError'
|
||||||
33
backend/openapi/path-items/auth-login-npub.yaml
Normal file
33
backend/openapi/path-items/auth-login-npub.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Sign in with npub only (limited)
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [npub]
|
||||||
|
properties:
|
||||||
|
npub:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Token and pubkey
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
pubkey:
|
||||||
|
type: string
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
example: npub
|
||||||
|
"400":
|
||||||
|
description: Invalid npub
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
20
backend/openapi/path-items/auth-login.yaml
Normal file
20
backend/openapi/path-items/auth-login.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
post:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Sign in with NIP-98 (returns JWT)
|
||||||
|
security:
|
||||||
|
- NIP98: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Token and pubkey
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
pubkey:
|
||||||
|
type: string
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
example: nip98
|
||||||
23
backend/openapi/path-items/auth-me.yaml
Normal file
23
backend/openapi/path-items/auth-me.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
get:
|
||||||
|
tags: [Auth]
|
||||||
|
summary: Current user from Bearer token
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Pubkey and method
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
pubkey:
|
||||||
|
type: string
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
"401":
|
||||||
|
description: Unauthorized or invalid token
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
39
backend/openapi/path-items/claim-confirm.yaml
Normal file
39
backend/openapi/path-items/claim-confirm.yaml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
post:
|
||||||
|
tags: [Claim]
|
||||||
|
summary: Confirm quote and pay (rate limited)
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [quote_id]
|
||||||
|
properties:
|
||||||
|
quote_id:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Success or already consumed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/claim.yaml#/ConfirmResult"
|
||||||
|
"400":
|
||||||
|
description: Invalid request
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
|
"404":
|
||||||
|
description: Quote not found or expired
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
|
"502":
|
||||||
|
description: Lightning payment failed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
28
backend/openapi/path-items/claim-quote.yaml
Normal file
28
backend/openapi/path-items/claim-quote.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
post:
|
||||||
|
tags: [Claim]
|
||||||
|
summary: Create claim quote (rate limited)
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [lightning_address]
|
||||||
|
properties:
|
||||||
|
lightning_address:
|
||||||
|
type: string
|
||||||
|
description: user@domain.tld
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Quote
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/claim.yaml#/QuoteResult"
|
||||||
|
"403":
|
||||||
|
description: Not eligible or budget exceeded
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
10
backend/openapi/path-items/config.yaml
Normal file
10
backend/openapi/path-items/config.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
get:
|
||||||
|
tags: [Public]
|
||||||
|
summary: Public faucet config
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Faucet configuration
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/faucet.yaml#/FaucetConfig"
|
||||||
48
backend/openapi/path-items/deposit-redeem-cashu.yaml
Normal file
48
backend/openapi/path-items/deposit-redeem-cashu.yaml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
post:
|
||||||
|
tags: [Public]
|
||||||
|
summary: Redeem Cashu token
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
required: [token]
|
||||||
|
properties:
|
||||||
|
token:
|
||||||
|
type: string
|
||||||
|
description: Cashu token (cashuA... or cashuB...)
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Redeem result
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
paid:
|
||||||
|
type: boolean
|
||||||
|
amount:
|
||||||
|
type: integer
|
||||||
|
invoiceAmount:
|
||||||
|
type: integer
|
||||||
|
netAmount:
|
||||||
|
type: integer
|
||||||
|
to:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
"400":
|
||||||
|
description: Invalid token or address
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
|
"502":
|
||||||
|
description: Redeem failed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
10
backend/openapi/path-items/deposit.yaml
Normal file
10
backend/openapi/path-items/deposit.yaml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
get:
|
||||||
|
tags: [Public]
|
||||||
|
summary: Deposit info
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Lightning address and LNURLp
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/faucet.yaml#/DepositInfo"
|
||||||
14
backend/openapi/path-items/health.yaml
Normal file
14
backend/openapi/path-items/health.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
get:
|
||||||
|
tags: [Public]
|
||||||
|
summary: Health check
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
example: ok
|
||||||
16
backend/openapi/path-items/stats.yaml
Normal file
16
backend/openapi/path-items/stats.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
get:
|
||||||
|
tags: [Public]
|
||||||
|
summary: Faucet stats
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Stats (balance, paid, claims)
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/faucet.yaml#/Stats"
|
||||||
|
"500":
|
||||||
|
description: Internal error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
18
backend/openapi/path-items/user-refresh-profile.yaml
Normal file
18
backend/openapi/path-items/user-refresh-profile.yaml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
post:
|
||||||
|
tags: [User]
|
||||||
|
summary: Refresh Nostr profile (kind 0)
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: lightning_address and name
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/user.yaml#/UserProfile"
|
||||||
|
"500":
|
||||||
|
description: Profile fetch failed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "../components/schemas/common.yaml#/ApiError"
|
||||||
197
backend/package-lock.json
generated
197
backend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "lnfaucet-backend",
|
"name": "lnfaucet-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "^12.1.0",
|
||||||
"better-sqlite3": "^11.6.0",
|
"better-sqlite3": "^11.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"nostr-tools": "^2.4.4",
|
"nostr-tools": "^2.4.4",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -23,11 +25,60 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@apidevtools/json-schema-ref-parser": {
|
||||||
|
"version": "14.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.1.tgz",
|
||||||
|
"integrity": "sha512-Oc96zvmxx1fqoSEdUmfmvvb59/KDOnUoJ7s2t7bISyAn0XEz57LCCw8k2Y4Pf3mwKaZLMciESALORLgfe2frCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/philsturgeon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/openapi-schemas": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-methods": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@apidevtools/swagger-parser": {
|
||||||
|
"version": "12.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-12.1.0.tgz",
|
||||||
|
"integrity": "sha512-e5mJoswsnAX0jG+J09xHFYQXb/bUc5S3pLpMxUuRUA2H8T2kni3yEoyz2R3Dltw5f4A6j6rPNMpWTK+iVDFlng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@apidevtools/json-schema-ref-parser": "14.0.1",
|
||||||
|
"@apidevtools/openapi-schemas": "^2.1.0",
|
||||||
|
"@apidevtools/swagger-methods": "^3.0.2",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-draft-04": "^1.0.0",
|
||||||
|
"call-me-maybe": "^1.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-types": ">=7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||||
@@ -509,6 +560,13 @@
|
|||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz",
|
||||||
@@ -619,6 +677,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json-schema": {
|
||||||
|
"version": "7.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -695,6 +759,17 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/swagger-ui-express": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*",
|
||||||
|
"@types/serve-static": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/uuid": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
@@ -715,6 +790,42 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
|
||||||
|
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-draft-04": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.5.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/array-flatten": {
|
"node_modules/array-flatten": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
@@ -858,6 +969,12 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-me-maybe": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||||
@@ -1185,6 +1302,28 @@
|
|||||||
"express": ">= 4.11"
|
"express": ">= 4.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/file-uri-to-path": {
|
"node_modules/file-uri-to-path": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||||
@@ -1422,6 +1561,24 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1622,6 +1779,13 @@
|
|||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-types": {
|
||||||
|
"version": "12.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||||
|
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -1883,6 +2047,15 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-pkg-maps": {
|
"node_modules/resolve-pkg-maps": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
@@ -2135,6 +2308,30 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swagger-ui-dist": {
|
||||||
|
"version": "5.32.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.0.tgz",
|
||||||
|
"integrity": "sha512-nKZB0OuDvacB0s/lC2gbge+RigYvGRGpLLMWMFxaTUwfM+CfndVk9Th2IaTinqXiz6Mn26GK2zriCpv6/+5m3Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/swagger-ui-express": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"swagger-ui-dist": ">=5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= v0.10.32"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">=4.0.0 || >=5.0.0-beta"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"migrate": "tsx src/db/migrate.ts"
|
"migrate": "tsx src/db/migrate.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@apidevtools/swagger-parser": "^12.1.0",
|
||||||
"better-sqlite3": "^11.6.0",
|
"better-sqlite3": "^11.6.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"express-rate-limit": "^7.4.1",
|
"express-rate-limit": "^7.4.1",
|
||||||
"nostr-tools": "^2.4.4",
|
"nostr-tools": "^2.4.4",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
|
"swagger-ui-express": "^5.0.1",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -25,6 +27,7 @@
|
|||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/node": "^22.9.0",
|
"@types/node": "^22.9.0",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.6.3"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import publicRoutes from "./routes/public.js";
|
|||||||
import authRoutes from "./routes/auth.js";
|
import authRoutes from "./routes/auth.js";
|
||||||
import claimRoutes from "./routes/claim.js";
|
import claimRoutes from "./routes/claim.js";
|
||||||
import userRoutes from "./routes/user.js";
|
import userRoutes from "./routes/user.js";
|
||||||
|
import docsRoutes from "./routes/docs.js";
|
||||||
|
|
||||||
const NONCE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
const NONCE_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
@@ -39,6 +40,7 @@ async function main() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.use("/", docsRoutes);
|
||||||
app.use("/", publicRoutes);
|
app.use("/", publicRoutes);
|
||||||
app.use("/auth", authRoutes);
|
app.use("/auth", authRoutes);
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
40
backend/src/routes/docs.ts
Normal file
40
backend/src/routes/docs.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import SwaggerParser from "@apidevtools/swagger-parser";
|
||||||
|
import swaggerUi from "swagger-ui-express";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const openApiPath = path.join(__dirname, "..", "..", "openapi", "index.yaml");
|
||||||
|
|
||||||
|
let cachedSpec: object | null = null;
|
||||||
|
|
||||||
|
async function getBundledSpec(): Promise<object> {
|
||||||
|
if (cachedSpec) return cachedSpec;
|
||||||
|
cachedSpec = (await SwaggerParser.bundle(openApiPath)) as object;
|
||||||
|
return cachedSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/openapi.json", async (_req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const spec = await getBundledSpec();
|
||||||
|
res.json(spec);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[docs] Failed to bundle OpenAPI spec:", err);
|
||||||
|
res.status(500).json({ code: "openapi_error", message: "Failed to load API spec" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.use(
|
||||||
|
"/docs",
|
||||||
|
swaggerUi.serve,
|
||||||
|
swaggerUi.setup(null, {
|
||||||
|
swaggerOptions: {
|
||||||
|
url: "/openapi.json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
Reference in New Issue
Block a user