From 2d842abe5bf87c017fad555376609e38264613e3 Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 25 May 2026 15:01:32 -0400 Subject: [PATCH] installer: restore cell identity prompts and domain setup Reverts 8d1ef39. The installer must collect cell name, domain mode, and provider tokens before 'make install' so that DDNS registration, availability checks, and Caddy TLS can be configured at first boot. Co-Authored-By: Claude Sonnet 4.6 --- api/config_manager.py | 5 + api/routes/setup.py | 7 -- api/setup_manager.py | 52 +++++++- install.sh | 243 ++++++++++++++++++++++++++++++++++-- tests/test_setup_manager.py | 57 +++++++++ 5 files changed, 345 insertions(+), 19 deletions(-) diff --git a/api/config_manager.py b/api/config_manager.py index 4473165..20582b7 100644 --- a/api/config_manager.py +++ b/api/config_manager.py @@ -510,6 +510,11 @@ class ConfigManager: cfg.setdefault('peer_exit_map', {}) return dict(cfg) + def set_ddns_config(self, ddns_cfg: Dict[str, Any]) -> None: + """Replace the top-level ddns section and persist.""" + self.configs['ddns'] = ddns_cfg + self._save_all_configs() + def set_connectivity_field(self, field: str, value: Any) -> bool: """Set a single field within the connectivity config and persist.""" cfg = self.configs.setdefault('connectivity', {'exits': {}, 'peer_exit_map': {}}) diff --git a/api/routes/setup.py b/api/routes/setup.py index b49baed..ab85ff5 100644 --- a/api/routes/setup.py +++ b/api/routes/setup.py @@ -55,11 +55,4 @@ def complete_setup(): payload = request.get_json(silent=True) or {} result = sm.complete_setup(payload) status_code = 200 if result.get('success') else 400 - - # TODO (Phase 3): if result.get('success') and domain_mode == 'pic_ngo': - # from app import ddns_manager - # name = payload.get('cell_name', '') - # ip = payload.get('public_ip', '') - # ddns_manager.register(name, ip) - return jsonify(result), status_code diff --git a/api/setup_manager.py b/api/setup_manager.py index e7adc23..e3f54d9 100644 --- a/api/setup_manager.py +++ b/api/setup_manager.py @@ -60,6 +60,37 @@ VALID_DOMAIN_MODES = {'pic_ngo', 'cloudflare', 'duckdns', 'http01', 'lan'} CELL_NAME_RE = re.compile(r'^[a-z][a-z0-9-]{1,30}$') +DDNS_API_BASE = os.environ.get('DDNS_URL', 'https://ddns.pic.ngo/api/v1').replace('/api/v1', '') +DDNS_TOTP_SECRET = os.environ.get('DDNS_TOTP_SECRET', '') + + +def _build_ddns_config(domain_mode: str, cloudflare_api_token: str = '', + duckdns_token: str = '', duckdns_subdomain: str = '') -> dict: + """Return the top-level ddns config dict for a given domain mode.""" + if domain_mode == 'pic_ngo': + return { + 'provider': 'pic_ngo', + 'api_base_url': DDNS_API_BASE, + 'totp_secret': DDNS_TOTP_SECRET, + 'enabled': True, + } + if domain_mode == 'cloudflare': + cfg = {'provider': 'cloudflare', 'enabled': True} + if cloudflare_api_token: + cfg['api_token'] = cloudflare_api_token + return cfg + if domain_mode == 'duckdns': + cfg = {'provider': 'duckdns', 'enabled': True} + if duckdns_token: + cfg['token'] = duckdns_token + if duckdns_subdomain: + cfg['subdomain'] = duckdns_subdomain + return cfg + if domain_mode == 'http01': + return {'provider': 'http01', 'enabled': True} + return {'provider': 'none', 'enabled': False} + + class SetupManager: """Manages the first-run setup wizard state and completion.""" @@ -209,10 +240,25 @@ class SetupManager: if duckdns_token: self.config_manager.set_identity_field('duckdns_token', duckdns_token) - logger.info( - 'DDNS registration deferred to Phase 3. ' - f'ddns_provider={ddns_provider!r} domain_name={domain_name!r}' + # ── write top-level ddns section so DDNSManager can find provider ── + duckdns_sub = domain_name.replace('.duckdns.org', '') if domain_mode == 'duckdns' else '' + ddns_cfg = _build_ddns_config( + domain_mode, + cloudflare_api_token=cloudflare_api_token, + duckdns_token=duckdns_token, + duckdns_subdomain=duckdns_sub, ) + self.config_manager.set_ddns_config(ddns_cfg) + + # ── trigger DDNS registration for pic_ngo ───────────────────────── + if domain_mode == 'pic_ngo': + try: + from ddns_manager import DDNSManager + ddns_mgr = DDNSManager(self.config_manager) + ddns_mgr.register(cell_name, '') + logger.info(f'DDNS registered: {cell_name}.pic.ngo') + except Exception as exc: + logger.warning(f'DDNS registration failed (will retry at next heartbeat): {exc}') # ── mark setup complete (must be last) ───────────────────────── self.config_manager.set_identity_field('setup_complete', True) diff --git a/install.sh b/install.sh index 74aaa18..ef6e35d 100755 --- a/install.sh +++ b/install.sh @@ -79,7 +79,56 @@ log_error() { printf "\n${RED}${BOLD}ERROR:${RESET}${RED} %s${RESET}\n" "$1" > die() { log_error "$1"; exit 1; } -TOTAL_STEPS=7 +# --------------------------------------------------------------------------- +# Interactive prompt helpers (use /dev/tty so they work even with piped stdin) +# --------------------------------------------------------------------------- +prompt() { + # prompt