fix: Improve DNS verification for portfolio domains
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
Some checks failed
CI / Frontend Lint & Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Backend Lint (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
Deploy / Build & Push Images (push) Has been cancelled
Deploy / Deploy to Server (push) Has been cancelled
Deploy / Notify (push) Has been cancelled
- Check TXT records at ROOT domain instead of _pounce subdomain - Much simpler for users - just add TXT record with verification code - Update frontend verification modal with clearer instructions - Add option to verify immediately when adding domain to portfolio
This commit is contained in:
@ -780,9 +780,9 @@ async def start_dns_verification(
|
||||
domain=domain.domain,
|
||||
verification_code=domain.verification_code,
|
||||
dns_record_type="TXT",
|
||||
dns_record_name=f"_pounce.{domain.domain}",
|
||||
dns_record_name="@",
|
||||
dns_record_value=domain.verification_code,
|
||||
instructions=f"Add a TXT record to your DNS settings:\n\nHost/Name: _pounce\nType: TXT\nValue: {domain.verification_code}\n\nDNS changes can take up to 48 hours to propagate, but usually complete within minutes.",
|
||||
instructions=f"Add a TXT record to your DNS settings:\n\nHost/Name: @ (or leave empty)\nType: TXT\nValue: {domain.verification_code}\n\nDNS changes usually propagate within 5 minutes.",
|
||||
status=domain.verification_status,
|
||||
)
|
||||
|
||||
@ -796,7 +796,7 @@ async def check_dns_verification(
|
||||
"""
|
||||
Check if DNS verification is complete.
|
||||
|
||||
Looks for the TXT record and verifies it matches the expected code.
|
||||
Looks for the TXT record at root domain and verifies it contains the expected code.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(PortfolioDomain).where(
|
||||
@ -827,8 +827,7 @@ async def check_dns_verification(
|
||||
detail="Verification not started. Call POST /verify-dns first.",
|
||||
)
|
||||
|
||||
# Check DNS TXT record
|
||||
txt_record_name = f"_pounce.{domain.domain}"
|
||||
# Check DNS TXT record at ROOT domain (simpler for users)
|
||||
verified = False
|
||||
|
||||
try:
|
||||
@ -836,24 +835,26 @@ async def check_dns_verification(
|
||||
resolver.timeout = 5
|
||||
resolver.lifetime = 10
|
||||
|
||||
answers = resolver.resolve(txt_record_name, 'TXT')
|
||||
# Check ROOT domain TXT records
|
||||
answers = resolver.resolve(domain.domain, 'TXT')
|
||||
|
||||
for rdata in answers:
|
||||
txt_value = rdata.to_text().strip('"')
|
||||
if txt_value == domain.verification_code:
|
||||
# Check if verification code is present anywhere in TXT records
|
||||
if domain.verification_code in txt_value:
|
||||
verified = True
|
||||
break
|
||||
except dns.resolver.NXDOMAIN:
|
||||
return DNSVerificationCheckResponse(
|
||||
verified=False,
|
||||
status="pending",
|
||||
message=f"TXT record not found. Please add a TXT record at _pounce.{domain.domain}",
|
||||
message=f"Domain {domain.domain} not found in DNS. Check your domain configuration.",
|
||||
)
|
||||
except dns.resolver.NoAnswer:
|
||||
return DNSVerificationCheckResponse(
|
||||
verified=False,
|
||||
status="pending",
|
||||
message="TXT record exists but has no value. Check your DNS configuration.",
|
||||
message=f"No TXT records found for {domain.domain}. Please add the TXT record.",
|
||||
)
|
||||
except dns.resolver.Timeout:
|
||||
return DNSVerificationCheckResponse(
|
||||
@ -883,6 +884,6 @@ async def check_dns_verification(
|
||||
return DNSVerificationCheckResponse(
|
||||
verified=False,
|
||||
status="pending",
|
||||
message=f"TXT record found but value doesn't match. Expected: {domain.verification_code}",
|
||||
message=f"TXT record found but verification code not detected. Make sure your TXT record contains: {domain.verification_code}",
|
||||
)
|
||||
|
||||
|
||||
@ -397,13 +397,16 @@ function EditModal({
|
||||
|
||||
function AddModal({
|
||||
onClose,
|
||||
onAdd
|
||||
onAdd,
|
||||
onStartVerify
|
||||
}: {
|
||||
onClose: () => void
|
||||
onAdd: (domain: string, data?: Partial<EditFormData>) => Promise<void>
|
||||
onAdd: (domain: string, data?: Partial<EditFormData>) => Promise<PortfolioDomain | void>
|
||||
onStartVerify?: (domain: PortfolioDomain) => void
|
||||
}) {
|
||||
const [domain, setDomain] = useState('')
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [wantToVerify, setWantToVerify] = useState(false)
|
||||
const [form, setForm] = useState<Partial<EditFormData>>({})
|
||||
const [adding, setAdding] = useState(false)
|
||||
|
||||
@ -412,8 +415,14 @@ function AddModal({
|
||||
if (!domain.trim()) return
|
||||
setAdding(true)
|
||||
try {
|
||||
await onAdd(domain.trim(), showDetails ? form : undefined)
|
||||
onClose()
|
||||
const created = await onAdd(domain.trim(), showDetails ? form : undefined) as PortfolioDomain | undefined
|
||||
if (wantToVerify && created && onStartVerify) {
|
||||
onClose()
|
||||
// Small delay to let the modal close before opening verify modal
|
||||
setTimeout(() => onStartVerify(created), 100)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
} finally {
|
||||
setAdding(false)
|
||||
}
|
||||
@ -493,13 +502,34 @@ function AddModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Verify Option */}
|
||||
<div className="pt-2 border-t border-white/10">
|
||||
<label className="flex items-center gap-3 cursor-pointer group">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={wantToVerify}
|
||||
onChange={e => setWantToVerify(e.target.checked)}
|
||||
className="w-4 h-4 accent-accent"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-mono text-white/70 group-hover:text-white">
|
||||
Verify DNS ownership
|
||||
</div>
|
||||
<div className="text-[10px] font-mono text-white/30">
|
||||
Required for Yield and For Sale features
|
||||
</div>
|
||||
</div>
|
||||
<ShieldCheck className="w-4 h-4 text-white/20 group-hover:text-accent" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={adding || !domain.trim()}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{adding ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
|
||||
Add to Portfolio
|
||||
{wantToVerify ? 'Add & Verify' : 'Add to Portfolio'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@ -520,24 +550,32 @@ function VerifyModal({
|
||||
onClose: () => void
|
||||
onVerified: () => void
|
||||
}) {
|
||||
const [step, setStep] = useState<'start' | 'pending' | 'checking'>('start')
|
||||
const [verificationCode, setVerificationCode] = useState<string | null>(null)
|
||||
const [verificationCode, setVerificationCode] = useState<string | null>(domain.verification_code || null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(!domain.verification_code)
|
||||
const [checking, setChecking] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!verificationCode) {
|
||||
startVerification()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startVerification = async () => {
|
||||
setStep('pending')
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await api.startPortfolioDnsVerification(domain.id)
|
||||
setVerificationCode(result.verification_code)
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Failed to start verification')
|
||||
setStep('start')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const checkVerification = async () => {
|
||||
setStep('checking')
|
||||
setChecking(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await api.checkPortfolioDnsVerification(domain.id)
|
||||
@ -545,18 +583,18 @@ function VerifyModal({
|
||||
onVerified()
|
||||
onClose()
|
||||
} else {
|
||||
setError('DNS record not found. Please wait a few minutes for DNS to propagate and try again.')
|
||||
setStep('pending')
|
||||
setError(result.message || 'DNS record not found yet. Please wait a few minutes and try again.')
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Verification failed')
|
||||
setStep('pending')
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
const copyCode = () => {
|
||||
if (!verificationCode) return
|
||||
navigator.clipboard.writeText(`pounce-verify=${verificationCode}`)
|
||||
navigator.clipboard.writeText(verificationCode)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
@ -564,80 +602,117 @@ function VerifyModal({
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm" onClick={onClose}>
|
||||
<div
|
||||
className="w-full max-w-md bg-[#0a0a0a] border border-white/10"
|
||||
className="w-full max-w-lg bg-[#0a0a0a] border border-white/10"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="w-4 h-4 text-accent" />
|
||||
<span className="text-sm font-mono text-white uppercase tracking-wider">Verify Ownership</span>
|
||||
<div className="flex items-center justify-between p-5 border-b border-white/10">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ShieldCheck className="w-4 h-4 text-accent" />
|
||||
<span className="text-[10px] font-mono text-accent uppercase tracking-wider">Verify Ownership</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold text-white font-mono">{domain.domain}</h2>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 text-white/40 hover:text-white">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="text-lg font-bold text-white font-mono">{domain.domain}</div>
|
||||
|
||||
{step === 'start' && (
|
||||
<div className="p-5 space-y-5">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-10">
|
||||
<Loader2 className="w-6 h-6 text-accent animate-spin" />
|
||||
</div>
|
||||
) : verificationCode ? (
|
||||
<>
|
||||
<p className="text-sm text-white/60 font-mono">
|
||||
Verify ownership to unlock Yield and For Sale features. You'll need to add a DNS TXT record.
|
||||
</p>
|
||||
{/* Step 1 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<span className="w-6 h-6 bg-accent text-black text-xs font-bold flex items-center justify-center">1</span>
|
||||
Add TXT Record at your Registrar
|
||||
</h3>
|
||||
|
||||
<div className="p-4 bg-[#020202] border border-white/[0.08] space-y-3">
|
||||
<p className="text-sm text-white/70">
|
||||
Go to your registrar's DNS settings and add this TXT record:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="p-3 bg-white/5 border border-white/10 text-center">
|
||||
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Type</div>
|
||||
<div className="text-base font-bold text-white font-mono">TXT</div>
|
||||
</div>
|
||||
<div className="p-3 bg-white/5 border border-white/10 text-center">
|
||||
<div className="text-[9px] font-mono text-white/40 uppercase mb-1">Name / Host</div>
|
||||
<div className="text-base font-bold text-white font-mono">@</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-accent/10 border border-accent/20">
|
||||
<div className="text-[9px] font-mono text-accent/60 uppercase mb-1">Value (copy this)</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 text-sm font-mono text-accent break-all">
|
||||
{verificationCode}
|
||||
</code>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="p-2 border border-accent/20 text-accent hover:bg-accent/10 transition-colors shrink-0"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] text-white/30">
|
||||
<strong className="text-white/50">Tip:</strong> The "@" symbol means root domain. Some registrars use "empty" or the domain name itself.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step 2 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<span className="w-6 h-6 bg-white/10 text-white/60 text-xs font-bold flex items-center justify-center">2</span>
|
||||
Verify
|
||||
</h3>
|
||||
|
||||
<p className="text-xs text-white/50">
|
||||
After adding the TXT record, wait 2-5 minutes for DNS propagation, then click verify.
|
||||
</p>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-amber-500/10 border border-amber-500/20 text-amber-400 text-xs font-mono flex items-start gap-2">
|
||||
<Clock className="w-4 h-4 shrink-0 mt-0.5" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={checkVerification}
|
||||
disabled={checking}
|
||||
className="w-full flex items-center justify-center gap-2 py-3.5 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{checking ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
)}
|
||||
Verify DNS Record
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<AlertTriangle className="w-10 h-10 text-amber-400/50 mx-auto mb-3" />
|
||||
<p className="text-sm text-white/50">{error || 'Failed to generate verification code'}</p>
|
||||
<button
|
||||
onClick={startVerification}
|
||||
className="w-full py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors"
|
||||
className="mt-4 px-4 py-2 bg-white/10 text-white text-xs font-bold uppercase"
|
||||
>
|
||||
Start Verification
|
||||
Try Again
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(step === 'pending' || step === 'checking') && verificationCode && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<p className="text-[10px] font-mono text-white/40 uppercase">Add this TXT record to your DNS:</p>
|
||||
<div className="flex items-center gap-2 p-3 bg-white/5 border border-white/10">
|
||||
<code className="flex-1 text-sm font-mono text-accent break-all">
|
||||
pounce-verify={verificationCode}
|
||||
</code>
|
||||
<button
|
||||
onClick={copyCode}
|
||||
className="p-2 text-white/40 hover:text-white transition-colors shrink-0"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-accent" /> : <Copy className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-mono text-white/40 space-y-1">
|
||||
<p>1. Log in to your domain registrar</p>
|
||||
<p>2. Find DNS settings for {domain.domain}</p>
|
||||
<p>3. Add a new TXT record with the value above</p>
|
||||
<p>4. Wait 2-5 minutes for propagation</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-rose-500/10 border border-rose-500/20 text-rose-400 text-sm font-mono">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={checkVerification}
|
||||
disabled={step === 'checking'}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
{step === 'checking' ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
)}
|
||||
Verify DNS Record
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -905,11 +980,12 @@ export default function PortfolioPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddDomain = async (domain: string, data?: any) => {
|
||||
const handleAddDomain = async (domain: string, data?: any): Promise<PortfolioDomain> => {
|
||||
try {
|
||||
const created = await api.addPortfolioDomain({ domain, ...data })
|
||||
setDomains(prev => [created, ...prev])
|
||||
showToast(`${domain} added to portfolio`, 'success')
|
||||
return created
|
||||
} catch (err: any) {
|
||||
showToast(err?.message || 'Failed to add domain', 'error')
|
||||
throw err
|
||||
@ -1719,7 +1795,11 @@ export default function PortfolioPage() {
|
||||
|
||||
{/* MODALS */}
|
||||
{showAddModal && (
|
||||
<AddModal onClose={() => setShowAddModal(false)} onAdd={handleAddDomain} />
|
||||
<AddModal
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onAdd={handleAddDomain}
|
||||
onStartVerify={(domain) => setVerifyingDomain(domain)}
|
||||
/>
|
||||
)}
|
||||
{editingDomain && (
|
||||
<EditModal
|
||||
|
||||
Reference in New Issue
Block a user