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: scale(0.92);
transform-origin: center top; 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 { .stats {
background: rgba(18, 28, 28, 0.55); background: rgba(18, 28, 28, 0.55);
border: 1px solid rgba(62, 207, 191, 0.28); 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-fold-body" id="more-fold-body" hidden>
<div class="more-card"> <div class="more-card">
<h2>Own bank account?</h2> <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="more-qr">
<div class="qr-taler-wrap"> <div class="qr-taler-wrap">
<div class="goa-pay-taler-qr" id="qr-webui-wrap"> <div class="goa-pay-taler-qr" id="qr-webui-wrap">
<div class="goa-pay-taler-qr__inner" id="qr-webui"></div> <div class="goa-pay-taler-qr__inner" id="qr-webui"></div>
<img class="goa-pay-taler-qr__logo" id="qr-webui-logo" alt="" /> <img class="goa-pay-taler-qr__logo" id="qr-webui-logo" alt="" />
</div> </div>
<div class="qr-label">Bank UI · register / login</div> <div class="qr-label">Bank UI · login with your credentials</div>
</div> </div>
</div> </div>
<div class="more-actions"> <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>
</div> </div>
@ -1095,6 +1181,71 @@
); );
webuiPainted = true; 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) { function fmtCest(unix, fallback) {

View file

@ -41,6 +41,14 @@ server {
add_header Cache-Control "no-store" always; add_header Cache-Control "no-store" always;
add_header Access-Control-Allow-Origin * 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/ { location /intro/ {
alias /var/www/bank-landing/; alias /var/www/bank-landing/;
} }

View file

@ -1,10 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
HTTP helper for bank landing: mint a fresh demo withdrawal from the shared HTTP helper for bank landing:
explorer pool so app users get GOA without registering a bank account. - 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). 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: Env:
BANK_URL default http://127.0.0.1:9012 BANK_URL default http://127.0.0.1:9012
@ -18,7 +20,9 @@ from __future__ import annotations
import json import json
import os import os
import re import re
import secrets
import ssl import ssl
import string
import time import time
import urllib.error import urllib.error
import urllib.request 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): class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args): def log_message(self, fmt, *args):
sys_stderr = __import__("sys").stderr sys_stderr = __import__("sys").stderr
@ -136,15 +205,25 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self): def do_GET(self):
path = self.path.split("?", 1)[0] 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: 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() raw = json.dumps(body).encode()
self.send_response(200) self.send_response(200)
self._cors() self._cors()

View file

@ -39,6 +39,21 @@ if "demo-withdraw.json" not in t:
print("nginx location added") print("nginx location added")
else: else:
print("nginx already has demo-withdraw") 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 PY
nginx -t && nginx -s reload || true nginx -t && nginx -s reload || true
else else