Skip to content

Commit

Permalink
JSON API (#28)
Browse files Browse the repository at this point in the history
Co-authored-by: Raphael <[email protected]>
Co-authored-by: Raphael <[email protected]>
  • Loading branch information
3 people authored Oct 27, 2021
1 parent 3102708 commit a8fd253
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 12 deletions.
165 changes: 165 additions & 0 deletions api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package main

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"

"github.com/gorilla/mux"
)

type Response struct {
Ok bool `json:"ok"`
Message string `json:"message"`
Data interface{} `json:"data"`
}

type SuccessClaim struct {
Name string `json:"name"`
PIN string `json:"pin"`
Invoice string `json:"invoice"`
}

// not authenticated, if correct pin is provided call returns the SuccessClaim
func ClaimAddress(w http.ResponseWriter, r *http.Request) {
params := parseParams(r)
pin, inv, err := SaveName(params.Name, params, params.Pin)
if err != nil {
sendError(w, 400, "could not register name: %s", err.Error())
return
}

response := Response{
Ok: true,
Message: fmt.Sprintf("claimed %v@%v", params.Name, s.Domain),
Data: SuccessClaim{params.Name, pin, inv},
}

// TODO: middleware for responses that adds this header
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}

func GetUser(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
params, err := GetName(name)
if err != nil {
sendError(w, 400, err.Error())
return
}

// add pin to response because sometimes not saved in database; after first call to /api/v1/claim
params.Pin = ComputePIN(name)

response := Response{
Ok: true,
Message: fmt.Sprintf("%v@%v found", params.Name, s.Domain),
Data: params,
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}

func UpdateUser(w http.ResponseWriter, r *http.Request) {
params := parseParams(r)
name := mux.Vars(r)["name"]

// if pin not in json request body get it from header
if params.Pin == "" {
// TODO: work with Context()?
params.Pin = r.Header.Get("X-Pin")
}

if _, _, err := SaveName(name, params, params.Pin); err != nil {
sendError(w, 500, err.Error())
return
}

updatedParams, err := GetName(name)
if err != nil {
sendError(w, 500, err.Error())
return
}

// return the updated values or just http.StatusCreated?
response := Response{
Ok: true,
Message: fmt.Sprintf("updated %v@%v parameters", params.Name, s.Domain),
Data: updatedParams,
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(response)
}

func DeleteUser(w http.ResponseWriter, r *http.Request) {
name := mux.Vars(r)["name"]
if err := DeleteName(name); err != nil {
sendError(w, 500, err.Error())
return
}

response := Response{
Ok: true,
Message: fmt.Sprintf("deleted %v@%v", name, s.Domain),
Data: nil,
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
}

// authentication middleware
func authenticate(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// exempt /claim from authentication check;
if strings.HasPrefix(r.URL.Path, "/api/v1/claim") {
next.ServeHTTP(w, r)
return
}

name := mux.Vars(r)["name"]
providedPin := r.Header.Get("X-Pin")

var err error

if providedPin == "" {
err = fmt.Errorf("X-Pin header not provided")
// pin should always be passed in header but search in json request body anyways
providedPin = parseParams(r).Pin
}

if providedPin != ComputePIN(name) {
err = fmt.Errorf("wrong pin")
}

if err != nil {
sendError(w, 401, "error fetching user: %s", err.Error())
return
}

next.ServeHTTP(w, r)
})
}

// helpers
func sendError(w http.ResponseWriter, code int, msg string, args ...interface{}) {
b, _ := json.Marshal(Response{false, fmt.Sprintf(msg, args...), nil})
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(b)
}

func parseParams(r *http.Request) *Params {
reqBody, _ := ioutil.ReadAll(r.Body)
var params Params
json.Unmarshal(reqBody, &params)
return &params
}
42 changes: 32 additions & 10 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import (
)

type Params struct {
Name string
Kind string
Host string
Key string
Pak string
Waki string
Name string `json:"name"`
Kind string `json:"kind"`
Host string `json:"host"`
Key string `json:"key"`
Pak string `json:"pak"`
Waki string `json:"waki"`
Pin string `json:"pin"`
MinSendable string `json:"minSendable"`
MaxSendable string `json:"maxSendable"`
}

func SaveName(
Expand All @@ -29,16 +32,17 @@ func SaveName(
name = strings.ToLower(name)
key := []byte(name)

mac := hmac.New(sha256.New, []byte(s.Secret))
mac.Write([]byte(name + "@" + s.Domain))
pin = hex.EncodeToString(mac.Sum(nil))
pin = ComputePIN(name)

if _, closer, err := db.Get(key); err == nil {
defer closer.Close()
if pin != providedPin {
return "", "", errors.New("name already exists! must provide pin.")
return "", "", errors.New("name already exists! must provide pin")
}
}
if err != nil {
return "", "", errors.New("that name does not exist")
}

params.Name = name

Expand Down Expand Up @@ -73,3 +77,21 @@ func GetName(name string) (*Params, error) {
params.Name = name
return &params, nil
}

func DeleteName(name string) error {
name = strings.ToLower(name)
key := []byte(name)

if err := db.Delete(key, pebble.Sync); err != nil {
return err
}

return nil
}

func ComputePIN(name string) string {
name = strings.ToLower(name)
mac := hmac.New(sha256.New, []byte(s.Secret))
mac.Write([]byte(name + "@" + s.Domain))
return hex.EncodeToString(mac.Sum(nil))
}
15 changes: 13 additions & 2 deletions lnurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,22 @@ func handleLNURL(w http.ResponseWriter, r *http.Request) {
var commentLength int64 = 0
// TODO: support webhook comments

// convert configured sendable amounts to integer
minSendable, err := strconv.ParseInt(params.MinSendable, 10, 64)
// set defaults
if err != nil {
minSendable = 1000
}
maxSendable, err := strconv.ParseInt(params.MaxSendable, 10, 64)
if err != nil {
maxSendable = 100000000
}

json.NewEncoder(w).Encode(lnurl.LNURLPayResponse1{
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
Callback: fmt.Sprintf("https://%s/.well-known/lnurlp/%s", s.Domain, username),
MinSendable: 1000,
MaxSendable: 100000000,
MinSendable: minSendable,
MaxSendable: maxSendable,
EncodedMetadata: makeMetadata(params),
CommentAllowed: commentLength,
Tag: "payRequest",
Expand Down
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ func main() {
},
)

api := router.PathPrefix("/api/v1").Subrouter()
api.Use(authenticate)

// unauthenticated
api.HandleFunc("/claim", ClaimAddress).Methods("POST")

// authenticated routes; X-Pin in header or in json request body
api.HandleFunc("/users/{name}", GetUser).Methods("GET")
api.HandleFunc("/users/{name}", UpdateUser).Methods("PUT")
api.HandleFunc("/users/{name}", DeleteUser).Methods("DELETE")

srv := &http.Server{
Handler: router,
Addr: s.Host + ":" + s.Port,
Expand Down

0 comments on commit a8fd253

Please sign in to comment.