merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 29dec4cf538daf22690386514387eb8a066b6872
parent e83e69a248ec1451e19bb233c6c2c5a825b67b66
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 24 Dec 2015 21:30:40 +0100

Merge branch 'master' of ssh://taler.net:/var/git/merchant

Diffstat:
Msrc/backend/merchant.conf | 2+-
Msrc/backend/taler-merchant-httpd_contract.c | 4++--
Msrc/backend/taler-merchant-httpd_mints.c | 10++++++++++
Msrc/backend/taler-merchant-httpd_pay.c | 4++++
Msrc/backenddb/plugin_merchantdb_postgres.c | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/frontend/checkout.php | 5+++--
Asrc/frontend/execute.js | 32++++++++++++++++++++++++++++++++
Msrc/frontend/execute.php | 16+++-------------
Asrc/frontend/execute.tsx | 40++++++++++++++++++++++++++++++++++++++++
Msrc/frontend/fulfillment.php | 4++--
Msrc/frontend/generate_taler_contract.php | 46++++++++++++++++++----------------------------
Msrc/frontend/index.html | 16++++------------
Msrc/frontend/pay.php | 29+++++++++++++++++++++++++----
Msrc/include/taler_merchantdb_plugin.h | 15++++++++++++++-
Asrc/tsconfig.json | 10++++++++++
15 files changed, 240 insertions(+), 66 deletions(-)

diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -25,7 +25,7 @@ EDATE = 3 week DB = postgres [mint-taler] -URI = mint.demo.taler.net +URI = http://mint.demo.taler.net/ MASTER_KEY = Q1WVGRGC1F4W7RYC6M23AEGFEXQEHQ730K3GG0B67VPHQSRR75H0 # Auditors must be in sections "auditor-", the rest of the section diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c @@ -103,13 +103,13 @@ MH_handler_contract (struct TMH_RequestHandler *rh, &contract.purpose, &contract_sig); - pay_url = json_object_get (root, "pay_url"); + pay_url = json_object_get (jcontract, "pay_url"); if (NULL == pay_url) { return TMH_RESPONSE_reply_internal_error (connection, "pay url missing"); } - exec_url = json_object_get (root, "exec_url"); + exec_url = json_object_get (jcontract, "exec_url"); if (NULL == exec_url) { return TMH_RESPONSE_reply_internal_error (connection, diff --git a/src/backend/taler-merchant-httpd_mints.c b/src/backend/taler-merchant-httpd_mints.c @@ -230,6 +230,8 @@ context_task (void *cls, struct GNUNET_NETWORK_FDSet *ws; struct GNUNET_TIME_Relative delay; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "In mint context polling task\n"); + poller_task = NULL; TALER_MINT_perform (ctx); max_fd = -1; @@ -243,6 +245,9 @@ context_task (void *cls, &except_fd_set, &max_fd, &timeout); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "In mint context polling task, max_fd=%d, timeout=%ld\n", + max_fd, timeout); if (timeout >= 0) delay = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, @@ -319,6 +324,11 @@ TMH_MINTS_find_mint (const char *chosen_mint, GNUNET_break (0); return NULL; } + + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Trying to find chosen mint `%s'\n", + chosen_mint); + /* Check if the mint is known */ for (mint = mint_head; NULL != mint; mint = mint->next) /* test it by checking public key --- FIXME: hostname or public key!? diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c @@ -710,6 +710,10 @@ MH_handler_pay (struct TMH_RequestHandler *rh, } } + /* Check if this payment attempt has already taken place */ + if (GNUNET_OK == db->check_payment (db->cls, pc->transaction_id)) + return TMH_RESPONSE_reply_external_error (connection, "payment already attempted"); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Looking up chosen mint '%s'\n", pc->chosen_mint); /* Find the responsible mint, this may take a while... */ diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -70,7 +70,7 @@ postgres_initialize (void *cls, "CREATE %1$s TABLE IF NOT EXISTS payments (" "h_contract BYTEA NOT NULL," "h_wire BYTEA NOT NULL," - "transaction_id INT8 PRIMARY KEY," + "transaction_id INT8," /*WARNING: this column used to be primary key, but that wrong since multiple coins belong to the same id*/ "timestamp INT8 NOT NULL," "refund_deadline INT8 NOT NULL," "amount_without_fee_val INT8 NOT NULL," @@ -108,6 +108,22 @@ postgres_initialize (void *cls, } return GNUNET_SYSERR; } + if ( (NULL == (res = PQprepare (pg->conn, + "check_payment", + "SELECT * " + "FROM payments " + "WHERE transaction_id=$1", + 1, NULL))) || + (PGRES_COMMAND_OK != (status = PQresultStatus(res))) ) + { + if (NULL != res) + { + PQSQL_strerror (GNUNET_ERROR_TYPE_ERROR, "PQprepare", res); + PQclear (res); + } + return GNUNET_SYSERR; + } + PQclear (res); return GNUNET_OK; } @@ -190,7 +206,61 @@ postgres_store_payment (void *cls, return GNUNET_OK; } +/** + * Check whether a payment has already been stored + * + * @param cls our plugin handle + * @param transaction_id the transaction id to search into + * the db + * + * @return GNUNET_OK if found, GNUNET_NO if not, GNUNET_SYSERR + * upon error + */ +static int +postgres_check_payment(void *cls, + uint64_t transaction_id) +{ + struct PostgresClosure *pg = cls; + PGresult *res; + ExecStatusType status; + struct TALER_PQ_QueryParam params[] = { + TALER_PQ_query_param_uint64 (&transaction_id), + TALER_PQ_query_param_end + }; + res = TALER_PQ_exec_prepared (pg->conn, + "check_payment", + params); + + status = PQresultStatus (res); + if (PGRES_TUPLES_OK != status) + { + const char *sqlstate; + + sqlstate = PQresultErrorField (res, PG_DIAG_SQLSTATE); + if (NULL == sqlstate) + { + /* very unexpected... */ + GNUNET_break (0); + PQclear (res); + return GNUNET_SYSERR; + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not check if contract %llu is in DB: %s\n", + transaction_id, + sqlstate); + PQclear (res); + return GNUNET_SYSERR; + } + /* count rows */ + if (PQntuples (res) > 0) + return GNUNET_OK; + return GNUNET_NO; + + + +} /** * Initialize Postgres database subsystem. * @@ -220,6 +290,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) plugin->cls = pg; plugin->initialize = &postgres_initialize; plugin->store_payment = &postgres_store_payment; + plugin->check_payment = &postgres_check_payment; return plugin; } diff --git a/src/frontend/checkout.php b/src/frontend/checkout.php @@ -55,6 +55,7 @@ // create PHP session and store donation information in session $donation_fraction = (float) ("0." . $donation_fraction); session_start(); + session_unset(); $_SESSION['receiver'] = $donation_receiver; $_SESSION['amount_value'] = (int) $donation_amount; $_SESSION['amount_fraction'] = (int) ($donation_fraction * 1000000); @@ -64,8 +65,8 @@ <header> <div id="logo"> <svg height="100" width="100"> - <circle cx="50" cy="50" r="40" stroke="black" stroke-width="6" fill="white" /> - <text x="19" y="82" font-family="Verdana" font-size="90" fill="black">S</text> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> </svg> </div> diff --git a/src/frontend/execute.js b/src/frontend/execute.js @@ -0,0 +1,32 @@ +"use strict"; +// JSX literals are compiled to calls to React.createElement calls. +let React = { + createElement: function (tag, props, ...children) { + let e = document.createElement(tag); + for (let k in props) { + e.setAttribute(k, props[k]); + } + for (let child of children) { + if ("string" === typeof child || "number" == typeof child) { + child = document.createTextNode(child); + } + e.appendChild(child); + } + return e; + } +}; +document.addEventListener("DOMContentLoaded", function (e) { + var eve = new CustomEvent('taler-execute-payment', { detail: { H_contract: h_contract } }); + document.dispatchEvent(eve); +}); +function replace(el, r) { + el.parentNode.replaceChild(r, el); +} +document.addEventListener("taler-payment-result", function (e) { + if (!e.detail.success) { + alert("Payment failed\n" + JSON.stringify(e.detail)); + } + console.log("finished payment"); + let msg = React.createElement("div", null, "Payment successful. View your ", React.createElement("a", {"href": e.detail.fulfillmentUrl}, "product"), "."); + replace(document.getElementById("loading"), msg); +}); diff --git a/src/frontend/execute.php b/src/frontend/execute.php @@ -31,26 +31,16 @@ session_start(); echo "var h_contract=\"$_SESSION[H_contract]\";\n"; ?> -document.addEventListener("DOMContentLoaded", function (e) { - var eve = new CustomEvent('taler-execute-payment', {detail: {H_contract: h_contract}}); - document.dispatchEvent(eve); -}); -document.addEventListener("taler-payment-result", function (e) { - if (!e.detail.success) { - alert("Payment failed\n" + JSON.strinfigy(e.detail)); - } - console.log("finished payment"); - document.getElementById("loading").innerHTML = "success!"; -}); </script> + <script type="text/javascript" src="execute.js"></script> </head> <body> <header> <div id="logo"> <svg height="100" width="100"> - <circle cx="50" cy="50" r="40" stroke="black" stroke-width="6" fill="white" /> - <text x="19" y="82" font-family="Verdana" font-size="90" fill="black">S</text> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> </svg> </div> <h1>Toy Store - Taler Demo</h1> diff --git a/src/frontend/execute.tsx b/src/frontend/execute.tsx @@ -0,0 +1,40 @@ +"use strict"; + + +// JSX literals are compiled to calls to React.createElement calls. +let React = { + createElement: function(tag, props, ...children) { + let e = document.createElement(tag); + for (let k in props) { + e.setAttribute(k, props[k]); + } + for (let child of children) { + if ("string" === typeof child || "number" == typeof child) { + child = document.createTextNode(child); + } + e.appendChild(child); + } + return e; + } +}; + +declare var h_contract: string; + +document.addEventListener("DOMContentLoaded", function (e) { + var eve = new CustomEvent('taler-execute-payment', {detail: {H_contract: h_contract}}); + document.dispatchEvent(eve); +}); + +function replace(el, r) { + el.parentNode.replaceChild(r, el); +} + +document.addEventListener("taler-payment-result", function (e: CustomEvent) { + if (!e.detail.success) { + alert("Payment failed\n" + JSON.stringify(e.detail)); + } + console.log("finished payment"); + let msg = + <div>Payment successful. View your <a href={e.detail.fulfillmentUrl}>product</a>.</div>; + replace(document.getElementById("loading"), msg); +}); diff --git a/src/frontend/fulfillment.php b/src/frontend/fulfillment.php @@ -9,8 +9,8 @@ <header> <div id="logo"> <svg height="100" width="100"> - <circle cx="50" cy="50" r="40" stroke="black" stroke-width="6" fill="white" /> - <text x="19" y="82" font-family="Verdana" font-size="90" fill="black">S</text> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> </svg> </div> diff --git a/src/frontend/generate_taler_contract.php b/src/frontend/generate_taler_contract.php @@ -30,18 +30,7 @@ if the whole "journey" to the backend is begin tested - $ curl http://merchant_url/generate_taler_contract.php?backend_test=no if just the frontend job is being tested - */ - - -register_shutdown_function(function() { - $lastError = error_get_last(); - - if (!empty($lastError) && $lastError['type'] == E_ERROR) { - header('Status: 500 Internal Server Error'); - header('HTTP/1.0 500 Internal Server Error'); - } -}); - +*/ $cli_debug = false; $backend_test = true; @@ -61,6 +50,7 @@ if (!$cli_debug && (! isset($_SESSION['receiver']))) { http_response_code (404); echo "Please select a contract before getting to this page..."; + echo "attempted : " . $_SESSION['receiver']; exit (0); } @@ -100,6 +90,9 @@ $teatax = array ('value' => 1, // Take a timestamp $now = new DateTime('now'); +$PAY_URL = "pay.php"; +$EXEC_URL = "execute.php"; + // pack the JSON for the contract // --- FIXME: exact format needs review! $contract = array ('amount' => array ('value' => $amount_value, @@ -120,6 +113,8 @@ $contract = array ('amount' => array ('value' => $amount_value, 'delivery_date' => "Some Date Format", 'delivery_location' => 'LNAME1')), 'timestamp' => "/Date(" . $now->getTimestamp() . ")/", + 'pay_url' => $PAY_URL, + 'exec_url' => $EXEC_URL, 'expiry' => "/Date(" . $now->add(new DateInterval('P2W'))->getTimestamp() . ")/", 'refund_deadline' => "/Date(" . $now->add(new DateInterval('P3M'))->getTimestamp() . ")/", 'merchant' => array ('address' => 'LNAME2', @@ -147,9 +142,8 @@ $contract = array ('amount' => array ('value' => $amount_value, 'state' => 'Test State', 'region' => 'Test Region', 'province' => 'Test Province', - 'ZIP code' => 4908))); - -$json = json_encode (array ("contract" => $contract, "pay_url" => "pay.php", "exec_url" => "execute.php")); + 'ZIP code' => 4908))); +$json = json_encode (array ('contract' => $contract, 'exec_url' => $EXEC_URL, 'pay_url' => $PAY_URL), JSON_PRETTY_PRINT); if ($cli_debug && !$backend_test) { echo $json . "\n"; @@ -157,14 +151,13 @@ if ($cli_debug && !$backend_test) } - -// Backend is relative to the shop site. -$url = (new http\URL("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]")) +$url = (new http\URL("http://".$_SERVER["HTTP_HOST"])) ->mod(array ("path" => "backend/contract"), http\Url::JOIN_PATH); -$req = new http\Client\Request ("POST", - $url, - array ("Content-Type" => "application/json")); +$req = new http\Client\Request("POST", + $url, + array ("Content-Type" => "application/json")); + $req->getBody()->append ($json); // Execute the HTTP request @@ -181,15 +174,12 @@ http_response_code ($status_code); // Now generate our body if ($status_code != 200) { - echo "Error while generating the contract:"; - echo "$resp"; + echo "Error while generating the contract"; + echo $resp->body->toString (); } else -{ - - $json = json_decode($resp->body->toString (), true); - $_SESSION["H_contract"] = $json["H_contract"]; - // send the contract back to the wallet without touching it +{ $got_json = json_decode ($resp->body->toString ()); + $_SESSION['H_contract'] = $got_json->H_contract; echo $resp->body->toString (); } ?> diff --git a/src/frontend/index.html b/src/frontend/index.html @@ -32,8 +32,8 @@ <header> <div id="logo"> <svg height="100" width="100"> - <circle cx="50" cy="50" r="40" stroke="black" stroke-width="6" fill="white" /> - <text x="19" y="82" font-family="Verdana" font-size="90" fill="black">S</text> + <circle cx="50" cy="50" r="40" stroke="darkcyan" stroke-width="6" fill="white" /> + <text x="19" y="82" font-family="Verdana" font-size="90" fill="darkcyan">S</text> </svg> </div> @@ -61,8 +61,6 @@ <tt>mint.demo.taler.net</tt> and <tt>bank.demo.taler.net</tt>, correspond to a Taler mint and bank with tight Taler integration respectively. - <!-- TODO: maybe offer the wallet at 'taler.net/extension' in the - future, instead of at 'demo.taler.net/'? --> </p> </article> @@ -70,14 +68,8 @@ <article id="installation"> <h2>Step 1: Installing the Taler wallet <sup>(once)</sup></h2> - <p>First, you need to install the Taler wallet browser extension. - It is currently only available for Firefox. If you run - Firefox, simply click <a href="http://demo.taler.net/extension">here</a> - to download and install the extension. You will then have to - click on "allow" and "install" dialogs shown by Firefox. - After that, the Taler logo should appear on the right side - of your navigation bar. - <!-- TODO: insert screenshot highlighting the icon? --> + <p>First, you need to <a href="http://demo.taler.net/">install</a> + the Taler wallet browser extension. </p> </article> diff --git a/src/frontend/pay.php b/src/frontend/pay.php @@ -41,8 +41,6 @@ if (isset($_GET['backend_test']) && $_GET['backend_test'] == 'no') $backend_test = false; } - - if (!isset($_SESSION['H_contract'])) { echo "No session active."; @@ -50,6 +48,17 @@ if (!isset($_SESSION['H_contract'])) return; } +if (isset($_SESSION['payment_ok']) && $_SESSION['payment_ok'] == true) +{ + $_SESSION['payment_ok'] = true; + http_response_code (301); + //$url = (new http\URL("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]")) + $url = (new http\URL("http://$_SERVER[HTTP_HOST]")) + ->mod(array ("path" => "fulfillment.php"), http\Url::JOIN_PATH); + header("Location: $url"); + die(); +} + $post_body = file_get_contents('php://input'); $now = new DateTime('now'); @@ -84,7 +93,13 @@ if ($cli_debug && !$backend_test) // Backend is relative to the shop site. -$url = (new http\URL("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]")) +/** + * WARNING: the "shop site" is '"http://".$_SERVER["HTTP_HOST"]' + * So do not attach $_SERVER["REQUEST_URI"] before proxying requests + * to the backend + */ +//$url = (new http\URL("http://".$_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"])) +$url = (new http\URL("http://".$_SERVER["HTTP_HOST"])) ->mod(array ("path" => "backend/pay"), http\Url::JOIN_PATH); $req = new http\Client\Request("POST", @@ -118,7 +133,13 @@ else { $_SESSION['payment_ok'] = true; http_response_code (301); - $url = (new http\URL("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]")) + /** + * WARNING: the "shop site" is '"http://".$_SERVER["HTTP_HOST"]' + * So do not attach $_SERVER["REQUEST_URI"] before proxying requests + * to the backend + */ + //$url = (new http\URL("http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]")) + $url = (new http\URL("http://$_SERVER[HTTP_HOST]")) ->mod(array ("path" => "fulfillment.php"), http\Url::JOIN_PATH); header("Location: $url"); die(); diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -83,6 +83,19 @@ struct TALER_MERCHANTDB_Plugin const struct TALER_CoinSpendPublicKeyP *coin_pub, json_t *mint_proof); -}; + /** + * Check whether a payment has already been stored + * + * @param cls our plugin handle + * @param transaction_id the transaction id to search into + * the db + * + * @return GNUNET_OK if found, GNUNET_NO if not, GNUNET_SYSERR + * upon error + */ + int + (*check_payment) (void *cls, + uint64_t transaction_id); +}; #endif diff --git a/src/tsconfig.json b/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "es6", + "jsx": "react" + }, + "files": [ + "frontend/execute.tsx" + ] +} +