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:
+70
-28
@@ -200,43 +200,42 @@ function NavButtons({ onBack, onNext, nextLabel = 'Next', nextDisabled = false,
|
||||
|
||||
function Step1CellName({ value, onChange, onNext }) {
|
||||
const [error, setError] = useState('');
|
||||
const [serverError, setServerError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [availability, setAvailability] = useState(null); // null | 'available' | 'taken' | 'unknown'
|
||||
|
||||
const validate = () => {
|
||||
if (!value.trim()) return 'Cell name is required.';
|
||||
if (!CELL_NAME_RE.test(value))
|
||||
return 'Use lowercase letters, numbers, and hyphens only. Must start with a letter. 2–31 characters.';
|
||||
return '';
|
||||
const isValid = CELL_NAME_RE.test(value);
|
||||
|
||||
const handleChange = v => {
|
||||
onChange(v.toLowerCase());
|
||||
setError('');
|
||||
setAvailability(null);
|
||||
};
|
||||
|
||||
const handleNext = async () => {
|
||||
const err = validate();
|
||||
setError(err);
|
||||
setServerError('');
|
||||
if (err) return;
|
||||
if (!value.trim()) { setError('Cell name is required.'); return; }
|
||||
if (!isValid) { setError('Use lowercase letters, numbers, and hyphens only. Must start with a letter. 2–31 characters.'); return; }
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
await setupAPI.validate('cell_name', { cell_name: value });
|
||||
const res = await setupAPI.validate('pic_ngo_available', { cell_name: value });
|
||||
const avail = res.data?.available;
|
||||
setAvailability(avail ? 'available' : 'taken');
|
||||
onNext();
|
||||
} catch (e) {
|
||||
setServerError(
|
||||
e?.response?.data?.error || 'Validation failed. Please try a different name.'
|
||||
);
|
||||
setAvailability('unknown');
|
||||
onNext(); // don't block — availability check is informational
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isValid = CELL_NAME_RE.test(value);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StepHeader
|
||||
step={1}
|
||||
title="Name your cell"
|
||||
description="This becomes your cell's identity and subdomain. If you choose pic.ngo it will be reachable at name.pic.ngo."
|
||||
description="This becomes your cell's identity. If you choose pic.ngo it will be reachable at name.pic.ngo."
|
||||
/>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-1.5" htmlFor="cell-name">
|
||||
@@ -248,29 +247,25 @@ function Step1CellName({ value, onChange, onNext }) {
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={value}
|
||||
onChange={e => {
|
||||
onChange(e.target.value.toLowerCase());
|
||||
setError('');
|
||||
setServerError('');
|
||||
}}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleNext()}
|
||||
placeholder="e.g. myhome"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500 placeholder-gray-600"
|
||||
aria-describedby={error || serverError ? 'cell-name-error' : undefined}
|
||||
/>
|
||||
{isValid ? (
|
||||
{isValid && (
|
||||
<p className="mt-1.5 text-xs text-blue-400 flex items-center gap-1">
|
||||
<Globe className="h-3.5 w-3.5" />
|
||||
pic.ngo preview: <span className="font-mono font-medium ml-1">{value}.pic.ngo</span>
|
||||
{availability === 'available' && <span className="text-green-400 ml-1">— available</span>}
|
||||
{availability === 'taken' && <span className="text-yellow-400 ml-1">— may already be taken</span>}
|
||||
</p>
|
||||
) : (
|
||||
)}
|
||||
{!isValid && value && (
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Lowercase letters, numbers, hyphens. Must start with a letter. 2–31 characters.
|
||||
</p>
|
||||
)}
|
||||
<div id="cell-name-error">
|
||||
<FieldError message={error || serverError} />
|
||||
</div>
|
||||
<FieldError message={error} />
|
||||
</div>
|
||||
<NavButtons onNext={handleNext} loading={loading} nextDisabled={!value.trim()} />
|
||||
</div>
|
||||
@@ -410,6 +405,26 @@ function Step3Domain({ value, onChange, onNext, onBack }) {
|
||||
);
|
||||
}
|
||||
|
||||
function TokenVerifyButton({ onVerify, status }) {
|
||||
const label = status === 'checking' ? 'Verifying…'
|
||||
: status === 'valid' ? '✓ Valid'
|
||||
: status === 'invalid' ? 'Invalid — retry'
|
||||
: 'Verify token';
|
||||
const cls = status === 'valid' ? 'text-green-400 border-green-600'
|
||||
: status === 'invalid' ? 'text-red-400 border-red-600'
|
||||
: 'text-blue-400 border-blue-600 hover:bg-blue-900/20';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onVerify}
|
||||
disabled={status === 'checking'}
|
||||
className={`mt-2 px-3 py-1 text-xs border rounded transition-colors ${cls}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Step4DomainConfig({
|
||||
domainType, cellName,
|
||||
customDomain, onCustomDomain,
|
||||
@@ -419,6 +434,25 @@ function Step4DomainConfig({
|
||||
onNext, onBack,
|
||||
}) {
|
||||
const [errors, setErrors] = useState({});
|
||||
const [cfStatus, setCfStatus] = useState(null); // null|checking|valid|invalid
|
||||
const [dnsStatus, setDnsStatus] = useState(null);
|
||||
|
||||
const verifyCf = async () => {
|
||||
setCfStatus('checking');
|
||||
try {
|
||||
const res = await setupAPI.validate('cloudflare_token', { token: cloudflareToken });
|
||||
setCfStatus(res.data?.valid ? 'valid' : 'invalid');
|
||||
} catch { setCfStatus('invalid'); }
|
||||
};
|
||||
|
||||
const verifyDns = async () => {
|
||||
const sub = customDomain.replace(/\.duckdns\.org$/i, '') || customDomain.split('.')[0];
|
||||
setDnsStatus('checking');
|
||||
try {
|
||||
const res = await setupAPI.validate('duckdns_token', { subdomain: sub, token: duckdnsToken });
|
||||
setDnsStatus(res.data?.valid ? 'valid' : 'invalid');
|
||||
} catch { setDnsStatus('invalid'); }
|
||||
};
|
||||
|
||||
// ── pic_ngo: just show the derived domain ────────────────────────────────
|
||||
if (domainType === 'pic_ngo') {
|
||||
@@ -538,6 +572,7 @@ function Step4DomainConfig({
|
||||
onChange={e => {
|
||||
onCloudflareToken(e.target.value);
|
||||
setErrors(p => ({ ...p, token: '' }));
|
||||
setCfStatus(null);
|
||||
}}
|
||||
placeholder="Cloudflare API token with DNS:Edit permission"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500 placeholder-gray-600"
|
||||
@@ -545,6 +580,9 @@ function Step4DomainConfig({
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Create at Cloudflare Dashboard → My Profile → API Tokens. Needs Zone / DNS / Edit.
|
||||
</p>
|
||||
{cloudflareToken.trim() && (
|
||||
<TokenVerifyButton onVerify={verifyCf} status={cfStatus} />
|
||||
)}
|
||||
<FieldError message={errors.token} />
|
||||
</div>
|
||||
)}
|
||||
@@ -562,6 +600,7 @@ function Step4DomainConfig({
|
||||
onChange={e => {
|
||||
onDuckdnsToken(e.target.value);
|
||||
setErrors(p => ({ ...p, token: '' }));
|
||||
setDnsStatus(null);
|
||||
}}
|
||||
placeholder="Your DuckDNS account token"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-2 text-white text-sm focus:outline-none focus:border-blue-500 placeholder-gray-600"
|
||||
@@ -569,6 +608,9 @@ function Step4DomainConfig({
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Found at duckdns.org after login. The subdomain must already exist in your account.
|
||||
</p>
|
||||
{duckdnsToken.trim() && (
|
||||
<TokenVerifyButton onVerify={verifyDns} status={dnsStatus} />
|
||||
)}
|
||||
<FieldError message={errors.token} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user