diff --git a/db/db.go b/db/db.go index fb5e8ca..3ff623a 100644 --- a/db/db.go +++ b/db/db.go @@ -29,6 +29,9 @@ type Card struct { One_time_code string Card_name string Allow_negative_balance string + Pin_enable string + Pin_number string + Pin_limit_sats int } type Payment struct { @@ -350,7 +353,8 @@ func Get_card_from_card_id(card_id int) (*Card, error) { `last_counter_value, lnurlw_request_timeout_sec, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` + `email_enable, email_address, card_name, ` + - `allow_negative_balance FROM cards WHERE card_id=$1;` + `allow_negative_balance, pin_enable, pin_number, ` + + `pin_limit_sats FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.Card_id, @@ -364,7 +368,10 @@ func Get_card_from_card_id(card_id int) (*Card, error) { &c.Email_enable, &c.Email_address, &c.Card_name, - &c.Allow_negative_balance) + &c.Allow_negative_balance, + &c.Pin_enable, + &c.Pin_number, + &c.Pin_limit_sats) if err != nil { return &c, err } @@ -385,7 +392,7 @@ func Get_card_from_card_name(card_name string) (*Card, error) { sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + ` last_counter_value, lnurlw_request_timeout_sec,` + - ` lnurlw_enable, tx_limit_sats, day_limit_sats` + + ` lnurlw_enable, tx_limit_sats, day_limit_sats, pin_enable, pin_limit_sats` + ` FROM cards WHERE card_name=$1 AND wiped = 'N';` row := db.QueryRow(sqlStatement, card_name) err = row.Scan( @@ -396,7 +403,9 @@ func Get_card_from_card_name(card_name string) (*Card, error) { &c.Lnurlw_request_timeout_sec, &c.Lnurlw_enable, &c.Tx_limit_sats, - &c.Day_limit_sats) + &c.Day_limit_sats, + &c.Pin_enable, + &c.Pin_limit_sats) if err != nil { return &c, err } @@ -773,7 +782,7 @@ func Get_card_name_count(card_name string) (card_count int, err error) { func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k3 string, k4 string, tx_limit_sats int, day_limit_sats int, lnurlw_enable bool, card_name string, uid_privacy bool, - allow_neg_bal_ptr bool) error { + allow_neg_bal_ptr bool, pin_enable bool, pin_number string, pin_limit_sats int) error { lnurlw_enable_yn := "N" if lnurlw_enable { @@ -790,6 +799,11 @@ func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k allow_neg_bal_yn = "Y" } + pin_enable_yn := "N" + if pin_enable { + pin_enable_yn = "Y" + } + db, err := open() if err != nil { return err @@ -811,11 +825,12 @@ func Insert_card(one_time_code string, k0_auth_key string, k2_cmac_key string, k sqlStatement = `INSERT INTO cards` + ` (one_time_code, k0_auth_key, k2_cmac_key, k3, k4, uid, last_counter_value,` + ` lnurlw_request_timeout_sec, tx_limit_sats, day_limit_sats, lnurlw_enable,` + - ` one_time_code_used, card_name, uid_privacy, allow_negative_balance)` + - ` VALUES ($1, $2, $3, $4, $5, '', 0, 60, $6, $7, $8, 'N', $9, $10, $11);` + ` one_time_code_used, card_name, uid_privacy, allow_negative_balance,` + + ` pin_enable, pin_number, pin_limit_sats)` + + ` VALUES ($1, $2, $3, $4, $5, '', 0, 60, $6, $7, $8, 'N', $9, $10, $11, $12, $13, $14);` res, err = db.Exec(sqlStatement, one_time_code, k0_auth_key, k2_cmac_key, k3, k4, tx_limit_sats, day_limit_sats, lnurlw_enable_yn, card_name, uid_privacy_yn, - allow_neg_bal_yn) + allow_neg_bal_yn, pin_enable_yn, pin_number, pin_limit_sats) if err != nil { return err } @@ -871,13 +886,19 @@ func Wipe_card(card_name string) (*Card_wipe_info, error) { return &card_wipe_info, nil } -func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int) error { +func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_limit_sats int, + pin_enable bool, pin_number string, pin_limit_sats int) error { lnurlw_enable_yn := "N" if lnurlw_enable { lnurlw_enable_yn = "Y" } + pin_enable_yn := "N" + if pin_enable { + pin_enable_yn = "Y" + } + db, err := open() if err != nil { @@ -886,10 +907,11 @@ func Update_card(card_name string, lnurlw_enable bool, tx_limit_sats int, day_li defer db.Close() - sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4 ` + - `WHERE card_name = $1 AND wiped = 'N';` + sqlStatement := `UPDATE cards SET lnurlw_enable = $2, tx_limit_sats = $3, day_limit_sats = $4, ` + + `pin_enable = $5, pin_number = $6, pin_limit_sats = $7 WHERE card_name = $1 AND wiped = 'N';` - res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats) + res, err := db.Exec(sqlStatement, card_name, lnurlw_enable_yn, tx_limit_sats, day_limit_sats, + pin_enable_yn, pin_number, pin_limit_sats) if err != nil { return err diff --git a/internalapi/createboltcard.go b/internalapi/createboltcard.go index ec4834a..ea9f913 100644 --- a/internalapi/createboltcard.go +++ b/internalapi/createboltcard.go @@ -83,12 +83,33 @@ func Createboltcard(w http.ResponseWriter, r *http.Request) { return } + pin_enable_flag_str := r.URL.Query().Get("enable_pin") + pin_enable_flag, err := strconv.ParseBool(pin_enable_flag_str) + if err != nil { + msg := "updateboltcard: enable_pin is not a valid boolean" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + pin_number := r.URL.Query().Get("pin_number") + + pin_limit_sats_str := r.URL.Query().Get("pin_limit_sats") + pin_limit_sats, err := strconv.Atoi(pin_limit_sats_str) + if err != nil { + msg := "updateboltcard: pin_limit_sats is not a valid integer" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + // log the request log.WithFields(log.Fields{ "card_name": card_name, "tx_max": tx_max, "day_max": day_max, "enable": enable_flag, "uid_privacy": uid_privacy_flag, - "allow_neg_bal": allow_neg_bal_flag}).Info("createboltcard API request") + "allow_neg_bal": allow_neg_bal_flag, "enable_pin": pin_enable_flag, + "pin_number": pin_number, "pin_limit_sats": pin_limit_sats}).Info("createboltcard API request") // create the keys @@ -102,7 +123,7 @@ func Createboltcard(w http.ResponseWriter, r *http.Request) { err = db.Insert_card(one_time_code, k0_auth_key, k2_cmac_key, k3, k4, tx_max, day_max, enable_flag, card_name, - uid_privacy_flag, allow_neg_bal_flag) + uid_privacy_flag, allow_neg_bal_flag, pin_enable_flag, pin_number, pin_limit_sats) if err != nil { log.Warn(err.Error()) return diff --git a/internalapi/getboltcard.go b/internalapi/getboltcard.go index aef2847..0103752 100644 --- a/internalapi/getboltcard.go +++ b/internalapi/getboltcard.go @@ -37,7 +37,9 @@ func Getboltcard(w http.ResponseWriter, r *http.Request) { `"uid": "` + c.Db_uid + `",` + `"lnurlw_enable": "` + c.Lnurlw_enable + `",` + `"tx_limit_sats": "` + strconv.Itoa(c.Tx_limit_sats) + `",` + - `"day_limit_sats": "` + strconv.Itoa(c.Day_limit_sats) + `"}`) + `"day_limit_sats": "` + strconv.Itoa(c.Day_limit_sats) + `", ` + + `"pin_enable": "` + c.Pin_enable + `", ` + + `"pin_limit_sats": "` + strconv.Itoa(c.Pin_limit_sats) + `"}`) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/internalapi/updateboltcard.go b/internalapi/updateboltcard.go index ae6ea20..ecc4f2b 100644 --- a/internalapi/updateboltcard.go +++ b/internalapi/updateboltcard.go @@ -43,6 +43,26 @@ func Updateboltcard(w http.ResponseWriter, r *http.Request) { return } + pin_enable_flag_str := r.URL.Query().Get("enable_pin") + pin_enable_flag, err := strconv.ParseBool(pin_enable_flag_str) + if err != nil { + msg := "updateboltcard: enable_pin is not a valid boolean" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + + pin_number := r.URL.Query().Get("pin_number") + + pin_limit_sats_str := r.URL.Query().Get("pin_limit_sats") + pin_limit_sats, err := strconv.Atoi(pin_limit_sats_str) + if err != nil { + msg := "updateboltcard: pin_limit_sats is not a valid integer" + log.Warn(msg) + resp_err.Write_message(w, msg) + return + } + card_name := r.URL.Query().Get("card_name") // check if card_name exists @@ -64,11 +84,12 @@ func Updateboltcard(w http.ResponseWriter, r *http.Request) { log.WithFields(log.Fields{ "card_name": card_name, "tx_max": tx_max, "day_max": day_max, - "enable": enable_flag}).Info("updateboltcard API request") + "enable": enable_flag, "enable_pin": pin_enable_flag, + "pin_number": pin_number, "pin_limit_sats": pin_limit_sats}).Info("updateboltcard API request") // update the card record - err = db.Update_card(card_name, enable_flag, tx_max, day_max) + err = db.Update_card(card_name, enable_flag, tx_max, day_max, pin_enable_flag, pin_number, pin_limit_sats) if err != nil { log.Warn(err.Error()) return diff --git a/lnurlw/lnurlw_callback.go b/lnurlw/lnurlw_callback.go index b5d5a61..f07e1b0 100644 --- a/lnurlw/lnurlw_callback.go +++ b/lnurlw/lnurlw_callback.go @@ -225,17 +225,15 @@ func Callback(w http.ResponseWriter, req *http.Request) { url := req.URL.RequestURI() log.WithFields(log.Fields{"url": url}).Debug("cb request") - // check k1 value - params_k1, ok := req.URL.Query()["k1"] + // get k1 value + param_k1 := req.URL.Query().Get("k1") - if !ok || len(params_k1[0]) < 1 { + if param_k1 == "" { log.WithFields(log.Fields{"url": url}).Debug("k1 not found") resp_err.Write(w) return } - param_k1 := params_k1[0] - p, err := db.Get_payment_k1(param_k1) if err != nil { log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err) @@ -263,14 +261,14 @@ func Callback(w http.ResponseWriter, req *http.Request) { return } - params_pr, ok := req.URL.Query()["pr"] - if !ok || len(params_pr[0]) < 1 { + // get the payment request + param_pr := req.URL.Query().Get("pr") + if param_pr == "" { log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("pr field not found") resp_err.Write(w) return } - param_pr := params_pr[0] bolt11, _ := decodepay.Decodepay(param_pr) // record the lightning invoice @@ -283,6 +281,23 @@ func Callback(w http.ResponseWriter, req *http.Request) { log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Debug("checking payment rules") + // get the pin if it has been passed in + param_pin := req.URL.Query().Get("pin") + + c, err := db.Get_card_from_card_id(p.Card_id) + if err != nil { + log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn(err) + resp_err.Write(w) + return + } + + // check the pin if needed + if c.Pin_enable == "Y" && int(bolt11.MSatoshi/1000) >= c.Pin_limit_sats && c.Pin_number != param_pin { + log.WithFields(log.Fields{"card_payment_id": p.Card_payment_id}).Warn("incorrect pin provided") + resp_err.Write(w) + return + } + // check if we are only sending funds to a defined test node testnode := db.Get_setting("LN_TESTNODE") if testnode != "" && bolt11.Payee != testnode { diff --git a/lnurlw/lnurlw_request.go b/lnurlw/lnurlw_request.go index 50bc025..eb0b15f 100644 --- a/lnurlw/lnurlw_request.go +++ b/lnurlw/lnurlw_request.go @@ -14,15 +14,6 @@ import ( "strings" ) -type ResponseData struct { - Tag string `json:"tag"` - Callback string `json:"callback"` - LnurlwK1 string `json:"k1"` - DefaultDescription string `json:"defaultDescription"` - MinWithdrawable int `json:"minWithdrawable"` - MaxWithdrawable int `json:"maxWithdrawable"` -} - func get_p_c(req *http.Request, p_name string, c_name string) (p string, c string) { params_p, ok := req.URL.Query()[p_name] @@ -255,6 +246,7 @@ func parse_request(req *http.Request) (int, error) { func Response(w http.ResponseWriter, req *http.Request) { env_host_domain := db.Get_setting("HOST_DOMAIN") + if req.Host != env_host_domain { log.Warn("wrong host domain") resp_err.Write(w) @@ -312,15 +304,29 @@ func Response(w http.ResponseWriter, req *http.Request) { return } - defalut_description := db.Get_setting("DEFAULT_DESCRIPTION") + // get pin_enable & pin_limit_sats - response := ResponseData{} - response.Tag = "withdrawRequest" - response.Callback = lnurlw_cb_url - response.LnurlwK1 = lnurlw_k1 - response.DefaultDescription = defalut_description - response.MinWithdrawable = min_withdraw_sats * 1000 // milliSats - response.MaxWithdrawable = max_withdraw_sats * 1000 // milliSats + c, err := db.Get_card_from_card_id(card_id) + if err != nil { + log.WithFields(log.Fields{"card_id": card_id}).Warn(err) + resp_err.Write(w) + return + } + + default_description := db.Get_setting("DEFAULT_DESCRIPTION") + + response := make(map[string]interface{}) + + response["tag"] = "withdrawRequest" + response["callback"] = lnurlw_cb_url + response["k1"] = lnurlw_k1 + response["defaultDescription"] = default_description + response["minWithdrawable"] = min_withdraw_sats * 1000 // milliSats + response["maxWithdrawable"] = max_withdraw_sats * 1000 // milliSats + + if c.Pin_enable == "Y" { + response["pinLimit"] = c.Pin_limit_sats * 1000 // milliSats + } jsonData, err := json.Marshal(response) diff --git a/script/s_create_db b/script/s_create_db index 1e0b7d8..aecb7b6 100755 --- a/script/s_create_db +++ b/script/s_create_db @@ -15,7 +15,7 @@ if [ "$x" = "y" ]; then psql postgres -f sql/create_db_init.sql psql postgres -f sql/create_db.sql psql postgres -f sql/create_db_user.sql - psql postgres -f sql/settings.sql + psql postgres -f sql/settings.sql.secret echo Database created else echo No action diff --git a/script/s_setup_test_data b/script/s_setup_test_data new file mode 100755 index 0000000..7426df8 --- /dev/null +++ b/script/s_setup_test_data @@ -0,0 +1,6 @@ +# to close any database connections +sudo systemctl stop postgresql +sudo systemctl start postgresql + +psql postgres -f sql/data.test.sql +echo Test data added diff --git a/sql/create_db.sql b/sql/create_db.sql index 03c80f7..6021a33 100644 --- a/sql/create_db.sql +++ b/sql/create_db.sql @@ -28,6 +28,9 @@ CREATE TABLE cards ( one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N', + pin_enable CHAR(1) NOT NULL DEFAULT 'N', + pin_number CHAR(4) NOT NULL DEFAULT '0000', + pin_limit_sats INT NOT NULL, wiped CHAR(1) NOT NULL DEFAULT 'N', PRIMARY KEY(card_id) ); diff --git a/sql/data.test.sql b/sql/data.test.sql new file mode 100644 index 0000000..cae568a --- /dev/null +++ b/sql/data.test.sql @@ -0,0 +1,24 @@ +-- connect to card_db +\c card_db; + +-- clear out table data +DELETE FROM settings; +DELETE FROM card_payments; +DELETE FROM card_receipts; +DELETE FROM cards; + +-- set up test data +INSERT INTO settings (name, value) VALUES ('LOG_LEVEL', 'DEBUG'); +INSERT INTO settings (name, value) VALUES ('AES_DECRYPT_KEY', '994de7f8156609a0effafbdb049337b1'); +INSERT INTO settings (name, value) VALUES ('HOST_DOMAIN', 'localhost:9000'); +INSERT INTO settings (name, value) VALUES ('FUNCTION_INTERNAL_API', 'ENABLE'); +INSERT INTO settings (name, value) VALUES ('MIN_WITHDRAW_SATS', '1'); +INSERT INTO settings (name, value) VALUES ('MAX_WITHDRAW_SATS', '1000'); + + +INSERT INTO cards + (k0_auth_key, k2_cmac_key, k3, k4, lnurlw_enable, last_counter_value, lnurlw_request_timeout_sec, + tx_limit_sats, day_limit_sats, card_name, pin_enable, pin_number, pin_limit_sats) + VALUES + ('', 'd3dffa1e12d2477e443a6ee9fcfeab18', '', '', 'Y', 0, 10, + 0, 0, 'test_card', 'Y', '1234', 1000);