diff --git a/api/setup_manager.py b/api/setup_manager.py index fed16f7..788fd28 100644 --- a/api/setup_manager.py +++ b/api/setup_manager.py @@ -128,9 +128,12 @@ class SetupManager: cell_name = payload.get('cell_name', '') password = payload.get('password', '') domain_mode = payload.get('domain_mode', '') + domain_name = payload.get('domain_name', '') timezone = payload.get('timezone', '') services_enabled = payload.get('services_enabled', []) ddns_provider = payload.get('ddns_provider', 'none') + cloudflare_api_token = payload.get('cloudflare_api_token', '') + duckdns_token = payload.get('duckdns_token', '') errors.extend(self.validate_cell_name(cell_name)) errors.extend(self.validate_password(password)) @@ -185,16 +188,19 @@ class SetupManager: # ── persist identity fields ──────────────────────────────────── self.config_manager.set_identity_field('cell_name', cell_name) self.config_manager.set_identity_field('domain_mode', domain_mode) + if domain_name: + self.config_manager.set_identity_field('domain_name', domain_name) self.config_manager.set_identity_field('timezone', timezone) self.config_manager.set_identity_field('services_enabled', services_enabled) self.config_manager.set_identity_field('ddns_provider', ddns_provider) + if cloudflare_api_token: + self.config_manager.set_identity_field('cloudflare_api_token', cloudflare_api_token) + if duckdns_token: + self.config_manager.set_identity_field('duckdns_token', duckdns_token) - # NOTE: DDNS registration is deferred to Phase 3. - # For now we just store ddns_provider in config. logger.info( - 'DDNS registration skipped (Phase 1). ' - 'DDNS registration will happen in Phase 3. ' - f'ddns_provider={ddns_provider!r} stored in identity config.' + 'DDNS registration deferred to Phase 3. ' + f'ddns_provider={ddns_provider!r} domain_name={domain_name!r}' ) # ── mark setup complete (must be last) ───────────────────────── diff --git a/webui/src/pages/Setup.jsx b/webui/src/pages/Setup.jsx index da867b5..f3d792d 100644 --- a/webui/src/pages/Setup.jsx +++ b/webui/src/pages/Setup.jsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Eye, EyeOff, CheckCircle, AlertCircle } from 'lucide-react'; +import { Eye, EyeOff, CheckCircle, AlertCircle, Globe } from 'lucide-react'; import { setupAPI } from '../services/api'; // ── constants ───────────────────────────────────────────────────────────────── @@ -13,27 +13,36 @@ const DOMAIN_OPTIONS = [ { value: 'pic_ngo', label: 'PIC.NGO subdomain', - description: 'Get a free yourname.pic.ngo domain — managed automatically.', + description: 'Get a free yourname.pic.ngo address — HTTPS and DDNS managed automatically.', }, { value: 'custom', label: 'Custom domain', - description: 'Bring your own domain. You will configure DNS records manually.', + description: 'Use your own domain with Cloudflare, DuckDNS, or standard HTTP challenge.', }, { - value: 'lan_only', + value: 'lan', label: 'LAN only', description: 'No public domain. Accessible only on your local network and via VPN.', }, ]; -const DDNS_OPTIONS = [ - { value: 'pic_ngo', label: 'pic.ngo (managed)', description: 'Automatic — no setup required.' }, - { value: 'cloudflare', label: 'Cloudflare', description: 'Use Cloudflare DNS with API token.' }, - { value: 'duckdns', label: 'DuckDNS', description: 'Free dynamic DNS via duckdns.org.' }, - { value: 'noip', label: 'No-IP', description: 'Free dynamic DNS via noip.com.' }, - { value: 'freedns', label: 'FreeDNS', description: 'Free DNS via freedns.afraid.org.' }, - { value: 'manual', label: 'Manual / None', description: 'You will handle DNS updates yourself.' }, +const CUSTOM_METHOD_OPTIONS = [ + { + value: 'cloudflare', + label: 'Cloudflare DNS', + description: 'DNS-01 via Cloudflare API. Your domain must use Cloudflare nameservers.', + }, + { + value: 'duckdns', + label: 'DuckDNS', + description: 'Free subdomain via duckdns.org with automatic DNS-01 challenge.', + }, + { + value: 'http01', + label: 'HTTP-01 (any registrar)', + description: 'Standard ACME challenge. Port 80 must be publicly reachable.', + }, ]; const OPTIONAL_SERVICES = [ @@ -55,35 +64,37 @@ function getAllTimezones() { try { return Intl.supportedValuesOf('timeZone'); } catch { - // Fallback list for older browsers return [ - 'UTC', - 'America/New_York', - 'America/Chicago', - 'America/Denver', - 'America/Los_Angeles', - 'Europe/London', - 'Europe/Paris', - 'Europe/Berlin', - 'Asia/Tokyo', - 'Asia/Shanghai', - 'Australia/Sydney', + 'UTC', 'America/New_York', 'America/Chicago', 'America/Denver', + 'America/Los_Angeles', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', + 'Asia/Tokyo', 'Asia/Shanghai', 'Australia/Sydney', ]; } } function passwordStrength(pw) { - if (!pw) return { label: '', color: '', width: '0%' }; + if (!pw) return { label: '', color: '', width: '0%', score: 0 }; let score = 0; if (pw.length >= 12) score++; if (pw.length >= 16) score++; if (/[A-Z]/.test(pw)) score++; + if (/[a-z]/.test(pw)) score++; if (/[0-9]/.test(pw)) score++; if (/[^A-Za-z0-9]/.test(pw)) score++; - if (score <= 1) return { label: 'Weak', color: 'bg-red-500', width: '20%' }; - if (score === 2) return { label: 'Fair', color: 'bg-yellow-500', width: '40%' }; - if (score === 3) return { label: 'Good', color: 'bg-blue-500', width: '65%' }; - return { label: 'Strong', color: 'bg-green-500', width: '100%' }; + if (score <= 2) return { label: 'Weak', color: 'bg-red-500', width: '20%', score }; + if (score === 3) return { label: 'Fair', color: 'bg-yellow-500', width: '45%', score }; + if (score === 4) return { label: 'Good', color: 'bg-blue-500', width: '70%', score }; + return { label: 'Strong', color: 'bg-green-500', width: '100%', score }; +} + +function meetsApiRequirements(pw) { + return pw.length >= 12 && /[A-Z]/.test(pw) && /[a-z]/.test(pw) && /[0-9]/.test(pw); +} + +function getDomainMode(domainType, customMethod) { + if (domainType === 'pic_ngo') return 'pic_ngo'; + if (domainType === 'lan') return 'lan'; + return customMethod || 'http01'; } // ── sub-components ──────────────────────────────────────────────────────────── @@ -218,12 +229,14 @@ function Step1CellName({ value, onChange, onNext }) { } }; + const isValid = CELL_NAME_RE.test(value); + return (