summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-08-27 03:56:54 +0200
committerFlorian Dold <florian.dold@gmail.com>2019-08-28 21:28:59 +0200
commit97835ef689b538cb3e4bee294bd0fb2b3f0a9df2 (patch)
treef0ef076bfcc45285a818c75faf07a2351d20cb6e
parent11a193449e291c240f3cac96fe21e6c21a2a2649 (diff)
downloadbank-97835ef689b538cb3e4bee294bd0fb2b3f0a9df2.tar.gz
bank-97835ef689b538cb3e4bee294bd0fb2b3f0a9df2.tar.bz2
bank-97835ef689b538cb3e4bee294bd0fb2b3f0a9df2.zip
implement new withdraw API and support taler://withdraw
-rw-r--r--.style.yapf5
-rwxr-xr-xsetup.py2
-rw-r--r--talerbank/app/amount.py27
-rw-r--r--talerbank/app/migrations/0001_initial.py (renamed from talerbank/app/migrations/0001_squashed_0013_remove_banktransaction_reimburses.py)47
-rw-r--r--talerbank/app/models.py14
-rw-r--r--talerbank/app/templates/profile_page.html18
-rw-r--r--talerbank/app/templates/withdraw_confirm.html (renamed from talerbank/app/templates/pin_tan.html)7
-rw-r--r--talerbank/app/templates/withdraw_show.html76
-rw-r--r--talerbank/app/tests.py2
-rw-r--r--talerbank/app/urls.py80
-rw-r--r--talerbank/app/views.py225
-rw-r--r--talerbank/jinja2.py13
-rw-r--r--talerbank/settings.py4
-rw-r--r--talerbank/urls.py3
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
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_banktransaction_reimburses')]
-
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
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