first commit
This commit is contained in:
93
internal/http/docs/docs.go
Normal file
93
internal/http/docs/docs.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package docs
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed openapi.yaml
|
||||
var openapiYAML []byte
|
||||
|
||||
var openapiJSON []byte
|
||||
|
||||
func init() {
|
||||
var raw any
|
||||
if err := yaml.Unmarshal(openapiYAML, &raw); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
clean := convertMaps(raw)
|
||||
b, err := json.Marshal(clean)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
openapiJSON = b
|
||||
}
|
||||
|
||||
// convertMaps recursively converts map[interface{}]interface{} to map[string]interface{}.
|
||||
func convertMaps(in any) any {
|
||||
switch v := in.(type) {
|
||||
case map[any]any:
|
||||
m := make(map[string]any, len(v))
|
||||
for k, val := range v {
|
||||
m[toString(k)] = convertMaps(val)
|
||||
}
|
||||
return m
|
||||
case map[string]any:
|
||||
m := make(map[string]any, len(v))
|
||||
for k, val := range v {
|
||||
m[k] = convertMaps(val)
|
||||
}
|
||||
return m
|
||||
case []any:
|
||||
out := make([]any, len(v))
|
||||
for i, item := range v {
|
||||
out[i] = convertMaps(item)
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func toString(v any) string {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ServeJSON(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=300")
|
||||
_, _ = w.Write(openapiJSON)
|
||||
}
|
||||
|
||||
const swaggerHTML = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>NIP-05 API</title>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
window.onload = () => {
|
||||
window.ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: "#swagger-ui",
|
||||
deepLinking: true,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
func ServeUI(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(swaggerHTML))
|
||||
}
|
||||
297
internal/http/docs/openapi.yaml
Normal file
297
internal/http/docs/openapi.yaml
Normal file
@@ -0,0 +1,297 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: NIP-05 API
|
||||
description: Single-domain NIP-05 identity service with Lightning-paid registration.
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: /
|
||||
tags:
|
||||
- name: Public
|
||||
description: Anonymous, infrastructure-level endpoints. Cacheable, no auth.
|
||||
- name: User
|
||||
description: Anonymous flows for end users — lookup, availability, payment.
|
||||
- name: Admin
|
||||
description: Privileged operations. Require an `X-API-Key` header.
|
||||
components:
|
||||
securitySchemes:
|
||||
AdminAPIKey:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-Key
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
error: { type: string }
|
||||
detail: { type: string }
|
||||
Pricing:
|
||||
type: object
|
||||
properties:
|
||||
yearly_sats: { type: integer }
|
||||
lifetime_sats: { type: integer }
|
||||
lightning_enabled: { type: boolean }
|
||||
Invoice:
|
||||
type: object
|
||||
properties:
|
||||
payment_hash: { type: string }
|
||||
payment_request: { type: string }
|
||||
amount_sats: { type: integer }
|
||||
expires_at: { type: string, format: date-time }
|
||||
username: { type: string }
|
||||
is_renewal: { type: boolean }
|
||||
InvoiceStatus:
|
||||
type: object
|
||||
properties:
|
||||
payment_hash: { type: string }
|
||||
status: { type: string, enum: [pending, paid, expired] }
|
||||
username: { type: string }
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
pubkey: { type: string }
|
||||
npub: { type: string }
|
||||
username: { type: string }
|
||||
subscription_type: { type: string, enum: [yearly, lifetime] }
|
||||
is_active: { type: boolean }
|
||||
expires_at: { type: string, format: date-time, nullable: true }
|
||||
deactivated_at: { type: string, format: date-time, nullable: true }
|
||||
UserLookup:
|
||||
type: object
|
||||
properties:
|
||||
pubkey: { type: string }
|
||||
npub: { type: string }
|
||||
is_whitelisted: { type: boolean }
|
||||
username: { type: string }
|
||||
expires_at: { type: string, format: date-time, nullable: true }
|
||||
in_grace: { type: boolean }
|
||||
reserved_username: { type: string }
|
||||
expired_at: { type: string, format: date-time }
|
||||
paths:
|
||||
/.well-known/nostr.json:
|
||||
get:
|
||||
tags: [Public]
|
||||
summary: NIP-05 lookup
|
||||
parameters:
|
||||
- in: query
|
||||
name: name
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: NIP-05 names map
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
names:
|
||||
type: object
|
||||
additionalProperties: { type: string }
|
||||
relays:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: array
|
||||
items: { type: string }
|
||||
/healthz:
|
||||
get:
|
||||
tags: [Public]
|
||||
summary: Health check
|
||||
responses:
|
||||
'200': { description: OK }
|
||||
'503': { description: Down }
|
||||
/v1/pricing:
|
||||
get:
|
||||
tags: [Public]
|
||||
summary: Pricing info
|
||||
responses:
|
||||
'200':
|
||||
description: Pricing
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Pricing' }
|
||||
/v1/invoices:
|
||||
post:
|
||||
tags: [User]
|
||||
summary: Create payment invoice
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [pubkey]
|
||||
properties:
|
||||
pubkey: { type: string, description: "Hex pubkey or npub" }
|
||||
username:
|
||||
type: string
|
||||
description: "Optional. Auto-generated from the pubkey if omitted."
|
||||
subscription_type:
|
||||
type: string
|
||||
enum: [yearly, lifetime]
|
||||
description: "Optional. Defaults to lifetime."
|
||||
years:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: "Optional. Defaults to 1 when subscription_type is yearly; ignored for lifetime."
|
||||
responses:
|
||||
'200':
|
||||
description: Invoice
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/Invoice' }
|
||||
'400': { description: Validation error, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
|
||||
'403': { description: Forbidden — user already has lifetime access, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
|
||||
'409': { description: Conflict — username unavailable or pending invoice already exists, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
|
||||
'503': { description: Lightning unavailable, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
|
||||
/v1/invoices/{payment_hash}:
|
||||
get:
|
||||
tags: [User]
|
||||
summary: Invoice status
|
||||
parameters:
|
||||
- in: path
|
||||
name: payment_hash
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: Status
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/InvoiceStatus' }
|
||||
'404': { description: Not found }
|
||||
/v1/users/{pubkey}:
|
||||
get:
|
||||
tags: [User]
|
||||
summary: Lookup user by pubkey (npub or hex)
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: User lookup
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/UserLookup' }
|
||||
'404': { description: Never registered }
|
||||
/v1/usernames/{name}/available:
|
||||
get:
|
||||
tags: [User]
|
||||
summary: Username availability
|
||||
parameters:
|
||||
- in: path
|
||||
name: name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: Availability
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
username: { type: string }
|
||||
available: { type: boolean }
|
||||
/v1/admin/users:
|
||||
post:
|
||||
tags: [Admin]
|
||||
summary: Add user (admin)
|
||||
security: [{ AdminAPIKey: [] }]
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [pubkey, username, subscription_type]
|
||||
properties:
|
||||
pubkey: { type: string }
|
||||
username: { type: string }
|
||||
subscription_type: { type: string, enum: [yearly, lifetime] }
|
||||
years: { type: integer }
|
||||
responses:
|
||||
'201':
|
||||
description: Created
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/User' } } }
|
||||
'401': { description: Unauthorized }
|
||||
get:
|
||||
tags: [Admin]
|
||||
summary: List users (admin)
|
||||
security: [{ AdminAPIKey: [] }]
|
||||
parameters:
|
||||
- in: query
|
||||
name: active
|
||||
schema: { type: boolean }
|
||||
- in: query
|
||||
name: q
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: User list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: '#/components/schemas/User' }
|
||||
'401': { description: Unauthorized }
|
||||
/v1/admin/users/{pubkey}:
|
||||
put:
|
||||
tags: [Admin]
|
||||
summary: Update username (admin)
|
||||
security: [{ AdminAPIKey: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [username]
|
||||
properties:
|
||||
username: { type: string }
|
||||
responses:
|
||||
'200': { description: Updated }
|
||||
'401': { description: Unauthorized }
|
||||
'404': { description: Not found }
|
||||
'409': { description: Conflict }
|
||||
delete:
|
||||
tags: [Admin]
|
||||
summary: Delete user (admin)
|
||||
security: [{ AdminAPIKey: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Deleted }
|
||||
'401': { description: Unauthorized }
|
||||
'404': { description: Not found }
|
||||
/v1/admin/users/{pubkey}/extend:
|
||||
post:
|
||||
tags: [Admin]
|
||||
summary: Extend subscription (admin)
|
||||
security: [{ AdminAPIKey: [] }]
|
||||
parameters:
|
||||
- in: path
|
||||
name: pubkey
|
||||
required: true
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
years: { type: integer }
|
||||
subscription_type: { type: string, enum: [yearly, lifetime] }
|
||||
responses:
|
||||
'200': { description: Extended }
|
||||
'401': { description: Unauthorized }
|
||||
'404': { description: Not found }
|
||||
Reference in New Issue
Block a user