wizard: move all config to /setup; install.sh is infrastructure-only
Unit Tests / test (push) Successful in 15m41s
Unit Tests / test (push) Successful in 15m41s
install.sh no longer prompts for anything. It installs packages (with sudo), creates the system user, clones the repo, and runs 'make install' — all as the invoking user. Only package installs and system-level ops use sudo. All folder creation happens under the user's own account, no chown needed. /setup wizard gains the missing validation that was previously in install.sh: - Step 1: checks pic.ngo name availability via backend (non-blocking) - Step 4: 'Verify token' button for Cloudflare and DuckDNS tokens, validated server-side through new /api/setup/validate steps API changes (routes/setup.py): - validate step 'pic_ngo_available': proxy check to ddns.pic.ngo - validate step 'cloudflare_token': verify via Cloudflare tokens API - validate step 'duckdns_token': verify via DuckDNS update endpoint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+71
-7
@@ -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': '<step_name>', '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
|
||||
|
||||
Reference in New Issue
Block a user