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
56
scripts/taler-merchant/README.md
Normal file
56
scripts/taler-merchant/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# taler-merchant scripts
|
||||
|
||||
Mirrored from podman `taler-hacktivism`.
|
||||
|
||||
| File | Location in container | User |
|
||||
|------|----------------------|------|
|
||||
| `start_base_services_for_taler.sh` | `/root/` | root |
|
||||
| `start_merchant.sh` | `/usr/local/bin/` | `taler-merchant-httpd` |
|
||||
| `ensure_merchant_helpers.sh` | `/usr/local/bin/` | root → `taler-merchant-httpd` |
|
||||
| `setup_credit_facade.sh` | host root (`/root/koopa-admin-log/…`) | root on koopa |
|
||||
| `check_merchant-health.sh` | `/usr/local/bin/` | any |
|
||||
| `certbot_renew.sh` | `/root/scripts/` | root (bg from base) |
|
||||
| `stats--merchant-payments.sh` | `/usr/local/bin/` | ops |
|
||||
| `taler-hacktivism-email-helper.sh` | `/usr/local/bin/` | merchant (SMTP password via env in git mirror) |
|
||||
| `taler-hacktivism-sms-helper-wrapper.sh` | `/usr/local/bin/` | merchant |
|
||||
|
||||
### Settlement (wired / transfers)
|
||||
|
||||
Automatic import needs:
|
||||
|
||||
1. **`credit_facade_url`** = `…/accounts/$BANK_USER/taler-revenue/` (not `taler-wire-gateway/`)
|
||||
2. **Bearer** bank token in `credit_facade_credentials` (Basic fails on history)
|
||||
3. Running **`taler-merchant-wirewatch`** + **`taler-merchant-depositcheck`**
|
||||
|
||||
```bash
|
||||
# on koopa as root
|
||||
./setup_credit_facade.sh
|
||||
./ensure_merchant_helpers.sh # inside container or via start_merchant
|
||||
```
|
||||
|
||||
SMS backends are symlinks into `/var/taler-src/...` (not copied).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# root in container
|
||||
./start_base_services_for_taler.sh
|
||||
# then as taler-merchant-httpd in /usr/local/bin:
|
||||
./start_merchant.sh --restart
|
||||
# nginx (TLS frontend) if not already up — root:
|
||||
# /etc/init.d/nginx start
|
||||
./check_merchant-health.sh
|
||||
```
|
||||
|
||||
### `check_merchant-health.sh`
|
||||
|
||||
| Check | Severity |
|
||||
|-------|----------|
|
||||
| socket + listener + httpd + nginx | FAIL |
|
||||
| merchant `/config` (unix sock or `:9010`) | FAIL |
|
||||
| each enabled `[merchant-exchange-*]`: `GET …/keys` | FAIL |
|
||||
| `MASTER_KEY` matches `master_public_key` in `/keys` | FAIL |
|
||||
| helpers (webhook, exchangekeyupdate, depositcheck) | WARN |
|
||||
|
||||
Disabled exchanges (`DISABLED = YES`) are skipped.
|
||||
If public exchange URL is unreachable from the container, falls back to `http://127.0.0.1:9011/keys` (and pasta host IPs).
|
||||
5
scripts/taler-merchant/certbot_renew.sh
Executable file
5
scripts/taler-merchant/certbot_renew.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/sh
|
||||
while true; do
|
||||
certbot renew --quiet
|
||||
sleep 12h
|
||||
done
|
||||
222
scripts/taler-merchant/check_merchant-health.sh
Executable file
222
scripts/taler-merchant/check_merchant-health.sh
Executable file
|
|
@ -0,0 +1,222 @@
|
|||
#!/bin/bash
|
||||
# Health check for manual taler-merchant (container taler-hacktivism).
|
||||
# Style: check_exchange-health.sh — [OK] / [FAIL] / [WARN], exit 1 on critical fail.
|
||||
#
|
||||
# Keys checks: for each enabled [merchant-exchange-*] with EXCHANGE_BASE_URL,
|
||||
# GET …/keys and (if set) verify MASTER_KEY appears in the response.
|
||||
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
OVERRIDES=/etc/taler-merchant/merchant-overrides.conf
|
||||
SOCK="/var/run/taler-merchant/httpd/merchant-http.sock"
|
||||
SS_BIN=$(command -v ss)
|
||||
CURL_BIN=$(command -v curl)
|
||||
|
||||
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; }
|
||||
warn() { yellow "[WARN] $1"; }
|
||||
|
||||
is_url() {
|
||||
case "$1" in
|
||||
http://*|https://*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# GET /keys; try primary URL then optional host-local fallbacks for same exchange.
|
||||
# Writes body to $1 (path). Returns 0 on HTTP success with non-empty body.
|
||||
fetch_keys() {
|
||||
local out="$1"
|
||||
local primary="$2"
|
||||
shift 2
|
||||
local url
|
||||
for url in "$primary" "$@"; do
|
||||
[ -z "$url" ] && continue
|
||||
if $CURL_BIN -skf -m 6 "$url" -o "$out" 2>/dev/null \
|
||||
|| $CURL_BIN -sf -m 6 "$url" -o "$out" 2>/dev/null; then
|
||||
if [ -s "$out" ] && grep -qE 'master_public_key|"currency"' "$out" 2>/dev/null; then
|
||||
echo "$url"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "=== Taler Merchant Health Check ==="
|
||||
|
||||
# --- 1–4: local service ---
|
||||
if [ -S "$SOCK" ]; then
|
||||
ok "socket exists: $SOCK"
|
||||
else
|
||||
bad "socket does NOT exist: $SOCK"
|
||||
fi
|
||||
|
||||
if [ -n "$SS_BIN" ] && $SS_BIN -xl 2>/dev/null | grep -q "$SOCK"; then
|
||||
ok "Merchant-HTTPD listening on socket"
|
||||
elif [ -n "$SS_BIN" ]; then
|
||||
bad "no listener on socket"
|
||||
else
|
||||
warn "ss not available — skip socket listener check"
|
||||
fi
|
||||
|
||||
if pgrep -f taler-merchant-httpd >/dev/null 2>&1; then
|
||||
ok "process taler-merchant-httpd"
|
||||
else
|
||||
bad "process taler-merchant-httpd is NOT running"
|
||||
fi
|
||||
|
||||
if pgrep -f "nginx: master" >/dev/null 2>&1; then
|
||||
ok "nginx master process"
|
||||
else
|
||||
bad "nginx master process is NOT running"
|
||||
fi
|
||||
|
||||
# --- 5: merchant /config ---
|
||||
if [ -n "$CURL_BIN" ]; then
|
||||
cfg_ok=0
|
||||
if [ -S "$SOCK" ] && $CURL_BIN -sf -m 3 --unix-socket "$SOCK" "http://localhost/config" >/dev/null 2>&1; then
|
||||
ok "merchant /config via unix socket"
|
||||
cfg_ok=1
|
||||
elif $CURL_BIN -skf -m 3 "https://127.0.0.1:9010/config" >/dev/null 2>&1; then
|
||||
ok "merchant /config via https://127.0.0.1:9010/config"
|
||||
cfg_ok=1
|
||||
elif $CURL_BIN -sf -m 3 "http://127.0.0.1:9010/config" >/dev/null 2>&1; then
|
||||
ok "merchant /config via http://127.0.0.1:9010/config"
|
||||
cfg_ok=1
|
||||
fi
|
||||
[ "$cfg_ok" -eq 0 ] && bad "merchant /config unreachable (socket and :9010)"
|
||||
else
|
||||
warn "curl missing — skip /config"
|
||||
fi
|
||||
|
||||
# --- 6: exchange /keys for configured exchanges ---
|
||||
echo "--- configured exchanges (/keys) ---"
|
||||
|
||||
# Emit lines: SECTION|DISABLED|BASE|MASTER (from overrides + optional taler-config)
|
||||
list_exchanges() {
|
||||
# Prefer site overrides file (authoritative for this host)
|
||||
if [ -f "$OVERRIDES" ]; then
|
||||
awk '
|
||||
BEGIN { sec=""; dis="NO"; base=""; master="" }
|
||||
/^\[merchant-exchange-/ {
|
||||
if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master
|
||||
sec=$0; gsub(/[\[\] \t\r]/, "", sec)
|
||||
dis="NO"; base=""; master=""
|
||||
next
|
||||
}
|
||||
/^\[/ {
|
||||
if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master
|
||||
sec=""; next
|
||||
}
|
||||
sec=="" { next }
|
||||
/^[ \t]*#/ { next }
|
||||
{
|
||||
line=$0
|
||||
sub(/[ \t]*#.*$/, "", line)
|
||||
if (match(line, /^[ \t]*DISABLED[ \t]*=[ \t]*/)) {
|
||||
dis=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", dis)
|
||||
} else if (match(line, /^[ \t]*EXCHANGE_BASE_URL[ \t]*=[ \t]*/)) {
|
||||
base=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", base)
|
||||
} else if (match(line, /^[ \t]*MASTER_KEY[ \t]*=[ \t]*/)) {
|
||||
master=substr(line, RSTART+RLENGTH); gsub(/^[ \t]+|[ \t]+$/, "", master)
|
||||
}
|
||||
}
|
||||
END { if (sec != "") printf "%s|%s|%s|%s\n", sec, dis, base, master }
|
||||
' "$OVERRIDES"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -z "$CURL_BIN" ]; then
|
||||
bad "curl missing — cannot check exchange /keys"
|
||||
else
|
||||
exch_count=0
|
||||
while IFS='|' read -r sec dis base master; do
|
||||
[ -z "$sec" ] && continue
|
||||
case "${dis:-NO}" in
|
||||
YES|yes|true|True|1)
|
||||
warn "exchange $sec: DISABLED — skip /keys"
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
if ! is_url "$base"; then
|
||||
# package stubs without URL (e.g. kudos only DISABLED)
|
||||
warn "exchange $sec: no EXCHANGE_BASE_URL — skip"
|
||||
continue
|
||||
fi
|
||||
exch_count=$((exch_count + 1))
|
||||
base_slash="${base%/}/"
|
||||
primary="${base_slash}keys"
|
||||
keys_tmp=$(mktemp 2>/dev/null || echo "/tmp/m-keys-$$-${exch_count}.json")
|
||||
|
||||
# Fallbacks when public name is not reachable from container:
|
||||
# host loopback ports published by podman (exchange :9011).
|
||||
got_url=
|
||||
if got_url=$(fetch_keys "$keys_tmp" "$primary" \
|
||||
"http://127.0.0.1:9011/keys" \
|
||||
"http://host.containers.internal:9011/keys" \
|
||||
"http://10.0.2.2:9011/keys"); then
|
||||
ok "exchange $sec: /keys reachable ($got_url)"
|
||||
if [ -n "$master" ]; then
|
||||
if grep -qF "$master" "$keys_tmp" 2>/dev/null; then
|
||||
ok "exchange $sec: MASTER_KEY matches /keys"
|
||||
else
|
||||
mpks=$(grep -oE '"master_public_key"[[:space:]]*:[[:space:]]*"[^"]+"' "$keys_tmp" 2>/dev/null | head -2)
|
||||
bad "exchange $sec: MASTER_KEY does not match /keys (config MASTER_KEY=$master)"
|
||||
[ -n "$mpks" ] && warn " seen in /keys: $mpks"
|
||||
fi
|
||||
else
|
||||
warn "exchange $sec: no MASTER_KEY in config — skip key match"
|
||||
fi
|
||||
else
|
||||
bad "exchange $sec: no /keys from $primary (and local :9011 fallbacks)"
|
||||
fi
|
||||
rm -f "$keys_tmp" 2>/dev/null || true
|
||||
done <<EOF
|
||||
$(list_exchanges)
|
||||
EOF
|
||||
[ "$exch_count" -eq 0 ] && warn "no enabled exchanges with EXCHANGE_BASE_URL"
|
||||
fi
|
||||
|
||||
# --- 7: helpers — ensure (no systemd) then require ---
|
||||
if [ "${SKIP_ENSURE:-0}" != "1" ]; then
|
||||
if [ -x /usr/local/bin/ensure_merchant_helpers.sh ]; then
|
||||
echo "--- ensure_merchant_helpers ---"
|
||||
/usr/local/bin/ensure_merchant_helpers.sh || warn "ensure_merchant_helpers exited non-zero"
|
||||
elif [ -x "$(dirname "$0")/ensure_merchant_helpers.sh" ]; then
|
||||
echo "--- ensure_merchant_helpers ---"
|
||||
"$(dirname "$0")/ensure_merchant_helpers.sh" || warn "ensure_merchant_helpers exited non-zero"
|
||||
fi
|
||||
fi
|
||||
|
||||
live_helper() {
|
||||
local p="$1"
|
||||
pgrep -f "(^|/)(${p})( |$)" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Settlement needs wirewatch + depositcheck; others needed for ops.
|
||||
for p in \
|
||||
taler-merchant-webhook \
|
||||
taler-merchant-kyccheck \
|
||||
taler-merchant-wirewatch \
|
||||
taler-merchant-depositcheck \
|
||||
taler-merchant-exchangekeyupdate \
|
||||
taler-merchant-reconciliation
|
||||
do
|
||||
if live_helper "$p"; then
|
||||
ok "process $p"
|
||||
else
|
||||
bad "process $p not running"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -eq 0 ]; then
|
||||
green "=== ALL CRITICAL CHECKS PASSED ==="
|
||||
exit 0
|
||||
fi
|
||||
red "=== SOME CHECKS FAILED ==="
|
||||
exit 1
|
||||
100
scripts/taler-merchant/ensure_merchant_helpers.sh
Executable file
100
scripts/taler-merchant/ensure_merchant_helpers.sh
Executable file
|
|
@ -0,0 +1,100 @@
|
|||
#!/bin/bash
|
||||
# Ensure merchant helper processes are running (no systemd).
|
||||
# Prefer run as taler-merchant-httpd; root may use runuser.
|
||||
#
|
||||
# Usage:
|
||||
# ensure_merchant_helpers.sh
|
||||
# (as root) ensure_merchant_helpers.sh # re-exec as taler-merchant-httpd
|
||||
set -euo pipefail
|
||||
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
LOG_DIR="${TALER_MERCHANT_LOG_DIR:-/var/log/taler-merchant}"
|
||||
|
||||
# As root: start wirewatch supervisor (needs root for runuser), then re-exec as httpd.
|
||||
if [ "$(id -un)" = "root" ]; then
|
||||
mkdir -p "$LOG_DIR"
|
||||
if [ -x /usr/local/bin/taler-merchant-wirewatch-supervise.sh ]; then
|
||||
if ! ps -eo args= 2>/dev/null | grep -q 'taler-merchant-wirewatch-supervise\.sh'; then
|
||||
echo "start: taler-merchant-wirewatch-supervise"
|
||||
nohup /usr/local/bin/taler-merchant-wirewatch-supervise.sh \
|
||||
>>"$LOG_DIR/wirewatch-supervise.nohup" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
else
|
||||
echo "already: taler-merchant-wirewatch-supervise"
|
||||
fi
|
||||
fi
|
||||
exec runuser -u taler-merchant-httpd -- "$0" "$@"
|
||||
fi
|
||||
|
||||
if [ "$(id -un)" != "taler-merchant-httpd" ]; then
|
||||
echo "run as taler-merchant-httpd or root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
chmod 755 "$LOG_DIR" 2>/dev/null || true
|
||||
|
||||
# Linux COMM is 15 chars — do not use pgrep -x for taler-merchant-wirewatch etc.
|
||||
is_running() {
|
||||
local bin="$1"
|
||||
local base
|
||||
base=$(basename "$bin")
|
||||
ps -u "$(id -un)" -o args= 2>/dev/null | grep -qE "(^|/)[${base:0:1}]${base:1}( |$)" \
|
||||
|| pgrep -u "$(id -un)" -f "(^|/)(${base})( |$)" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_one() {
|
||||
local name="$1"
|
||||
local bin="$2"
|
||||
shift 2
|
||||
if is_running "$bin"; then
|
||||
echo "already: $name"
|
||||
return 0
|
||||
fi
|
||||
echo "start: $name"
|
||||
nohup "$bin" "$@" >>"$LOG_DIR/${name}.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
local i
|
||||
for i in 1 2 3 4 5 6; do
|
||||
sleep 0.4
|
||||
if is_running "$bin"; then
|
||||
echo " ok: $name"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo " FAIL: $name did not stay up (see $LOG_DIR/${name}.log)" >&2
|
||||
tail -20 "$LOG_DIR/${name}.log" 2>/dev/null || true
|
||||
return 1
|
||||
}
|
||||
|
||||
ec=0
|
||||
# Helpers needed for settlement / ops (not only httpd).
|
||||
start_one taler-merchant-webhook /usr/bin/taler-merchant-webhook || ec=1
|
||||
start_one taler-merchant-kyccheck /usr/bin/taler-merchant-kyccheck || ec=1
|
||||
# Prefer already-running supervisor (started as root above); else bare wirewatch.
|
||||
if ps -eo args= 2>/dev/null | grep -q 'taler-merchant-wirewatch-supervise\.sh'; then
|
||||
echo "already: taler-merchant-wirewatch (via supervise)"
|
||||
elif is_running /usr/bin/taler-merchant-wirewatch; then
|
||||
echo "already: taler-merchant-wirewatch"
|
||||
else
|
||||
start_one taler-merchant-wirewatch /usr/bin/taler-merchant-wirewatch \
|
||||
-c "$CONF" -L INFO || ec=1
|
||||
fi
|
||||
start_one taler-merchant-depositcheck /usr/bin/taler-merchant-depositcheck || ec=1
|
||||
start_one taler-merchant-exchangekeyupdate /usr/bin/taler-merchant-exchangekeyupdate || ec=1
|
||||
start_one taler-merchant-reconciliation /usr/bin/taler-merchant-reconciliation || ec=1
|
||||
|
||||
if ! is_running taler-merchant-httpd; then
|
||||
echo "start: taler-merchant-httpd"
|
||||
nohup /usr/bin/taler-merchant-httpd --log=info \
|
||||
>>"$LOG_DIR/taler-merchant-httpd-$(date +%Y-%m-%d).log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 1
|
||||
is_running taler-merchant-httpd || ec=1
|
||||
fi
|
||||
|
||||
echo "--- live merchant ---"
|
||||
ps -eo pid,user,stat,etime,args 2>/dev/null \
|
||||
| grep taler-merchant | grep -vE 'grep| Z |ensure_merchant' || true
|
||||
|
||||
exit "$ec"
|
||||
232
scripts/taler-merchant/install_dual_terms.sh
Normal file
232
scripts/taler-merchant/install_dual_terms.sh
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
#!/bin/bash
|
||||
# Install dual-currency "No Formal Terms" ToS for the merchant backend.
|
||||
# Run as root inside the merchant container (taler-hacktivism).
|
||||
#
|
||||
# Sets files under TERMS_DIR/en/ for TERMS_ETAG = merchant-tos-dual-v0
|
||||
# (see configs/taler-hacktivism/merchant-overrides.conf).
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
ETAG="${TERMS_ETAG:-merchant-tos-dual-v2}"
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
|
||||
DATA_HOME=""
|
||||
if command -v taler-config >/dev/null 2>&1; then
|
||||
DATA_HOME=$(taler-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
fi
|
||||
# common fallbacks
|
||||
for d in \
|
||||
"$DATA_HOME" \
|
||||
/var/lib/taler-merchant/ \
|
||||
/var/lib/taler-merchant
|
||||
do
|
||||
[ -n "${d:-}" ] || continue
|
||||
d="${d%/}/"
|
||||
if [ -d "$d" ] || mkdir -p "$d" 2>/dev/null; then
|
||||
DATA_HOME="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-merchant/}"
|
||||
TERMS_DIR="${TERMS_DIR:-${DATA_HOME%/}/terms}"
|
||||
LANG_DIR="$TERMS_DIR/en"
|
||||
mkdir -p "$LANG_DIR"
|
||||
|
||||
TITLE="No Formal Terms · Dual Currency Notice"
|
||||
|
||||
BODY_MD='# No Formal Terms · Dual Currency Notice
|
||||
|
||||
This is a **self-hosted GNU Taler merchant backend** at `taler.hacktivism.ch` (hacktivism.ch).
|
||||
|
||||
**No formal terms of service** from Taler Operations AG (or any other third-party portal operator) apply to this instance. This short notice is the site policy for using the backend.
|
||||
|
||||
## Dual currency
|
||||
|
||||
This backend is configured for **two currencies at once**:
|
||||
|
||||
- **GOA** — explorational / experimental currency of the local stack (`exchange.hacktivism.ch`, `bank.hacktivism.ch`). GOA is **not** legal tender. It has **no guaranteed real-world value**, redemption, or convertibility.
|
||||
- **CHF** — Swiss francs, a **real** currency. CHF amounts are real money settled via the CHF exchange configured on this host (taler-ops / TOPS infrastructure as deployed). Treat CHF with the seriousness of ordinary payments.
|
||||
|
||||
By creating a merchant instance, accepting payments, or otherwise using this service you acknowledge that:
|
||||
|
||||
- GOA is for exploration and testing only.
|
||||
- CHF involves real money — only use funds you control and can afford to risk on a self-hosted experimental stack.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset GOA state, change configuration, delete instance data, or interrupt service without notice.
|
||||
- Software is provided as-is, without warranty.
|
||||
|
||||
If you do not agree, do not use this merchant backend.
|
||||
|
||||
## Related
|
||||
|
||||
- Exchange (GOA): https://exchange.hacktivism.ch/terms
|
||||
- Bank intro: https://bank.hacktivism.ch/intro/
|
||||
- Merchant intro: https://taler.hacktivism.ch/intro/
|
||||
'
|
||||
|
||||
BODY_TXT='No Formal Terms · Dual Currency Notice
|
||||
|
||||
This is a self-hosted GNU Taler merchant backend at taler.hacktivism.ch (hacktivism.ch).
|
||||
|
||||
No formal terms of service from Taler Operations AG (or any other third-party portal operator) apply to this instance. This short notice is the site policy for using the backend.
|
||||
|
||||
Dual currency
|
||||
-------------
|
||||
This backend is configured for two currencies at once:
|
||||
|
||||
- GOA — explorational / experimental currency of the local stack
|
||||
(exchange.hacktivism.ch, bank.hacktivism.ch). GOA is not legal tender.
|
||||
It has no guaranteed real-world value, redemption, or convertibility.
|
||||
- CHF — Swiss francs, a real currency. CHF amounts are real money
|
||||
settled via the CHF exchange configured on this host (taler-ops / TOPS
|
||||
infrastructure as deployed). Treat CHF with the seriousness of
|
||||
ordinary payments.
|
||||
|
||||
By creating a merchant instance, accepting payments, or otherwise using
|
||||
this service you acknowledge that:
|
||||
|
||||
- GOA is for exploration and testing only.
|
||||
- CHF involves real money — only use funds you control and can afford
|
||||
to risk on a self-hosted experimental stack.
|
||||
- There is no guaranteed availability, support, or uptime.
|
||||
- Operators may reset GOA state, change configuration, delete instance
|
||||
data, or interrupt service without notice.
|
||||
- Software is provided as-is, without warranty.
|
||||
|
||||
If you do not agree, do not use this merchant backend.
|
||||
|
||||
Related
|
||||
-------
|
||||
- Exchange (GOA): https://exchange.hacktivism.ch/terms
|
||||
- Bank intro: https://bank.hacktivism.ch/intro/
|
||||
- Merchant intro: https://taler.hacktivism.ch/intro/
|
||||
'
|
||||
|
||||
printf '%s\n' "$BODY_TXT" >"$LANG_DIR/${ETAG}.txt"
|
||||
printf '%s\n' "$BODY_MD" >"$LANG_DIR/${ETAG}.md"
|
||||
|
||||
# HTML (browser-friendly; style close to exchange short terms)
|
||||
cat >"$LANG_DIR/${ETAG}.html" <<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>${TITLE}</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 40rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.4rem 0 0.5rem; color: #e8c878; }
|
||||
p, li { font-size: 0.98rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.pair {
|
||||
display: grid; gap: 0.65rem; margin: 0.85rem 0 1rem;
|
||||
}
|
||||
@media (min-width: 520px) { .pair { grid-template-columns: 1fr 1fr; } }
|
||||
.cur {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
}
|
||||
.cur strong { display: block; font-size: 1.05rem; margin-bottom: 0.25rem; }
|
||||
.cur.go a, .cur.go strong { color: #5eead4; }
|
||||
.cur.chf strong { color: #e8c878; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">taler.hacktivism.ch · merchant</div>
|
||||
<h1>${TITLE}</h1>
|
||||
<p>This is a <strong>self-hosted GNU Taler merchant backend</strong> at
|
||||
<code>taler.hacktivism.ch</code> (hacktivism.ch).</p>
|
||||
<p><strong>No formal terms of service</strong> from Taler Operations AG
|
||||
(or any other third-party portal operator) apply to this instance.
|
||||
This short notice is the site policy for using the backend.</p>
|
||||
|
||||
<h2>Dual currency</h2>
|
||||
<p>This backend is configured for <strong>two currencies at once</strong>:</p>
|
||||
<div class="pair">
|
||||
<div class="cur go">
|
||||
<strong>GOA · explorational</strong>
|
||||
Local stack currency
|
||||
(<a href="https://exchange.hacktivism.ch/">exchange</a>,
|
||||
<a href="https://bank.hacktivism.ch/intro/">bank</a>).
|
||||
Not legal tender. No guaranteed real-world value, redemption, or convertibility.
|
||||
</div>
|
||||
<div class="cur chf">
|
||||
<strong>CHF · real</strong>
|
||||
Swiss francs. Real money, settled via the CHF exchange configured
|
||||
on this host (taler-ops / TOPS infrastructure as deployed).
|
||||
Treat CHF with the seriousness of ordinary payments.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>By using this service you acknowledge</h2>
|
||||
<ul>
|
||||
<li>GOA is for exploration and testing only.</li>
|
||||
<li>CHF involves real money — only use funds you control and can afford to risk on a self-hosted experimental stack.</li>
|
||||
<li>There is no guaranteed availability, support, or uptime.</li>
|
||||
<li>Operators may reset GOA state, change configuration, delete instance data, or interrupt service without notice.</li>
|
||||
<li>Software is provided as-is, without warranty.</li>
|
||||
</ul>
|
||||
<p>If you do not agree, do not use this merchant backend.</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://exchange.hacktivism.ch/terms">Exchange terms (GOA)</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/">Bank intro</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/intro/">Merchant intro</a></li>
|
||||
<li><a href="https://taler.hacktivism.ch/privacy">Merchant privacy</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Privacy</h2>
|
||||
<p class="muted">Processing under Swiss FADP (revDSG). What data is retained
|
||||
(instances, orders, deposits, logs, …) is listed on
|
||||
<a href="https://taler.hacktivism.ch/privacy">/privacy</a>.</p>
|
||||
<footer class="muted">Version ${ETAG} · hacktivism.ch</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
# Optional PDF so Accept: */* / wallets preferring PDF still get content
|
||||
PDF="$LANG_DIR/${ETAG}.pdf"
|
||||
if command -v pandoc >/dev/null 2>&1; then
|
||||
if pandoc -f markdown -t pdf -o "$PDF" "$LANG_DIR/${ETAG}.md" 2>/dev/null; then
|
||||
echo "pdf via pandoc: $PDF"
|
||||
elif command -v wkhtmltopdf >/dev/null 2>&1; then
|
||||
wkhtmltopdf "$LANG_DIR/${ETAG}.html" "$PDF" 2>/dev/null && echo "pdf via wkhtmltopdf" || true
|
||||
fi
|
||||
elif command -v wkhtmltopdf >/dev/null 2>&1; then
|
||||
wkhtmltopdf "$LANG_DIR/${ETAG}.html" "$PDF" 2>/dev/null && echo "pdf via wkhtmltopdf" || true
|
||||
fi
|
||||
# If no PDF toolchain: leave absent — httpd will serve md/html/txt by Accept
|
||||
|
||||
chmod -R a+rX "$TERMS_DIR"
|
||||
if id taler-merchant-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-merchant-httpd: "$TERMS_DIR" 2>/dev/null \
|
||||
|| chown -R taler-merchant-httpd:www-data "$TERMS_DIR" 2>/dev/null \
|
||||
|| true
|
||||
fi
|
||||
|
||||
echo "Installed under $LANG_DIR:"
|
||||
ls -la "$LANG_DIR"/${ETAG}.* 2>/dev/null || ls -la "$LANG_DIR"
|
||||
echo
|
||||
echo "Config should set:"
|
||||
echo " [merchant]"
|
||||
echo " TERMS_ETAG = ${ETAG}"
|
||||
echo " TERMS_DIR = \${TALER_DATA_HOME}terms/"
|
||||
echo "Then restart taler-merchant-httpd."
|
||||
195
scripts/taler-merchant/install_swiss_privacy.sh
Normal file
195
scripts/taler-merchant/install_swiss_privacy.sh
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
#!/bin/bash
|
||||
# Install Swiss FADP privacy policy for merchant backend (taler.hacktivism.ch).
|
||||
# Run as root inside taler-hacktivism. Sets files for PRIVACY_ETAG=merchant-pp-swiss-v0
|
||||
set -euo pipefail
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
|
||||
ETAG="${PRIVACY_ETAG:-merchant-pp-swiss-v0}"
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
DATA_HOME=""
|
||||
if command -v taler-config >/dev/null 2>&1; then
|
||||
DATA_HOME=$(taler-config -c "$CONF" -f -s PATHS -o TALER_DATA_HOME 2>/dev/null || true)
|
||||
fi
|
||||
DATA_HOME="${DATA_HOME:-/var/lib/taler-merchant/}"
|
||||
PRIVACY_DIR="${PRIVACY_DIR:-${DATA_HOME%/}/terms}"
|
||||
LANG_DIR="$PRIVACY_DIR/en"
|
||||
mkdir -p "$LANG_DIR"
|
||||
|
||||
TITLE="Privacy notice · GOA/CHF Merchant · Swiss FADP"
|
||||
BODY_TXT='Privacy notice — taler.hacktivism.ch merchant backend (Swiss FADP / revDSG)
|
||||
|
||||
Controller: operators of the hacktivism.ch GNU Taler stack.
|
||||
This dual-currency merchant backend accepts GOA (explorational) and CHF (via taler-ops exchange configuration).
|
||||
|
||||
Data retained (precise):
|
||||
1) Merchant instances — merchant_id, serial, creation metadata, config flags: lifetime of instance + up to 12 months after delete or stack wipe.
|
||||
2) Authentication — API tokens / login tokens (hashed or bearer secrets as stored by software): until expiry, revoke, or instance delete.
|
||||
3) Contract terms / orders — order_id, amount (currency:amount), summary (free text), paid/wired flags, creation time: for service lifetime; public landing stats may show anonymised aggregates and recent amounts/summaries.
|
||||
4) Deposits & refunds — deposit proofs, refund amounts/reasons, timestamps: for service lifetime or until operational wipe.
|
||||
5) Settlement accounts — payto URIs / account labels configured on instances: while configured.
|
||||
6) Exchange interaction metadata — which exchange (GOA/CHF) was used for deposits: with related order records.
|
||||
7) Technical logs — HTTP/access and application logs (IP, path, status): typically days–weeks via rotation.
|
||||
8) Public stats.json — aggregate counts and recent activity without full account credentials: overwritten minutely.
|
||||
|
||||
Not retained by this merchant backend: wallet private keys; full card PANs; unsolicited government ID documents unless an operator enables separate KYC tooling.
|
||||
|
||||
Purposes: accept GNU Taler payments, refunds, settlement, abuse prevention, operations.
|
||||
Legal basis (FADP): performance of service requested by merchant operators/customers; proportionate operation of a public experimental stack.
|
||||
Recipients: configured exchanges (exchange.hacktivism.ch for GOA; exchange.taler-ops.ch for CHF as configured); host operators. No sale of data.
|
||||
Rights: access, rectification, deletion, objection under FADP; complaint to Swiss FDPIC (EDÖB).
|
||||
Security: TLS; experimental — no certified ISMS. Do not put sensitive personal data in order summaries.
|
||||
|
||||
Related: https://taler.hacktivism.ch/terms · https://exchange.hacktivism.ch/privacy · https://bank.hacktivism.ch/intro/privacy.html
|
||||
'
|
||||
|
||||
BODY_MD='# Privacy notice · Merchant · Swiss FADP
|
||||
|
||||
Controller: operators of the **hacktivism.ch** GNU Taler stack (`taler.hacktivism.ch`).
|
||||
Dual currency: **GOA** (explorational) and **CHF** (taler-ops exchange path).
|
||||
|
||||
Processing under the Swiss Federal Act on Data Protection (**FADP / revDSG**, since 1 Sep 2023).
|
||||
|
||||
## Data retained
|
||||
|
||||
| Data | Examples | Retention |
|
||||
|------|----------|-----------|
|
||||
| Instances | merchant_id, serial, config | Instance life + up to 12 months after delete/wipe |
|
||||
| Auth | API / login tokens | Until expiry, revoke, or instance delete |
|
||||
| Orders | order_id, amount, summary, paid/wired, time | Service life or wipe; public stats may show recent aggregates |
|
||||
| Deposits / refunds | proofs, amounts, reasons | Service life or wipe |
|
||||
| Settlement | payto / account labels | While configured |
|
||||
| Exchange metadata | GOA/CHF exchange used | With related order records |
|
||||
| Technical logs | IP, path, status | Days–weeks (rotation) |
|
||||
| Public stats.json | Aggregates, recent activity | Overwritten continuously |
|
||||
|
||||
**Not retained:** wallet private keys; card PANs; government ID unless separate KYC is enabled.
|
||||
|
||||
## Purposes
|
||||
|
||||
Accept payments, refunds, settlement; security and operations of this experimental backend.
|
||||
|
||||
## Rights
|
||||
|
||||
Access, correction, deletion, objection (FADP). Complaint: Swiss **FDPIC / EDÖB**.
|
||||
|
||||
## Related
|
||||
|
||||
- [Merchant terms](https://taler.hacktivism.ch/terms)
|
||||
- [Exchange privacy](https://exchange.hacktivism.ch/privacy)
|
||||
- [Bank privacy](https://bank.hacktivism.ch/intro/privacy.html)
|
||||
'
|
||||
|
||||
cat >"$LANG_DIR/${ETAG}.txt" <<EOF
|
||||
$BODY_TXT
|
||||
EOF
|
||||
cat >"$LANG_DIR/${ETAG}.md" <<EOF
|
||||
$BODY_MD
|
||||
EOF
|
||||
|
||||
cat >"$LANG_DIR/${ETAG}.html" <<'HTML'
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<title>Privacy notice · GOA/CHF Merchant · Swiss FADP</title>
|
||||
<style>
|
||||
:root { color-scheme: dark light; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 2rem auto; padding: 0 1.1rem 3rem;
|
||||
line-height: 1.5; color: #e8e6e3; background: #1a1520;
|
||||
}
|
||||
h1 { font-size: 1.35rem; font-weight: 800; margin: 0 0 1rem; color: #f5f0ea; }
|
||||
h2 { font-size: 1.05rem; margin: 1.5rem 0 0.5rem; color: #e8c878; }
|
||||
p, li, td, th { font-size: 0.95rem; }
|
||||
ul { padding-left: 1.2rem; }
|
||||
code, a { color: #5eead4; }
|
||||
a { text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
.badge {
|
||||
display: inline-block; font-size: 0.72rem; font-weight: 700;
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
color: #c4b5fd; border: 1px solid rgba(196,181,253,0.35);
|
||||
border-radius: 999px; padding: 0.2rem 0.65rem; margin-bottom: 0.85rem;
|
||||
}
|
||||
.note {
|
||||
border-radius: 12px; padding: 0.75rem 0.9rem; margin: 0.85rem 0 1rem;
|
||||
border: 1px solid rgba(255,255,255,0.1); background: rgba(0,0,0,0.25);
|
||||
font-size: 0.9rem; color: #c8c4bf;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; margin: 0.6rem 0 1rem; font-size: 0.88rem; }
|
||||
th, td { border: 1px solid rgba(255,255,255,0.12); padding: 0.45rem 0.55rem; text-align: left; vertical-align: top; }
|
||||
th { background: rgba(0,0,0,0.35); color: #e8c878; font-weight: 700; }
|
||||
.muted { color: #a39e98; font-size: 0.88rem; }
|
||||
footer { margin-top: 2rem; font-size: 0.85rem; color: #a39e98; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="badge">taler.hacktivism.ch · privacy · CH</div>
|
||||
<h1>Privacy notice · GOA/CHF Merchant</h1>
|
||||
<p class="note">
|
||||
Swiss Federal Act on Data Protection (<strong>FADP / revDSG</strong>, since 1 Sep 2023).
|
||||
Dual-currency merchant backend at <code>taler.hacktivism.ch</code>:
|
||||
<strong>GOA</strong> (explorational) and <strong>CHF</strong> (taler-ops path).
|
||||
</p>
|
||||
|
||||
<h2>1. Controller</h2>
|
||||
<p>Operators of the hacktivism.ch GNU Taler stack. Experimental public deployment;
|
||||
no separate DPO appointed.</p>
|
||||
|
||||
<h2>2. Data retained</h2>
|
||||
<table>
|
||||
<thead><tr><th>Data</th><th>Examples</th><th>Typical retention</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Instances</td><td>merchant_id, serial, config</td><td>Instance life + up to 12 months after delete/wipe</td></tr>
|
||||
<tr><td>Authentication</td><td>API / login tokens</td><td>Until expiry, revoke, or instance delete</td></tr>
|
||||
<tr><td>Orders</td><td>order_id, amount, summary, paid/wired, time</td><td>Service life or wipe; public stats may list recent amounts/summaries</td></tr>
|
||||
<tr><td>Deposits / refunds</td><td>proofs, amounts, reasons</td><td>Service life or wipe</td></tr>
|
||||
<tr><td>Settlement accounts</td><td>payto / account labels</td><td>While configured</td></tr>
|
||||
<tr><td>Exchange metadata</td><td>GOA/CHF exchange used</td><td>With related order records</td></tr>
|
||||
<tr><td>Technical logs</td><td>IP, path, status</td><td>Days–weeks (rotation)</td></tr>
|
||||
<tr><td>Public stats.json</td><td>Aggregates, recent activity</td><td>Overwritten continuously</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>Not retained:</strong> wallet private keys; payment card PANs; government ID documents
|
||||
unless a separate KYC feature is enabled by operators.</p>
|
||||
|
||||
<h2>3. Purposes</h2>
|
||||
<ul>
|
||||
<li>Accept GNU Taler payments (GOA and/or CHF), refunds, and settlement</li>
|
||||
<li>Authenticate instance operators</li>
|
||||
<li>Security, abuse prevention, debugging</li>
|
||||
</ul>
|
||||
|
||||
<h2>4. Recipients</h2>
|
||||
<ul>
|
||||
<li>Local GOA exchange (<code>exchange.hacktivism.ch</code>)</li>
|
||||
<li>CHF exchange as configured (e.g. <code>exchange.taler-ops.ch</code>)</li>
|
||||
<li>Host/infrastructure operators under this deployment</li>
|
||||
</ul>
|
||||
<p>No sale of personal data.</p>
|
||||
|
||||
<h2>5. Your rights</h2>
|
||||
<p>Access, rectification, deletion, and objection under the FADP (within legal limits).
|
||||
Complaint: Swiss Federal Data Protection and Information Commissioner
|
||||
(<strong>FDPIC / EDÖB</strong>).</p>
|
||||
|
||||
<h2>Related</h2>
|
||||
<ul>
|
||||
<li><a href="https://taler.hacktivism.ch/terms">Merchant terms</a></li>
|
||||
<li><a href="https://exchange.hacktivism.ch/privacy">Exchange privacy</a></li>
|
||||
<li><a href="https://bank.hacktivism.ch/intro/privacy.html">Bank privacy</a></li>
|
||||
</ul>
|
||||
<footer class="muted">merchant-pp-swiss-v0 · Swiss FADP (revDSG)</footer>
|
||||
</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
chmod -R a+rX "$PRIVACY_DIR"
|
||||
if id taler-merchant-httpd >/dev/null 2>&1; then
|
||||
chown -R taler-merchant-httpd: "$PRIVACY_DIR" 2>/dev/null \
|
||||
|| chown -R taler-merchant-httpd:www-data "$PRIVACY_DIR" 2>/dev/null || true
|
||||
fi
|
||||
echo "ok privacy $ETAG -> $LANG_DIR"
|
||||
ls -la "$LANG_DIR"/${ETAG}.*
|
||||
439
scripts/taler-merchant/landing-stats-merchant.sh
Normal file
439
scripts/taler-merchant/landing-stats-merchant.sh
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
#!/bin/bash
|
||||
# Run INSIDE taler-hacktivism. Writes /var/www/merchant-landing/stats.json
|
||||
#
|
||||
# Current taler-merchant schema:
|
||||
# merchant.merchant_instances → merchant_serial, merchant_id
|
||||
# merchant_instance_<serial>.* → per-instance tables
|
||||
#
|
||||
# IMPORTANT:
|
||||
# - cron uses a minimal PATH. Without /usr/sbin, runuser is missing and
|
||||
# every query used to fail silently → all zeros (and overwrote good data).
|
||||
# - Never write stats.json on failure: leave the previous good file in place.
|
||||
# - Avoid: psql -F$'\t' -v ON_ERROR_STOP=… (if -F loses the tab arg, -v is
|
||||
# eaten as fieldsep and ON_ERROR_STOP becomes the *username*).
|
||||
set -euo pipefail
|
||||
export TZ="${TZ:-Europe/Zurich}"
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"
|
||||
LANDING_DIR="${LANDING_DIR:-/var/www/merchant-landing}"
|
||||
OUT="$LANDING_DIR/stats.json"
|
||||
RUN="$LANDING_DIR/stats-run.json"
|
||||
TMP="${OUT}.tmp.$$"
|
||||
DB="${MERCHANT_DB:-taler-merchant}"
|
||||
ACTIVITY_LIMIT="${ACTIVITY_LIMIT:-12}"
|
||||
ERRLOG="${LANDING_STATS_ERRLOG:-/var/log/landing-stats-merchant.err}"
|
||||
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_human() { date +"%Y-%m-%d %H:%M %Z"; }
|
||||
json_str() {
|
||||
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr '\n\r\t' ' ' | sed 's/ */ /g')"
|
||||
}
|
||||
|
||||
# Public run status for the 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
|
||||
}
|
||||
|
||||
abort() {
|
||||
# Do NOT touch $OUT — previous good stats stay live; site shows run note.
|
||||
write_run false "$*"
|
||||
printf '%s\n' "abort merchant-stats: $*" | tee -a "$ERRLOG" >&2
|
||||
rm -f "$TMP" /tmp/merch_cur_$$.tsv /tmp/merch_act_$$.tsv 2>/dev/null || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
as_postgres() {
|
||||
if command -v runuser >/dev/null 2>&1; then
|
||||
runuser -u postgres -- "$@"
|
||||
elif command -v su >/dev/null 2>&1; then
|
||||
su -s /bin/bash postgres -c "$*"
|
||||
else
|
||||
return 127
|
||||
fi
|
||||
}
|
||||
|
||||
# pipe SQL on stdin — never -f on root-owned temps (postgres cannot read them)
|
||||
psql_pipe() {
|
||||
local fs=$'\t' ec
|
||||
set +e
|
||||
as_postgres psql -d "$DB" -At -F"$fs" 2>>"$ERRLOG"
|
||||
ec=$?
|
||||
set -e
|
||||
return "$ec"
|
||||
}
|
||||
|
||||
# Required single-value query. Empty/fail → abort (no site write).
|
||||
psqlq() {
|
||||
local sql="$1" out ec
|
||||
set +e
|
||||
out=$(as_postgres psql -d "$DB" -At -c "$sql" 2>>"$ERRLOG")
|
||||
ec=$?
|
||||
set -e
|
||||
if [ "$ec" -ne 0 ]; then
|
||||
abort "psql failed (ec=$ec): ${sql:0:120}"
|
||||
fi
|
||||
# strip trailing newlines only
|
||||
printf '%s' "$out" | tr -d '\r'
|
||||
}
|
||||
|
||||
# "64.03" or "64.03000001" → clean CUR:value using decimal string (no binary float)
|
||||
fmt_cur_num() {
|
||||
local c="$1" n="$2"
|
||||
awk -v c="$c" -v n="$n" 'BEGIN{
|
||||
gsub(/ /,"",n)
|
||||
if (n == "" || n+0 == 0 && n !~ /[1-9]/) { printf "%s:0", c; exit }
|
||||
if (n ~ /[eE]/) {
|
||||
x = n+0
|
||||
s = sprintf("%.8f", x)
|
||||
} else {
|
||||
s = n
|
||||
}
|
||||
if (s ~ /\./) {
|
||||
split(s, a, ".")
|
||||
whole = a[1]; frac = substr(a[2] "00000000", 1, 8)
|
||||
extra = substr(a[2], 9, 1)
|
||||
if (extra != "" && extra+0 >= 5) {
|
||||
f = frac+0 + 1
|
||||
if (f >= 100000000) { whole = whole+1; f = f - 100000000 }
|
||||
frac = sprintf("%08d", f)
|
||||
}
|
||||
sub(/0+$/, "", frac)
|
||||
if (frac == "") printf "%s:%s", c, whole
|
||||
else printf "%s:%s.%s", c, whole, frac
|
||||
} else {
|
||||
printf "%s:%s", c, s
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
# --- preflight: tools + DB reachability (fail closed) ---
|
||||
command -v psql >/dev/null 2>&1 || abort "psql not in PATH ($PATH)"
|
||||
command -v runuser >/dev/null 2>&1 || command -v su >/dev/null 2>&1 || abort "neither runuser nor su in PATH"
|
||||
|
||||
PROBE=$(psqlq "SELECT 1;")
|
||||
[ "$PROBE" = "1" ] || abort "postgres not reachable (SELECT 1 → '$PROBE')"
|
||||
|
||||
SERIALS=$(psqlq "SELECT merchant_serial::text FROM merchant.merchant_instances ORDER BY merchant_serial;")
|
||||
# If table exists but we got nothing, still ok (empty install). If table missing, psqlq aborted.
|
||||
|
||||
# Build list of existing instance schemas
|
||||
SCHEMAS=""
|
||||
INSTANCES=0
|
||||
while read -r serial; do
|
||||
[ -z "$serial" ] && continue
|
||||
# only pure integers
|
||||
[[ "$serial" =~ ^[0-9]+$ ]] || abort "bad merchant_serial='$serial'"
|
||||
sch="merchant_instance_${serial}"
|
||||
exists=$(psqlq "SELECT 1 FROM pg_namespace WHERE nspname = '${sch}';")
|
||||
if [ "$exists" != "1" ]; then
|
||||
# schema lag — skip, do not abort (instance row without schema yet)
|
||||
continue
|
||||
fi
|
||||
INSTANCES=$((INSTANCES + 1))
|
||||
SCHEMAS="${SCHEMAS}${SCHEMAS:+ }$sch:$serial"
|
||||
done <<<"$SERIALS"
|
||||
|
||||
# Sanity: if instance schemas exist in PG but SERIALS empty → query path broken
|
||||
NS_COUNT=$(psqlq "SELECT count(*)::text FROM pg_namespace WHERE nspname LIKE 'merchant_instance_%';")
|
||||
NS_COUNT=$(printf '%s' "$NS_COUNT" | tr -d '[:space:]')
|
||||
if [ "${NS_COUNT:-0}" -gt 0 ] && [ "$INSTANCES" -eq 0 ]; then
|
||||
abort "pg has ${NS_COUNT} merchant_instance_* schemas but resolved INSTANCES=0 (query/PATH bug)"
|
||||
fi
|
||||
|
||||
# --- dual-currency aggregation entirely in PostgreSQL (numeric) ---
|
||||
{
|
||||
echo "SELECT"
|
||||
echo " coalesce(nullif(split_part(c.contract_terms->>'amount', ':', 1), ''), '?') AS currency,"
|
||||
echo " count(*)::bigint,"
|
||||
echo " count(*) FILTER (WHERE c.paid)::bigint,"
|
||||
echo " count(*) FILTER (WHERE NOT c.paid)::bigint,"
|
||||
echo " count(*) FILTER (WHERE c.wired)::bigint,"
|
||||
echo " round(coalesce(sum(NULLIF(split_part(c.contract_terms->>'amount', ':', 2), '')::numeric), 0), 8)::text,"
|
||||
echo " round(coalesce(sum(NULLIF(split_part(c.contract_terms->>'amount', ':', 2), '')::numeric) FILTER (WHERE c.paid), 0), 8)::text"
|
||||
echo "FROM ("
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
echo "SELECT contract_terms, paid, wired FROM ${sch}.merchant_contract_terms"
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT NULL::jsonb AS contract_terms, false AS paid, false AS wired WHERE false"
|
||||
fi
|
||||
echo ") c"
|
||||
echo "WHERE c.contract_terms ? 'amount'"
|
||||
echo "GROUP BY 1 ORDER BY 1;"
|
||||
} | psql_pipe >"/tmp/merch_cur_$$.tsv" || abort "currency aggregate query failed"
|
||||
CUR_TSV=$(cat "/tmp/merch_cur_$$.tsv" 2>/dev/null || true)
|
||||
rm -f "/tmp/merch_cur_$$.tsv"
|
||||
|
||||
TOTAL_CONTRACTS=0; TOTAL_PAID=0; TOTAL_WIRED=0; TOTAL_UNPAID=0
|
||||
declare -A C_CONTRACTS C_PAID C_UNPAID C_WIRED C_AMT C_AMT_PAID
|
||||
|
||||
while IFS=$'\t' read -r cur contracts paid unpaid wired amt amt_paid; do
|
||||
[ -z "${cur:-}" ] && continue
|
||||
TOTAL_CONTRACTS=$((TOTAL_CONTRACTS + contracts))
|
||||
TOTAL_PAID=$((TOTAL_PAID + paid))
|
||||
TOTAL_UNPAID=$((TOTAL_UNPAID + unpaid))
|
||||
TOTAL_WIRED=$((TOTAL_WIRED + wired))
|
||||
C_CONTRACTS[$cur]=$contracts
|
||||
C_PAID[$cur]=$paid
|
||||
C_UNPAID[$cur]=$unpaid
|
||||
C_WIRED[$cur]=$wired
|
||||
C_AMT[$cur]=$amt
|
||||
C_AMT_PAID[$cur]=$amt_paid
|
||||
done <<<"$CUR_TSV"
|
||||
|
||||
# deposits / refunds
|
||||
DEPOSITS=0; REFUNDS=0
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
d=$(psqlq "SELECT count(*)::text FROM ${sch}.merchant_deposits;")
|
||||
r=$(psqlq "SELECT count(*)::text FROM ${sch}.merchant_refunds;")
|
||||
d=$(printf '%s' "$d" | tr -d '[:space:]')
|
||||
r=$(printf '%s' "$r" | tr -d '[:space:]')
|
||||
[[ "$d" =~ ^[0-9]+$ ]] || abort "bad deposits count for $sch: '$d'"
|
||||
[[ "$r" =~ ^[0-9]+$ ]] || abort "bad refunds count for $sch: '$r'"
|
||||
DEPOSITS=$((DEPOSITS + d))
|
||||
REFUNDS=$((REFUNDS + r))
|
||||
done
|
||||
|
||||
# --- recent activity: 5 latest events per currency (GOA + CHF), payments + refunds ---
|
||||
ACT_PER_CURRENCY="${ACT_PER_CURRENCY:-5}"
|
||||
|
||||
# Query latest ACT_PER_CURRENCY events for one currency code (payments ∪ refunds).
|
||||
# Writes TSV rows: ts kind order_id amount summary status
|
||||
query_activity_for_currency() {
|
||||
local cur="$1"
|
||||
local out="$2"
|
||||
{
|
||||
echo "SELECT * FROM ("
|
||||
echo "SELECT * FROM ("
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
cat <<SQL
|
||||
SELECT creation_time AS ts,
|
||||
'payment'::text AS kind,
|
||||
order_id,
|
||||
coalesce(contract_terms->>'amount','') AS amount,
|
||||
left(translate(coalesce(contract_terms->>'summary',''), E'\t\n\r', ' '), 64) AS summary,
|
||||
CASE WHEN wired THEN 'wired' ELSE 'paid' END AS status
|
||||
FROM ${sch}.merchant_contract_terms
|
||||
WHERE paid
|
||||
AND upper(split_part(coalesce(contract_terms->>'amount',''), ':', 1)) = upper('${cur}')
|
||||
SQL
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||
fi
|
||||
echo " UNION ALL "
|
||||
first=1
|
||||
for item in $SCHEMAS; do
|
||||
sch=${item%%:*}
|
||||
if [ "$first" = 1 ]; then first=0; else echo " UNION ALL "; fi
|
||||
cat <<SQL
|
||||
SELECT r.refund_timestamp AS ts,
|
||||
'refund'::text AS kind,
|
||||
ct.order_id,
|
||||
(r.refund_amount).curr || ':' ||
|
||||
CASE WHEN (r.refund_amount).frac = 0
|
||||
THEN (r.refund_amount).val::text
|
||||
ELSE trim(trailing '0' FROM trim(trailing '.' FROM (
|
||||
((r.refund_amount).val + (r.refund_amount).frac::numeric / 100000000)::numeric(32,8)
|
||||
)::text))
|
||||
END AS amount,
|
||||
left(translate(coalesce(r.reason,''), E'\t\n\r', ' '), 64) AS summary,
|
||||
'refunded'::text AS status
|
||||
FROM ${sch}.merchant_refunds r
|
||||
JOIN ${sch}.merchant_contract_terms ct ON ct.order_serial = r.order_serial
|
||||
WHERE upper((r.refund_amount).curr) = upper('${cur}')
|
||||
SQL
|
||||
done
|
||||
if [ "$first" = 1 ]; then
|
||||
echo "SELECT 0::bigint AS ts, ''::text AS kind, ''::text AS order_id, ''::text AS amount, ''::text AS summary, ''::text AS status WHERE false"
|
||||
fi
|
||||
echo ") u ORDER BY ts DESC LIMIT ${ACT_PER_CURRENCY}"
|
||||
echo ") act;"
|
||||
} | psql_pipe >"$out" || abort "activity query failed for $cur"
|
||||
}
|
||||
|
||||
# Build JSON array of activity items from a TSV file
|
||||
activity_tsv_to_json() {
|
||||
local tsv_file="$1"
|
||||
local json="["
|
||||
local af=1
|
||||
local ts kind oid amt sum st sec human_fmt iso
|
||||
while IFS=$'\t' read -r ts kind oid amt sum st; do
|
||||
[ -z "${ts:-}" ] && continue
|
||||
[ "$ts" = "0" ] && [ -z "$kind" ] && continue
|
||||
sec=$(awk -v t="$ts" 'BEGIN{printf "%d", int(t/1000000)}')
|
||||
human_fmt=$(date -d "@${sec}" +"%Y-%m-%d %H:%M %Z" 2>/dev/null || echo "$sec")
|
||||
iso=$(date -d "@${sec}" +"%Y-%m-%dT%H:%M:%S%z" 2>/dev/null | sed -E 's/([+-][0-9]{2})([0-9]{2})$/\1:\2/' || true)
|
||||
if [ "$af" = 1 ]; then af=0; else json="${json},"; fi
|
||||
json="${json}
|
||||
{
|
||||
\"ts_us\": ${ts:-0},
|
||||
\"ts\": $(json_str "$iso"),
|
||||
\"ts_human\": $(json_str "$human_fmt"),
|
||||
\"kind\": $(json_str "$kind"),
|
||||
\"order_id\": $(json_str "$oid"),
|
||||
\"amount\": $(json_str "$amt"),
|
||||
\"summary\": $(json_str "$sum"),
|
||||
\"status\": $(json_str "$st")
|
||||
}"
|
||||
done <"$tsv_file"
|
||||
json="${json}
|
||||
]"
|
||||
printf '%s' "$json"
|
||||
}
|
||||
|
||||
query_activity_for_currency GOA "/tmp/merch_act_goa_$$.tsv"
|
||||
query_activity_for_currency CHF "/tmp/merch_act_chf_$$.tsv"
|
||||
ACT_GOA_JSON=$(activity_tsv_to_json "/tmp/merch_act_goa_$$.tsv")
|
||||
ACT_CHF_JSON=$(activity_tsv_to_json "/tmp/merch_act_chf_$$.tsv")
|
||||
rm -f "/tmp/merch_act_goa_$$.tsv" "/tmp/merch_act_chf_$$.tsv"
|
||||
|
||||
# Flat recent_activity: GOA then CHF (compat; landing prefers by_currency)
|
||||
ACT_JSON="["
|
||||
af=1
|
||||
for block in "$ACT_GOA_JSON" "$ACT_CHF_JSON"; do
|
||||
# strip outer [ ] and inject if non-empty
|
||||
inner=$(printf '%s' "$block" | sed '1s/^\s*\[//; $s/\]\s*$//')
|
||||
# empty array → skip
|
||||
if printf '%s' "$inner" | grep -q '"kind"'; then
|
||||
if [ "$af" = 1 ]; then af=0; else ACT_JSON="${ACT_JSON},"; fi
|
||||
ACT_JSON="${ACT_JSON}${inner}"
|
||||
fi
|
||||
done
|
||||
ACT_JSON="${ACT_JSON}
|
||||
]"
|
||||
|
||||
ACT_BY_CUR_JSON="[
|
||||
{
|
||||
\"currency\": \"GOA\",
|
||||
\"limit\": ${ACT_PER_CURRENCY},
|
||||
\"items\": ${ACT_GOA_JSON}
|
||||
},
|
||||
{
|
||||
\"currency\": \"CHF\",
|
||||
\"limit\": ${ACT_PER_CURRENCY},
|
||||
\"items\": ${ACT_CHF_JSON}
|
||||
}
|
||||
]"
|
||||
|
||||
# by_currency JSON — GOA then CHF then rest
|
||||
CUR_JSON="["
|
||||
cf=1
|
||||
emit_cur() {
|
||||
local cur="$1"
|
||||
[ -n "${C_CONTRACTS[$cur]+x}" ] || return 1
|
||||
local amt_fmt paid_fmt
|
||||
amt_fmt=$(fmt_cur_num "$cur" "${C_AMT[$cur]}")
|
||||
paid_fmt=$(fmt_cur_num "$cur" "${C_AMT_PAID[$cur]}")
|
||||
if [ "$cf" = 1 ]; then cf=0; else CUR_JSON="${CUR_JSON},"; fi
|
||||
CUR_JSON="${CUR_JSON}
|
||||
{
|
||||
\"currency\": $(json_str "$cur"),
|
||||
\"contracts\": ${C_CONTRACTS[$cur]:-0},
|
||||
\"paid\": ${C_PAID[$cur]:-0},
|
||||
\"unpaid\": ${C_UNPAID[$cur]:-0},
|
||||
\"wired\": ${C_WIRED[$cur]:-0},
|
||||
\"amount_sum\": $(json_str "$amt_fmt"),
|
||||
\"amount_paid_sum\": $(json_str "$paid_fmt")
|
||||
}"
|
||||
}
|
||||
emit_cur GOA || true
|
||||
emit_cur CHF || true
|
||||
for cur in "${!C_CONTRACTS[@]}"; do
|
||||
case "$cur" in GOA|CHF) continue ;; esac
|
||||
emit_cur "$cur" || true
|
||||
done
|
||||
CUR_JSON="${CUR_JSON}
|
||||
]"
|
||||
|
||||
GEN=$(now_iso)
|
||||
HUMAN=$(now_human)
|
||||
|
||||
# Live performance probes (exchange-style)
|
||||
measure_ms() {
|
||||
local url="$1" t
|
||||
t=$(curl -skS -o /dev/null -m 8 -w '%{time_total}' "$url" 2>/dev/null || echo "")
|
||||
[ -z "$t" ] && { echo "null"; return; }
|
||||
awk -v t="$t" 'BEGIN{printf "%d", (t+0)*1000}'
|
||||
}
|
||||
num_or_null() { case "${1:-}" in ''|null) echo null ;; *) echo "$1" ;; esac; }
|
||||
MERCHANT_PUBLIC_BASE="${MERCHANT_PUBLIC_URL:-https://taler.hacktivism.ch}"
|
||||
MERCHANT_LOCAL="${MERCHANT_LOCAL_URL:-https://127.0.0.1:9010}"
|
||||
CONFIG_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/config")
|
||||
CONFIG_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_PUBLIC_BASE}/config" 2>/dev/null || echo "000")
|
||||
if [ "$CONFIG_HTTP" != "200" ]; then
|
||||
CONFIG_MS=$(measure_ms "${MERCHANT_LOCAL}/config")
|
||||
CONFIG_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_LOCAL}/config" 2>/dev/null || echo "000")
|
||||
fi
|
||||
TERMS_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/terms")
|
||||
TERMS_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' -H "Accept: text/html" "${MERCHANT_PUBLIC_BASE}/terms" 2>/dev/null || echo "000")
|
||||
WEBUI_MS=$(measure_ms "${MERCHANT_PUBLIC_BASE}/webui/")
|
||||
WEBUI_HTTP=$(curl -skS -o /dev/null -m 8 -w '%{http_code}' "${MERCHANT_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
|
||||
|
||||
# Atomic write only after full success
|
||||
cat >"$TMP" <<EOF
|
||||
{
|
||||
"ok": true,
|
||||
"source": "merchant-db",
|
||||
"schema": "merchant.merchant_instances + merchant_instance_<serial>",
|
||||
"dual_currency": true,
|
||||
"currencies_note": "CHF (taler-ops) + GOA (hacktivism)",
|
||||
"timezone": $(json_str "$TZ"),
|
||||
"generated_at": $(json_str "$GEN"),
|
||||
"generated_at_human": $(json_str "$HUMAN"),
|
||||
"instances": ${INSTANCES:-0},
|
||||
"orders": ${TOTAL_CONTRACTS:-0},
|
||||
"paid": ${TOTAL_PAID:-0},
|
||||
"unpaid": ${TOTAL_UNPAID:-0},
|
||||
"wired": ${TOTAL_WIRED:-0},
|
||||
"deposits": ${DEPOSITS:-0},
|
||||
"refunds": ${REFUNDS:-0},
|
||||
"by_currency": $CUR_JSON,
|
||||
"recent_activity_limit_per_currency": ${ACT_PER_CURRENCY},
|
||||
"recent_activity_by_currency": $ACT_BY_CUR_JSON,
|
||||
"recent_activity": $ACT_JSON,
|
||||
"performance": {
|
||||
"config_http": $(json_str "$CONFIG_HTTP"),
|
||||
"config_ms": $(num_or_null "$CONFIG_MS"),
|
||||
"terms_http": $(json_str "$TERMS_HTTP"),
|
||||
"terms_ms": $(num_or_null "$TERMS_MS"),
|
||||
"webui_http": $(json_str "$WEBUI_HTTP"),
|
||||
"webui_ms": $(num_or_null "$WEBUI_MS"),
|
||||
"loadavg": $(json_str "${LOADAVG:-}"),
|
||||
"memory": {
|
||||
${MEM_JSON}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# basic JSON sanity before publish
|
||||
grep -q '"ok": true' "$TMP" || abort "tmp json missing ok:true"
|
||||
mv -f "$TMP" "$OUT"
|
||||
write_run true
|
||||
echo "ok merchant instances=$INSTANCES orders=$TOTAL_CONTRACTS paid=$TOTAL_PAID refunds=$REFUNDS -> $OUT"
|
||||
91
scripts/taler-merchant/setup_credit_facade.sh
Executable file
91
scripts/taler-merchant/setup_credit_facade.sh
Executable file
|
|
@ -0,0 +1,91 @@
|
|||
#!/bin/bash
|
||||
# Configure merchant bank account credit facade for automatic settlement import.
|
||||
#
|
||||
# Root cause (2026-07-09): merchant wirewatch must call the **Taler Revenue API**
|
||||
# (`…/taler-revenue/`), not the exchange wire-gateway (`…/taler-wire-gateway/`).
|
||||
# On this bank, history endpoints accept **Bearer** tokens only (Basic → 401).
|
||||
#
|
||||
# Usage (as root on koopa host):
|
||||
# setup_credit_facade.sh
|
||||
# MERCHANT_INSTANCE=goa-demo-cp4zqk setup_credit_facade.sh
|
||||
# BANK_USER=… BANK_PW_FILE=… MERCHANT_PW_FILE=… setup_credit_facade.sh
|
||||
#
|
||||
# Effects:
|
||||
# - PATCH /instances/$INST/private/accounts/$H_WIRE with credit_facade_*
|
||||
# - long-lived refreshable bank token (1y)
|
||||
# - restarts taler-merchant-wirewatch (once, after facade is set)
|
||||
set -euo pipefail
|
||||
|
||||
INST="${MERCHANT_INSTANCE:-goa-demo-cp4zqk}"
|
||||
BANK_USER="${BANK_USER:-$INST}"
|
||||
MERCHANT_PW_FILE="${MERCHANT_PW_FILE:-/root/merchant-${INST}-password.txt}"
|
||||
BANK_PW_FILE="${BANK_PW_FILE:-/root/bank-${BANK_USER}-password.txt}"
|
||||
MER_URL="${MERCHANT_URL:-https://127.0.0.1:9010}"
|
||||
BANK_URL="${BANK_URL:-http://127.0.0.1:9012}"
|
||||
PUBLIC_BANK="${PUBLIC_BANK_BASE:-https://bank.hacktivism.ch}"
|
||||
FACADE_URL="${CREDIT_FACADE_URL:-${PUBLIC_BANK}/accounts/${BANK_USER}/taler-revenue/}"
|
||||
|
||||
if [ "$(id -un)" != "root" ]; then
|
||||
echo "run as root on koopa host" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -r "$MERCHANT_PW_FILE" ] || [ ! -r "$BANK_PW_FILE" ]; then
|
||||
echo "need readable $MERCHANT_PW_FILE and $BANK_PW_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MPW=$(tr -d '\n' <"$MERCHANT_PW_FILE")
|
||||
BPW=$(tr -d '\n' <"$BANK_PW_FILE")
|
||||
AUTH="Authorization: Bearer secret-token:${MPW}"
|
||||
|
||||
echo "=== bank token for $BANK_USER ==="
|
||||
TOK=$(curl -sS -u "${BANK_USER}:${BPW}" -H 'Content-Type: application/json' \
|
||||
-d '{"scope":"readwrite","refreshable":true,"duration":{"d_us":31536000000000}}' \
|
||||
"${BANK_URL}/accounts/${BANK_USER}/token" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])')
|
||||
echo "tok_len=${#TOK}"
|
||||
|
||||
echo "=== verify revenue history ==="
|
||||
code=$(curl -sS -m 15 -o /tmp/rev-check.json -w "%{http_code}" \
|
||||
-H "Authorization: Bearer ${TOK}" \
|
||||
"${BANK_URL}/accounts/${BANK_USER}/taler-revenue/history?delta=-3")
|
||||
echo "revenue_history_http=$code"
|
||||
python3 -c 'import json;d=json.load(open("/tmp/rev-check.json"));print("incoming",len(d.get("incoming_transactions")or[]))'
|
||||
[ "$code" = "200" ] || { echo "FAIL revenue history"; exit 1; }
|
||||
|
||||
echo "=== PATCH credit_facade ($FACADE_URL) ==="
|
||||
H_WIRE=$(curl -sk -H "$AUTH" "${MER_URL}/instances/${INST}/private/accounts" \
|
||||
| python3 -c 'import sys,json;print(json.load(sys.stdin)["accounts"][0]["h_wire"])')
|
||||
export FACADE_URL TOK
|
||||
BODY=$(python3 - <<'PY'
|
||||
import json, os
|
||||
print(json.dumps({
|
||||
"credit_facade_url": os.environ["FACADE_URL"],
|
||||
"credit_facade_credentials": {"type": "bearer", "token": os.environ["TOK"]},
|
||||
}))
|
||||
PY
|
||||
)
|
||||
curl -sk -X PATCH -H "$AUTH" -H 'Content-Type: application/json' -d "$BODY" \
|
||||
"${MER_URL}/instances/${INST}/private/accounts/${H_WIRE}" \
|
||||
-w "PATCH_HTTP=%{http_code}\n" -o /dev/null
|
||||
|
||||
echo "=== restart wirewatch in merchant container ==="
|
||||
su - hernani -c 'podman exec taler-hacktivism bash -c "
|
||||
set +e
|
||||
for p in \$(ps -eo pid=,args= | awk \"\\\$0 ~ /\\/usr\\/bin\\/taler-merchant-wirewatch/ {print \\\$1}\"); do
|
||||
kill \$p 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
runuser -u taler-merchant-httpd -- nohup /usr/bin/taler-merchant-wirewatch \
|
||||
-c /etc/taler-merchant/taler-merchant.conf -L INFO \
|
||||
>>/var/log/taler-merchant/wirewatch.log 2>&1 &
|
||||
sleep 2
|
||||
ps -eo pid,etime,args | grep -E \"[t]aler-merchant-wirewatch\" || echo FAIL_no_wirewatch
|
||||
tail -8 /var/log/taler-merchant/wirewatch.log
|
||||
"'
|
||||
|
||||
echo "=== private/transfers (sample) ==="
|
||||
curl -sk -H "$AUTH" "${MER_URL}/instances/${INST}/private/transfers" \
|
||||
| python3 -c 'import sys,json;d=json.load(sys.stdin);t=d.get("transfers")or[];print("transfers",len(t))'
|
||||
|
||||
echo OK_credit_facade
|
||||
133
scripts/taler-merchant/start_base_services_for_taler.sh
Executable file
133
scripts/taler-merchant/start_base_services_for_taler.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
|||
#!/bin/bash
|
||||
# Root: base services for manual merchant (no systemd).
|
||||
# Pattern (all three stacks): root base → shell as service user → start_*.sh
|
||||
#
|
||||
# Container: taler-hacktivism
|
||||
# Then as taler-merchant-httpd: /usr/local/bin/start_merchant.sh [--restart]
|
||||
#
|
||||
# Usage:
|
||||
# /root/start_base_services_for_taler.sh # interactive shell as service user
|
||||
# /root/start_base_services_for_taler.sh --no-shell # base only (automation)
|
||||
|
||||
set -e
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Run as root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_SHELL=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--no-shell|-n) NO_SHELL=1 ;;
|
||||
--help|-h)
|
||||
echo "Usage: $0 [--no-shell]"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PWD_BIN="/usr/local/bin"
|
||||
MERCHANT_STARTER="start_merchant.sh"
|
||||
LOG_DIR=/var/log/taler-merchant
|
||||
RUN_DIR=/var/run/taler-merchant/httpd
|
||||
|
||||
# --- 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
|
||||
# Debian package uses setgid sticky on run dir
|
||||
chmod 2775 /var/run/postgresql 2>/dev/null || chmod 775 /var/run/postgresql
|
||||
|
||||
if pg_isready -q 2>/dev/null; then
|
||||
echo " already accepting connections"
|
||||
pg_isready || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Stale socket lock only when not accepting
|
||||
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 "Start certbot renewal (background)..."
|
||||
if [ -x /root/scripts/certbot_renew.sh ]; then
|
||||
/root/scripts/certbot_renew.sh &
|
||||
elif [ -x ./scripts/certbot_renew.sh ]; then
|
||||
./scripts/certbot_renew.sh &
|
||||
fi
|
||||
|
||||
echo "Create log + runtime dirs... permissions for taler-merchant-httpd / www-data:"
|
||||
mkdir -p "$LOG_DIR" /var/run/taler-merchant "$RUN_DIR"
|
||||
chown taler-merchant-httpd: "$LOG_DIR"
|
||||
chmod 755 "$LOG_DIR"
|
||||
chown taler-merchant-httpd:www-data /var/run/taler-merchant "$RUN_DIR"
|
||||
chmod 755 /var/run/taler-merchant "$RUN_DIR"
|
||||
# merchant app data (package default home)
|
||||
if [ -d /var/lib/taler-merchant ]; then
|
||||
chown -R taler-merchant-httpd:www-data /var/lib/taler-merchant 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Start base services needed for Taler Merchant."
|
||||
echo ""
|
||||
|
||||
if [ -f /root/.taler-secrets-env ]; then
|
||||
echo -n "Read secrets needed for SMS delivery:"
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source /root/.taler-secrets-env && echo " OK"
|
||||
set +a
|
||||
else
|
||||
echo "No /root/.taler-secrets-env (SMS may fail)"
|
||||
fi
|
||||
|
||||
echo "1. postgresql:"
|
||||
ensure_postgresql
|
||||
|
||||
echo "2. nginx:"
|
||||
if [ -x /etc/init.d/nginx ]; then
|
||||
/etc/init.d/nginx start 2>/dev/null || nginx || true
|
||||
else
|
||||
service nginx start 2>/dev/null || nginx || true
|
||||
fi
|
||||
|
||||
if [ "$NO_SHELL" -eq 1 ]; then
|
||||
echo "Base services started (--no-shell). Next: runuser -u taler-merchant-httpd -- $PWD_BIN/$MERCHANT_STARTER [--restart]"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "3. Switching now to user taler-merchant-httpd, in $PWD_BIN; find executable $MERCHANT_STARTER there!"
|
||||
echo ""
|
||||
cd "$PWD_BIN"
|
||||
# util-linux: -u and -s/--shell are mutually exclusive
|
||||
exec runuser -u taler-merchant-httpd -- env \
|
||||
CLICKSEND_API_KEY="${CLICKSEND_API_KEY:-}" \
|
||||
CLICKSEND_USERNAME="${CLICKSEND_USERNAME:-}" \
|
||||
TELESIGN_AUTH_TOKEN="${TELESIGN_AUTH_TOKEN:-}" \
|
||||
bash
|
||||
126
scripts/taler-merchant/start_merchant.sh
Executable file
126
scripts/taler-merchant/start_merchant.sh
Executable file
|
|
@ -0,0 +1,126 @@
|
|||
#!/bin/bash
|
||||
# Start / restart taler-merchant (manual, no systemd).
|
||||
# Run as: taler-merchant-httpd
|
||||
#
|
||||
# Usage:
|
||||
# start_merchant.sh
|
||||
# start_merchant.sh --restart | -r
|
||||
# start_merchant.sh --help
|
||||
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: start_merchant.sh [--restart|-r] [--help|-h]
|
||||
|
||||
(default) Start merchant helpers + httpd.
|
||||
--restart Stop live taler-merchant-* daemons, then start cleanly.
|
||||
Does not touch postgres/nginx.
|
||||
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)" != "taler-merchant-httpd" ]; then
|
||||
echo "This script must be run as user taler-merchant-httpd" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list_merchant_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_merchant.sh*) continue ;;
|
||||
esac
|
||||
case "$args" in
|
||||
*taler-merchant-httpd\ *|taler-merchant-httpd\ *)
|
||||
echo "$pid" ;;
|
||||
*taler-merchant-webhook*|*taler-merchant-kyccheck*|*taler-merchant-wirewatch*)
|
||||
echo "$pid" ;;
|
||||
*taler-merchant-depositcheck*|*taler-merchant-exchangekeyupdate*|*taler-merchant-reconciliation*)
|
||||
echo "$pid" ;;
|
||||
esac
|
||||
done | sort -u
|
||||
}
|
||||
|
||||
kill_taler_merchant() {
|
||||
local pids
|
||||
pids=$(list_merchant_pids | tr '\n' ' ')
|
||||
if [ -z "${pids// }" ]; then
|
||||
echo "No live taler-merchant processes 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_merchant_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 "taler-merchant daemons stopped."
|
||||
}
|
||||
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M")
|
||||
BACKUP_DIR="/var/taler-backups"
|
||||
LOG_DIR="/var/log/taler-merchant"
|
||||
|
||||
if [ "$DO_RESTART" -eq 1 ]; then
|
||||
echo "=== restart: kill taler-merchant-* ==="
|
||||
kill_taler_merchant
|
||||
fi
|
||||
|
||||
BACKUP_NAME="taler-merchant-$TIMESTAMP.sql"
|
||||
echo -n "Backup taler-merchant DB: "
|
||||
mkdir -p "$BACKUP_DIR" 2>/dev/null || true
|
||||
if pg_dump taler-merchant >"$BACKUP_DIR/$BACKUP_NAME" 2>/dev/null; then
|
||||
echo "OK"
|
||||
else
|
||||
echo "SKIP/FAIL (postgres?)"
|
||||
fi
|
||||
echo "Start taler-merchant components:"
|
||||
|
||||
LOG_FILE="$LOG_DIR/taler-merchant-httpd-$(date +%Y-%m-%d).log"
|
||||
mkdir -p "$LOG_DIR"
|
||||
touch "$LOG_FILE"
|
||||
|
||||
# Prefer dedicated ensure script (nohup + logs per helper).
|
||||
if [ -x /usr/local/bin/ensure_merchant_helpers.sh ]; then
|
||||
/usr/local/bin/ensure_merchant_helpers.sh || true
|
||||
elif [ -x "$(dirname "$0")/ensure_merchant_helpers.sh" ]; then
|
||||
"$(dirname "$0")/ensure_merchant_helpers.sh" || true
|
||||
else
|
||||
nohup taler-merchant-httpd --log=info >>"$LOG_FILE" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 2
|
||||
nohup taler-merchant-webhook >>"$LOG_DIR/taler-merchant-webhook.log" 2>&1 </dev/null &
|
||||
nohup taler-merchant-kyccheck >>"$LOG_DIR/taler-merchant-kyccheck.log" 2>&1 </dev/null &
|
||||
nohup taler-merchant-wirewatch -c /etc/taler-merchant/taler-merchant.conf -L INFO \
|
||||
>>"$LOG_DIR/taler-merchant-wirewatch.log" 2>&1 </dev/null &
|
||||
nohup taler-merchant-depositcheck >>"$LOG_DIR/taler-merchant-depositcheck.log" 2>&1 </dev/null &
|
||||
nohup taler-merchant-exchangekeyupdate >>"$LOG_DIR/taler-merchant-exchangekeyupdate.log" 2>&1 </dev/null &
|
||||
nohup taler-merchant-reconciliation >>"$LOG_DIR/taler-merchant-reconciliation.log" 2>&1 </dev/null &
|
||||
disown 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
echo "Live processes:"
|
||||
ps -eo pid,stat,args 2>/dev/null | grep taler-merchant | grep -v grep | grep -v ' Z ' || true
|
||||
|
||||
if [ -x /usr/local/bin/check_merchant-health.sh ]; then
|
||||
/usr/local/bin/check_merchant-health.sh || exit 1
|
||||
elif [ -x ./check_merchant-health.sh ]; then
|
||||
./check_merchant-health.sh || exit 1
|
||||
fi
|
||||
exit 0
|
||||
33
scripts/taler-merchant/stats--merchant-payments.sh
Executable file
33
scripts/taler-merchant/stats--merchant-payments.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
# Tested for DB scheme v38:
|
||||
|
||||
set -eu
|
||||
|
||||
runuser -u taler-merchant-httpd -- bash -c '
|
||||
printf "%-38s | %8s | %12s\n" "merchant_id" "payments" "total_amount"
|
||||
printf "%-38s-+-%-8s-+-%-12s\n" "--------------------------------------" "--------" "------------"
|
||||
|
||||
for s in $(psql -t -A taler-merchant -c "
|
||||
SELECT schemaname FROM pg_tables
|
||||
WHERE schemaname LIKE '\''merchant_instance_%'\''
|
||||
AND tablename = '\''merchant_deposits'\''
|
||||
"); do
|
||||
mid=$(psql -t -A taler-merchant -c "
|
||||
SELECT merchant_id FROM merchant.merchant_instances
|
||||
WHERE merchant_serial = ${s#merchant_instance_}
|
||||
" 2>/dev/null || echo "?")
|
||||
|
||||
psql -t -A taler-merchant -c "
|
||||
SELECT
|
||||
'\''$mid'\'',
|
||||
count(*),
|
||||
round(
|
||||
(COALESCE(sum((amount_with_fee).val), 0) +
|
||||
COALESCE(sum((amount_with_fee).frac), 0)::numeric / 1000000000)
|
||||
, 2)
|
||||
FROM $s.merchant_deposits
|
||||
" 2>/dev/null
|
||||
done | while read line; do
|
||||
printf "%-38s | %8s | %12s\n" $(echo "$line" | tr "|" " ")
|
||||
done
|
||||
'
|
||||
19
scripts/taler-merchant/taler-hacktivism-email-helper.sh
Executable file
19
scripts/taler-merchant/taler-hacktivism-email-helper.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
# Usage: echo "body text" | ./taler-hacktivism-email-helper.sh email@example.com
|
||||
# Needs ``swaks'' to be installed.
|
||||
TO="$1"
|
||||
SUBJECT="Taler Merchant Auth Code"
|
||||
BODY=$(cat)
|
||||
|
||||
# SMTP password: set SMTP_PASSWORD in the environment (not stored in git).
|
||||
# Live container may still use a literal in-file password — do not re-commit it.
|
||||
swaks --server mail.cyon.ch \
|
||||
--port 587 \
|
||||
--auth LOGIN \
|
||||
--auth-user taler-merchant@hacktivism.ch \
|
||||
--auth-password "${SMTP_PASSWORD:?set SMTP_PASSWORD}" \
|
||||
--tls \
|
||||
--from taler-merchant@hacktivism.ch \
|
||||
--to "$TO" \
|
||||
--header "Subject: $SUBJECT" \
|
||||
--body "$BODY"
|
||||
5
scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh
Executable file
5
scripts/taler-merchant/taler-hacktivism-sms-helper-wrapper.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/bin/bash
|
||||
|
||||
PHONE_NO="$1"
|
||||
JSON_STRING="{\"CONTACT_PHONE\":\"${PHONE_NO}\"}"
|
||||
exec /usr/local/bin/taler-hacktivism-sms-helper.sh $JSON_STRING
|
||||
26
scripts/taler-merchant/taler-merchant-wirewatch-supervise.sh
Executable file
26
scripts/taler-merchant/taler-merchant-wirewatch-supervise.sh
Executable file
|
|
@ -0,0 +1,26 @@
|
|||
#!/bin/bash
|
||||
# Run inside merchant container (as root).
|
||||
# Restarts taler-merchant-wirewatch when it exits.
|
||||
# Without systemd, wirewatch exits on PG config NOTIFY and would stay down.
|
||||
set +e
|
||||
LOG=/var/log/taler-merchant/wirewatch-supervise.log
|
||||
WW_LOG=/var/log/taler-merchant/wirewatch.log
|
||||
CONF="${TALER_MERCHANT_CONFIG:-/etc/taler-merchant/taler-merchant.conf}"
|
||||
mkdir -p /var/log/taler-merchant
|
||||
echo "$(date -u +%FT%TZ) supervise start" >>"$LOG"
|
||||
for p in $(ps -eo pid=,args= | awk '$2=="/usr/bin/taler-merchant-wirewatch"{print $1}'); do
|
||||
kill "$p" 2>/dev/null || true
|
||||
done
|
||||
sleep 1
|
||||
while true; do
|
||||
echo "$(date -u +%FT%TZ) start wirewatch" >>"$LOG"
|
||||
if [ "$(id -un)" = "root" ]; then
|
||||
runuser -u taler-merchant-httpd -- /usr/bin/taler-merchant-wirewatch -c "$CONF" -L INFO >>"$WW_LOG" 2>&1
|
||||
ec=$?
|
||||
else
|
||||
/usr/bin/taler-merchant-wirewatch -c "$CONF" -L INFO >>"$WW_LOG" 2>&1
|
||||
ec=$?
|
||||
fi
|
||||
echo "$(date -u +%FT%TZ) wirewatch exit=$ec; sleep 2" >>"$LOG"
|
||||
sleep 2
|
||||
done
|
||||
Loading…
Add table
Add a link
Reference in a new issue