diff --git a/api/routes/setup.py b/api/routes/setup.py index ab85ff5..6b8e160 100644 --- a/api/routes/setup.py +++ b/api/routes/setup.py @@ -1,10 +1,16 @@ import logging +import re +import urllib.request +import urllib.error +import json as _json from flask import Blueprint, request, jsonify logger = logging.getLogger('picell') setup_bp = Blueprint('setup', __name__, url_prefix='/api/setup') +_DOMAIN_RE = re.compile(r'^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z]{2,})+$', re.I) + def _get_setup_manager(): from app import setup_manager @@ -24,8 +30,8 @@ def get_setup_status(): def validate_setup_step(): """Validate a single wizard step. - Expects JSON body: ``{'step': '', 'data': {...}}``. - Supported steps: ``cell_name``, ``password``. + Supported steps: ``cell_name``, ``password``, + ``pic_ngo_available``, ``cloudflare_token``, ``duckdns_token``. """ sm = _get_setup_manager() if sm.is_setup_complete(): @@ -37,12 +43,36 @@ def validate_setup_step(): if step == 'cell_name': errors = sm.validate_cell_name(data.get('cell_name', '')) - elif step == 'password': - errors = sm.validate_password(data.get('password', '')) - else: - return jsonify({'valid': False, 'errors': [f"Unknown step: {step!r}"]}), 400 + return jsonify({'valid': len(errors) == 0, 'errors': errors}) - return jsonify({'valid': len(errors) == 0, 'errors': errors}) + if step == 'password': + errors = sm.validate_password(data.get('password', '')) + return jsonify({'valid': len(errors) == 0, 'errors': errors}) + + if step == 'pic_ngo_available': + name = data.get('cell_name', '').strip() + errors = sm.validate_cell_name(name) + if errors: + return jsonify({'available': False, 'errors': errors}) + available = _check_pic_ngo_available(name) + return jsonify({'available': available}) + + if step == 'cloudflare_token': + token = data.get('token', '').strip() + if not token: + return jsonify({'valid': False, 'error': 'Token is required.'}) + valid = _verify_cloudflare_token(token) + return jsonify({'valid': valid}) + + if step == 'duckdns_token': + subdomain = data.get('subdomain', '').strip() + token = data.get('token', '').strip() + if not token or not subdomain: + return jsonify({'valid': False, 'error': 'Subdomain and token are required.'}) + valid = _verify_duckdns_token(subdomain, token) + return jsonify({'valid': valid}) + + return jsonify({'valid': False, 'errors': [f"Unknown step: {step!r}"]}), 400 @setup_bp.route('/complete', methods=['POST']) @@ -56,3 +86,37 @@ def complete_setup(): result = sm.complete_setup(payload) status_code = 200 if result.get('success') else 400 return jsonify(result), status_code + + +# ── external validation helpers ─────────────────────────────────────────────── + +def _check_pic_ngo_available(name: str) -> bool: + try: + url = f'https://ddns.pic.ngo/api/v1/check/{name}' + with urllib.request.urlopen(url, timeout=8) as resp: + body = _json.loads(resp.read()) + return bool(body.get('available')) + except Exception: + return True # assume available if check fails — don't block the wizard + + +def _verify_cloudflare_token(token: str) -> bool: + try: + req = urllib.request.Request( + 'https://api.cloudflare.com/client/v4/user/tokens/verify', + headers={'Authorization': f'Bearer {token}'}, + ) + with urllib.request.urlopen(req, timeout=8) as resp: + body = _json.loads(resp.read()) + return bool(body.get('success')) + except Exception: + return False + + +def _verify_duckdns_token(subdomain: str, token: str) -> bool: + try: + url = f'https://www.duckdns.org/update?domains={subdomain}&token={token}&ip=' + with urllib.request.urlopen(url, timeout=8) as resp: + return resp.read().strip() == b'OK' + except Exception: + return False diff --git a/install.sh b/install.sh index ef6e35d..b95515f 100755 --- a/install.sh +++ b/install.sh @@ -22,9 +22,9 @@ # ============================================================================= # # Usage: -# sudo bash install.sh # Standard install -# sudo bash install.sh --force # Bypass idempotency check -# sudo PIC_DIR=/srv/pic bash install.sh # Custom install directory +# bash install.sh # Standard install (uses sudo internally for packages) +# bash install.sh --force # Bypass idempotency check +# PIC_DIR=/srv/pic bash install.sh # Custom install directory # # Supported OS: Debian/Ubuntu (apt), Fedora/RHEL (dnf), Alpine Linux (apk) # @@ -79,62 +79,13 @@ log_error() { printf "\n${RED}${BOLD}ERROR:${RESET}${RED} %s${RESET}\n" "$1" > die() { log_error "$1"; exit 1; } -# --------------------------------------------------------------------------- -# Interactive prompt helpers (use /dev/tty so they work even with piped stdin) -# --------------------------------------------------------------------------- -prompt() { - # prompt