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

@ -448,6 +448,67 @@
transform: scale(0.92);
transform-origin: center top;
}
.own-acct-lead {
margin: 0 0 0.85rem;
color: var(--muted);
font-size: 0.95rem;
line-height: 1.45;
}
.own-acct-lead strong { color: #e8c878; }
.own-acct-creds {
margin: 0 0 0.85rem;
padding: 0.75rem 0.85rem;
border-radius: 12px;
background: rgba(94, 234, 212, 0.08);
border: 1px solid rgba(94, 234, 212, 0.35);
text-align: left;
}
.own-acct-warn {
margin: 0 0 0.65rem;
font-size: 0.86rem;
color: #ccfbf1;
line-height: 1.4;
}
.own-acct-warn strong { color: #5eead4; }
.own-acct-dl {
margin: 0;
display: grid;
gap: 0.45rem;
}
.own-acct-dl div {
display: grid;
grid-template-columns: 5.5rem 1fr;
gap: 0.35rem;
align-items: baseline;
}
.own-acct-dl dt {
margin: 0;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.own-acct-dl dd {
margin: 0;
}
.own-acct-dl code {
font-size: 0.95rem;
font-weight: 700;
color: #f0d090;
word-break: break-all;
user-select: all;
}
#auto-account-btn {
border: 0;
cursor: pointer;
width: 100%;
max-width: 22rem;
}
#auto-account-btn:disabled {
opacity: 0.55;
cursor: wait;
}
.stats {
background: rgba(18, 28, 28, 0.55);
border: 1px solid rgba(62, 207, 191, 0.28);
@ -763,18 +824,43 @@
<div class="more-fold-body" id="more-fold-body" hidden>
<div class="more-card">
<h2>Own bank account?</h2>
<p>Register in the bank UI for your own balance (separate from the shared pool).</p>
<p class="own-acct-lead">
We can <strong>create a personal bank account for you</strong> automatically
(you do not pick a username or password). Balance starts at
<strong>GOA:0</strong> — separate from the shared pool.
</p>
<div class="more-actions" style="margin-bottom:0.75rem">
<button type="button" class="cta teal" id="auto-account-btn">
Create my bank account →
</button>
</div>
<div class="own-acct-creds" id="auto-account-box" hidden>
<p class="own-acct-warn" role="status">
<strong>Created for you.</strong>
Copy these credentials now — they are not stored for recovery.
Login at the bank UI; balance is <strong>GOA:0</strong>.
</p>
<dl class="own-acct-dl">
<div><dt>Username</dt><dd><code id="auto-account-user"></code></dd></div>
<div><dt>Password</dt><dd><code id="auto-account-pass"></code></dd></div>
<div><dt>Balance</dt><dd><code id="auto-account-bal">GOA:0</code></dd></div>
</dl>
<p class="meta" id="auto-account-status" style="margin:0.5rem 0 0"></p>
</div>
<div class="more-qr">
<div class="qr-taler-wrap">
<div class="goa-pay-taler-qr" id="qr-webui-wrap">
<div class="goa-pay-taler-qr__inner" id="qr-webui"></div>
<img class="goa-pay-taler-qr__logo" id="qr-webui-logo" alt="" />
</div>
<div class="qr-label">Bank UI · register / login</div>
<div class="qr-label">Bank UI · login with your credentials</div>
</div>
</div>
<div class="more-actions">
<a class="cta-sec" href="/webui/">Open bank UI →</a>
<a class="cta-sec" href="/webui/" id="auto-account-webui">Open bank UI →</a>
<button type="button" class="cta-sec" id="auto-account-again" hidden style="margin-top:0.45rem">
Generate another account
</button>
</div>
</div>
@ -1095,6 +1181,71 @@
);
webuiPainted = true;
});
function createAutoAccount() {
var btn = document.getElementById("auto-account-btn");
var box = document.getElementById("auto-account-box");
var st = document.getElementById("auto-account-status");
var again = document.getElementById("auto-account-again");
if (btn) {
btn.disabled = true;
btn.textContent = "Creating account…";
}
if (st) st.textContent = "";
fetch("/intro/auto-account.json", { cache: "no-store" })
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(function (d) {
if (!d || !d.ok) throw new Error((d && d.error) || "create failed");
if (box) box.hidden = false;
var u = document.getElementById("auto-account-user");
var p = document.getElementById("auto-account-pass");
var b = document.getElementById("auto-account-bal");
if (u) u.textContent = d.username || "—";
if (p) p.textContent = d.password || "—";
if (b) b.textContent = d.balance || "GOA:0";
if (st) {
st.textContent =
"Account created for you · save username & password · balance " +
(d.balance || "GOA:0");
}
if (btn) {
btn.hidden = true;
btn.disabled = false;
btn.textContent = "Create my bank account →";
}
if (again) again.hidden = false;
})
.catch(function (err) {
if (box) box.hidden = false;
if (st) {
st.textContent =
"Could not create account — try again. " +
(err && err.message ? err.message : "");
}
if (btn) {
btn.disabled = false;
btn.hidden = false;
btn.textContent = "Create my bank account →";
}
});
}
var btn = document.getElementById("auto-account-btn");
var again = document.getElementById("auto-account-again");
if (btn) btn.addEventListener("click", createAutoAccount);
if (again) {
again.addEventListener("click", function () {
var b = document.getElementById("auto-account-btn");
if (b) {
b.hidden = false;
b.disabled = false;
}
again.hidden = true;
createAutoAccount();
});
}
})();
function fmtCest(unix, fallback) {

View file

@ -41,6 +41,14 @@ server {
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
# Auto-create personal bank account (credentials returned once; balance GOA:0)
location = /intro/auto-account.json {
proxy_pass http://127.0.0.1:19096/auto-account.json;
proxy_http_version 1.1;
proxy_set_header Host $host;
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
location /intro/ {
alias /var/www/bank-landing/;
}

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()

View file

@ -39,6 +39,21 @@ if "demo-withdraw.json" not in t:
print("nginx location added")
else:
print("nginx already has demo-withdraw")
if "auto-account.json" not in t:
t=p.read_text()
block2=""" location = /intro/auto-account.json {
proxy_pass http://127.0.0.1:19096/auto-account.json;
proxy_http_version 1.1;
proxy_set_header Host \$host;
add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * always;
}
"""
t=t.replace(" location /intro/ {", block2+" location /intro/ {", 1)
p.write_text(t)
print("nginx auto-account location added")
else:
print("nginx already has auto-account")
PY
nginx -t && nginx -s reload || true
else