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:
parent
65629ea57c
commit
f341116371
4 changed files with 267 additions and 14 deletions
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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/;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"):
|
try:
|
||||||
|
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.send_response(404)
|
||||||
self._cors()
|
self._cors()
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(b'{"ok":false,"error":"not found"}')
|
self.wfile.write(b'{"ok":false,"error":"not found"}')
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
body = mint_withdraw()
|
|
||||||
raw = json.dumps(body).encode()
|
raw = json.dumps(body).encode()
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self._cors()
|
self._cors()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue