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