bank: API for auto-created personal accounts (balance zero)

Add GET /intro/auto-account.json on the bank helper (demo-withdraw-api):
public registration with generated username/password, balance starts GOA:0,
credentials returned once for the client to display.

Nginx proxies the new path; install-demo-withdraw-api.sh installs the
location. This is separate from the shared explorer demo withdraw.
This commit is contained in:
Hernâni Marques 2026-07-10 20:42:20 +02:00
parent 65629ea57c
commit f341116371
No known key found for this signature in database
4 changed files with 267 additions and 14 deletions

View file

@ -1,10 +1,12 @@
#!/usr/bin/env python3
"""
HTTP helper for bank landing: mint a fresh demo withdrawal from the shared
explorer pool so app users get GOA without registering a bank account.
HTTP helper for bank landing:
- GET /demo-withdraw.json mint shared-pool (explorer) demo withdraw
- GET /auto-account.json create a personal bank account (balance 0)
and return one-time credentials for the user to copy
Listens on 127.0.0.1:19096 (only inside bank container / localhost).
Nginx proxies GET /intro/demo-withdraw.json this service.
Nginx proxies /intro/*.json this service.
Env:
BANK_URL default http://127.0.0.1:9012
@ -18,7 +20,9 @@ from __future__ import annotations
import json
import os
import re
import secrets
import ssl
import string
import time
import urllib.error
import urllib.request
@ -119,6 +123,71 @@ def mint_withdraw() -> dict:
}
def _rand_username() -> str:
# bank usernames: keep simple [a-z0-9]
alphabet = string.ascii_lowercase + string.digits
return "guest" + "".join(secrets.choice(alphabet) for _ in range(8))
def _rand_password() -> str:
# readable enough to type; no ambiguous 0/O/1/l
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
return "".join(secrets.choice(alphabet) for _ in range(14))
def create_personal_account() -> dict:
"""Public registration: auto username/password, balance starts at 0."""
username = _rand_username()
password = _rand_password()
name = "Guest " + username[5:9].upper()
code, body = http_json(
"POST",
f"{BANK}/accounts",
{
"username": username,
"password": password,
"name": name,
},
)
if code not in (200, 201, 204):
# retry once on conflict
if code in (409, 400):
username = _rand_username()
code, body = http_json(
"POST",
f"{BANK}/accounts",
{
"username": username,
"password": password,
"name": name,
},
)
if code not in (200, 201, 204):
raise RuntimeError(f"register failed HTTP {code}: {body}")
payto = ""
if isinstance(body, dict):
payto = body.get("internal_payto_uri") or body.get("payto_uri") or ""
if not payto:
payto = f"payto://x-taler-bank/bank.hacktivism.ch/{username}?receiver-name={username}"
return {
"ok": True,
"created_for_you": True,
"username": username,
"password": password,
"name": name,
"balance": "GOA:0",
"balance_note": "Starts at zero — not the shared community pool.",
"payto_uri": payto,
"webui": "https://bank.hacktivism.ch/webui/",
"hint": (
"This bank account was created for you automatically. "
"Copy username and password now — we do not store them for later recovery. "
"Balance starts at GOA:0."
),
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
}
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
sys_stderr = __import__("sys").stderr
@ -136,15 +205,25 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
path = self.path.split("?", 1)[0]
if path not in ("/", "/demo-withdraw.json", "/intro/demo-withdraw.json"):
self.send_response(404)
self._cors()
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":false,"error":"not found"}')
return
try:
body = mint_withdraw()
if path in (
"/",
"/demo-withdraw.json",
"/intro/demo-withdraw.json",
):
body = mint_withdraw()
elif path in (
"/auto-account.json",
"/intro/auto-account.json",
):
body = create_personal_account()
else:
self.send_response(404)
self._cors()
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(b'{"ok":false,"error":"not found"}')
return
raw = json.dumps(body).encode()
self.send_response(200)
self._cors()