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

- 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:
2025-12-18 15:22:17 +01:00
parent dae4da3f38
commit fc40a4784d
2 changed files with 168 additions and 87 deletions

View File

@ -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}",
)

View File

@ -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)
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">
<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-sm font-mono text-white uppercase tracking-wider">Verify Ownership</span>
<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.
{/* 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>
<button
onClick={startVerification}
className="w-full py-3 bg-accent text-black text-sm font-bold uppercase tracking-wider hover:bg-white transition-colors"
>
Start Verification
</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">
<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">
pounce-verify={verificationCode}
{verificationCode}
</code>
<button
onClick={copyCode}
className="p-2 text-white/40 hover:text-white transition-colors shrink-0"
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 text-accent" /> : <Copy className="w-4 h-4" />}
{copied ? <Check className="w-4 h-4" /> : <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 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-rose-500/10 border border-rose-500/20 text-rose-400 text-sm font-mono">
<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={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"
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"
>
{step === 'checking' ? (
{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="mt-4 px-4 py-2 bg-white/10 text-white text-xs font-bold uppercase"
>
Try Again
</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