docs: new root as prior history lost (orphan + GC); ~215 commits not recoverable
This commit is contained in:
commit
96961f23f5
268 changed files with 24161 additions and 0 deletions
71
scripts/taler-bank/README.md
Normal file
71
scripts/taler-bank/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# taler-bank scripts
|
||||
|
||||
Container: **`taler-hacktivism-bank`** (libeufin-bank, GOA, **no IBAN**).
|
||||
|
||||
| File | Container path | User |
|
||||
|------|----------------|------|
|
||||
| `start_base_services_for_taler_bank.sh` | `/root/` | **root** |
|
||||
| `start_bank.sh` | `/usr/local/bin/` | **libeufin-bank** |
|
||||
| `check_bank-health.sh` | `/usr/local/bin/` | libeufin-bank / any |
|
||||
| `landing-stats.sh` | `/usr/local/bin/` | root (in container) — writes `/var/www/bank-landing/stats.json` |
|
||||
| `landing-stats-install.sh` | host only | root/podman — copies + runs + optional cron |
|
||||
| `demo-withdraw-api.py` | `/usr/local/bin/` | root — loopback **:19096** |
|
||||
| `install-demo-withdraw-api.sh` | host only | installs API + nginx + auto-confirm |
|
||||
| `auto-confirm-withdrawals.sh` | `/usr/local/bin/` | root — **explorer-only** confirm loop |
|
||||
| `refresh-demo-withdraw.sh` | `/usr/local/bin/` | refresh static `withdraw.uri` |
|
||||
| `credit-account.sh` | host/ops | admin → user credit |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# root in container
|
||||
./start_base_services_for_taler_bank.sh
|
||||
# then as libeufin-bank in /usr/local/bin:
|
||||
./start_bank.sh --restart
|
||||
```
|
||||
|
||||
## Landing stats (inside container)
|
||||
|
||||
```bash
|
||||
# on koopa host — copy + run (writes /var/www/bank-landing/stats.json)
|
||||
./landing-stats-install.sh
|
||||
./landing-stats-install.sh --cron # every minute inside container (* * * * *)
|
||||
./landing-stats-install.sh --run-only
|
||||
```
|
||||
|
||||
Details + JSON schema: `configs/bank-landing/README.md`.
|
||||
|
||||
## Demo withdraw + auto-account API
|
||||
|
||||
`demo-withdraw-api.py` listens on **127.0.0.1:19096** (proxied by nginx on the landing):
|
||||
|
||||
| Path | Behaviour |
|
||||
|------|-----------|
|
||||
| `GET /demo-withdraw.json` | Mint one-shot withdraw from shared **`explorer`** pool; write `withdraw.uri` + watch ids |
|
||||
| `GET /auto-account.json` | Public `POST /accounts` with generated **`goa-account-<random>`** user + password containing **pleasechangeme**; **balance GOA:0**; return credentials once |
|
||||
|
||||
Install / restart:
|
||||
|
||||
```bash
|
||||
./install-demo-withdraw-api.sh
|
||||
# Public checks:
|
||||
curl -sS https://bank.hacktivism.ch/intro/demo-withdraw.json | head
|
||||
curl -sS https://bank.hacktivism.ch/intro/auto-account.json | head # creates a real account
|
||||
```
|
||||
|
||||
Requires **python3** in the bank container. Env: `BANK_URL`, `BANK_USER`/`BANK_PASS`
|
||||
(or `/root/bank-explorer-password.txt`), `AMOUNT` (default `GOA:10` for shared withdraws).
|
||||
|
||||
### Auto-confirm (explorer only)
|
||||
|
||||
```bash
|
||||
# loop inside container — refuses non-explorer unless ALLOW_NON_EXPLORER=1
|
||||
auto-confirm-withdrawals.sh --loop 4
|
||||
```
|
||||
|
||||
Only confirms withdrawals owned by **`explorer`** when status is `selected`
|
||||
(community demo path). Does not confirm arbitrary customer withdraws.
|
||||
|
||||
## Config
|
||||
|
||||
See `configs/taler-hacktivism-bank/` and `configs/bank-landing/`.
|
||||
143
scripts/taler-bank/auto-confirm-withdrawals.sh
Executable file
143
scripts/taler-bank/auto-confirm-withdrawals.sh
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
#!/bin/bash
|
||||
# Auto-confirm bank withdrawals for the community demo pool ONLY.
|
||||
#
|
||||
# Only account: explorer (override only if you really mean another pool user
|
||||
# via BANK_USER, but still confirms with that user's token only — never
|
||||
# confirms other customers' withdrawals).
|
||||
#
|
||||
# Run once: auto-confirm-withdrawals.sh
|
||||
# Loop: auto-confirm-withdrawals.sh --loop [SECS]
|
||||
#
|
||||
# Env:
|
||||
# BANK_URL (default http://127.0.0.1:9012)
|
||||
# BANK_USER (default explorer) — must be the pool account
|
||||
# BANK_PASS or /root/bank-explorer-password.txt
|
||||
# LANDING_DIR (default /var/www/bank-landing)
|
||||
# ALLOW_NON_EXPLORER=1 — allow BANK_USER other than explorer (off by default)
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
BANK="${BANK%/}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
LOOP=0
|
||||
SLEEP=5
|
||||
if [ "${1:-}" = "--loop" ]; then
|
||||
LOOP=1
|
||||
SLEEP="${2:-5}"
|
||||
fi
|
||||
|
||||
# Safety: only the shared community account unless explicitly overridden
|
||||
if [ "$USER" != "explorer" ] && [ "${ALLOW_NON_EXPLORER:-0}" != "1" ]; then
|
||||
echo "refusing BANK_USER=$USER — auto-confirm is for explorer only (set ALLOW_NON_EXPLORER=1 to override)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PASS="${BANK_PASS:-}"
|
||||
if [ -z "$PASS" ]; then
|
||||
for f in "/root/bank-${USER}-password.txt" /root/bank-explorer-password.txt; do
|
||||
if [ -f "$f" ]; then PASS=$(tr -d '\n' <"$f"); break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$PASS" ] || { echo "no password for $USER" >&2; exit 1; }
|
||||
|
||||
# JSON field extract without python (bank container may lack python3)
|
||||
json_str() {
|
||||
# json_str FIELD < json-text-or-file
|
||||
local field="$1"
|
||||
local data
|
||||
if [ -f "${2:-}" ]; then data=$(cat "$2"); else data=$(cat); fi
|
||||
printf '%s' "$data" | sed -n "s/.*\"${field}\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p" | head -1
|
||||
}
|
||||
|
||||
token() {
|
||||
curl -sS -m 12 -u "${USER}:${PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true}' \
|
||||
"${BANK}/accounts/${USER}/token"
|
||||
}
|
||||
|
||||
# IDs created for the community pool (landing + watch list only)
|
||||
known_ids() {
|
||||
if [ -f "${LANDING_DIR}/withdraw.uri" ]; then
|
||||
basename "$(tr -d '\n' <"${LANDING_DIR}/withdraw.uri")"
|
||||
fi
|
||||
if [ -f "${LANDING_DIR}/withdraw-watch.ids" ]; then
|
||||
# strip empty / comments
|
||||
grep -E '^[0-9a-fA-F-]{36}$' "${LANDING_DIR}/withdraw-watch.ids" || true
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_one() {
|
||||
local wid="$1"
|
||||
local tok="$2"
|
||||
local info st uname conf
|
||||
|
||||
# Public status — must belong to explorer (pool), not another customer
|
||||
info=$(curl -sS -m 10 "${BANK}/withdrawals/${wid}" 2>/dev/null || true)
|
||||
[ -n "$info" ] || return 0
|
||||
|
||||
st=$(printf '%s' "$info" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
uname=$(printf '%s' "$info" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
|
||||
# Only confirm withdrawals owned by the pool account
|
||||
if [ -n "$uname" ] && [ "$uname" != "$USER" ]; then
|
||||
echo "skip $wid owner=$uname (only confirm $USER)"
|
||||
return 0
|
||||
fi
|
||||
# If API omits username, still only confirm via explorer token (cannot confirm others)
|
||||
if [ -z "$uname" ]; then
|
||||
# require sender_wire / payto to mention explorer when present
|
||||
case "$info" in
|
||||
*explorer*) ;;
|
||||
*)
|
||||
# still try only if status selected — confirm endpoint is under explorer account
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [ "$st" != "selected" ]; then
|
||||
echo "skip $wid status=${st:-?} owner=${uname:-?}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "confirming $wid as $USER (community pool) ..."
|
||||
conf=$(curl -sS -m 15 -o /tmp/acw-conf.out -w '%{http_code}' \
|
||||
-X POST \
|
||||
-H "Authorization: Bearer ${tok}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{}' \
|
||||
"${BANK}/accounts/${USER}/withdrawals/${wid}/confirm")
|
||||
echo " HTTP $conf $(head -c 200 /tmp/acw-conf.out 2>/dev/null || true)"
|
||||
curl -sS -m 8 "${BANK}/withdrawals/${wid}" 2>/dev/null \
|
||||
| sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/ now status=\1/p' | head -1 || true
|
||||
}
|
||||
|
||||
once() {
|
||||
local tjson tok ids
|
||||
tjson=$(token)
|
||||
tok=$(printf '%s' "$tjson" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
|
||||
if [ -z "$tok" ]; then
|
||||
echo "token fail for $USER: $tjson" >&2
|
||||
return 1
|
||||
fi
|
||||
ids=$(known_ids | sort -u)
|
||||
if [ -z "$ids" ]; then
|
||||
echo "no withdrawal ids to watch (landing withdraw.uri / withdraw-watch.ids)"
|
||||
return 0
|
||||
fi
|
||||
while read -r wid; do
|
||||
[ -n "$wid" ] || continue
|
||||
confirm_one "$wid" "$tok"
|
||||
done <<<"$ids"
|
||||
}
|
||||
|
||||
if [ "$LOOP" -eq 1 ]; then
|
||||
echo "auto-confirm loop every ${SLEEP}s for pool user=$USER only"
|
||||
while true; do
|
||||
once || true
|
||||
sleep "$SLEEP"
|
||||
done
|
||||
else
|
||||
once
|
||||
fi
|
||||
48
scripts/taler-bank/check_bank-health.sh
Executable file
48
scripts/taler-bank/check_bank-health.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
# Health check for manual libeufin-bank (same style as merchant/exchange health).
|
||||
# Container: taler-hacktivism-bank
|
||||
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
PORT=${PORT:-9012}
|
||||
|
||||
green() { echo -e "\e[32m$1\e[0m"; }
|
||||
red() { echo -e "\e[31m$1\e[0m"; }
|
||||
yellow() { echo -e "\e[33m$1\e[0m"; }
|
||||
|
||||
fail=0
|
||||
ok() { green "[OK] $1"; }
|
||||
bad() { red "[FAIL] $1"; fail=1; }
|
||||
|
||||
echo "=== Taler Bank (libeufin-bank) Health Check ==="
|
||||
|
||||
if pgrep -f 'libeufin-bank serve|MainKt serve|tech.libeufin.bank.MainKt' >/dev/null 2>&1; then
|
||||
ok "process libeufin-bank serve"
|
||||
else
|
||||
bad "process libeufin-bank serve is NOT running"
|
||||
fi
|
||||
|
||||
if curl -sf -m 3 "http://127.0.0.1:${PORT}/config" >/dev/null 2>&1; then
|
||||
ok "HTTP /config on 127.0.0.1:${PORT}"
|
||||
else
|
||||
bad "no HTTP response on 127.0.0.1:${PORT}/config"
|
||||
fi
|
||||
|
||||
if curl -sf -m 3 "http://127.0.0.1:${PORT}/taler-integration/config" >/dev/null 2>&1; then
|
||||
ok "HTTP /taler-integration/config on 127.0.0.1:${PORT}"
|
||||
else
|
||||
yellow "[WARN] no HTTP response on /taler-integration/config"
|
||||
fi
|
||||
|
||||
if pg_isready >/dev/null 2>&1; then
|
||||
ok "postgresql accepting connections"
|
||||
else
|
||||
yellow "[WARN] postgresql not ready (or pg_isready missing)"
|
||||
fi
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
green "=== ALL CRITICAL CHECKS PASSED ==="
|
||||
exit 0
|
||||
fi
|
||||
red "=== SOME CHECKS FAILED ==="
|
||||
exit 1
|
||||
96
scripts/taler-bank/credit-account.sh
Executable file
96
scripts/taler-bank/credit-account.sh
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
#!/bin/bash
|
||||
# Credit a bank user by transferring from admin (creates regional GOA via admin debit).
|
||||
# Run as root on koopa host (bank on 127.0.0.1:9012).
|
||||
#
|
||||
# Usage:
|
||||
# credit-account.sh [USERNAME] [AMOUNT]
|
||||
# credit-account.sh explorer GOA:1000
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
TO_USER="${1:-explorer}"
|
||||
AMOUNT="${2:-GOA:1000}"
|
||||
ADMIN_PASS="${BANK_ADMIN_PASS:-}"
|
||||
if [ -z "$ADMIN_PASS" ] && [ -f /root/bank-admin-password.txt ]; then
|
||||
ADMIN_PASS=$(tr -d '\n' </root/bank-admin-password.txt)
|
||||
fi
|
||||
[ -n "$ADMIN_PASS" ] || { echo "Need BANK_ADMIN_PASS or /root/bank-admin-password.txt" >&2; exit 1; }
|
||||
|
||||
W=$(mktemp -d)
|
||||
trap 'rm -rf "$W"' EXIT
|
||||
|
||||
echo '{"scope":"readwrite"}' >"$W/tok.json"
|
||||
curl -sS -m 15 -u "admin:${ADMIN_PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$W/tok.json" \
|
||||
"${BANK}/accounts/admin/token" >"$W/tok.out"
|
||||
TOKEN=$(python3 -c "import json;print(json.load(open('$W/tok.out')).get('access_token',''))")
|
||||
[ -n "$TOKEN" ] || { echo "admin token failed:"; cat "$W/tok.out"; exit 1; }
|
||||
|
||||
# payto for x-taler-bank regional accounts
|
||||
PAYTO="payto://x-taler-bank/bank.hacktivism.ch/${TO_USER}?receiver-name=${TO_USER}&message=credit-from-admin"
|
||||
|
||||
python3 - "$AMOUNT" "$PAYTO" "$W/tx.json" <<'PY'
|
||||
import json, sys, os
|
||||
amount, payto, out = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
# ShortHashCode: Crockford base32 of 32 random bytes (52 chars)
|
||||
_ALPH = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
def crockford32(data: bytes) -> str:
|
||||
n = int.from_bytes(data, "big")
|
||||
bits = len(data) * 8
|
||||
outc = []
|
||||
while bits > 0:
|
||||
bits -= 5
|
||||
outc.append(_ALPH[(n >> bits) & 31] if bits >= 0 else _ALPH[(n << (-bits)) & 31])
|
||||
if bits <= 0:
|
||||
break
|
||||
# pad to full groups
|
||||
s = "".join(outc)
|
||||
# simpler bit stream
|
||||
return None
|
||||
def crock32(b: bytes) -> str:
|
||||
bits = 0
|
||||
val = 0
|
||||
outc = []
|
||||
for byte in b:
|
||||
val = (val << 8) | byte
|
||||
bits += 8
|
||||
while bits >= 5:
|
||||
bits -= 5
|
||||
outc.append(_ALPH[(val >> bits) & 31])
|
||||
if bits:
|
||||
outc.append(_ALPH[(val << (5 - bits)) & 31])
|
||||
return "".join(outc)
|
||||
uid = crock32(os.urandom(32))
|
||||
json.dump({"payto_uri": payto, "amount": amount, "request_uid": uid}, open(out, "w"))
|
||||
print("request_uid", uid, "len", len(uid))
|
||||
PY
|
||||
|
||||
echo "POST admin -> $TO_USER amount=$AMOUNT"
|
||||
curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$W/tx.json" \
|
||||
"${BANK}/accounts/admin/transactions" | tee "$W/tx.out"
|
||||
echo
|
||||
|
||||
# show balances
|
||||
for u in admin "$TO_USER"; do
|
||||
curl -sS -m 10 -u "admin:${ADMIN_PASS}" \
|
||||
-H 'Content-Type: application/json' -d '{"scope":"readonly"}' \
|
||||
"${BANK}/accounts/admin/token" >"$W/rtok" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# re-token readonly and print target balance
|
||||
echo '{"scope":"readonly"}' >"$W/rtok.json"
|
||||
# admin can GET any account
|
||||
curl -sS -m 10 -H "Authorization: Bearer ${TOKEN}" \
|
||||
"${BANK}/accounts/${TO_USER}" | tee "$W/acc.out"
|
||||
echo
|
||||
python3 -c "
|
||||
import json
|
||||
d=json.load(open('$W/acc.out'))
|
||||
b=d.get('balance') or {}
|
||||
print('RESULT', '${TO_USER}', 'balance=', b.get('amount'), b.get('credit_debit_indicator'))
|
||||
print('debit_threshold=', d.get('debit_threshold'))
|
||||
"
|
||||
316
scripts/taler-bank/demo-withdraw-api.py
Executable file
316
scripts/taler-bank/demo-withdraw-api.py
Executable file
|
|
@ -0,0 +1,316 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
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 /intro/*.json → this service.
|
||||
|
||||
Env:
|
||||
BANK_URL default http://127.0.0.1:9012
|
||||
BANK_USER default explorer
|
||||
BANK_PASS or /root/bank-explorer-password.txt
|
||||
AMOUNT default GOA:10
|
||||
LANDING_DIR default /var/www/bank-landing
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import ssl
|
||||
import string
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
BANK = os.environ.get("BANK_URL", "http://127.0.0.1:9012").rstrip("/")
|
||||
# Public HTTPS base (Caddy) — absolute webui / login links
|
||||
BANK_PUBLIC = os.environ.get("BANK_PUBLIC", "https://bank.hacktivism.ch").rstrip("/")
|
||||
USER = os.environ.get("BANK_USER", "explorer")
|
||||
AMOUNT = os.environ.get("AMOUNT", "GOA:10")
|
||||
LANDING = Path(os.environ.get("LANDING_DIR", "/var/www/bank-landing"))
|
||||
LISTEN = ("127.0.0.1", int(os.environ.get("DEMO_WITHDRAW_PORT", "19096")))
|
||||
|
||||
|
||||
def public_webui_url() -> str:
|
||||
return f"{BANK_PUBLIC}/webui/"
|
||||
|
||||
|
||||
def load_pass() -> str:
|
||||
p = os.environ.get("BANK_PASS", "").strip()
|
||||
if p:
|
||||
return p
|
||||
for f in (
|
||||
Path(f"/root/bank-{USER}-password.txt"),
|
||||
Path("/root/bank-explorer-password.txt"),
|
||||
):
|
||||
if f.is_file():
|
||||
return f.read_text().strip()
|
||||
raise RuntimeError("no BANK_PASS / explorer password file")
|
||||
|
||||
|
||||
def http_json(method: str, url: str, body=None, headers=None, auth=None):
|
||||
data = None if body is None else json.dumps(body).encode()
|
||||
h = dict(headers or {})
|
||||
if body is not None:
|
||||
h["Content-Type"] = "application/json"
|
||||
if auth:
|
||||
import base64
|
||||
|
||||
token = base64.b64encode(f"{auth[0]}:{auth[1]}".encode()).decode()
|
||||
h["Authorization"] = f"Basic {token}"
|
||||
req = urllib.request.Request(url, data=data, method=method, headers=h)
|
||||
ctx = ssl.create_default_context()
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ctx, timeout=20) as r:
|
||||
raw = r.read().decode()
|
||||
return r.status, json.loads(raw) if raw.strip() else {}
|
||||
except urllib.error.HTTPError as e:
|
||||
raw = e.read().decode()
|
||||
try:
|
||||
return e.code, json.loads(raw)
|
||||
except Exception:
|
||||
return e.code, {"raw": raw[:500]}
|
||||
|
||||
|
||||
def mint_withdraw() -> dict:
|
||||
pw = load_pass()
|
||||
code, tok = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts/{USER}/token",
|
||||
{"scope": "readwrite", "refreshable": True},
|
||||
auth=(USER, pw),
|
||||
)
|
||||
if code != 200 or not tok.get("access_token"):
|
||||
raise RuntimeError(f"token failed HTTP {code}: {tok}")
|
||||
access = tok["access_token"]
|
||||
code, wd = http_json(
|
||||
"POST",
|
||||
f"{BANK}/accounts/{USER}/withdrawals",
|
||||
{"suggested_amount": AMOUNT},
|
||||
headers={"Authorization": f"Bearer {access}"},
|
||||
)
|
||||
if code not in (200, 201):
|
||||
raise RuntimeError(f"withdrawal create HTTP {code}: {wd}")
|
||||
uri = wd.get("taler_withdraw_uri") or ""
|
||||
wid = wd.get("withdrawal_id") or ""
|
||||
if not uri:
|
||||
raise RuntimeError(f"no taler_withdraw_uri: {wd}")
|
||||
if not wid:
|
||||
wid = uri.rstrip("/").split("/")[-1]
|
||||
# Keep host:port from libeufin (e.g. bank.hacktivism.ch:443). Stripping :443
|
||||
# breaks taler-integration withdraw links / main landing QR on HTTPS banks.
|
||||
uri = str(uri).strip()
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
(LANDING / "withdraw.uri").write_text(uri + "\n")
|
||||
(LANDING / "withdraw.amount").write_text(AMOUNT + "\n")
|
||||
(LANDING / "withdraw.created").write_text(
|
||||
time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()) + "\n"
|
||||
)
|
||||
watch_withdrawal(wid)
|
||||
return {
|
||||
"ok": True,
|
||||
"taler_withdraw_uri": uri,
|
||||
"withdrawal_id": wid,
|
||||
"amount": AMOUNT,
|
||||
"pool_account": USER,
|
||||
"taler_integration_base": f"{BANK_PUBLIC}/taler-integration/",
|
||||
"hint": "Open in GNU Taler Wallet (iOS/Android/desktop). No bank registration.",
|
||||
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
||||
}
|
||||
|
||||
|
||||
def watch_withdrawal(wid: str) -> None:
|
||||
"""Queue withdrawal_id for auto-confirm loop (shared pool + personal)."""
|
||||
if not wid:
|
||||
return
|
||||
LANDING.mkdir(parents=True, exist_ok=True)
|
||||
watch = LANDING / "withdraw-watch.ids"
|
||||
ids = set()
|
||||
if watch.is_file():
|
||||
ids = {ln.strip() for ln in watch.read_text().splitlines() if ln.strip()}
|
||||
ids.add(str(wid).strip())
|
||||
watch.write_text("\n".join(sorted(ids)) + "\n")
|
||||
|
||||
|
||||
|
||||
# Funny stems for usernames (bank-safe [a-z0-9-]).
|
||||
# Source: hand-curated in this file only — not scraped from the web.
|
||||
# Keep culture-neutral: light space / physics wordplay, no animals, foods,
|
||||
# body parts, religion, politics, or slang that can offend.
|
||||
_FUNNY_STEMS = (
|
||||
"nebula-nudge",
|
||||
"orbit-echo",
|
||||
"voidwave-vibe",
|
||||
"comet-crumb",
|
||||
"quark-pulse",
|
||||
"plasma-spark",
|
||||
"astro-glint",
|
||||
"lunar-loop",
|
||||
"warp-ripple",
|
||||
"photon-bloom",
|
||||
"galaxy-drift",
|
||||
"rocket-ribbon",
|
||||
"satellite-swirl",
|
||||
"meteor-mint",
|
||||
"stardust-swirl",
|
||||
"hyperdrive-hum",
|
||||
"cosmic-coral",
|
||||
"space-spark",
|
||||
"nova-nibble",
|
||||
"aurora-arc",
|
||||
"solar-swish",
|
||||
"pulsar-pop",
|
||||
"comet-cloud",
|
||||
"orbit-opal",
|
||||
"zenith-zip",
|
||||
"eclipse-echo",
|
||||
"horizon-hum",
|
||||
"starlight-step",
|
||||
)
|
||||
|
||||
|
||||
def _rand_username_and_name() -> tuple[str, str]:
|
||||
# Shown as goa-account-<funnypiece>-<tag> (e.g. goa-account-space-potato-k3m9x)
|
||||
stem = secrets.choice(_FUNNY_STEMS)
|
||||
alphabet = string.ascii_lowercase + string.digits
|
||||
tag = "".join(secrets.choice(alphabet) for _ in range(5))
|
||||
username = f"goa-account-{stem}-{tag}"
|
||||
name = username
|
||||
return username, name
|
||||
|
||||
|
||||
def _rand_password() -> str:
|
||||
# Embed "pleasechangeme" with random material before and after.
|
||||
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
||||
prefix = "".join(secrets.choice(alphabet) for _ in range(4))
|
||||
suffix = "".join(secrets.choice(alphabet) for _ in range(6))
|
||||
return prefix + "pleasechangeme" + suffix
|
||||
|
||||
|
||||
def create_personal_account() -> dict:
|
||||
"""Public registration: auto username/password, balance starts at 0."""
|
||||
username, name = _rand_username_and_name()
|
||||
password = _rand_password()
|
||||
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, name = _rand_username_and_name()
|
||||
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}")
|
||||
webui = public_webui_url()
|
||||
# Same shared-pool withdraw as step 2 (explorer + auto-confirm)
|
||||
wd = mint_withdraw()
|
||||
withdraw_uri = wd["taler_withdraw_uri"]
|
||||
return {
|
||||
"ok": True,
|
||||
"created_for_you": True,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"name": name,
|
||||
"display_name": name,
|
||||
"balance": "GOA:0",
|
||||
"balance_note": "Starts at zero — not the shared community pool.",
|
||||
"taler_withdraw_uri": withdraw_uri,
|
||||
"withdrawal_id": wd["withdrawal_id"],
|
||||
"withdraw_amount": wd.get("amount") or AMOUNT,
|
||||
"pool_account": wd.get("pool_account") or USER,
|
||||
"qr_payload": withdraw_uri,
|
||||
"webui": webui,
|
||||
"account_url": webui,
|
||||
"login_url": webui,
|
||||
"hint": (
|
||||
f"Login at {webui} with username {username} and the password shown "
|
||||
"(not stored for recovery). Wallet QR is taler://withdraw/… from the "
|
||||
f"shared pool ({USER}), same as step 2."
|
||||
),
|
||||
"created": time.strftime("%Y-%m-%dT%H:%MZ", time.gmtime()),
|
||||
"created_human": time.strftime("%Y-%m-%d %H:%M %Z", time.localtime()),
|
||||
}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt, *args):
|
||||
sys_stderr = __import__("sys").stderr
|
||||
sys_stderr.write("%s - %s\n" % (self.address_string(), fmt % args))
|
||||
|
||||
def _cors(self):
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
self._cors()
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?", 1)[0]
|
||||
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._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()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
except Exception as e:
|
||||
raw = json.dumps({"ok": False, "error": str(e)}).encode()
|
||||
self.send_response(500)
|
||||
self._cors()
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(raw)))
|
||||
self.end_headers()
|
||||
self.wfile.write(raw)
|
||||
|
||||
|
||||
def main():
|
||||
httpd = ThreadingHTTPServer(LISTEN, Handler)
|
||||
print(f"demo-withdraw-api on http://{LISTEN[0]}:{LISTEN[1]}/", flush=True)
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
80
scripts/taler-bank/install-demo-withdraw-api.sh
Executable file
80
scripts/taler-bank/install-demo-withdraw-api.sh
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
#!/bin/bash
|
||||
# Install + start demo-withdraw API + auto-confirm loop inside bank container.
|
||||
# Host:
|
||||
# ./scripts/taler-bank/install-demo-withdraw-api.sh
|
||||
set -euo pipefail
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
CTR="${BANK_CONTAINER:-taler-hacktivism-bank}"
|
||||
|
||||
podman cp "$ROOT/demo-withdraw-api.py" "$CTR:/usr/local/bin/demo-withdraw-api.py"
|
||||
podman cp "$ROOT/auto-confirm-withdrawals.sh" "$CTR:/usr/local/bin/auto-confirm-withdrawals.sh"
|
||||
podman cp "$ROOT/refresh-demo-withdraw.sh" "$CTR:/usr/local/bin/refresh-demo-withdraw.sh"
|
||||
podman exec -u root "$CTR" chmod 755 \
|
||||
/usr/local/bin/demo-withdraw-api.py \
|
||||
/usr/local/bin/auto-confirm-withdrawals.sh \
|
||||
/usr/local/bin/refresh-demo-withdraw.sh
|
||||
|
||||
# nginx: proxy demo-withdraw.json
|
||||
NGX=/etc/nginx/sites-available/bank-landing
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
set -e
|
||||
f=/etc/nginx/sites-available/bank-landing
|
||||
if ! grep -q demo-withdraw.json "$f"; then
|
||||
# insert before location /intro/
|
||||
python3 - <<PY
|
||||
from pathlib import Path
|
||||
p=Path("/etc/nginx/sites-available/bank-landing")
|
||||
t=p.read_text()
|
||||
block=""" location = /intro/demo-withdraw.json {
|
||||
proxy_pass http://127.0.0.1:19096/demo-withdraw.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;
|
||||
}
|
||||
"""
|
||||
if "demo-withdraw.json" not in t:
|
||||
t=t.replace(" location /intro/ {", block+" location /intro/ {", 1)
|
||||
p.write_text(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
|
||||
echo "nginx already configured"
|
||||
fi
|
||||
'
|
||||
|
||||
# start/restart API
|
||||
podman exec -u root "$CTR" bash -lc '
|
||||
pkill -f "demo-withdraw-api.py" 2>/dev/null || true
|
||||
nohup python3 /usr/local/bin/demo-withdraw-api.py \
|
||||
>>/var/log/demo-withdraw-api.log 2>&1 </dev/null &
|
||||
echo "api pid $!"
|
||||
# auto-confirm loop
|
||||
pkill -f "auto-confirm-withdrawals.sh --loop" 2>/dev/null || true
|
||||
nohup /usr/local/bin/auto-confirm-withdrawals.sh --loop 4 \
|
||||
>>/var/log/auto-confirm-withdrawals.log 2>&1 </dev/null &
|
||||
echo "auto-confirm pid $!"
|
||||
sleep 1
|
||||
curl -sS -m 8 http://127.0.0.1:19096/demo-withdraw.json | head -c 300; echo
|
||||
'
|
||||
|
||||
echo "OK: demo-withdraw API + auto-confirm in $CTR"
|
||||
echo "Public: https://bank.hacktivism.ch/intro/demo-withdraw.json"
|
||||
78
scripts/taler-bank/landing-stats-install.sh
Normal file
78
scripts/taler-bank/landing-stats-install.sh
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#!/bin/bash
|
||||
# Install + run landing-stats.sh *inside* the bank container, and optionally
|
||||
# install a cron entry there. Documented in configs/bank-landing/README.md.
|
||||
#
|
||||
# Run on koopa host (root or user in podman group):
|
||||
# ./landing-stats-install.sh # copy + one-shot run
|
||||
# ./landing-stats-install.sh --cron # also install *minutely* cron inside container
|
||||
# ./landing-stats-install.sh --run-only # only exec existing script
|
||||
set -euo pipefail
|
||||
|
||||
DO_CRON=0
|
||||
RUN_ONLY=0
|
||||
# cron schedule (default: every minute)
|
||||
: "${LANDING_STATS_CRON:=* * * * *}"
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--cron) DO_CRON=1 ;;
|
||||
--run-only) RUN_ONLY=1 ;;
|
||||
-h|--help)
|
||||
sed -n '2,12p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ROOT=$(cd "$(dirname "$0")" && pwd)
|
||||
SRC="$ROOT/landing-stats.sh"
|
||||
[ -f "$SRC" ] || { echo "missing $SRC" >&2; exit 1; }
|
||||
|
||||
# Prefer the known names from ops docs
|
||||
C=""
|
||||
for name in taler-hacktivism-bank taler-bank-hacktivism; do
|
||||
if podman ps --format '{{.Names}}' 2>/dev/null | grep -qx "$name"; then
|
||||
C=$name
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$C" ]; then
|
||||
C=$(podman ps --format '{{.Names}}' 2>/dev/null | grep -i bank | head -1 || true)
|
||||
fi
|
||||
[ -n "$C" ] || { echo "no bank container running" >&2; exit 1; }
|
||||
echo "container=$C"
|
||||
|
||||
# Host password → container /root (if not already there)
|
||||
if [ -f /root/bank-explorer-password.txt ]; then
|
||||
podman cp /root/bank-explorer-password.txt "$C:/root/bank-explorer-password.txt" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ "$RUN_ONLY" != "1" ]; then
|
||||
podman exec "$C" mkdir -p /usr/local/bin /var/www/bank-landing
|
||||
podman cp "$SRC" "$C:/usr/local/bin/landing-stats.sh"
|
||||
podman exec "$C" chmod 755 /usr/local/bin/landing-stats.sh
|
||||
echo "installed /usr/local/bin/landing-stats.sh"
|
||||
fi
|
||||
|
||||
# Ensure landing dir exists for nginx intro
|
||||
podman exec "$C" mkdir -p /var/www/bank-landing
|
||||
|
||||
echo "running landing-stats.sh inside $C …"
|
||||
podman exec \
|
||||
-e LANDING_DIR=/var/www/bank-landing \
|
||||
-e BANK_USER=explorer \
|
||||
"$C" /usr/local/bin/landing-stats.sh
|
||||
|
||||
if [ "$DO_CRON" = "1" ]; then
|
||||
# default: every minute (* * * * *); override with LANDING_STATS_CRON='*/5 * * * *'
|
||||
podman exec -e LANDING_STATS_CRON="$LANDING_STATS_CRON" "$C" bash -c '
|
||||
sched="${LANDING_STATS_CRON:-* * * * *}"
|
||||
line="$sched TZ=Europe/Zurich LANDING_DIR=/var/www/bank-landing /usr/local/bin/landing-stats.sh >>/var/log/landing-stats.log 2>&1"
|
||||
(crontab -l 2>/dev/null | grep -v landing-stats.sh; echo "$line") | crontab -
|
||||
echo "cron installed:"; crontab -l | grep landing-stats
|
||||
'
|
||||
fi
|
||||
|
||||
echo "public check: curl -sS https://bank.hacktivism.ch/intro/stats.json | head"
|
||||
# If host maps 9013 → container landing, show local file
|
||||
podman exec "$C" head -c 400 /var/www/bank-landing/stats.json 2>/dev/null || true
|
||||
echo
|
||||
612
scripts/taler-bank/landing-stats.sh
Executable file
612
scripts/taler-bank/landing-stats.sh
Executable file
|
|
@ -0,0 +1,612 @@
|
|||
#!/bin/bash
|
||||
# Generate public landing stats JSON for bank.hacktivism.ch intro page.
|
||||
#
|
||||
# *** Run INSIDE the bank container *** (taler-hacktivism-bank).
|
||||
# Writes: $LANDING_DIR/stats.json → https://bank.hacktivism.ch/intro/stats.json
|
||||
#
|
||||
# Pure bash + curl + awk (no python). Times in Europe/Zurich (CET/CEST).
|
||||
#
|
||||
# Documented: configs/bank-landing/README.md
|
||||
set -euo pipefail
|
||||
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
# Demo funding account (for recent withdraw list / flow)
|
||||
BANK_USER="${BANK_USER:-explorer}"
|
||||
# Admin lists all accounts + can read other accounts' txs
|
||||
ADMIN_USER="${ADMIN_USER:-admin}"
|
||||
# Per-account transaction window (libeufin delta). Was -100 → systematically
|
||||
# undercounted credits/withdraws on active accounts (broken public stats).
|
||||
TX_DELTA="${TX_DELTA:--50000}"
|
||||
# Max accounts to list + scan (was 80 → missed later accounts; bank has 100+).
|
||||
MAX_SCAN_ACCOUNTS="${MAX_SCAN_ACCOUNTS:-500}"
|
||||
# Account-list page size for GET /accounts?delta=… (must cover all users)
|
||||
ACCOUNTS_DELTA="${ACCOUNTS_DELTA:--500}"
|
||||
# curl timeout per account (deeper history needs more headroom)
|
||||
TX_CURL_TIMEOUT="${TX_CURL_TIMEOUT:-25}"
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
|
||||
PASS="${BANK_PASS:-}"
|
||||
ADMIN_PASS="${BANK_ADMIN_PASS:-}"
|
||||
|
||||
detect_bank() {
|
||||
if [ -n "${BANK_URL:-}" ]; then
|
||||
echo "${BANK_URL%/}"
|
||||
return
|
||||
fi
|
||||
local port="" conf u code
|
||||
for conf in /etc/libeufin/bank-overrides.conf /etc/libeufin/libeufin-bank.conf; do
|
||||
if [ -f "$conf" ]; then
|
||||
port=$(grep -E '^\s*PORT\s*=' "$conf" 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
[ -n "$port" ] && break
|
||||
fi
|
||||
done
|
||||
port="${port:-9012}"
|
||||
for u in "http://127.0.0.1:${port}" "http://127.0.0.1:9012" "http://127.0.0.1:8080"; do
|
||||
code=$(curl -sS -m 2 -o /dev/null -w '%{http_code}' "$u/config" 2>/dev/null || echo 000)
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "$u"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "http://127.0.0.1:${port}"
|
||||
}
|
||||
|
||||
BANK="$(detect_bank)"
|
||||
BANK="${BANK%/}"
|
||||
|
||||
read_pass_file() {
|
||||
local f
|
||||
for f in "$@"; do
|
||||
if [ -f "$f" ] && [ -r "$f" ]; then
|
||||
tr -d '\n' <"$f"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
PASS=$(read_pass_file \
|
||||
"/root/bank-${BANK_USER}-password.txt" \
|
||||
/root/bank-explorer-password.txt \
|
||||
"/etc/libeufin/secrets/bank-${BANK_USER}-password.txt" || true)
|
||||
fi
|
||||
if [ -z "$ADMIN_PASS" ]; then
|
||||
ADMIN_PASS=$(read_pass_file \
|
||||
/root/bank-admin-password.txt \
|
||||
/etc/libeufin/secrets/bank-admin-password.txt || true)
|
||||
fi
|
||||
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
OUT="$LANDING_DIR/stats.json"
|
||||
RUN="$LANDING_DIR/stats-run.json"
|
||||
TMP="${OUT}.tmp.$$"
|
||||
mkdir -p "$LANDING_DIR"
|
||||
|
||||
now_iso() { date +%Y-%m-%dT%H:%M%z | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/'; }
|
||||
now_unix() { date +%s; }
|
||||
now_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||
iso_from_unix() {
|
||||
local u="$1"
|
||||
date -d "@${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|
||||
|| date -r "${u}" +%Y-%m-%dT%H:%M%z 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' \
|
||||
|| echo ""
|
||||
}
|
||||
# Human CEST/CET label for display: "2026-07-09 20:49 CEST"
|
||||
human_from_unix() {
|
||||
local u="$1"
|
||||
date -d "@${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|
||||
|| date -r "${u}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null \
|
||||
|| echo ""
|
||||
}
|
||||
|
||||
json_str() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr '\n' ' ')"
|
||||
}
|
||||
|
||||
# Public run status for intro page. Never wipe stats.json on failure.
|
||||
write_run() {
|
||||
local ok_json="$1" msg="${2:-}"
|
||||
cat >"$RUN" <<EOF
|
||||
{
|
||||
"ok": ${ok_json},
|
||||
"at": $(json_str "$(now_iso)"),
|
||||
"at_human": $(json_str "$(now_human)"),
|
||||
"error": $( [ -n "$msg" ] && json_str "$msg" || echo null )
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
write_err() {
|
||||
local msg="$1"
|
||||
write_run false "$msg"
|
||||
rm -f "$TMP" 2>/dev/null || true
|
||||
echo "error: $msg (stats.json left unchanged; stats-run.json updated)" >&2
|
||||
}
|
||||
|
||||
token_for() {
|
||||
local user="$1" pass="$2" out="$3"
|
||||
echo '{"scope":"readonly"}' >"$WORKDIR/tok-req.json"
|
||||
curl -sS -m 12 -u "${user}:${pass}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @"$WORKDIR/tok-req.json" \
|
||||
"${BANK}/accounts/${user}/token" >"$out" || true
|
||||
awk -F'"' '/access_token/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="access_token") { print $(i+2); exit }
|
||||
}' "$out" 2>/dev/null || true
|
||||
}
|
||||
|
||||
extract_field() {
|
||||
# extract "key":"value" or "key": number from a JSON blob (first hit)
|
||||
local key="$1" file="$2"
|
||||
awk -v key="$key" '
|
||||
function between(s, k, p, rest, q2, r) {
|
||||
p = index(s, "\"" k "\"")
|
||||
if (p == 0) return ""
|
||||
rest = substr(s, p + length(k) + 2)
|
||||
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
|
||||
if (substr(rest, 1, 1) == "\"") {
|
||||
rest = substr(rest, 2)
|
||||
q2 = index(rest, "\"")
|
||||
if (q2 == 0) return ""
|
||||
return substr(rest, 1, q2 - 1)
|
||||
}
|
||||
r = ""
|
||||
while (rest ~ /^[0-9]/) {
|
||||
r = r substr(rest, 1, 1)
|
||||
rest = substr(rest, 2)
|
||||
}
|
||||
return r
|
||||
}
|
||||
{ print between($0, key); exit }
|
||||
' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
# --- tokens ---
|
||||
if [ -z "$PASS" ] && [ -z "$ADMIN_PASS" ]; then
|
||||
write_err "no bank password (explorer or admin) for stats"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ADMIN_TOKEN=""
|
||||
if [ -n "$ADMIN_PASS" ]; then
|
||||
ADMIN_TOKEN=$(token_for "$ADMIN_USER" "$ADMIN_PASS" "$WORKDIR/admin-tok.json")
|
||||
fi
|
||||
USER_TOKEN=""
|
||||
if [ -n "$PASS" ]; then
|
||||
USER_TOKEN=$(token_for "$BANK_USER" "$PASS" "$WORKDIR/user-tok.json")
|
||||
fi
|
||||
# Prefer admin for everything when available
|
||||
AUTH_TOKEN="${ADMIN_TOKEN:-$USER_TOKEN}"
|
||||
if [ -z "$AUTH_TOKEN" ]; then
|
||||
write_err "token failed (admin/explorer)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- account list ---
|
||||
ACCOUNTS_N=0
|
||||
ACCOUNTS_USERS=0
|
||||
: >"$WORKDIR/usernames.txt"
|
||||
if [ -n "$ADMIN_TOKEN" ]; then
|
||||
# Use a large negative delta so we list *all* accounts (delta=-80 truncated
|
||||
# the roster and undercounted bank_accounts + flow).
|
||||
curl -sS -m 30 -H "Authorization: Bearer ${ADMIN_TOKEN}" \
|
||||
"${BANK}/accounts?delta=${ACCOUNTS_DELTA}" >"$WORKDIR/accounts.json" || true
|
||||
# usernames from "username":"..."
|
||||
tr -d '\n' <"$WORKDIR/accounts.json" \
|
||||
| sed 's/},{/}\n{/g' \
|
||||
| while IFS= read -r line; do
|
||||
u=$(printf '%s' "$line" | sed -n 's/.*"username"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$u" ] && echo "$u"
|
||||
done >"$WORKDIR/usernames.txt" || true
|
||||
ACCOUNTS_N=$(grep -cve '^\s*$' "$WORKDIR/usernames.txt" 2>/dev/null || echo 0)
|
||||
ACCOUNTS_USERS=$(grep -Eve '^(admin|exchange)$' "$WORKDIR/usernames.txt" 2>/dev/null | grep -cve '^\s*$' || echo 0)
|
||||
else
|
||||
echo "$BANK_USER" >"$WORKDIR/usernames.txt"
|
||||
ACCOUNTS_N=1
|
||||
ACCOUNTS_USERS=1
|
||||
fi
|
||||
|
||||
# --- scan txs: distinguish incoming (credit) vs withdraw (Taler debit) ---
|
||||
# all-wd.tsv: amount \t t_s \t subject \t username \t reserve
|
||||
# all-in.tsv: amount \t t_s \t subject \t username
|
||||
# flow.tsv: kind \t amount_num kind = incoming | withdraw | other_out
|
||||
: >"$WORKDIR/all-wd.tsv"
|
||||
: >"$WORKDIR/all-in.tsv"
|
||||
: >"$WORKDIR/flow.tsv"
|
||||
SCAN_OK=0
|
||||
SCAN_EMPTY=0
|
||||
SCAN_FAIL=0
|
||||
# Scan customer accounts only (not exchange/admin — exchange credits would double-count)
|
||||
{
|
||||
echo "$BANK_USER"
|
||||
grep -Eve "^(admin|exchange|${BANK_USER})$" "$WORKDIR/usernames.txt" 2>/dev/null || true
|
||||
} | awk 'NF && !seen[$0]++' | head -n "$MAX_SCAN_ACCOUNTS" >"$WORKDIR/scan-users.txt"
|
||||
|
||||
while IFS= read -r uname; do
|
||||
[ -n "$uname" ] || continue
|
||||
case "$uname" in admin|exchange) continue ;; esac
|
||||
# Safe filename (usernames are mostly [A-Za-z0-9_-])
|
||||
safe=$(printf '%s' "$uname" | tr -c 'A-Za-z0-9._-' '_')
|
||||
code=$(curl -sS -m "${TX_CURL_TIMEOUT}" -o "$WORKDIR/tx-${safe}.json" -w '%{http_code}' \
|
||||
-H "Authorization: Bearer ${AUTH_TOKEN}" \
|
||||
"${BANK}/accounts/${uname}/transactions?delta=${TX_DELTA}" 2>/dev/null || echo 000)
|
||||
# 204 / empty body: account with no transactions — normal, not an error
|
||||
if [ "$code" = "204" ] || [ ! -s "$WORKDIR/tx-${safe}.json" ]; then
|
||||
SCAN_EMPTY=$((SCAN_EMPTY + 1))
|
||||
continue
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
SCAN_FAIL=$((SCAN_FAIL + 1))
|
||||
continue
|
||||
fi
|
||||
SCAN_OK=$((SCAN_OK + 1))
|
||||
tr -d '\n' <"$WORKDIR/tx-${safe}.json" \
|
||||
| sed 's/},{/}\n{/g' \
|
||||
>"$WORKDIR/tx-lines.txt" 2>/dev/null || continue
|
||||
awk -v uname="$uname" -v flowf="$WORKDIR/flow.tsv" -v inf="$WORKDIR/all-in.tsv" '
|
||||
function between(s, key, p, rest, q2, r) {
|
||||
p = index(s, "\"" key "\"")
|
||||
if (p == 0) return ""
|
||||
rest = substr(s, p + length(key) + 2)
|
||||
while (rest ~ /^[[:space:]:]/) rest = substr(rest, 2)
|
||||
if (substr(rest, 1, 1) == "\"") {
|
||||
rest = substr(rest, 2)
|
||||
q2 = index(rest, "\"")
|
||||
if (q2 == 0) return ""
|
||||
return substr(rest, 1, q2 - 1)
|
||||
}
|
||||
r = ""
|
||||
while (rest ~ /^[0-9]/) {
|
||||
r = r substr(rest, 1, 1)
|
||||
rest = substr(rest, 2)
|
||||
}
|
||||
return r
|
||||
}
|
||||
function amt_n(a, p) {
|
||||
p = index(a, ":")
|
||||
if (p == 0) return 0
|
||||
return substr(a, p + 1) + 0
|
||||
}
|
||||
{
|
||||
line = $0
|
||||
dir = between(line, "direction")
|
||||
amt = between(line, "amount")
|
||||
ts = between(line, "t_s")
|
||||
subj = between(line, "subject")
|
||||
low = tolower(subj)
|
||||
if (amt == "") next
|
||||
n = amt_n(amt)
|
||||
if (dir == "credit") {
|
||||
# Incoming: bank credits into customer accounts (admin top-up, transfers in)
|
||||
print "incoming\t" n >> flowf
|
||||
printf "%s\t%s\t%s\t%s\n", amt, ts, subj, uname >> inf
|
||||
} else if (dir == "debit" && index(low, "withdraw") > 0) {
|
||||
# Withdraw: Taler withdrawal debit → exchange → wallet coins
|
||||
print "withdraw\t" n >> flowf
|
||||
res = subj
|
||||
sub(/^.*[Ww]ithdrawal[ ]+/, "", res)
|
||||
gsub(/[^A-Za-z0-9]/, "", res)
|
||||
printf "%s\t%s\t%s\t%s\t%s\n", amt, ts, subj, uname, res
|
||||
} else if (dir == "debit") {
|
||||
# Other debits (non-withdraw transfers)
|
||||
print "other_out\t" n >> flowf
|
||||
}
|
||||
}
|
||||
' "$WORKDIR/tx-lines.txt" >>"$WORKDIR/all-wd.tsv" 2>/dev/null || true
|
||||
done <"$WORKDIR/scan-users.txt"
|
||||
|
||||
# Sum by kind
|
||||
TOTAL_IN_N=$(awk -F'\t' '$1=="incoming"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
TOTAL_WD_FLOW_N=$(awk -F'\t' '$1=="withdraw"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
TOTAL_OTHER_OUT_N=$(awk -F'\t' '$1=="other_out"{s+=$2} END{printf "%.8f", s+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
N_INCOMING=$(awk -F'\t' '$1=="incoming"{c++} END{print c+0}' "$WORKDIR/flow.tsv" 2>/dev/null || echo 0)
|
||||
# total out = withdraw + other debits
|
||||
TOTAL_OUT_N=$(awk -v a="${TOTAL_WD_FLOW_N:-0}" -v b="${TOTAL_OTHER_OUT_N:-0}" 'BEGIN{printf "%.8f", a+b}')
|
||||
|
||||
# Sort by t_s descending
|
||||
sort -t$'\t' -k2,2nr "$WORKDIR/all-wd.tsv" -o "$WORKDIR/all-wd-sorted.tsv" 2>/dev/null \
|
||||
|| cp "$WORKDIR/all-wd.tsv" "$WORKDIR/all-wd-sorted.tsv"
|
||||
sort -t$'\t' -k2,2nr "$WORKDIR/all-in.tsv" -o "$WORKDIR/all-in-sorted.tsv" 2>/dev/null \
|
||||
|| cp "$WORKDIR/all-in.tsv" "$WORKDIR/all-in-sorted.tsv"
|
||||
|
||||
# Unique reserves = individual wallet withdraws (each wallet reserve_pub)
|
||||
WALLETS_N=$(awk -F'\t' '$5!=""{print $5}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
# Accounts that funded at least one withdraw
|
||||
ACCOUNTS_WITH_WD=$(awk -F'\t' '$4!=""{print $4}' "$WORKDIR/all-wd-sorted.tsv" | sort -u | grep -cve '^\s*$' || echo 0)
|
||||
|
||||
# Aggregates
|
||||
NOW=$(now_unix)
|
||||
GEN_ISO=$(now_iso)
|
||||
GEN_HUMAN=$(date +"%Y-%m-%d %H:%M %Z")
|
||||
|
||||
# Build recent withdraws JSON array (up to 10) with CEST times — landing shows all 10
|
||||
RECENT_WD_N="${RECENT_WD_N:-10}"
|
||||
: >"$WORKDIR/recent.jsonl"
|
||||
N_WD=0
|
||||
TOTAL_WD=0
|
||||
W24=0; N24=0; W7=0; N7=0
|
||||
DAY=$((NOW - 86400))
|
||||
WEEK=$((NOW - 7 * 86400))
|
||||
LAST_AMT=""; LAST_TS=""; LAST_SUBJ=""
|
||||
|
||||
while IFS=$'\t' read -r amt ts subj uname res; do
|
||||
[ -n "$amt" ] || continue
|
||||
n=$(printf '%s' "$amt" | awk -F: '{print $2+0}')
|
||||
N_WD=$((N_WD + 1))
|
||||
TOTAL_WD=$(awk -v a="$TOTAL_WD" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
ts_n=${ts:-0}
|
||||
if [ "$ts_n" -ge "$DAY" ] 2>/dev/null; then
|
||||
W24=$(awk -v a="$W24" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
N24=$((N24 + 1))
|
||||
fi
|
||||
if [ "$ts_n" -ge "$WEEK" ] 2>/dev/null; then
|
||||
W7=$(awk -v a="$W7" -v b="$n" 'BEGIN{printf "%.8f", a+b}')
|
||||
N7=$((N7 + 1))
|
||||
fi
|
||||
if [ -z "$LAST_AMT" ]; then
|
||||
LAST_AMT=$amt
|
||||
LAST_TS=$ts_n
|
||||
LAST_SUBJ=$subj
|
||||
fi
|
||||
if [ "$N_WD" -le "$RECENT_WD_N" ]; then
|
||||
at_h=""; at_iso=""
|
||||
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
|
||||
at_h=$(human_from_unix "$ts_n")
|
||||
at_iso=$(iso_from_unix "$ts_n")
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" "$res" >>"$WORKDIR/recent.jsonl"
|
||||
fi
|
||||
done <"$WORKDIR/all-wd-sorted.tsv"
|
||||
|
||||
fmt_goa() {
|
||||
awk -v n="$1" 'BEGIN{
|
||||
if (n+0 == int(n+0)) printf "GOA:%d", int(n+0);
|
||||
else printf "GOA:%.8g", n+0;
|
||||
}'
|
||||
}
|
||||
|
||||
TOTAL_AMT=$(fmt_goa "$TOTAL_WD")
|
||||
TOTAL_IN_AMT=$(fmt_goa "${TOTAL_IN_N:-0}")
|
||||
TOTAL_OUT_AMT=$(fmt_goa "${TOTAL_OUT_N:-0}")
|
||||
TOTAL_WD_FLOW_AMT=$(fmt_goa "${TOTAL_WD_FLOW_N:-0}")
|
||||
TOTAL_OTHER_OUT_AMT=$(fmt_goa "${TOTAL_OTHER_OUT_N:-0}")
|
||||
W24_AMT=$(fmt_goa "$W24")
|
||||
W7_AMT=$(fmt_goa "$W7")
|
||||
|
||||
LAST_AT_H=""; LAST_AT_ISO=""
|
||||
if [ -n "${LAST_TS:-}" ] && [ "$LAST_TS" != "0" ]; then
|
||||
LAST_AT_H=$(human_from_unix "$LAST_TS")
|
||||
LAST_AT_ISO=$(iso_from_unix "$LAST_TS")
|
||||
fi
|
||||
|
||||
# recent_withdraws JSON
|
||||
RECENT_JSON="["
|
||||
first=1
|
||||
while IFS=$'\t' read -r amt ts_n at_h at_iso uname res; do
|
||||
[ -n "$amt" ] || continue
|
||||
if [ "$first" = 1 ]; then first=0; else RECENT_JSON="${RECENT_JSON},"; fi
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
{
|
||||
\"kind\": \"withdraw\",
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"at\": $(json_str "$at_h"),
|
||||
\"at_iso\": $(json_str "$at_iso"),
|
||||
\"at_unix\": ${ts_n:-null},
|
||||
\"account\": $(json_str "$uname"),
|
||||
\"reserve\": $(json_str "$res")
|
||||
}"
|
||||
done <"$WORKDIR/recent.jsonl"
|
||||
RECENT_JSON="${RECENT_JSON}
|
||||
]"
|
||||
|
||||
# recent incoming (up to 5)
|
||||
: >"$WORKDIR/recent-in.jsonl"
|
||||
N_IN_LIST=0
|
||||
while IFS=$'\t' read -r amt ts subj uname; do
|
||||
[ -n "$amt" ] || continue
|
||||
N_IN_LIST=$((N_IN_LIST + 1))
|
||||
[ "$N_IN_LIST" -le 5 ] || break
|
||||
ts_n=${ts:-0}
|
||||
at_h=""; at_iso=""
|
||||
if [ -n "$ts_n" ] && [ "$ts_n" != "0" ]; then
|
||||
at_h=$(human_from_unix "$ts_n")
|
||||
at_iso=$(iso_from_unix "$ts_n")
|
||||
fi
|
||||
printf '%s\t%s\t%s\t%s\t%s\n' "$amt" "$ts_n" "$at_h" "$at_iso" "$uname" >>"$WORKDIR/recent-in.jsonl"
|
||||
done <"$WORKDIR/all-in-sorted.tsv"
|
||||
|
||||
RECENT_IN_JSON="["
|
||||
first=1
|
||||
while IFS=$'\t' read -r amt ts_n at_h at_iso uname; do
|
||||
[ -n "$amt" ] || continue
|
||||
if [ "$first" = 1 ]; then first=0; else RECENT_IN_JSON="${RECENT_IN_JSON},"; fi
|
||||
RECENT_IN_JSON="${RECENT_IN_JSON}
|
||||
{
|
||||
\"kind\": \"incoming\",
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"at\": $(json_str "$at_h"),
|
||||
\"at_iso\": $(json_str "$at_iso"),
|
||||
\"at_unix\": ${ts_n:-null},
|
||||
\"account\": $(json_str "$uname")
|
||||
}"
|
||||
done <"$WORKDIR/recent-in.jsonl"
|
||||
RECENT_IN_JSON="${RECENT_IN_JSON}
|
||||
]"
|
||||
|
||||
# Balance of explorer (optional display not required in footer)
|
||||
BALANCE="GOA:0"
|
||||
if [ -n "${USER_TOKEN:-$AUTH_TOKEN}" ]; then
|
||||
curl -sS -m 10 -H "Authorization: Bearer ${USER_TOKEN:-$AUTH_TOKEN}" \
|
||||
"${BANK}/accounts/${BANK_USER}" >"$WORKDIR/acct.json" || true
|
||||
BALANCE=$(awk -F'"' '/"amount"/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="amount") { print $(i+2); exit }
|
||||
}' "$WORKDIR/acct.json" 2>/dev/null || echo "GOA:0")
|
||||
fi
|
||||
|
||||
# Demo block kept for page QR logic only (not shown in footer)
|
||||
DEMO_URI=""; DEMO_AMT=""; DEMO_CREATED=""; DEMO_WID=""; DEMO_STATUS=""
|
||||
[ -f "$LANDING_DIR/withdraw.uri" ] && DEMO_URI=$(tr -d '\n' <"$LANDING_DIR/withdraw.uri")
|
||||
[ -f "$LANDING_DIR/withdraw.amount" ] && DEMO_AMT=$(tr -d '\n' <"$LANDING_DIR/withdraw.amount")
|
||||
[ -f "$LANDING_DIR/withdraw.created" ] && DEMO_CREATED=$(tr -d '\n' <"$LANDING_DIR/withdraw.created")
|
||||
if [ -n "$DEMO_URI" ]; then
|
||||
DEMO_WID=$(basename "$DEMO_URI")
|
||||
curl -sS -m 8 \
|
||||
"${BANK}/taler-integration/withdrawal-operation/${DEMO_WID}" \
|
||||
>"$WORKDIR/demo-wd.json" 2>/dev/null || true
|
||||
DEMO_STATUS=$(awk -F'"' '/"status"/ {
|
||||
for (i=1;i<=NF;i++) if ($i=="status") { print $(i+2); exit }
|
||||
}' "$WORKDIR/demo-wd.json" 2>/dev/null || true)
|
||||
fi
|
||||
DEMO_READY=false
|
||||
case "$DEMO_STATUS" in
|
||||
pending|selected) DEMO_READY=true ;;
|
||||
"") [ -n "$DEMO_URI" ] && DEMO_READY=true ;;
|
||||
esac
|
||||
[ "$DEMO_STATUS" = "confirmed" ] && DEMO_READY=false
|
||||
[ "$DEMO_STATUS" = "aborted" ] && DEMO_READY=false
|
||||
|
||||
# Sanity: never publish an empty-looking success if admin scan should have accounts
|
||||
if [ -n "$ADMIN_TOKEN" ] && [ "${ACCOUNTS_N:-0}" = "0" ]; then
|
||||
write_err "accounts list empty after admin scan"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Live performance probes (same idea as exchange landing-stats)
|
||||
# Milliseconds as integer, rounded (not truncated) so sub-ms loopback does not
|
||||
# always show 0 when we fall back to in-container URLs.
|
||||
measure_ms() {
|
||||
local url="$1" t
|
||||
t=$(curl -sS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||
[ -z "$t" ] && { echo "null"; return; }
|
||||
awk -v t="$t" 'BEGIN{
|
||||
ms = (t+0)*1000
|
||||
if (ms > 0 && ms < 1) ms = 1
|
||||
printf "%d", int(ms + 0.5)
|
||||
}'
|
||||
}
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
# Prefer public URL for real client latency (Caddy → bank); fall back to loopback.
|
||||
# Integration used to probe only $BANK (127.0.0.1:9012) → ~0–1 ms and not comparable
|
||||
# to /config which already used the public host.
|
||||
BANK_PUBLIC_BASE="${BANK_PUBLIC_URL:-https://bank.hacktivism.ch}"
|
||||
CONFIG_MS=$(measure_ms "${BANK_PUBLIC_BASE}/config")
|
||||
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
|
||||
if [ "$CONFIG_HTTP" != "200" ]; then
|
||||
CONFIG_MS=$(measure_ms "${BANK}/config")
|
||||
CONFIG_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
INT_MS=$(measure_ms "${BANK_PUBLIC_BASE}/taler-integration/config")
|
||||
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/taler-integration/config" 2>/dev/null || echo "000")
|
||||
if [ "$INT_HTTP" != "200" ]; then
|
||||
INT_MS=$(measure_ms "${BANK}/taler-integration/config")
|
||||
INT_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK}/taler-integration/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
WEBUI_MS=$(measure_ms "${BANK_PUBLIC_BASE}/webui/")
|
||||
WEBUI_HTTP=$(curl -sS -o /dev/null -m 8 -w '%{http_code}' "${BANK_PUBLIC_BASE}/webui/" 2>/dev/null || echo "000")
|
||||
LOADAVG=""
|
||||
[ -r /proc/loadavg ] && LOADAVG=$(awk '{print $1","$2","$3}' /proc/loadavg)
|
||||
|
||||
MEM_JSON='"container_rss_human": "—"'
|
||||
MEM_HELPER="${MEM_HELPER:-/usr/local/lib/landing-mem-snapshot.sh}"
|
||||
if [ -f "$MEM_HELPER" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$MEM_HELPER"
|
||||
mem_snapshot_json || true
|
||||
fi
|
||||
|
||||
cat >"$TMP" <<EOF
|
||||
{
|
||||
"ok": true,
|
||||
"currency": "GOA",
|
||||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN_ISO"),
|
||||
"generated_at_human": $(json_str "$GEN_HUMAN"),
|
||||
"generated_at_unix": $NOW,
|
||||
"source": "in-container landing-stats.sh",
|
||||
"bank_url": $(json_str "$BANK"),
|
||||
"scan": {
|
||||
"tx_delta": $(json_str "$TX_DELTA"),
|
||||
"accounts_delta": $(json_str "$ACCOUNTS_DELTA"),
|
||||
"max_scan_accounts": ${MAX_SCAN_ACCOUNTS:-0},
|
||||
"accounts_listed": ${ACCOUNTS_N:-0},
|
||||
"accounts_scanned_ok": ${SCAN_OK:-0},
|
||||
"accounts_empty_tx": ${SCAN_EMPTY:-0},
|
||||
"accounts_scan_fail": ${SCAN_FAIL:-0},
|
||||
"note": "empty_tx includes HTTP 204 (no ledger rows) — normal for new auto-accounts"
|
||||
},
|
||||
"bank_accounts": {
|
||||
"total": ${ACCOUNTS_N:-0},
|
||||
"users": ${ACCOUNTS_USERS:-0},
|
||||
"with_withdraws": ${ACCOUNTS_WITH_WD:-0}
|
||||
},
|
||||
"wallets": {
|
||||
"unique_reserves": ${WALLETS_N:-0},
|
||||
"note": "unique reserve pubs from Taler withdrawals (one per wallet withdraw)"
|
||||
},
|
||||
"balance_explorer": $(json_str "$BALANCE"),
|
||||
"flow": {
|
||||
"incoming": {
|
||||
"label": "Incoming bank credits",
|
||||
"count": ${N_INCOMING:-0},
|
||||
"amount": $(json_str "$TOTAL_IN_AMT"),
|
||||
"value": ${TOTAL_IN_N:-0}
|
||||
},
|
||||
"withdraw": {
|
||||
"label": "Taler withdrawals to wallets",
|
||||
"count": ${N_WD:-0},
|
||||
"amount": $(json_str "$TOTAL_WD_FLOW_AMT"),
|
||||
"value": ${TOTAL_WD_FLOW_N:-0}
|
||||
},
|
||||
"other_out": {
|
||||
"label": "Other debits (non-withdraw)",
|
||||
"amount": $(json_str "$TOTAL_OTHER_OUT_AMT"),
|
||||
"value": ${TOTAL_OTHER_OUT_N:-0}
|
||||
},
|
||||
"total_in": $(json_str "$TOTAL_IN_AMT"),
|
||||
"total_in_value": ${TOTAL_IN_N:-0},
|
||||
"total_out": $(json_str "$TOTAL_OUT_AMT"),
|
||||
"total_out_value": ${TOTAL_OUT_N:-0},
|
||||
"note": "incoming=credits; withdraw=Taler withdrawal debits; excl. admin+exchange accounts"
|
||||
},
|
||||
"withdraws": {
|
||||
"count": ${N_WD:-0},
|
||||
"total_amount": $(json_str "$TOTAL_AMT"),
|
||||
"total_value": ${TOTAL_WD:-0},
|
||||
"last_amount": $( [ -n "$LAST_AMT" ] && json_str "$LAST_AMT" || echo null ),
|
||||
"last_at": $( [ -n "$LAST_AT_H" ] && json_str "$LAST_AT_H" || echo null ),
|
||||
"last_at_iso": $( [ -n "$LAST_AT_ISO" ] && json_str "$LAST_AT_ISO" || echo null ),
|
||||
"last_at_unix": ${LAST_TS:-null},
|
||||
"last_subject": $( [ -n "$LAST_SUBJ" ] && json_str "$LAST_SUBJ" || echo null ),
|
||||
"last_24h": { "count": ${N24:-0}, "amount": $(json_str "$W24_AMT"), "value": ${W24:-0} },
|
||||
"last_7d": { "count": ${N7:-0}, "amount": $(json_str "$W7_AMT"), "value": ${W7:-0} }
|
||||
},
|
||||
"recent_withdraws": $RECENT_JSON,
|
||||
"recent_incoming": $RECENT_IN_JSON,
|
||||
"demo": {
|
||||
"uri": $( [ -n "$DEMO_URI" ] && json_str "$DEMO_URI" || echo null ),
|
||||
"amount": $( [ -n "$DEMO_AMT" ] && json_str "$DEMO_AMT" || echo null ),
|
||||
"created": $( [ -n "$DEMO_CREATED" ] && json_str "$DEMO_CREATED" || echo null ),
|
||||
"withdrawal_id": $( [ -n "$DEMO_WID" ] && json_str "$DEMO_WID" || echo null ),
|
||||
"status": $( [ -n "$DEMO_STATUS" ] && json_str "$DEMO_STATUS" || echo null ),
|
||||
"ready": $DEMO_READY
|
||||
},
|
||||
"performance": {
|
||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||
"integration_http": $(json_str "$INT_HTTP"),
|
||||
"integration_ms": $(num_or_null "$INT_MS"),
|
||||
"webui_http": $(json_str "$WEBUI_HTTP"),
|
||||
"webui_ms": $(num_or_null "$WEBUI_MS"),
|
||||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||
"memory": {
|
||||
${MEM_JSON}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
grep -q '"ok": true' "$TMP" || { write_err "tmp json missing ok:true"; exit 1; }
|
||||
mv -f "$TMP" "$OUT"
|
||||
write_run true
|
||||
echo "ok accounts=${ACCOUNTS_N} wallets=${WALLETS_N} withdraws=${N_WD} total=${TOTAL_AMT} tz=${TZ} -> $OUT"
|
||||
112
scripts/taler-bank/make-demo-withdraw-qr.sh
Executable file
112
scripts/taler-bank/make-demo-withdraw-qr.sh
Executable file
|
|
@ -0,0 +1,112 @@
|
|||
#!/bin/bash
|
||||
# Create a demo GOA withdrawal for user explorer and write QR assets under LANDING_DIR.
|
||||
# Run on koopa (host) with bank on 127.0.0.1:9012.
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
PASS="${BANK_PASS:-}"
|
||||
AMOUNT="${AMOUNT:-GOA:10}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
if [ -f /root/bank-explorer-password.txt ]; then
|
||||
PASS=$(tr -d '\n' </root/bank-explorer-password.txt)
|
||||
elif [ -f /tmp/bank-caddy-wire.log ]; then
|
||||
PASS=$(grep -E '^explorer=' /tmp/bank-caddy-wire.log | tail -1 | cut -d= -f2-)
|
||||
fi
|
||||
fi
|
||||
if [ -z "$PASS" ]; then
|
||||
echo "Set BANK_PASS or put password in /root/bank-explorer-password.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$WORKDIR"' EXIT
|
||||
|
||||
echo "{\"scope\":\"readwrite\",\"refreshable\":true}" >"$WORKDIR/tok.json"
|
||||
curl -sS -m 15 -u "${USER}:${PASS}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$WORKDIR/tok.json" \
|
||||
"${BANK}/accounts/${USER}/token" >"$WORKDIR/tok.out"
|
||||
|
||||
TOKEN=$(python3 - "$WORKDIR/tok.out" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
print(d.get("access_token",""))
|
||||
PY
|
||||
)
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "token failed:" >&2
|
||||
cat "$WORKDIR/tok.out" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "{\"suggested_amount\":\"${AMOUNT}\"}" >"$WORKDIR/wd.json"
|
||||
curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @"$WORKDIR/wd.json" \
|
||||
"${BANK}/accounts/${USER}/withdrawals" >"$WORKDIR/wd.out"
|
||||
|
||||
URI=$(python3 - "$WORKDIR/wd.out" <<'PY'
|
||||
import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
print(d.get("taler_withdraw_uri") or "")
|
||||
if not d.get("taler_withdraw_uri"):
|
||||
sys.stderr.write(open(sys.argv[1]).read()+"\n")
|
||||
sys.exit(1)
|
||||
print(d.get("withdrawal_id",""), file=sys.stderr)
|
||||
PY
|
||||
)
|
||||
|
||||
mkdir -p "$LANDING_DIR"
|
||||
printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri"
|
||||
printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount"
|
||||
date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created"
|
||||
# track id for auto-confirm-withdrawals.sh
|
||||
WID=$(basename "$URI")
|
||||
echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids"
|
||||
sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids"
|
||||
echo "watch_id=$WID (auto-confirm when wallet selects)"
|
||||
|
||||
# QR as SVG (no external deps) via python qrcode if present, else pure matrix via segno/qrcode
|
||||
python3 - "$URI" "$LANDING_DIR/withdraw-qr.svg" <<'PY'
|
||||
import sys
|
||||
uri, out = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
import qrcode
|
||||
import qrcode.image.svg
|
||||
img = qrcode.make(uri, image_factory=qrcode.image.svg.SvgPathImage)
|
||||
img.save(out)
|
||||
print("qrcode lib ok")
|
||||
except Exception as e:
|
||||
# minimal fallback: write HTML with data attribute for client-side QR
|
||||
open(out, "w").write(
|
||||
f'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="8" height="8">'
|
||||
f'<!-- QR_FALLBACK uri={uri} --></svg>\n'
|
||||
)
|
||||
open(out + ".uri", "w").write(uri)
|
||||
print("fallback:", e)
|
||||
PY
|
||||
|
||||
# Also PNG if pillow/qrcode available
|
||||
python3 - "$URI" "$LANDING_DIR/withdraw-qr.png" <<'PY' || true
|
||||
import sys
|
||||
uri, out = sys.argv[1], sys.argv[2]
|
||||
import qrcode
|
||||
img = qrcode.make(uri, box_size=8, border=2)
|
||||
img.save(out)
|
||||
print("png ok", out)
|
||||
PY
|
||||
|
||||
echo "URI=$URI"
|
||||
echo "wrote under $LANDING_DIR"
|
||||
ls -la "$LANDING_DIR"/withdraw* 2>/dev/null || true
|
||||
|
||||
# Refresh public stats.json when landing-stats is available (in-container path)
|
||||
if [ -x /usr/local/bin/landing-stats.sh ]; then
|
||||
LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true
|
||||
elif [ -x "$(dirname "$0")/landing-stats.sh" ]; then
|
||||
LANDING_DIR="$LANDING_DIR" "$(dirname "$0")/landing-stats.sh" || true
|
||||
fi
|
||||
54
scripts/taler-bank/refresh-demo-withdraw.sh
Executable file
54
scripts/taler-bank/refresh-demo-withdraw.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# Create a fresh demo GOA withdraw for landing QR (no python — runs in bank container).
|
||||
# Writes under LANDING_DIR (default /var/www/bank-landing).
|
||||
#
|
||||
# Inside container:
|
||||
# /usr/local/bin/refresh-demo-withdraw.sh
|
||||
# Host:
|
||||
# podman exec taler-hacktivism-bank /usr/local/bin/refresh-demo-withdraw.sh
|
||||
set -euo pipefail
|
||||
|
||||
BANK="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
USER="${BANK_USER:-explorer}"
|
||||
AMOUNT="${AMOUNT:-GOA:10}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/bank-landing}"
|
||||
PASS="${BANK_PASS:-}"
|
||||
|
||||
if [ -z "$PASS" ]; then
|
||||
for f in "/root/bank-${USER}-password.txt" /root/bank-explorer-password.txt; do
|
||||
if [ -f "$f" ]; then PASS=$(tr -d '\n' <"$f"); break; fi
|
||||
done
|
||||
fi
|
||||
[ -n "$PASS" ] || { echo "Set BANK_PASS or /root/bank-explorer-password.txt" >&2; exit 1; }
|
||||
|
||||
BANK="${BANK%/}"
|
||||
TOK=$(curl -sS -m 12 -u "${USER}:${PASS}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true}' \
|
||||
"${BANK}/accounts/${USER}/token")
|
||||
TOKEN=$(printf '%s' "$TOK" | sed -n 's/.*"access_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$TOKEN" ] || { echo "token fail: $TOK" >&2; exit 1; }
|
||||
|
||||
WD=$(curl -sS -m 15 \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"suggested_amount\":\"${AMOUNT}\"}" \
|
||||
"${BANK}/accounts/${USER}/withdrawals")
|
||||
URI=$(printf '%s' "$WD" | sed -n 's/.*"taler_withdraw_uri"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
WID=$(printf '%s' "$WD" | sed -n 's/.*"withdrawal_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
||||
[ -n "$URI" ] || { echo "no URI from: $WD" >&2; exit 1; }
|
||||
[ -n "$WID" ] || WID=$(basename "$URI")
|
||||
|
||||
mkdir -p "$LANDING_DIR"
|
||||
printf '%s\n' "$URI" >"$LANDING_DIR/withdraw.uri"
|
||||
printf '%s\n' "$AMOUNT" >"$LANDING_DIR/withdraw.amount"
|
||||
date -u +%Y-%m-%dT%H:%MZ >"$LANDING_DIR/withdraw.created"
|
||||
echo "$WID" >>"$LANDING_DIR/withdraw-watch.ids"
|
||||
sort -u "$LANDING_DIR/withdraw-watch.ids" -o "$LANDING_DIR/withdraw-watch.ids" 2>/dev/null || true
|
||||
|
||||
echo "URI=$URI"
|
||||
echo "WID=$WID"
|
||||
|
||||
if [ -x /usr/local/bin/landing-stats.sh ]; then
|
||||
LANDING_DIR="$LANDING_DIR" /usr/local/bin/landing-stats.sh || true
|
||||
fi
|
||||
109
scripts/taler-bank/start_bank.sh
Executable file
109
scripts/taler-bank/start_bank.sh
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/bin/bash
|
||||
# Start / restart libeufin-bank serve (manual, no systemd).
|
||||
# Run as: libeufin-bank
|
||||
# Same role as start_merchant.sh / start_exchange.sh.
|
||||
#
|
||||
# Container: taler-hacktivism-bank
|
||||
# Prerequisite: /root/start_base_services_for_taler_bank.sh (as root) for postgres.
|
||||
#
|
||||
# Usage:
|
||||
# start_bank.sh
|
||||
# start_bank.sh --restart | -r
|
||||
# start_bank.sh --help
|
||||
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: start_bank.sh [--restart|-r] [--help|-h]
|
||||
|
||||
(default) Start libeufin-bank serve if not already running.
|
||||
--restart Stop live serve process, then start cleanly.
|
||||
Does not touch postgres (use /root/start_base_services_for_taler_bank.sh).
|
||||
EOF
|
||||
}
|
||||
|
||||
DO_RESTART=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--restart|-r) DO_RESTART=1 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $arg" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$(id -un)" != "libeufin-bank" ]; then
|
||||
echo "This script must be run as user libeufin-bank" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
LOG_DIR=/var/log/libeufin-bank
|
||||
PORT=$(grep -E '^\s*PORT\s*=' /etc/libeufin/bank-overrides.conf 2>/dev/null | tail -1 | awk -F= '{gsub(/ /,"",$2); print $2}')
|
||||
PORT=${PORT:-9012}
|
||||
|
||||
# Match both wrapper name and the Java MainKt process that actually listens.
|
||||
list_serve_pids() {
|
||||
ps -eo pid=,stat=,args= 2>/dev/null | while read -r pid stat args; do
|
||||
case "$stat" in Z*) continue ;; esac
|
||||
case "$args" in
|
||||
*start_bank.sh*) continue ;;
|
||||
*check_bank-health*) continue ;;
|
||||
esac
|
||||
case "$args" in
|
||||
*libeufin-bank\ serve*|/usr/bin/libeufin-bank\ serve*|*MainKt\ serve*|*tech.libeufin.bank.MainKt*)
|
||||
echo "$pid"
|
||||
;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
kill_serve() {
|
||||
local pids
|
||||
pids=$(list_serve_pids | tr '\n' ' ')
|
||||
if [ -z "${pids// }" ]; then
|
||||
echo "No live libeufin-bank serve to stop."
|
||||
return 0
|
||||
fi
|
||||
echo "Stopping PIDs: $pids"
|
||||
# shellcheck disable=SC2086
|
||||
kill -TERM $pids 2>/dev/null || true
|
||||
sleep 2
|
||||
local left
|
||||
left=$(list_serve_pids | tr '\n' ' ')
|
||||
if [ -n "${left// }" ]; then
|
||||
echo "SIGKILL remaining: $left"
|
||||
# shellcheck disable=SC2086
|
||||
kill -KILL $left 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
echo "libeufin-bank serve stopped."
|
||||
}
|
||||
|
||||
if [ "$DO_RESTART" -eq 1 ]; then
|
||||
echo "=== restart: kill libeufin-bank serve ==="
|
||||
kill_serve
|
||||
fi
|
||||
|
||||
echo "Start libeufin-bank serve (port $PORT):"
|
||||
LOG_FILE="$LOG_DIR/libeufin-bank-$(date +%Y-%m-%d).log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE" 2>/dev/null || true
|
||||
|
||||
if [ "$DO_RESTART" -eq 0 ] && [ -n "$(list_serve_pids)" ]; then
|
||||
echo "libeufin-bank serve already running"
|
||||
else
|
||||
nohup libeufin-bank serve -c "$CONF" >>"$LOG_FILE" 2>&1 &
|
||||
disown 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "Live processes:"
|
||||
ps -eo pid,stat,args 2>/dev/null | grep -E 'libeufin-bank serve|MainKt serve' | grep -v grep | grep -v ' Z ' || true
|
||||
|
||||
if [ -x /usr/local/bin/check_bank-health.sh ]; then
|
||||
/usr/local/bin/check_bank-health.sh || exit 1
|
||||
elif [ -x ./check_bank-health.sh ]; then
|
||||
./check_bank-health.sh || exit 1
|
||||
fi
|
||||
exit 0
|
||||
148
scripts/taler-bank/start_base_services_for_taler_bank.sh
Executable file
148
scripts/taler-bank/start_base_services_for_taler_bank.sh
Executable file
|
|
@ -0,0 +1,148 @@
|
|||
#!/bin/bash
|
||||
# Root: base services for manual libeufin-bank (like merchant/exchange start_base).
|
||||
# Then interactive shell as libeufin-bank → run start_bank.sh there.
|
||||
#
|
||||
# Container: taler-hacktivism-bank
|
||||
#
|
||||
# Usage:
|
||||
# /root/start_base_services_for_taler_bank.sh
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank
|
||||
# /root/start_base_services_for_taler_bank.sh --no-shell --start-bank --restart
|
||||
|
||||
set -e
|
||||
CONF=/etc/libeufin/libeufin-bank.conf
|
||||
LOG_DIR=/var/log/libeufin-bank
|
||||
PWD_BIN=/usr/local/bin
|
||||
BANK_STARTER=start_bank.sh
|
||||
BANK_USER=libeufin-bank
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_SHELL=0
|
||||
START_BANK=0
|
||||
BANK_RESTART=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-shell|-n) NO_SHELL=1 ;;
|
||||
--start-bank) START_BANK=1; NO_SHELL=1 ;;
|
||||
--restart|-r) BANK_RESTART=1 ;;
|
||||
--help|-h)
|
||||
cat <<'EOF'
|
||||
Usage: start_base_services_for_taler_bank.sh [options]
|
||||
|
||||
(default) Start postgres + dirs, then interactive shell as libeufin-bank
|
||||
--no-shell Start base services only, do not open a shell
|
||||
--start-bank After base services, run /usr/local/bin/start_bank.sh as libeufin-bank
|
||||
(implies --no-shell)
|
||||
--restart With --start-bank: pass --restart to start_bank.sh
|
||||
EOF
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $arg" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# --- Debian postgresql defaults (pg_createcluster layout) ---
|
||||
ensure_postgresql() {
|
||||
echo " Debian perms on /etc/postgresql + data/log/run..."
|
||||
if [ -d /etc/postgresql ]; then
|
||||
chown -R root:postgres /etc/postgresql
|
||||
find /etc/postgresql -type d -exec chmod 755 {} \;
|
||||
find /etc/postgresql -type f -name '*.conf' -exec chmod 640 {} \;
|
||||
fi
|
||||
chown -R postgres:postgres /var/lib/postgresql /var/log/postgresql 2>/dev/null || true
|
||||
mkdir -p /var/run/postgresql
|
||||
chown postgres:postgres /var/run/postgresql
|
||||
chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql
|
||||
|
||||
# Snakeoil TLS key: postgres fails if it cannot read the key (common in rootless images).
|
||||
# Prefer fixing perms; if still broken, force ssl=off for local-only DB.
|
||||
if [ -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then
|
||||
chown root:ssl-cert /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
||||
chmod 640 /etc/ssl/private/ssl-cert-snakeoil.key 2>/dev/null || true
|
||||
fi
|
||||
for pgconf in /etc/postgresql/*/main/postgresql.conf; do
|
||||
[ -f "$pgconf" ] || continue
|
||||
if grep -qE '^\s*ssl\s*=' "$pgconf"; then
|
||||
sed -i 's/^\s*ssl\s*=.*/ssl = off/' "$pgconf"
|
||||
else
|
||||
echo "ssl = off" >>"$pgconf"
|
||||
fi
|
||||
done
|
||||
|
||||
if pg_isready -q 2>/dev/null; then
|
||||
echo " already accepting connections"
|
||||
pg_isready || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f /var/run/postgresql/.s.PGSQL.*.lock 2>/dev/null || true
|
||||
if ! pgrep -u postgres -x postgres >/dev/null 2>&1; then
|
||||
rm -f /var/lib/postgresql/*/main/postmaster.pid 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if command -v pg_ctlcluster >/dev/null 2>&1 && command -v pg_lsclusters >/dev/null 2>&1; then
|
||||
while read -r ver name _rest; do
|
||||
[ -n "$ver" ] || continue
|
||||
echo " pg_ctlcluster $ver $name start"
|
||||
pg_ctlcluster "$ver" "$name" start 2>/dev/null || true
|
||||
done < <(pg_lsclusters --no-header 2>/dev/null || true)
|
||||
fi
|
||||
if ! pg_isready -q 2>/dev/null; then
|
||||
if [ -x /etc/init.d/postgresql ]; then
|
||||
/etc/init.d/postgresql start || true
|
||||
else
|
||||
service postgresql start || true
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
pg_isready || true
|
||||
}
|
||||
|
||||
echo "Create log + data dirs... giving permission to ${BANK_USER}:"
|
||||
mkdir -p "$LOG_DIR" /var/lib/libeufin-bank
|
||||
chown "${BANK_USER}:${BANK_USER}" "$LOG_DIR" /var/lib/libeufin-bank
|
||||
chmod 755 "$LOG_DIR" /var/lib/libeufin-bank
|
||||
|
||||
echo "Start base services needed for GOA Exploration Bank."
|
||||
echo ""
|
||||
|
||||
echo "1. postgresql:"
|
||||
ensure_postgresql
|
||||
# ensure role + DB (postgres:///libeufin) — Debian createuser/createdb as postgres
|
||||
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='libeufin-bank'\" | grep -q 1 || createuser -s libeufin-bank" || true
|
||||
su -s /bin/bash postgres -c "psql -tc \"SELECT 1 FROM pg_database WHERE datname='libeufin'\" | grep -q 1 || createdb -O libeufin-bank libeufin" || true
|
||||
pg_isready || true
|
||||
|
||||
if [ "$START_BANK" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "2. start_bank.sh as ${BANK_USER}:"
|
||||
if [ ! -x "$PWD_BIN/$BANK_STARTER" ]; then
|
||||
echo "missing $PWD_BIN/$BANK_STARTER" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$BANK_RESTART" -eq 1 ]; then
|
||||
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER" --restart
|
||||
else
|
||||
runuser -u "$BANK_USER" -- "$PWD_BIN/$BANK_STARTER"
|
||||
fi
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if [ "$NO_SHELL" -eq 1 ]; then
|
||||
echo "Base services started (--no-shell). Next: runuser -u ${BANK_USER} -- $PWD_BIN/$BANK_STARTER [--restart]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "2. Switching now to user ${BANK_USER}, in $PWD_BIN; find executable $BANK_STARTER there!"
|
||||
echo ""
|
||||
cd "$PWD_BIN"
|
||||
# util-linux: -u and -s are mutually exclusive (same as exchange/merchant)
|
||||
exec runuser -u "$BANK_USER" -- bash
|
||||
Loading…
Add table
Add a link
Reference in a new issue