gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-bank] branch master updated: implement new withdraw


From: gnunet
Subject: [GNUnet-SVN] [taler-bank] branch master updated: implement new withdraw API and support taler://withdraw
Date: Wed, 28 Aug 2019 21:30:03 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository bank.

The following commit(s) were added to refs/heads/master by this push:
     new 97835ef  implement new withdraw API and support taler://withdraw
     new d95ade1  Merge branch 'dev/dold/new-withdraw'
97835ef is described below

commit 97835ef689b538cb3e4bee294bd0fb2b3f0a9df2
Author: Florian Dold <address@hidden>
AuthorDate: Tue Aug 27 03:56:54 2019 +0200

    implement new withdraw API and support taler://withdraw
---
 .style.yapf                                        |   5 +
 setup.py                                           |   2 +
 talerbank/app/amount.py                            |  27 +--
 ...nktransaction_reimburses.py => 0001_initial.py} |  47 ++---
 talerbank/app/models.py                            |  14 +-
 talerbank/app/templates/profile_page.html          |  18 +-
 .../{pin_tan.html => withdraw_confirm.html}        |   7 +-
 talerbank/app/templates/withdraw_show.html         |  76 +++++++
 talerbank/app/tests.py                             |   2 +-
 talerbank/app/urls.py                              |  80 +++++---
 talerbank/app/views.py                             | 225 +++++++++++----------
 talerbank/jinja2.py                                |  13 +-
 talerbank/settings.py                              |   4 +-
 talerbank/urls.py                                  |   3 +-
 14 files changed, 318 insertions(+), 205 deletions(-)

diff --git a/.style.yapf b/.style.yapf
new file mode 100644
index 0000000..3b39780
--- /dev/null
+++ b/.style.yapf
@@ -0,0 +1,5 @@
+[style]
+based_on_style = pep8
+coalesce_brackets=True
+column_limit=80
+dedent_closing_brackets=True
diff --git a/setup.py b/setup.py
index 14c6ace..acd36ba 100755
--- a/setup.py
+++ b/setup.py
@@ -17,6 +17,8 @@ setup(name='talerbank',
                         "mock",
                         "jinja2",
                         "pylint",
+                        "qrcode",
+                        "lxml",
                         "django-lint"],
       scripts=["./bin/taler-bank-manage"],
       package_data={
diff --git a/talerbank/app/amount.py b/talerbank/app/amount.py
index 83f91e0..2a04013 100644
--- a/talerbank/app/amount.py
+++ b/talerbank/app/amount.py
@@ -224,25 +224,26 @@ class Amount:
         self.fraction -= amount.fraction
 
     ##
-    # Dump string from this amount, will put 'ndigits' numbers
-    # after the dot.
+    # Convert the amount to a string.
     #
     # @param self this object.
     # @param ndigits how many digits we want for the fractional part.
     # @param pretty if True, put the currency in the last position and
     #        omit the colon.
-    def stringify(self, ndigits: int, pretty=False) -> str:
-        if ndigits <= 0:
-            raise BadFormatAmount("ndigits must be > 0")
-        tmp = self.fraction
-        fraction_str = ""
-        while ndigits > 0:
-            fraction_str += str(int(tmp / (Amount._fraction() / 10)))
-            tmp = (tmp * 10) % (Amount._fraction())
-            ndigits -= 1
+    def stringify(self, ndigits=0, pretty=False) -> str:
+        s = str(self.value)
+        if self.fraction != 0:
+            s += "." 
+            frac = self.fraction
+            while frac != 0 or ndigits != 0:
+                s += str(int(frac / (Amount._fraction() / 10)))
+                frac = (frac * 10) % (Amount._fraction())
+                ndigits -= 1
+        elif ndigits != 0:
+            s += "." + ("0" * ndigits)
         if not pretty:
-            return "%s:%d.%s" % (self.currency, self.value, fraction_str)
-        return "%d.%s %s" % (self.value, fraction_str, self.currency)
+            return f"{self.currency}:{s}" 
+        return f"{s} {self.currency}"
 
     ##
     # Dump the Taler-compliant 'dict' amount from
diff --git 
a/talerbank/app/migrations/0001_squashed_0013_remove_banktransaction_reimburses.py
 b/talerbank/app/migrations/0001_initial.py
similarity index 52%
rename from 
talerbank/app/migrations/0001_squashed_0013_remove_banktransaction_reimburses.py
rename to talerbank/app/migrations/0001_initial.py
index 31c38a3..8fa3da0 100644
--- 
a/talerbank/app/migrations/0001_squashed_0013_remove_banktransaction_reimburses.py
+++ b/talerbank/app/migrations/0001_initial.py
@@ -1,15 +1,14 @@
-# Generated by Django 2.0.1 on 2018-02-13 10:23
+# Generated by Django 2.2.4 on 2019-08-27 18:55
 
 from django.conf import settings
 from django.db import migrations, models
 import django.db.models.deletion
 import talerbank.app.models
+import uuid
 
 
 class Migration(migrations.Migration):
 
-    replaces = [('app', '0001_initial'), ('app', '0002_bankaccount_amount'), 
('app', '0003_auto_20171030_1346'), ('app', '0004_auto_20171030_1428'), ('app', 
'0005_remove_banktransaction_currency'), ('app', '0006_auto_20171031_0823'), 
('app', '0007_auto_20171031_0906'), ('app', '0008_auto_20171031_0938'), ('app', 
'0009_auto_20171120_1642'), ('app', '0010_banktransaction_cancelled'), ('app', 
'0011_banktransaction_reimburses'), ('app', '0012_auto_20171212_1540'), ('app', 
'0013_remove_banktr [...]
-
     initial = True
 
     dependencies = [
@@ -22,45 +21,33 @@ class Migration(migrations.Migration):
             fields=[
                 ('is_public', models.BooleanField(default=False)),
                 ('debit', models.BooleanField(default=False)),
-                ('balance_value', models.IntegerField(default=0)),
-                ('balance_fraction', models.IntegerField(default=0)),
-                ('balance', models.FloatField(default=0)),
-                ('currency', models.CharField(default='', max_length=12)),
                 ('account_no', models.AutoField(primary_key=True, 
serialize=False)),
+                ('amount', 
talerbank.app.models.AmountField(default=talerbank.app.models.get_zero_amount)),
                 ('user', 
models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, 
to=settings.AUTH_USER_MODEL)),
             ],
         ),
         migrations.CreateModel(
+            name='TalerWithdrawOperation',
+            fields=[
+                ('withdraw_id', models.UUIDField(default=uuid.uuid4, 
editable=False, primary_key=True, serialize=False)),
+                ('amount', talerbank.app.models.AmountField(default=False)),
+                ('selection_done', models.BooleanField(default=False)),
+                ('withdraw_done', models.BooleanField(default=False)),
+                ('selected_reserve_pub', models.TextField(null=True)),
+                ('selected_exchange_account', models.ForeignKey(null=True, 
on_delete=django.db.models.deletion.CASCADE, 
related_name='selected_exchange_account', to='app.BankAccount')),
+                ('withdraw_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='withdraw_account', to='app.BankAccount')),
+            ],
+        ),
+        migrations.CreateModel(
             name='BankTransaction',
             fields=[
                 ('id', models.AutoField(auto_created=True, primary_key=True, 
serialize=False, verbose_name='ID')),
+                ('amount', talerbank.app.models.AmountField(default=False)),
                 ('subject', models.CharField(default='(no subject given)', 
max_length=200)),
                 ('date', models.DateTimeField(auto_now=True, db_index=True)),
+                ('cancelled', models.BooleanField(default=False)),
                 ('credit_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='credit_account', to='app.BankAccount')),
                 ('debit_account', 
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, 
related_name='debit_account', to='app.BankAccount')),
-                ('amount', talerbank.app.models.AmountField(default=False)),
-                ('cancelled', models.BooleanField(default=False)),
             ],
         ),
-        migrations.AddField(
-            model_name='bankaccount',
-            name='amount',
-            
field=talerbank.app.models.AmountField(default=talerbank.app.models.get_zero_amount),
-        ),
-        migrations.RemoveField(
-            model_name='bankaccount',
-            name='balance',
-        ),
-        migrations.RemoveField(
-            model_name='bankaccount',
-            name='balance_fraction',
-        ),
-        migrations.RemoveField(
-            model_name='bankaccount',
-            name='balance_value',
-        ),
-        migrations.RemoveField(
-            model_name='bankaccount',
-            name='currency',
-        ),
     ]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index d7340a0..7c7717d 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -19,7 +19,6 @@
 #  @author Florian Dold
 
 import uuid
-from __future__ import unicode_literals
 from typing import Any, Tuple
 from django.contrib.auth.models import User
 from django.db import models
@@ -152,12 +151,19 @@ class BankTransaction(models.Model):
     cancelled = models.BooleanField(default=False)
 
 
-class ApprovedWithdrawOperation(models.Model):
+class TalerWithdrawOperation(models.Model):
+    withdraw_id = models.UUIDField(primary_key=True, default=uuid.uuid4, 
editable=False)
     amount = AmountField(default=False)
     withdraw_account = models.ForeignKey(
         BankAccount,
         on_delete=models.CASCADE,
         db_index=True,
         related_name="withdraw_account")
-    finished = models.BooleanField(default=False)
-    withdraw_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, 
editable=False)
+    selection_done = models.BooleanField(default=False)
+    withdraw_done = models.BooleanField(default=False)
+    selected_exchange_account = models.ForeignKey(
+        BankAccount,
+        null=True,
+        on_delete=models.CASCADE,
+        related_name="selected_exchange_account")
+    selected_reserve_pub = models.TextField(null=True)
diff --git a/talerbank/app/templates/profile_page.html 
b/talerbank/app/templates/profile_page.html
index abbb784..61fcd61 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -17,18 +17,6 @@
   @author Marcello Stanisci
 #}
 
-{% block head %}
-  <meta name="currency" value="{{ currency }}">
-  <meta name="precision" value="{{ precision }}">
-  <meta name="callback-url" value="{{ url('pin-question') }}">
-  {% if withdraw and withdraw == "success" %}
-    <meta name="reserve-pub" value="{{ reserve_pub }}">
-  {% endif %}
-  {% if suggested_exchange %}
-    <meta name="suggested-exchange" value="{{ suggested_exchange }}">
-  {% endif %}
-  <link rel="stylesheet" type="text/css" href="{{ 
static('disabled-button.css') }}">
-{% endblock head %}
 {% block headermsg %}
   <div>
     <h1 class="nav">Welcome <em>{{ name }}</em>!</h1>
@@ -64,11 +52,11 @@
     </article>
     <article>
       <div>
-        <h2>Withdraw digital coins using Taler</h2>
+        <h2>Withdraw Money into a Taler wallet</h2>
 
         <form id="reserve-form"
               class="pure-form"
-              action="{{ url('withdraw-nojs') }}"
+              action="{{ url('start-withdrawal') }}"
               method="post"
               name="tform">
           <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}" />
@@ -84,7 +72,7 @@
           <input id="select-exchange"
                  class="pure-button pure-button-primary"
                  type="submit"
-                 value="Select exchange provider" />
+                 value="Start withdrawal" />
         </form>
         <h2>Wire transfer</h2>
         <form id="wt-form"
diff --git a/talerbank/app/templates/pin_tan.html 
b/talerbank/app/templates/withdraw_confirm.html
similarity index 90%
rename from talerbank/app/templates/pin_tan.html
rename to talerbank/app/templates/withdraw_confirm.html
index 746f889..594d5a5 100644
--- a/talerbank/app/templates/pin_tan.html
+++ b/talerbank/app/templates/withdraw_confirm.html
@@ -20,7 +20,7 @@
 {% extends "base.html" %}
 
 {% block headermsg %}
-  <h1 class="nav">PIN/TAN:  Confirm transaction</h1>
+  <h1 class="nav">Confirm Withdrawal</h1>
 {% endblock %}
 
 {% block content %}
@@ -31,15 +31,14 @@
   {% endif %}
   <p>
     {{ settings_value("TALER_CURRENCY") }} Bank needs to verify that you
-    intend to withdraw <b>{{ amount }}</b> from
-    <b>{{ exchange }}</b>.
+    intend to withdraw <b>{{ amount }}</b> from <b>{{ exchange }}</b>.
     To prove that you are the account owner, please answer the
     following &quot;security question&quot; (*):
   </p>
   <p>
     What is {{ question }} ?
   </p>
-  <form method="post" action="{{ url('pin-verify') }}" class="pure-form">
+  <form method="post" action="{{ url('withdraw-confirm', withdraw_id) }}" 
class="pure-form">
     <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />
     <input type="text" name="pin_0" value="" autocomplete="off" autofocus />
     <input type="hidden" name="pin_1" value="{{ hashed_answer }}" />
diff --git a/talerbank/app/templates/withdraw_show.html 
b/talerbank/app/templates/withdraw_show.html
new file mode 100644
index 0000000..3c66732
--- /dev/null
+++ b/talerbank/app/templates/withdraw_show.html
@@ -0,0 +1,76 @@
+<!-- 
+  This file is part of GNU Taler.
+  Copyright (C) 2019 GNUnet e.V.
+
+  GNU Taler is free software; you can redistribute it and/or modify it under 
the
+  terms of the GNU Lesser General Public License as published by the Free 
Software
+  Foundation; either version 2.1, or (at your option) any later version.
+
+  GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+  A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
details.
+
+  You should have received a copy of the GNU Lesser General Public License 
along with
+  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+
+  @author Florian Dold
+-->
+
+{% extends "base.html" %}
+
+{% block headermsg %}
+<h1 class="nav">Withdraw to a Taler Wallet</h1>
+{% endblock %}
+
+{% block head %}
+<script>
+  // prettier-ignore
+  let checkUrl = JSON.parse('{{ withdraw_check_url | tojson }}');
+  let delayMs = 500;
+  function check() {
+    let req = new XMLHttpRequest();
+    req.onreadystatechange = function () {
+      if (req.readyState === XMLHttpRequest.DONE) {
+        if (req.status === 200) {
+          try {
+            let resp = JSON.parse(req.responseText);
+            if (resp.selection_done) {
+              document.location.reload(true);
+            }
+          } catch (e) {
+            console.error("could not parse response:", e);
+          }
+        }
+        setTimeout(check, delayMs);
+      }
+    };
+    req.onerror = function () {
+      setTimeout(check, delayMs);
+    }
+    req.open("GET", checkUrl);
+    req.send();
+  }
+
+  setTimeout(check, delayMs);
+
+</script>
+{% endblock head %}
+
+{% block content %}
+<div class="taler-installed-hide">
+  <p>
+    Looks like your browser doesn't support GNU Taler payments. You can try
+    installing a <a href="https://taler.net/en/wallet.html";>wallet browser 
extension</a>.
+  </p>
+</div>
+
+<p>
+  You can use this QR code to withdraw to your mobile wallet:
+</p>
+{{ qrcode_svg | safe }}
+<p>
+  Click <a href="{{ taler_withdraw_uri }}">this link</a> to open your system's 
Taler wallet if it exists.
+</p>
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index 06e4af4..50f1dd4 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -116,7 +116,7 @@ class WithdrawTestCase(TestCase):
         mocked_time.return_value = 0
 
         response = self.client.post(
-            reverse("pin-verify", urlconf=urls),
+            reverse("withdraw-confirm", urlconf=urls),
             {"pin_1": "0"})
 
         args, kwargs = mocked_wire_transfer.call_args
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index d9b4491..7f2006b 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -16,42 +16,64 @@
 # <http://www.gnu.org/licenses/>.
 #
 #  @author Marcello Stanisci
+#  @author Florian Dold
 
-from django.conf.urls import include, url
+from django.urls import include, path
 from django.views.generic.base import RedirectView
 from django.contrib.auth import views as auth_views
 from . import views
 
 urlpatterns = [
-    url(r'^', include('talerbank.urls')),
-    url(r'^$', RedirectView.as_view(pattern_name="profile"),
-        name="index"),
-    url(r'^favicon\.ico$', views.ignore),
-    url(r'^admin/add/incoming$', views.add_incoming,
-        name="add-incoming"),
-    url(r'^login/$',
+    path("", RedirectView.as_view(pattern_name="profile"), name="index"),
+    path("favicon.ico", views.ignore),
+    path("admin/add/incoming", views.add_incoming, name="add-incoming"),
+    path(
+        "login/",
         auth_views.LoginView.as_view(
             template_name="login.html",
-            authentication_form=views.TalerAuthenticationForm),
-        name="login"),
-    url(r'^logout/$', views.logout_view, name="logout"),
-    url(r'^accounts/register/$', views.register, name="register"),
-    url(r'^register$', views.register_headless, name="register-headless"),
-    url(r'^profile$', views.profile_page, name="profile"),
-    url(r'^history$', views.serve_history, name="history"),
-    url(r'^history-range$', views.serve_history_range, name="history-range"),
-    url(r'^reject$', views.reject, name="reject"),
-    url(r'^withdraw$', views.withdraw_nojs, name="withdraw-nojs"),
-    url(r'^taler/withdraw$', views.withdraw_headless, 
name="withdraw-headless"),
-    url(r'^public-accounts$', views.serve_public_accounts,
-        name="public-accounts"),
-    url(r'^public-accounts/(?P<name>[a-zA-Z0-9]+)$',
+            authentication_form=views.TalerAuthenticationForm
+        ),
+        name="login"
+    ),
+    path("logout/", views.logout_view, name="logout"),
+    path("accounts/register", views.register, name="register"),
+    path("profile", views.profile_page, name="profile"),
+    path("history", views.serve_history, name="history"),
+    path("history-range", views.serve_history_range, name="history-range"),
+    path("reject", views.reject, name="reject"),
+    path(
+        "api/withdraw-operation/<str:withdraw_id>",
+        views.api_withdraw_operation,
+        name="api-withdraw-operation"
+    ),
+    path(
+        "api/withdraw-headless",
+        views.withdraw_headless,
+        name="withdraw-headless"
+    ),
+    path("api/register", views.register_headless, name="register-headless"),
+    path("start-withdrawal", views.start_withdrawal, name="start-withdrawal"),
+    path(
+        "show-withdrawal/<str:withdraw_id>",
+        views.show_withdrawal,
+        name="withdraw-show"
+    ),
+    path(
+        "confirm-withdrawal/<str:withdraw_id>",
+        views.confirm_withdrawal,
+        name="withdraw-confirm"
+    ),
+    path(
+        "public-accounts", views.serve_public_accounts, name="public-accounts"
+    ),
+    path(
+        "public-accounts/<str:name>",
         views.serve_public_accounts,
-        name="public-accounts"),
-    url(r'^public-accounts/(?P<name>[a-zA-Z0-9]+)/(?P<page>[0-9]+)$',
+        name="public-accounts"
+    ),
+    path(
+        "public-accounts/<str:name>/<int:page>",
         views.serve_public_accounts,
-        name="public-accounts"),
-    url(r'^pin/question$', views.pin_tan_question,
-        name="pin-question"),
-    url(r'^pin/verify$', views.pin_tan_verify, name="pin-verify"),
-    ]
+        name="public-accounts"
+    ),
+]
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index d035049..56c6993 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -41,11 +41,14 @@ from django.contrib.auth.models import User
 from django.db.models import Q
 from django.http import JsonResponse, HttpResponse
 from django.shortcuts import render, redirect
+from django.core.exceptions import ObjectDoesNotExist
 from datetime import datetime
-from .models import BankAccount, BankTransaction
+from .models import BankAccount, BankTransaction, TalerWithdrawOperation
 from .amount import Amount
-from .schemas import \
-    (HistoryParams, HistoryRangeParams,
+import qrcode
+import qrcode.image.svg
+import lxml
+from .schemas import (HistoryParams, HistoryRangeParams,
      URLParamValidationError, RejectData,
      AddIncomingData, JSONFieldException,
      PinTanParams, InvalidSession,
@@ -170,6 +173,7 @@ def predefined_accounts_list():
 ##
 # Thanks to [1], this class provides a dropdown menu that
 # can be used within a <select> element, in a <form>.
+# [1] 
https://stackoverflow.com/questions/24783275/django-form-with-choices-but-also-with-freetext-option
 class InputDatalist(forms.TextInput):
 
     ##
@@ -202,6 +206,7 @@ class InputDatalist(forms.TextInput):
     # @param renderer render engine (left as None, typically); it
     #        is a class that respects the low-level render API from
     #        Django, see [2]
+    # [2] 
https://docs.djangoproject.com/en/2.1/ref/forms/renderers/#low-level-widget-render-api
     def render(self, name, value, attrs=None, renderer=None):
         html = super().render(
             name, value, attrs=attrs, renderer=renderer)
@@ -319,6 +324,7 @@ def make_question():
     question = "{} {} {}".format(num1, operand, num2)
     return question, hash_answer(answer)
 
+
 def get_acct_from_payto(uri_str: str) -> int:
     wire_uri = urlparse(uri_str)
     if wire_uri.scheme != "payto":
@@ -327,87 +333,6 @@ def get_acct_from_payto(uri_str: str) -> int:
 
 
 ##
-# This method build the page containing the math CAPTCHA that
-# protects coins withdrawal.  It takes all the values from the
-# URL and puts them into the state, for further processing after
-# a successful answer from the user.
-#
-# @param request Django-specific HTTP request object
-# @return Django-specific HTTP response object
-@require_GET
-@login_required
-def pin_tan_question(request):
-    
-    get_params = PinTanParams(request.GET.dict())
-    if not get_params.is_valid():
-        raise URLParamValidationError(get_params.errors, 400)
-
-
-    user_account = BankAccount.objects.get(user=request.user)
-    wire_details = get_params.cleaned_data["exchange_wire_details"]
-
-    request.session["exchange_account_number"] = \
-        get_acct_from_payto(wire_details)
-    amount = Amount(get_params.cleaned_data["amount_currency"],
-                    get_params.cleaned_data["amount_value"],
-                    get_params.cleaned_data["amount_fraction"])
-    request.session["amount"] = amount.dump()
-    request.session["reserve_pub"] = \
-        get_params.cleaned_data["reserve_pub"]
-
-    fail_message, success_message, hint = get_session_hint(
-        request,
-        "captcha_failed")
-
-    question, hashed_answer = make_question()
-    context = dict(
-        question=question,
-        hashed_answer=hashed_answer,
-        amount=amount.stringify(settings.TALER_DIGITS),
-        exchange=get_params.cleaned_data["exchange"],
-        fail_message=fail_message,
-        success_message=success_message,
-        hint=hint)
-    return render(request, "pin_tan.html", context)
-
-
-##
-# This method serves the user's answer to the math CAPTCHA,
-# and reacts accordingly to its correctness.  If correct (wrong),
-# it redirects the user to the profile page showing a success
-# (failure) message into the informational bar.
-@require_POST
-@login_required
-def pin_tan_verify(request):
-    hashed_attempt = hash_answer(request.POST.get("pin_0", ""))
-    hashed_solution = request.POST.get("pin_1", "")
-    if hashed_attempt != hashed_solution:
-        LOGGER.warning("Wrong CAPTCHA answer: %s vs %s",
-                       type(hashed_attempt),
-                       type(request.POST.get("pin_1")))
-        request.session["captcha_failed"] = True, False, "Wrong CAPTCHA 
answer."
-        return redirect(request.POST.get("question_url", "profile"))
-    # Check the session is a "pin tan" one
-
-    if not WithdrawSessionData(request.session):
-        # The session is not valid: either because the client simply
-        # requested the page without passing through the prior step,
-        # or because the bank broke it in the meanwhile.  Let's blame
-        # ourselves for now.
-        raise InvalidSession(503)
-
-    amount = Amount(**request.session["amount"])
-    exchange_bank_account = BankAccount.objects.get(
-        account_no=request.session["exchange_account_number"])
-    wire_transfer(amount,
-                  BankAccount.objects.get(user=request.user),
-                  exchange_bank_account,
-                  request.session["reserve_pub"])
-    request.session["profile_hint"] = False, True, "Withdrawal successful!"
-    request.session["just_withdrawn"] = True
-    return redirect("profile")
-
-##
 # Class representing the registration form.
 class UserReg(forms.Form):
     username = forms.CharField()
@@ -954,6 +879,52 @@ def withdraw_headless(request, user):
         data.cleaned_data["reserve_pub"])
 
     return JsonResponse(ret_obj)
+
+
+# Endpoint used by the browser and wallet to check withdraw status and
+# put in the exchange info.
+@csrf_exempt
+def api_withdraw_operation(request, withdraw_id):
+    try:
+        op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id)
+    except ObjectDoesNotExist:
+        return JsonResponse(dict(error="withdraw operation does not exist"), 
status=404)
+    user_acct_no = op.withdraw_account.account_no
+    host = request.get_host()
+
+    if request.method == "POST":
+        if op.selection_done or op.withdraw_done:
+            return JsonResponse(dict(error="selection of withdraw parameters 
already done"), status=409)
+        data = json.loads(request.body.decode("utf-8"))
+        exchange_payto_uri = data.get("selected_exchange")
+        try:
+            account_no = get_acct_from_payto(exchange_payto_uri)
+        except:
+            return JsonResponse(dict(error="exchange payto URI malformed"), 
status=400)
+        try:
+            exchange_acct = BankAccount.objects.get(account_no=account_no)
+        except ObjectDoesNotExist:
+            return JsonResponse(dict(error="bank accound in payto URI 
unknown"), status=400)
+        op.selected_exchange_account = exchange_acct
+        selected_reserve_pub = data.get("reserve_pub")
+        if not isinstance(selected_reserve_pub, str):
+            return JsonResponse(dict(error="reserve_pub must be a string"), 
status=400)
+        op.selected_reserve_pub = selected_reserve_pub
+        op.selection_done = True
+        op.save()
+        return JsonResponse(dict(), status=200)
+    elif request.method == "GET":
+        return JsonResponse(dict(
+            selection_done=op.selection_done,
+            transfer_done=op.withdraw_done,
+            amount=op.amount.stringify(),
+            wire_types=["x-taler-bank"],
+            sender_wire=f"payto://x-taler-bank/{host}/{user_acct_no}",
+            suggested_exchange=settings.TALER_SUGGESTED_EXCHANGE,
+            
confirm_transfer_url=request.build_absolute_uri(reverse("withdraw-confirm", 
args=(withdraw_id,)))))
+    else:
+        return JsonResponse(dict(error="only GET and POST are allowed"), 
status=305)
+
     
 ##
 # Serve a Taler withdrawal request; takes the amount chosen
@@ -964,25 +935,76 @@ def withdraw_headless(request, user):
 # @return Django-specific HTTP response object.
 @login_required
 @require_POST
-def withdraw_nojs(request):
-
-    amount = Amount.parse(
-        request.POST.get("kudos_amount", "not-given"))
+def start_withdrawal(request):
     user_account = BankAccount.objects.get(user=request.user)
-    response = HttpResponse(status=202)
-    response["X-Taler-Operation"] = "create-reserve"
-    response["X-Taler-Callback-Url"] = reverse("pin-question")
-    response["X-Taler-Wt-Types"] = '["x-taler-bank"]'
-    response["X-Taler-Amount"] = json.dumps(amount.dump())
-    response["X-Taler-Sender-Wire"] = "payto://x-taler-bank/{}/{}".format(
-            request.get_host(),
-            user_account.account_no,
+    amount = Amount.parse(request.POST.get("kudos_amount", "not-given"))
+    op = TalerWithdrawOperation(amount=amount, withdraw_account=user_account)
+    op.save()
+    return redirect("withdraw-show", withdraw_id=op.withdraw_id)
+
+
+def get_qrcode_svg(data):
+    factory = qrcode.image.svg.SvgImage
+    img = qrcode.make(data, image_factory=factory)
+    return lxml.etree.tostring(img.get_image()).decode("utf-8")
+
+
+@login_required
+@require_GET
+def show_withdrawal(request, withdraw_id):
+    op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id)
+    if op.selection_done:
+        return redirect("withdraw-confirm", withdraw_id=op.withdraw_id)
+    host = request.get_host()
+    taler_withdraw_uri = f"taler://withdraw/{host}/-/{op.withdraw_id}"
+    qrcode_svg = get_qrcode_svg(taler_withdraw_uri)
+    context = dict(
+            taler_withdraw_uri=taler_withdraw_uri,
+            qrcode_svg=qrcode_svg,
+            withdraw_check_url=reverse("api-withdraw-operation", 
kwargs=dict(withdraw_id=op.withdraw_id)),
     )
-    if settings.TALER_SUGGESTED_EXCHANGE:
-        response["X-Taler-Suggested-Exchange"] = \
-            settings.TALER_SUGGESTED_EXCHANGE
-    return response
+    resp = render(request, "withdraw_show.html", context, status=402)
+    resp["Taler"] = taler_withdraw_uri
+    return resp
+
 
+@login_required
+@require_http_methods(["GET", "POST"])
+def confirm_withdrawal(request, withdraw_id):
+    op = TalerWithdrawOperation.objects.get(withdraw_id=withdraw_id)
+    if not op.selection_done:
+        raise Exception("invalid state (withdrawal parameter selection not 
done)")
+    if op.withdraw_done:
+        return redirect("profile")
+    if request.method == "POST":
+        hashed_attempt = hash_answer(request.POST.get("pin_0", ""))
+        hashed_solution = request.POST.get("pin_1", "")
+        if hashed_attempt != hashed_solution:
+            LOGGER.warning("Wrong CAPTCHA answer: %s vs %s",
+               type(hashed_attempt),
+                type(request.POST.get("pin_1")))
+            request.session["captcha_failed"] = True, False, "Wrong CAPTCHA 
answer."
+            return redirect("withdraw-confirm", withdraw_id=withdraw_id)
+        op.withdraw_done = True
+        op.save()
+        wire_transfer(op.amount,
+                  BankAccount.objects.get(user=request.user),
+                  op.selected_exchange_account,
+                  op.selected_reserve_pub)
+        request.session["profile_hint"] = False, True, "Withdrawal successful!"
+        request.session["just_withdrawn"] = True
+        return redirect("profile")
+    if request.method == "GET":
+        question, hashed_answer = make_question()
+        context = dict(
+            question=question,
+            hashed_answer=hashed_answer,
+            withdraw_id=withdraw_id,
+            amount=op.amount.stringify(settings.TALER_DIGITS),
+            exchange=op.selected_exchange_account.user
+        )
+        return render(request, "withdraw_confirm.html", context)
+    raise Exception("not reached")
 
 ##
 # Make a wire transfer between two accounts (internal to the bank)
@@ -1051,6 +1073,3 @@ def wire_transfer(amount, debit_account, credit_account, 
subject):
         transaction_item.save()
 
     return transaction_item
-
-# [1] 
https://stackoverflow.com/questions/24783275/django-form-with-choices-but-also-with-freetext-option
-# [2] 
https://docs.djangoproject.com/en/2.1/ref/forms/renderers/#low-level-widget-render-api
diff --git a/talerbank/jinja2.py b/talerbank/jinja2.py
index d6a1e2b..79a4675 100644
--- a/talerbank/jinja2.py
+++ b/talerbank/jinja2.py
@@ -21,6 +21,7 @@
 
 import os
 import math
+import json
 from urllib.parse import urlparse
 from django.urls import reverse, get_script_prefix
 from django.conf import settings
@@ -87,11 +88,11 @@ def settings_value(name):
 # @param url_name URL's name as defined in urlargs.py
 # @param kwargs key-value list that will be appended
 #        to the URL as the parameter=value pairs.
-def url(url_name, **kwargs):
+def url(url_name, *args, **kwargs):
     # strangely, Django's 'reverse' function
     # takes a named parameter 'kwargs' instead
     # of real kwargs.
-    return reverse(url_name, kwargs=kwargs)
+    return reverse(url_name, args=args, kwargs=kwargs)
 
 
 ##
@@ -115,6 +116,11 @@ def is_valid_amount(amount):
     return True
 
 
+def tojson(x):
+    """Convert object to json"""
+    return json.dumps(x)
+
+
 ##
 # Stringifies amount.
 #
@@ -131,6 +137,7 @@ def environment(**options):
         'settings_value': settings_value,
         'env': env_get,
         'is_valid_amount': is_valid_amount,
-        'amount_stringify': amount_stringify
+        'amount_stringify': amount_stringify,
+        'tojson': tojson,
     })
     return env
diff --git a/talerbank/settings.py b/talerbank/settings.py
index 44fcf70..97a18dd 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -182,8 +182,8 @@ STATICFILES_DIRS = [
     os.path.join(BASE_DIR, "talerbank/app/static/web-common"),
 ]
 
-STATIC_ROOT = '/tmp/talerbankstatic/'
-ROOT_URLCONF = "talerbank.app.urls"
+STATIC_ROOT = None
+ROOT_URLCONF = "talerbank.urls"
 
 try:
     TALER_CURRENCY = TC.value_string(
diff --git a/talerbank/urls.py b/talerbank/urls.py
index 3ab39b2..26f0851 100644
--- a/talerbank/urls.py
+++ b/talerbank/urls.py
@@ -16,5 +16,6 @@ Including another URLconf
 """
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.conf.urls import url
+from .app.urls import urlpatterns as app_urlpatterns
 
-urlpatterns = staticfiles_urlpatterns()
+urlpatterns = staticfiles_urlpatterns() + app_urlpatterns

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]