summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-02-17 20:01:29 +0100
committerFlorian Dold <florian.dold@gmail.com>2020-02-17 20:01:29 +0100
commitbda6379eb4ddbb355c8c1452b848049178f87598 (patch)
tree40bf0276ce1b94d4b2ab8e3f2fe2e453d9faa08c
parenta81fd7a44bbd7513def6c67a0d1fa0dbf0c13b30 (diff)
downloadbank-bda6379eb4ddbb355c8c1452b848049178f87598.tar.gz
bank-bda6379eb4ddbb355c8c1452b848049178f87598.tar.bz2
bank-bda6379eb4ddbb355c8c1452b848049178f87598.zip
accounts API
-rw-r--r--talerbank/app/management/commands/add_bank_account.py2
-rw-r--r--talerbank/app/management/commands/changepassword_unsafe.py6
-rw-r--r--talerbank/app/migrations/0001_initial.py104
-rw-r--r--talerbank/app/models.py3
-rw-r--r--talerbank/app/tests.py8
-rw-r--r--talerbank/app/urls.py50
-rw-r--r--talerbank/app/views.py154
-rw-r--r--talerbank/settings.py4
8 files changed, 283 insertions, 48 deletions
diff --git a/talerbank/app/management/commands/add_bank_account.py b/talerbank/app/management/commands/add_bank_account.py
index b0e6513..cf01162 100644
--- a/talerbank/app/management/commands/add_bank_account.py
+++ b/talerbank/app/management/commands/add_bank_account.py
@@ -35,6 +35,7 @@ import uuid
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)
+
class Command(BaseCommand):
help = "Add bank accounts."
@@ -67,4 +68,3 @@ class Command(BaseCommand):
)
else:
print(f"Bank account {accountname} already exists.")
-
diff --git a/talerbank/app/management/commands/changepassword_unsafe.py b/talerbank/app/management/commands/changepassword_unsafe.py
index d2f88d1..56a730f 100644
--- a/talerbank/app/management/commands/changepassword_unsafe.py
+++ b/talerbank/app/management/commands/changepassword_unsafe.py
@@ -35,6 +35,7 @@ import uuid
LOGGER = logging.getLogger(__name__)
LOGGER.setLevel(logging.INFO)
+
class Command(BaseCommand):
help = "Add bank accounts."
@@ -59,10 +60,7 @@ class Command(BaseCommand):
existing_user.set_password(password)
existing_user.save()
except User.DoesNotExist:
- print(
- f"Account {accountname} does not exist"
- )
+ print(f"Account {accountname} does not exist")
sys.exit(1)
else:
print(f"Password for {accountname} changed")
-
diff --git a/talerbank/app/migrations/0001_initial.py b/talerbank/app/migrations/0001_initial.py
index 73a5206..a25b205 100644
--- a/talerbank/app/migrations/0001_initial.py
+++ b/talerbank/app/migrations/0001_initial.py
@@ -17,37 +17,97 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='BankAccount',
+ name="BankAccount",
fields=[
- ('is_public', models.BooleanField(default=False)),
- ('account_no', models.AutoField(primary_key=True, serialize=False)),
- ('balance', talerbank.app.models.SignedAmountField(default=talerbank.app.models.get_zero_signed_amount)),
- ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ("is_public", models.BooleanField(default=False)),
+ ("account_no", models.AutoField(primary_key=True, serialize=False)),
+ (
+ "balance",
+ talerbank.app.models.SignedAmountField(
+ default=talerbank.app.models.get_zero_signed_amount
+ ),
+ ),
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
],
),
migrations.CreateModel(
- name='TalerWithdrawOperation',
+ 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')),
+ (
+ "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)),
+ ("confirmation_done", models.BooleanField(default=False)),
+ ("aborted", 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',
+ 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)),
- ('request_uid', models.CharField(max_length=128, unique=True)),
- ('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')),
+ (
+ "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)),
+ ("request_uid", models.CharField(max_length=128, unique=True)),
+ (
+ "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",
+ ),
+ ),
],
),
]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index 3833c22..5c4c9ac 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -205,7 +205,8 @@ class TalerWithdrawOperation(models.Model):
related_name="withdraw_account",
)
selection_done = models.BooleanField(default=False)
- withdraw_done = models.BooleanField(default=False)
+ confirmation_done = models.BooleanField(default=False)
+ aborted = models.BooleanField(default=False)
selected_exchange_account = models.ForeignKey(
BankAccount,
null=True,
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index ad792b7..abbf9f3 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -385,9 +385,10 @@ class AddIncomingTestCase(TestCase):
def test_add_incoming(self):
client = Client()
request_body = dict(
- reserve_pub="TESTWTID",
- amount=f"{settings.TALER_CURRENCY}:1.0",
- debit_account="payto://x-taler-bank/bank_user")
+ reserve_pub="TESTWTID",
+ amount=f"{settings.TALER_CURRENCY}:1.0",
+ debit_account="payto://x-taler-bank/bank_user",
+ )
response = client.post(
reverse("twg-add-incoming", urlconf=urls, args=["user_user"]),
data=json.dumps(request_body),
@@ -430,7 +431,6 @@ class HistoryTestCase(TestCase):
clear_db()
def test_history(self):
-
def histquery(**urlargs):
response = self.client.get(
reverse("history", urlconf=urls),
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index e929125..c2a524c 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -23,23 +23,63 @@ from django.views.generic.base import RedirectView
from django.contrib.auth import views as auth_views
from . import views
+# These paths are part of the GNU Taler wire gatweay API
taler_wire_gateway_patterns = [
path("<str:acct_id>/", views.twg_base, name="twg-base"),
- path("<str:acct_id>/admin/add-incoming", views.twg_add_incoming, name="twg-add-incoming"),
- path("<str:acct_id>/history/incoming", views.twg_history_incoming, name="twg-history-incoming"),
- path("<str:acct_id>/history/outgoing", views.twg_history_outgoing, name="twg-history-outgoing"),
+ path(
+ "<str:acct_id>/admin/add-incoming",
+ views.twg_add_incoming,
+ name="twg-add-incoming",
+ ),
+ path(
+ "<str:acct_id>/history/incoming",
+ views.twg_history_incoming,
+ name="twg-history-incoming",
+ ),
+ path(
+ "<str:acct_id>/history/outgoing",
+ views.twg_history_outgoing,
+ name="twg-history-outgoing",
+ ),
path("<str:acct_id>/transfer", views.twg_transfer, name="twg-transfer"),
]
+# These paths are part of the bank integration API
taler_bank_api_patterns = [
+ path(
+ "withdrawal-operation/<str:withdraw_id>",
+ views.register_headless,
+ name="tba-withdrawal-operation",
+ ),
+]
+
+taler_bank_accounts_api_patterns = [
+ path("accounts/<str:acct_id>/balance", views.bank_accounts_api_balance),
+ path(
+ "accounts/<str:acct_id>/withdrawals", views.bank_accounts_api_create_withdrawal
+ ),
+ path(
+ "accounts/<str:acct_id>/withdrawals/<str:wid>",
+ views.bank_accounts_api_get_withdrawal,
+ ),
+ path(
+ "accounts/<str:acct_id>/withdrawals/<str:wid>/confirm",
+ views.bank_accounts_api_confirm_withdrawal,
+ ),
+ path(
+ "accounts/<str:acct_id>/withdrawals/<str:wid>/abort",
+ views.bank_accounts_api_abort_withdrawal,
+ ),
path("testing/withdraw", views.withdraw_headless, name="testing-withdraw"),
- path("testing/withdraw-uri", views.withdraw_headless_uri, name="testing-withdraw-uri"),
+ path(
+ "testing/withdraw-uri", views.withdraw_headless_uri, name="testing-withdraw-uri"
+ ),
path("testing/register", views.register_headless, name="testing-withdraw-register"),
- path("withdrawal-operation/<str:withdraw_id>", views.register_headless, name="tba-withdrawal-operation"),
]
urlpatterns = [
path("taler-bank-api/", include(taler_bank_api_patterns)),
+ path("", include(taler_bank_accounts_api_patterns)),
path("taler-wire-gateway/", include(taler_wire_gateway_patterns)),
path("", RedirectView.as_view(pattern_name="profile"), name="index"),
path("favicon.ico", views.ignore),
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index 16a4b06..2e74f8f 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -408,12 +408,13 @@ def internal_register(request):
# Registration goes through.
with transaction.atomic():
+ bank_internal_account = BankAccount.objects.get(account_no=1)
+
user = User.objects.create_user(username=username, password=password)
user_account = BankAccount(user=user)
user_account.save()
# Give the user their joining bonus
- bank_internal_account = BankAccount.objects.get(account_no=1)
wire_transfer(
Amount(settings.TALER_CURRENCY, 100, 0),
bank_internal_account,
@@ -835,7 +836,9 @@ def twg_transfer(request, user_account, acct_id):
subject = f"{wtid} {exchange_base_url}"
- wtrans = wire_transfer(amount, exchange_account, credit_account, subject, request_uid)
+ wtrans = wire_transfer(
+ amount, exchange_account, credit_account, subject, request_uid
+ )
return JsonResponse(
{
@@ -1015,7 +1018,7 @@ def api_withdraw_operation(request, withdraw_id):
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)
- if op.selection_done or op.withdraw_done:
+ if op.selection_done or op.confirmation_done:
if (
op.selected_exchange_account != exchange_account
or op.selected_reserve_pub != selected_reserve_pub
@@ -1026,17 +1029,26 @@ def api_withdraw_operation(request, withdraw_id):
)
# No conflict, same data!
return JsonResponse(dict(), status=200)
- op.selected_exchange_account = exchange_account
- op.selected_reserve_pub = selected_reserve_pub
- op.selection_done = True
- op.save()
+ with transaction.atomic():
+ op.selected_exchange_account = exchange_account
+ op.selected_reserve_pub = selected_reserve_pub
+ if op.confirmation_done and not op.selection_done:
+ # Confirmation already happened, we still need to transfer funds!
+ wire_transfer(
+ op.amount,
+ user_account,
+ op.selected_exchange_account,
+ op.selected_reserve_pub,
+ )
+ op.selection_done = True
+ op.save()
return JsonResponse(dict(), status=200)
elif request.method == "GET":
host = request.get_host()
return JsonResponse(
dict(
selection_done=op.selection_done,
- transfer_done=op.withdraw_done,
+ transfer_done=op.confirmation_done,
amount=op.amount.stringify(),
wire_types=["x-taler-bank"],
sender_wire=f"payto://x-taler-bank/{host}/{op.withdraw_account.user.username}",
@@ -1106,7 +1118,7 @@ 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:
+ if op.confirmation_done:
return redirect("profile")
if request.method == "POST":
hashed_attempt = hash_answer(request.POST.get("pin_0", ""))
@@ -1119,7 +1131,7 @@ def confirm_withdrawal(request, withdraw_id):
)
request.session["captcha_failed"] = True, False, "Wrong CAPTCHA answer."
return redirect("withdraw-confirm", withdraw_id=withdraw_id)
- op.withdraw_done = True
+ op.confirmation_done = True
op.save()
wire_transfer(
op.amount,
@@ -1218,3 +1230,125 @@ def wire_transfer(amount, debit_account, credit_account, subject, request_uid=No
transaction_item.save()
return transaction_item
+
+
+@csrf_exempt
+@require_GET
+@login_via_headers
+def bank_accounts_api_balance(request, user_account, acct_id):
+ """
+ Query the balance for an account.
+ """
+ acct = user_account.bankaccount
+
+ if acct_id != user_account.username:
+ # FIXME: respond nicely
+ raise Exception(
+ f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')"
+ )
+
+ return JsonResponse(dict(balance=acct.balance.stringify()))
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_create_withdrawal(request, user, acct_id):
+ user_account = BankAccount.objects.get(user=user)
+
+ if acct_id != user_account.user.username:
+ # FIXME: respond nicely
+ raise Exception(
+ f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')"
+ )
+
+ data = WithdrawHeadlessUri(json.loads(decode_body(request)))
+ amount = Amount.parse(data.get("amount"))
+ withdraw_amount = SignedAmount(True, amount)
+ debt_threshold = SignedAmount.parse(settings.TALER_MAX_DEBT)
+ user_balance = user_account.balance
+ if user_balance - withdraw_amount < -debt_threshold:
+ raise DebitLimitException(
+ f"Aborting payment initiated by '{user_account.user.username}', debit limit {debt_threshold} crossed."
+ )
+ op = TalerWithdrawOperation(amount=amount, withdraw_account=user_account)
+ op.save()
+ host = request.get_host()
+ taler_withdraw_uri = f"taler://withdraw/{host}/-/{op.withdraw_id}"
+ return JsonResponse(
+ {"taler_withdraw_uri": taler_withdraw_uri, "withdrawal_id": op.withdraw_id}
+ )
+
+
+@csrf_exempt
+@require_GET
+@login_via_headers
+def bank_accounts_api_get_withdrawal(request, user, acct_id, wid):
+ user_account = BankAccount.objects.get(user=user)
+ if acct_id != user_account.user.username:
+ # FIXME: respond nicely
+ raise Exception(
+ f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')"
+ )
+ op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+ selected_exchange_account = None
+ if op.selected_exchange_account:
+ selected_exchange_account = op.selected_exchange_account.user.name
+ return JsonResponse(
+ {
+ "amount": op.amount.stringify(),
+ "selection_done": op.selection_done,
+ "confirmation_done": op.confirmation_done,
+ "selected_reserve_pub": op.selected_reserve_pub,
+ "selected_exchange_account": selected_exchange_account,
+ "aborted": op.aborted,
+ }
+ )
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_abort_withdrawal(request, user, acct_id, wid):
+ user_account = BankAccount.objects.get(user=user)
+ if acct_id != user_account.user.username:
+ # FIXME: respond nicely
+ raise Exception(
+ f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')"
+ )
+ op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+
+ if op.confirmation_done:
+ return JsonResponse(dict(hint="can't abort confirmed withdrawal"), status=409)
+ op.aborted = True
+ op.save()
+ return JsonResponse(dict(), status=200)
+
+
+@csrf_exempt
+@require_POST
+@login_via_headers
+def bank_accounts_api_confirm_withdrawal(request, user, acct_id, wid):
+ user_account = BankAccount.objects.get(user=user)
+ if acct_id != user_account.user.username:
+ # FIXME: respond nicely
+ raise Exception(
+ f"credentials do not match URL ('{acct_id}' vs '{user_account.username}')"
+ )
+ op = TalerWithdrawOperation.objects.get(withdraw_id=wid)
+ if op.confirmation_done:
+ return JsonResponse(dict(), status=200)
+ if op.aborted:
+ return JsonResponse(dict(hint="can't confirm aborted withdrawal"), status=409)
+
+ with transaction.atomic():
+ if op.selection_done:
+ wire_transfer(
+ op.amount,
+ user_account,
+ op.selected_exchange_account,
+ op.selected_reserve_pub,
+ )
+ op.confirmation_done = True
+ op.save()
+ return JsonResponse(dict(), status=200)
diff --git a/talerbank/settings.py b/talerbank/settings.py
index ba880e3..8024907 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -217,7 +217,9 @@ else:
ALLOW_REGISTRATIONS = False
-_show_freeform_withdrawal = TC.value_string("bank", "SHOW_FREEFORM_WITHDRAWAL", default="no")
+_show_freeform_withdrawal = TC.value_string(
+ "bank", "SHOW_FREEFORM_WITHDRAWAL", default="no"
+)
if _show_freeform_withdrawal.lower() == "yes":
SHOW_FREEFORM_WITHDRAWAL = True
else: