You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

448 lines
12 KiB

package app
import (
"encoding/json"
"net/http"
"strings"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
"git.mleku.dev/mleku/nostr/crypto/keys"
"git.mleku.dev/mleku/nostr/encoders/hex"
"git.mleku.dev/mleku/nostr/httpauth"
"next.orly.dev/pkg/acl"
"next.orly.dev/pkg/database"
)
// getCashuMintURL returns the Cashu mint URL based on relay configuration.
// Returns empty string if Cashu is not enabled.
func (s *Server) getCashuMintURL() string {
if !s.Config.CashuEnabled || s.CashuIssuer == nil {
return ""
}
// Use configured relay URL with /cashu/mint path
relayURL := strings.TrimSuffix(s.Config.RelayURL, "/")
if relayURL == "" {
return ""
}
return relayURL + "/cashu/mint"
}
// NRCConnectionResponse is the response structure for NRC connection API.
type NRCConnectionResponse struct {
ID string `json:"id"`
Label string `json:"label"`
CreatedAt int64 `json:"created_at"`
LastUsed int64 `json:"last_used"`
UseCashu bool `json:"use_cashu"`
URI string `json:"uri,omitempty"` // Only included when specifically requested
}
// NRCConnectionsResponse is the response for listing all connections.
type NRCConnectionsResponse struct {
Connections []NRCConnectionResponse `json:"connections"`
Config NRCConfigResponse `json:"config"`
}
// NRCConfigResponse contains NRC configuration status.
type NRCConfigResponse struct {
Enabled bool `json:"enabled"`
RendezvousURL string `json:"rendezvous_url"`
MintURL string `json:"mint_url,omitempty"`
RelayPubkey string `json:"relay_pubkey"`
}
// NRCCreateRequest is the request body for creating a connection.
type NRCCreateRequest struct {
Label string `json:"label"`
UseCashu bool `json:"use_cashu"`
}
// handleNRCConnections handles GET /api/nrc/connections
func (s *Server) handleNRCConnections(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Get all connections
conns, err := badgerDB.GetAllNRCConnections()
if chk.E(err) {
http.Error(w, "Failed to get connections", http.StatusInternalServerError)
return
}
// Get relay identity for config
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
nrcEnabled, nrcRendezvousURL, _, nrcUseCashu, _ := s.Config.GetNRCConfigValues()
// Build response
response := NRCConnectionsResponse{
Connections: make([]NRCConnectionResponse, 0, len(conns)),
Config: NRCConfigResponse{
Enabled: nrcEnabled,
RendezvousURL: nrcRendezvousURL,
RelayPubkey: string(hex.Enc(relayPubkey)),
},
}
// Add mint URL if Cashu is enabled
mintURL := s.getCashuMintURL()
if nrcUseCashu && mintURL != "" {
response.Config.MintURL = mintURL
}
for _, conn := range conns {
response.Connections = append(response.Connections, NRCConnectionResponse{
ID: conn.ID,
Label: conn.Label,
CreatedAt: conn.CreatedAt,
LastUsed: conn.LastUsed,
UseCashu: conn.UseCashu,
})
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleNRCCreate handles POST /api/nrc/connections
func (s *Server) handleNRCCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Parse request body
var req NRCCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
// Validate label
req.Label = strings.TrimSpace(req.Label)
if req.Label == "" {
http.Error(w, "Label is required", http.StatusBadRequest)
return
}
// Create the connection
conn, err := badgerDB.CreateNRCConnection(req.Label, req.UseCashu)
if chk.E(err) {
http.Error(w, "Failed to create connection", http.StatusInternalServerError)
return
}
// Get relay identity for URI generation
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
_, nrcRendezvousURL, _, nrcUseCashu, _ := s.Config.GetNRCConfigValues()
// Get mint URL if Cashu enabled
mintURL := ""
if nrcUseCashu {
mintURL = s.getCashuMintURL()
}
// Generate URI
uri, err := badgerDB.GetNRCConnectionURI(conn, relayPubkey, nrcRendezvousURL, mintURL)
if chk.E(err) {
log.W.F("failed to generate URI for new connection: %v", err)
}
// Update bridge authorized secrets if bridge is running
s.updateNRCBridgeSecrets(badgerDB)
// Build response with URI
response := NRCConnectionResponse{
ID: conn.ID,
Label: conn.Label,
CreatedAt: conn.CreatedAt,
LastUsed: conn.LastUsed,
UseCashu: conn.UseCashu,
URI: uri,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}
// handleNRCDelete handles DELETE /api/nrc/connections/{id}
func (s *Server) handleNRCDelete(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Extract connection ID from URL path
// URL format: /api/nrc/connections/{id}
path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
connID := strings.TrimSpace(path)
if connID == "" {
http.Error(w, "Connection ID required", http.StatusBadRequest)
return
}
// Delete the connection
if err := badgerDB.DeleteNRCConnection(connID); chk.E(err) {
http.Error(w, "Failed to delete connection", http.StatusInternalServerError)
return
}
// Update bridge authorized secrets if bridge is running
s.updateNRCBridgeSecrets(badgerDB)
log.I.F("deleted NRC connection: %s", connID)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
// handleNRCGetURI handles GET /api/nrc/connections/{id}/uri
func (s *Server) handleNRCGetURI(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Validate NIP-98 authentication
valid, pubkey, err := httpauth.CheckAuth(r)
if chk.E(err) || !valid {
errorMsg := "NIP-98 authentication validation failed"
if err != nil {
errorMsg = err.Error()
}
http.Error(w, errorMsg, http.StatusUnauthorized)
return
}
// Check permissions - require owner level
accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
if accessLevel != "owner" {
http.Error(w, "Owner permission required", http.StatusForbidden)
return
}
// Get database (must be Badger)
badgerDB, ok := s.DB.(*database.D)
if !ok {
http.Error(w, "NRC requires Badger database backend", http.StatusServiceUnavailable)
return
}
// Extract connection ID from URL path
// URL format: /api/nrc/connections/{id}/uri
path := strings.TrimPrefix(r.URL.Path, "/api/nrc/connections/")
path = strings.TrimSuffix(path, "/uri")
connID := strings.TrimSpace(path)
if connID == "" {
http.Error(w, "Connection ID required", http.StatusBadRequest)
return
}
// Get the connection
conn, err := badgerDB.GetNRCConnection(connID)
if err != nil {
http.Error(w, "Connection not found", http.StatusNotFound)
return
}
// Get relay identity
relaySecretKey, err := s.DB.GetOrCreateRelayIdentitySecret()
if chk.E(err) {
http.Error(w, "Failed to get relay identity", http.StatusInternalServerError)
return
}
relayPubkey, _ := keys.SecretBytesToPubKeyBytes(relaySecretKey)
// Get NRC config values
_, nrcRendezvousURL, _, nrcUseCashu, _ := s.Config.GetNRCConfigValues()
// Get mint URL if Cashu enabled
mintURL := ""
if nrcUseCashu {
mintURL = s.getCashuMintURL()
}
// Generate URI
uri, err := badgerDB.GetNRCConnectionURI(conn, relayPubkey, nrcRendezvousURL, mintURL)
if chk.E(err) {
http.Error(w, "Failed to generate URI", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"uri": uri})
}
// updateNRCBridgeSecrets updates the NRC bridge with current authorized secrets from database.
func (s *Server) updateNRCBridgeSecrets(badgerDB *database.D) {
if s.nrcBridge == nil {
return
}
secrets, err := badgerDB.GetNRCAuthorizedSecrets()
if chk.E(err) {
log.W.F("failed to get NRC authorized secrets: %v", err)
return
}
s.nrcBridge.UpdateAuthorizedSecrets(secrets)
log.D.F("updated NRC bridge with %d authorized secrets", len(secrets))
}
// handleNRCConnectionsRouter routes NRC connection requests.
func (s *Server) handleNRCConnectionsRouter(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
// Exact match for /api/nrc/connections
if path == "/api/nrc/connections" {
switch r.Method {
case http.MethodGet:
s.handleNRCConnections(w, r)
case http.MethodPost:
s.handleNRCCreate(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return
}
// Check for /api/nrc/connections/{id}/uri
if strings.HasSuffix(path, "/uri") {
s.handleNRCGetURI(w, r)
return
}
// Otherwise it's /api/nrc/connections/{id}
s.handleNRCDelete(w, r)
}
// handleNRCConfig returns NRC configuration status.
func (s *Server) handleNRCConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Get NRC config values
nrcEnabled, nrcRendezvousURL, _, nrcUseCashu, _ := s.Config.GetNRCConfigValues()
// Check if Badger is available (NRC requires Badger)
_, badgerAvailable := s.DB.(*database.D)
response := struct {
Enabled bool `json:"enabled"`
BadgerRequired bool `json:"badger_required"`
RendezvousURL string `json:"rendezvous_url,omitempty"`
UseCashu bool `json:"use_cashu"`
MintURL string `json:"mint_url,omitempty"`
}{
Enabled: nrcEnabled && badgerAvailable,
BadgerRequired: !badgerAvailable,
RendezvousURL: nrcRendezvousURL,
UseCashu: nrcUseCashu,
}
// Add mint URL if Cashu is enabled
if nrcUseCashu {
mintURL := s.getCashuMintURL()
if mintURL != "" {
response.MintURL = mintURL
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}