Add step-by-step domain wizard with CF account selection and NS setup
This commit is contained in:
parent
801db4616b
commit
045f46bce6
|
|
@ -0,0 +1,547 @@
|
|||
/**
|
||||
* Add Domain Wizard - Step-by-step domain addition process
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
XMarkIcon,
|
||||
CheckCircleIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowLeftIcon,
|
||||
GlobeAltIcon,
|
||||
CloudIcon,
|
||||
DocumentTextIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import api from '../services/api';
|
||||
import CFTokenGuide from './CFTokenGuide';
|
||||
import NSInstructions from './NSInstructions';
|
||||
|
||||
const AddDomainWizard = ({ onClose, onSuccess, customer }) => {
|
||||
const [currentStep, setCurrentStep] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Form data
|
||||
const [domainName, setDomainName] = useState('');
|
||||
const [cfAccountType, setCfAccountType] = useState(''); // 'company' or 'own'
|
||||
const [selectedCompanyAccount, setSelectedCompanyAccount] = useState(null);
|
||||
const [ownCfToken, setOwnCfToken] = useState('');
|
||||
const [ownCfEmail, setOwnCfEmail] = useState('');
|
||||
|
||||
// Company CF accounts
|
||||
const [companyAccounts, setCompanyAccounts] = useState([]);
|
||||
|
||||
// Domain setup data
|
||||
const [domainId, setDomainId] = useState(null);
|
||||
const [dnsPreview, setDnsPreview] = useState(null);
|
||||
const [nsInstructions, setNsInstructions] = useState(null);
|
||||
const [nsStatus, setNsStatus] = useState(null);
|
||||
|
||||
// UI helpers
|
||||
const [showTokenGuide, setShowTokenGuide] = useState(false);
|
||||
|
||||
const steps = [
|
||||
{ number: 1, title: 'Domain Name', icon: GlobeAltIcon },
|
||||
{ number: 2, title: 'Cloudflare Account', icon: CloudIcon },
|
||||
{ number: 3, title: cfAccountType === 'own' ? 'API Token' : 'DNS Preview', icon: DocumentTextIcon },
|
||||
{ number: 4, title: 'Nameserver Setup', icon: ServerIcon },
|
||||
{ number: 5, title: 'Verification', icon: ShieldCheckIcon },
|
||||
];
|
||||
|
||||
// Fetch company CF accounts
|
||||
useEffect(() => {
|
||||
if (currentStep === 2) {
|
||||
fetchCompanyAccounts();
|
||||
}
|
||||
}, [currentStep]);
|
||||
|
||||
const fetchCompanyAccounts = async () => {
|
||||
try {
|
||||
const response = await api.get('/api/customer/cloudflare-accounts');
|
||||
setCompanyAccounts(response.data.accounts || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch CF accounts:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate domain name
|
||||
const validateDomain = (domain) => {
|
||||
const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
|
||||
return domainRegex.test(domain);
|
||||
};
|
||||
|
||||
// Step 1: Submit domain name
|
||||
const handleStep1Next = async () => {
|
||||
if (!domainName.trim()) {
|
||||
setError('Please enter a domain name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateDomain(domainName)) {
|
||||
setError('Please enter a valid domain name (e.g., example.com)');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
// Step 2: Select CF account type
|
||||
const handleStep2Next = () => {
|
||||
if (!cfAccountType) {
|
||||
setError('Please select a Cloudflare account option');
|
||||
return;
|
||||
}
|
||||
|
||||
if (cfAccountType === 'company' && !selectedCompanyAccount) {
|
||||
setError('Please select a company Cloudflare account');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setCurrentStep(3);
|
||||
};
|
||||
|
||||
// Step 3: Handle based on account type
|
||||
const handleStep3Next = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (cfAccountType === 'own') {
|
||||
// Validate own CF token
|
||||
if (!ownCfToken.trim() || !ownCfEmail.trim()) {
|
||||
setError('Please enter both Cloudflare email and API token');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create domain with own CF account
|
||||
const response = await api.post('/api/customer/domains', {
|
||||
domain_name: domainName,
|
||||
cf_account_type: 'own',
|
||||
cf_email: ownCfEmail,
|
||||
cf_api_token: ownCfToken,
|
||||
});
|
||||
|
||||
setDomainId(response.data.domain.id);
|
||||
setNsInstructions(response.data.ns_instructions);
|
||||
setCurrentStep(4);
|
||||
} else {
|
||||
// Create domain with company CF account
|
||||
const response = await api.post('/api/customer/domains', {
|
||||
domain_name: domainName,
|
||||
cf_account_type: 'company',
|
||||
cf_account_id: selectedCompanyAccount.id,
|
||||
});
|
||||
|
||||
setDomainId(response.data.domain.id);
|
||||
setDnsPreview(response.data.dns_preview);
|
||||
setNsInstructions(response.data.ns_instructions);
|
||||
setCurrentStep(4);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Failed to create domain');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 4: Check nameserver status
|
||||
const checkNsStatus = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.get(`/api/customer/domains/${domainId}/ns-status`);
|
||||
setNsStatus(response.data);
|
||||
|
||||
if (response.data.is_cloudflare) {
|
||||
setCurrentStep(5);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to check NS status:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Add New Domain</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="px-6 py-4 bg-gray-50">
|
||||
<div className="flex items-center justify-between">
|
||||
{steps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
const isActive = currentStep === step.number;
|
||||
const isCompleted = currentStep > step.number;
|
||||
|
||||
return (
|
||||
<div key={step.number} className="flex items-center flex-1">
|
||||
<div className="flex flex-col items-center flex-1">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center transition-all ${
|
||||
isCompleted
|
||||
? 'bg-green-500 text-white'
|
||||
: isActive
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-gray-200 text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CheckCircleIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<Icon className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs mt-2 font-medium ${
|
||||
isActive ? 'text-primary-600' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
</div>
|
||||
{index < steps.length - 1 && (
|
||||
<div
|
||||
className={`h-0.5 flex-1 mx-2 ${
|
||||
isCompleted ? 'bg-green-500' : 'bg-gray-200'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mx-6 mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="p-6">
|
||||
{/* Step 1: Domain Name */}
|
||||
{currentStep === 1 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Enter Your Domain Name</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Enter the domain name you want to add to your hosting platform.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Domain Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={domainName}
|
||||
onChange={(e) => setDomainName(e.target.value.toLowerCase().trim())}
|
||||
placeholder="example.com"
|
||||
className="input-field w-full"
|
||||
autoFocus
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Enter without http:// or https://
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-blue-900 mb-2">📋 Requirements:</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>• You must own this domain</li>
|
||||
<li>• You must have access to domain registrar settings</li>
|
||||
<li>• Domain limit: {customer?.max_domains} domains</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Cloudflare Account Selection */}
|
||||
{currentStep === 2 && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Select Cloudflare Account</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Choose how you want to manage your domain's DNS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Company Account Option */}
|
||||
<div
|
||||
onClick={() => setCfAccountType('company')}
|
||||
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
||||
cfAccountType === 'company'
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
checked={cfAccountType === 'company'}
|
||||
onChange={() => setCfAccountType('company')}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">Use Company Cloudflare Account</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
We'll manage your DNS using our Cloudflare account. Easier setup, no API token needed.
|
||||
</p>
|
||||
|
||||
{cfAccountType === 'company' && companyAccounts.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Account:
|
||||
</label>
|
||||
<select
|
||||
value={selectedCompanyAccount?.id || ''}
|
||||
onChange={(e) => {
|
||||
const account = companyAccounts.find(a => a.id === parseInt(e.target.value));
|
||||
setSelectedCompanyAccount(account);
|
||||
}}
|
||||
className="input-field w-full"
|
||||
>
|
||||
<option value="">Choose an account...</option>
|
||||
{companyAccounts.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.email})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Own Account Option */}
|
||||
<div
|
||||
onClick={() => setCfAccountType('own')}
|
||||
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
||||
cfAccountType === 'own'
|
||||
? 'border-primary-500 bg-primary-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start">
|
||||
<input
|
||||
type="radio"
|
||||
checked={cfAccountType === 'own'}
|
||||
onChange={() => setCfAccountType('own')}
|
||||
className="mt-1 mr-3"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-gray-900">Use My Own Cloudflare Account</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Use your own Cloudflare account. You'll need to provide an API token.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Own CF Token OR DNS Preview */}
|
||||
{currentStep === 3 && cfAccountType === 'own' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Enter Cloudflare API Token</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
Provide your Cloudflare API token to manage DNS records.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p className="text-sm text-blue-800 mb-2">
|
||||
<strong>Don't have an API token?</strong>
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setShowTokenGuide(true)}
|
||||
className="text-blue-600 hover:text-blue-800 font-medium text-sm underline"
|
||||
>
|
||||
📖 View API Token Creation Guide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Cloudflare Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={ownCfEmail}
|
||||
onChange={(e) => setOwnCfEmail(e.target.value)}
|
||||
placeholder="your-email@example.com"
|
||||
className="input-field w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
API Token *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={ownCfToken}
|
||||
onChange={(e) => setOwnCfToken(e.target.value)}
|
||||
placeholder="Your Cloudflare API Token"
|
||||
className="input-field w-full font-mono text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Your token will be encrypted and stored securely
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 3 && cfAccountType === 'company' && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">DNS Records Preview</h3>
|
||||
<p className="text-gray-600 text-sm mb-4">
|
||||
These DNS records will be created for your domain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{dnsPreview && (
|
||||
<div className="bg-gray-50 border border-gray-200 rounded-lg p-4">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-300">
|
||||
<th className="text-left py-2 px-2">Type</th>
|
||||
<th className="text-left py-2 px-2">Name</th>
|
||||
<th className="text-left py-2 px-2">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{dnsPreview.records?.map((record, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-200">
|
||||
<td className="py-2 px-2 font-mono">{record.type}</td>
|
||||
<td className="py-2 px-2 font-mono">{record.name}</td>
|
||||
<td className="py-2 px-2 font-mono text-xs">{record.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<p className="text-sm text-green-800">
|
||||
✓ DNS records will be automatically configured when you complete the setup.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Nameserver Setup */}
|
||||
{currentStep === 4 && (
|
||||
<div className="space-y-4">
|
||||
<NSInstructions
|
||||
domain={domainName}
|
||||
nsInstructions={nsInstructions}
|
||||
nsStatus={nsStatus}
|
||||
onCheck={checkNsStatus}
|
||||
onContinue={() => setCurrentStep(5)}
|
||||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 5: Verification & Completion */}
|
||||
{currentStep === 5 && (
|
||||
<div className="space-y-4 text-center py-8">
|
||||
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto">
|
||||
<CheckCircleIcon className="w-12 h-12 text-green-600" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">
|
||||
Domain Successfully Added!
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
Your domain <strong>{domainName}</strong> has been configured and is ready to use.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 text-left max-w-md mx-auto">
|
||||
<h4 className="font-semibold text-green-900 mb-2">✓ What's Next?</h4>
|
||||
<ul className="text-sm text-green-800 space-y-1">
|
||||
<li>• DNS records have been configured</li>
|
||||
<li>• SSL certificate will be issued automatically</li>
|
||||
<li>• Your domain will be active within a few minutes</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
onSuccess();
|
||||
onClose();
|
||||
}}
|
||||
className="btn-primary mx-auto"
|
||||
>
|
||||
Go to Domains
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="sticky bottom-0 bg-gray-50 border-t px-6 py-4 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentStep > 1) setCurrentStep(currentStep - 1);
|
||||
else onClose();
|
||||
}}
|
||||
className="btn-secondary inline-flex items-center"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
{currentStep === 1 ? 'Cancel' : 'Back'}
|
||||
</button>
|
||||
|
||||
{currentStep < 5 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (currentStep === 1) handleStep1Next();
|
||||
else if (currentStep === 2) handleStep2Next();
|
||||
else if (currentStep === 3) handleStep3Next();
|
||||
}}
|
||||
disabled={loading}
|
||||
className="btn-primary inline-flex items-center disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Processing...' : 'Next'}
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Guide Modal */}
|
||||
{showTokenGuide && <CFTokenGuide onClose={() => setShowTokenGuide(false)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddDomainWizard;
|
||||
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
ClockIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import api from '../services/api';
|
||||
import AddDomainWizard from '../components/AddDomainWizard';
|
||||
|
||||
// Domains Content Component
|
||||
const DomainsContent = ({ customer }) => {
|
||||
|
|
@ -131,20 +132,13 @@ const DomainsContent = ({ customer }) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Domain Modal - Placeholder */}
|
||||
{/* Add Domain Wizard */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4">
|
||||
<h3 className="text-xl font-bold mb-4">Add New Domain</h3>
|
||||
<p className="text-gray-600 mb-4">Domain wizard coming soon...</p>
|
||||
<button
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="btn-secondary"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<AddDomainWizard
|
||||
onClose={() => setShowAddModal(false)}
|
||||
onSuccess={fetchDomains}
|
||||
customer={customer}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in New Issue