Files
avbeat-com/src/app/admin/email/domains/page.tsx

227 lines
13 KiB
TypeScript

'use client';
import React, { useState, useEffect, useCallback, Suspense } from 'react';
import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter, useSearchParams } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsRow({ label, valid, checkedAt }: { label: string; valid: boolean; checkedAt: string | null }) {
return (
<div className="flex items-center justify-between py-1">
<span className="text-xs text-[#888]">{label}</span>
<div className="flex items-center gap-2">
<span className={`text-xs font-medium ${valid ? 'text-green-400' : 'text-red-400'}`}>{valid ? '✓ Pass' : '✗ Fail'}</span>
{checkedAt && <span className="text-xs text-[#555]">{new Date(checkedAt).toLocaleDateString()}</span>}
</div>
</div>
);
}
function DomainsContent() {
const { user, loading } = useAuth();
const router = useRouter();
const sp = useSearchParams();
const [domains, setDomains] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(true);
const [filterSite, setFilterSite] = useState(sp.get('site') ?? '');
const [expanded, setExpanded] = useState<string | null>(null);
const [verifying, setVerifying] = useState<string | null>(null);
const [showBypass, setShowBypass] = useState<string | null>(null);
const [bypassReason, setBypassReason] = useState('');
const [showAddForm, setShowAddForm] = useState(false);
const [addForm, setAddForm] = useState({ site: '', domain: '', domain_type: 'subdomain', is_primary: false, sort_order: 0 });
const [saving, setSaving] = useState(false);
useEffect(() => { if (!loading && !user) router.push('/login'); }, [user, loading, router]);
const load = useCallback(async () => {
setLoadingData(true);
const params = new URLSearchParams();
if (filterSite) params.set('site', filterSite);
const r = await fetch(`/api/email/rmp/domains?${params}`);
const d = await r.json();
setDomains(d.domains ?? []);
setLoadingData(false);
}, [filterSite]);
useEffect(() => { if (user) load(); }, [user, load]);
async function runVerify(domainId: string) {
setVerifying(domainId);
await fetch(`/api/email/domains/${domainId}/verify`);
setVerifying(null);
load();
}
async function toggleActive(domainId: string, current: boolean) {
await fetch(`/api/email/rmp/domains/${domainId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ is_active: !current }) });
load();
}
async function setBypass(domainId: string) {
await fetch(`/api/email/rmp/domains/${domainId}/bypass`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: bypassReason }) });
setShowBypass(null);
setBypassReason('');
load();
}
async function handleAddDomain(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
await fetch('/api/email/rmp/domains', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(addForm) });
setSaving(false);
setShowAddForm(false);
load();
}
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null;
return (
<div className="min-h-screen bg-[#0a0a0a] text-white p-6">
<div className="max-w-6xl mx-auto space-y-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold">Sending Domains</h1>
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
</div>
<div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
{showAddForm && (
<form onSubmit={handleAddDomain} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Rotation Domain</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
<option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Domain *</label>
<input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#3b82f6]" />
</div>
<div>
<label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none">
{['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)}
</select>
</div>
</div>
<div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</form>
)}
{/* Bypass modal */}
{showBypass && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3>
<p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p>
<textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" />
<div className="flex gap-2">
<button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button>
<button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div>
</div>
</div>
)}
<div className="space-y-2">
{loadingData ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div>
) : domains.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div>
) : domains.map((d: any) => (
<div key={d.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
<div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#161616]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-white">{d.domain}</span>
<span className="text-xs text-[#555] capitalize">{d.site}</span>
{d.is_primary && <span className="px-1.5 py-0.5 bg-blue-400/10 text-blue-400 rounded text-xs">Primary</span>}
<span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span>
{d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>}
{d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>}
<span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span>
</div>
<div className="flex gap-3 mt-1 text-xs text-[#555]">
<span>Health: {d.health_score}/100</span>
<span>Sends today: {d.sends_today}/{d.daily_send_limit}</span>
</div>
</div>
<span className="text-[#555] text-xs">{expanded === d.id ? '▲' : '▼'}</span>
</div>
{expanded === d.id && (
<div className="border-t border-[#1a1a1a] px-4 py-3 space-y-3">
{/* DNS checks */}
<div className="grid grid-cols-2 gap-x-6">
<DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} />
<DnsRow label="DKIM" valid={d.dkim_valid} checkedAt={d.dkim_checked_at} />
<DnsRow label="DMARC" valid={d.dmarc_valid} checkedAt={d.dmarc_checked_at} />
<DnsRow label="PTR" valid={d.ptr_valid} checkedAt={d.ptr_checked_at} />
<DnsRow label="MX" valid={d.mx_valid} checkedAt={d.mx_checked_at} />
</div>
{/* DNS helper panel */}
<div className="bg-[#0a0a0a] border border-[#1a1a1a] rounded p-3 space-y-2">
<p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p>
<div className="space-y-1 text-xs text-[#555] font-mono">
<p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p>
<p>DKIM: <span className="text-[#888]">Add CNAME records from your ESP</span></p>
<p>DMARC: <span className="text-[#888]">v=DMARC1; p=quarantine; rua=mailto:dmarc@{d.domain}</span></p>
<p>MX: <span className="text-[#888]">Point to your mail server</span></p>
<p>PTR: <span className="text-[#888]">Set via hosting provider (reverse DNS)</span></p>
</div>
</div>
{d.bypass_verification && (
<div className="bg-yellow-400/5 border border-yellow-400/20 rounded p-2 text-xs text-yellow-400">
DNS bypass active: {d.bypass_reason}
</div>
)}
{/* Actions */}
<div className="flex gap-2 flex-wrap">
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] disabled:opacity-50">
{verifying === d.id ? 'Verifying…' : 'Run Full Verification'}
</button>
<button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white disabled:opacity-30">
{d.is_active ? 'Deactivate' : 'Activate'}
</button>
<button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50">
Override DNS Verification
</button>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
);
}
export default function EmailDomainsPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" /></div>}>
<DomainsContent />
</Suspense>
);
}