EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
TabDomains.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { useNavigate } from 'react-router-dom';
3import { apiFetch } from '../../lib/api';
4import { notifySuccess, notifyError } from '../../utils/notifications';
5import DomainStats from './domains/DomainStats';
6import DomainTable from './domains/DomainTable';
7import DomainFormModal from './domains/DomainFormModal';
8import DomainRegistrationModal from './domains/DomainRegistrationModal';
9import DomainDNSModal from './domains/DomainDNSModal';
10import DomainManagementModal from './domains/DomainManagementModal';
11import './TabDomains.css';
12
13const initialFormData = {
14 customer_id: '',
15 domain_name: '',
16 registrar: 'enom',
17 registration_date: '',
18 expiry_date: '',
19 auto_renew: true,
20 status: 'active',
21 contract_id: '',
22 nameservers: '',
23 dns_provider: '',
24 privacy_protection: false,
25 notes: ''
26};
27
28const initialRegistrationData = {
29 customer_id: '',
30 contract_id: '',
31 domain_name: '',
32 domain_type: 'registration',
33 years: 1,
34 price: null,
35 currency: 'USD',
36 nameservers: '',
37 registrant_firstName: '',
38 registrant_lastName: '',
39 registrant_organization: '',
40 registrant_email: '',
41 registrant_phone: '',
42 registrant_address: '',
43 registrant_city: '',
44 registrant_state: '',
45 registrant_postalCode: '',
46 registrant_country: ''
47};
48
49export default function TabDomains() {
50 const navigate = useNavigate();
51 const [domains, setDomains] = useState([]);
52 const [customers, setCustomers] = useState([]);
53 const [contracts, setContracts] = useState([]);
54 const [loading, setLoading] = useState(true);
55 const [showModal, setShowModal] = useState(false);
56 const [showRegistrationModal, setShowRegistrationModal] = useState(false);
57 const [editingDomain, setEditingDomain] = useState(null);
58 const [formData, setFormData] = useState(initialFormData);
59 const [registrationData, setRegistrationData] = useState(initialRegistrationData);
60 const [searchQuery, setSearchQuery] = useState('');
61 const [searchResult, setSearchResult] = useState(null);
62 const [searching, setSearching] = useState(false);
63 const [showDNSModal, setShowDNSModal] = useState(false);
64 const [dnsEditingDomain, setDnsEditingDomain] = useState(null);
65 const [showManagementModal, setShowManagementModal] = useState(false);
66 const [managingDomain, setManagingDomain] = useState(null);
67 const [syncing, setSyncing] = useState(false);
68 const [activeTab, setActiveTab] = useState('all');
69
70 // Check if user is root tenant
71 const token = localStorage.getItem('token');
72 const parseJwt = (token) => {
73 try {
74 const payload = token.split('.')[1];
75 return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
76 } catch {
77 return null;
78 }
79 };
80 const jwtPayload = token ? parseJwt(token) : null;
81 const isRootTenant = jwtPayload?.is_msp || false;
82
83 useEffect(() => {
84 loadDomains();
85 loadCustomers();
86 loadContracts();
87 }, []);
88
89 const loadDomains = async () => {
90 try {
91 setLoading(true);
92 console.log('[Domains] Fetching domains from database...');
93 const res = await apiFetch('/domains');
94 if (!res.ok) throw new Error(`HTTP ${res.status}`);
95 const data = await res.json();
96 console.log('[Domains] Received data:', data);
97 const domainsList = Array.isArray(data) ? data : data.domains || [];
98 console.log('[Domains] Loaded', domainsList.length, 'domains from database');
99 setDomains(domainsList);
100 } catch (err) {
101 console.error('[Domains] Error loading domains:', err);
102 await notifyError('Load Failed', 'Failed to load domains from database');
103 setDomains([]);
104 } finally {
105 setLoading(false);
106 }
107 };
108
109 const loadCustomers = async () => {
110 try {
111 const res = await apiFetch('/customers?limit=1000&status=active');
112 if (!res.ok) return;
113 const data = await res.json();
114 setCustomers(Array.isArray(data) ? data : data.customers || []);
115 } catch (err) {
116 console.error('Error loading customers:', err);
117 }
118 };
119
120 const loadContracts = async () => {
121 try {
122 const res = await apiFetch('/contracts?limit=1000&status=active');
123 if (!res.ok) return;
124 const data = await res.json();
125 setContracts(Array.isArray(data) ? data : data.contracts || []);
126 } catch (err) {
127 console.error('Error loading contracts:', err);
128 }
129 };
130
131 const openModal = (domain = null) => {
132 if (domain) {
133 setEditingDomain(domain);
134 setFormData({
135 customer_id: domain.customer_id || '',
136 domain_name: domain.domain_name || '',
137 registrar: domain.registrar || 'enom',
138 registration_date: domain.registration_date?.split('T')[0] || '',
139 expiry_date: domain.expiry_date?.split('T')[0] || '',
140 auto_renew: domain.auto_renew !== false,
141 status: domain.status || 'active',
142 contract_id: domain.contract_id || '',
143 nameservers: domain.nameservers || '',
144 dns_provider: domain.dns_provider || '',
145 privacy_protection: domain.privacy_protection || false,
146 notes: domain.notes || ''
147 });
148 } else {
149 setEditingDomain(null);
150 setFormData(initialFormData);
151 }
152 setShowModal(true);
153 };
154
155 const closeModal = () => {
156 setShowModal(false);
157 setShowRegistrationModal(false);
158 setEditingDomain(null);
159 setSearchResult(null);
160 setSearchQuery('');
161 };
162
163 const openRegistrationModal = (domainType = 'registration') => {
164 // Pre-fill from search result if available
165 if (searchResult && searchResult.available && !searchResult.alreadyOwned) {
166 setRegistrationData({
167 ...initialRegistrationData,
168 domain_name: searchResult.domain,
169 domain_type: domainType,
170 price: searchResult.price || null,
171 currency: searchResult.currency || 'USD'
172 });
173 } else {
174 setRegistrationData({
175 ...initialRegistrationData,
176 domain_type: domainType
177 });
178 }
179 setShowRegistrationModal(true);
180 };
181
182 const handleDomainSearch = async () => {
183 if (!searchQuery.trim()) return;
184
185 setSearching(true);
186 try {
187 const result = await apiFetch('/domains/search', {
188 method: 'POST',
189 headers: { 'Content-Type': 'application/json' },
190 body: JSON.stringify({ domain: searchQuery.trim() })
191 });
192
193 setSearchResult(result);
194
195 // If available, pre-fill form with search result
196 if (result.available && !result.alreadyOwned) {
197 const expiryDate = new Date();
198 expiryDate.setFullYear(expiryDate.getFullYear() + 1);
199
200 setFormData({
201 ...formData,
202 domain_name: result.domain,
203 registrar: result.registrar || 'cloudflare',
204 registration_date: new Date().toISOString().split('T')[0],
205 expiry_date: expiryDate.toISOString().split('T')[0],
206 auto_renew: true,
207 privacy_protection: true
208 });
209 }
210 } catch (err) {
211 await notifyError('Search Failed', err.message || 'Failed to search domain');
212 setSearchResult(null);
213 } finally {
214 setSearching(false);
215 }
216 };
217
218 const handleSubmit = async (data, editing) => {
219 try {
220 const payload = {
221 ...data,
222 register_now: !editing && data.registrar === 'cloudflare'
223 };
224
225 if (editing) {
226 await apiFetch(`/domains/${editing.domain_id}`, {
227 method: 'PUT',
228 body: JSON.stringify(payload)
229 });
230 await notifySuccess('Domain Updated', 'Domain has been updated successfully');
231 } else {
232 await apiFetch('/domains', {
233 method: 'POST',
234 body: JSON.stringify(payload)
235 });
236
237 if (data.registrar === 'cloudflare') {
238 await notifySuccess('Domain Registered', `Domain ${data.domain_name} has been registered with Cloudflare`);
239 } else {
240 await notifySuccess('Domain Created', 'Domain has been created successfully');
241 }
242 }
243 closeModal();
244 loadDomains();
245 } catch (err) {
246 const errorMsg = err.message || 'Error saving domain';
247 await notifyError('Save Failed', errorMsg);
248 }
249 };
250
251 const handleRegistrationRequest = async (data) => {
252 try {
253 // Convert form data to API format
254 const nameserversArray = data.nameservers
255 ? data.nameservers.split('\n').map(ns => ns.trim()).filter(ns => ns)
256 : [];
257
258 const payload = {
259 customer_id: parseInt(data.customer_id),
260 contract_id: data.contract_id ? parseInt(data.contract_id) : null,
261 domain_name: data.domain_name,
262 years: data.years,
263 price: data.price,
264 currency: data.currency,
265 nameservers: nameserversArray,
266 registrant_contact: {
267 firstName: data.registrant_firstName,
268 lastName: data.registrant_lastName,
269 organization: data.registrant_organization || '',
270 email: data.registrant_email,
271 phone: data.registrant_phone,
272 address: data.registrant_address,
273 city: data.registrant_city,
274 state: data.registrant_state,
275 postalCode: data.registrant_postalCode,
276 country: data.registrant_country
277 }
278 };
279
280 await apiFetch('/domain-requests', {
281 method: 'POST',
282 body: JSON.stringify(payload)
283 });
284
285 await notifySuccess(
286 'Registration Request Submitted',
287 `Your domain registration request for ${data.domain_name} has been submitted for admin approval`
288 );
289
290 closeModal();
291 // Optionally reload domains to show pending request
292 } catch (err) {
293 const errorMsg = err.message || 'Error submitting registration request';
294 await notifyError('Request Failed', errorMsg);
295 }
296 };
297
298 const handleDelete = async (domainId) => {
299 if (!confirm('Delete this domain? This cannot be undone.')) return;
300 try {
301 await apiFetch(`/domains/${domainId}`, { method: 'DELETE' });
302 await notifySuccess('Domain Deleted', 'Domain has been deleted successfully');
303 loadDomains();
304 } catch (err) {
305 const errorMsg = err.message || 'Error deleting domain';
306 await notifyError('Delete Failed', errorMsg);
307 }
308 };
309
310 const handleManageDNS = (domain) => {
311 // Navigate to the new DNS management page
312 navigate(`/dns-management/${domain.domain_name}`);
313 };
314
315 const closeDNSModal = () => {
316 setShowDNSModal(false);
317 setDnsEditingDomain(null);
318 };
319
320 const handleManageDomain = (domain) => {
321 setManagingDomain(domain);
322 setShowManagementModal(true);
323 };
324
325 const closeManagementModal = () => {
326 setShowManagementModal(false);
327 setManagingDomain(null);
328 };
329
330 const handleSyncDomains = async () => {
331 if (!isRootTenant) return;
332
333 setSyncing(true);
334 try {
335 await apiFetch('/domains/sync', { method: 'POST' });
336 await notifySuccess('Sync Started', 'Domain sync has been started. This may take a few minutes.');
337 // Reload domains after a short delay
338 setTimeout(() => {
339 loadDomains();
340 }, 3000);
341 } catch (err) {
342 await notifyError('Sync Failed', err.message || 'Failed to start domain sync');
343 } finally {
344 setSyncing(false);
345 }
346 };
347
348 // Filter domains by metadata/configuration based on active tab
349 const getFilteredDomains = () => {
350 const now = new Date();
351 const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
352
353 switch (activeTab) {
354 case 'all':
355 return domains;
356 case 'registered':
357 return domains.filter(d => {
358 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
359 return metadata.domain_type === 'registered';
360 });
361 case 'dns_only':
362 return domains.filter(d => {
363 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
364 return metadata.domain_type === 'dns_only';
365 });
366 case 'cloudflare':
367 return domains.filter(d => {
368 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
369 return metadata.has_cloudflare_dns || metadata.cloudflare_zone_id;
370 });
371 case 'locked':
372 return domains.filter(d => {
373 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
374 return metadata.locked === true;
375 });
376 case 'expiring':
377 return domains.filter(d => {
378 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
379 if (metadata.domain_type === 'dns_only') return false;
380 if (!d.expiration_date) return false;
381 const expiryDate = new Date(d.expiration_date);
382 return expiryDate <= thirtyDaysFromNow && expiryDate >= now;
383 });
384 case 'pending':
385 return domains.filter(d => d.status === 'pending');
386 default:
387 return domains;
388 }
389 };
390
391 const filteredDomains = getFilteredDomains();
392
393 if (loading) {
394 return <div>Loading domains...</div>;
395 }
396
397 return (
398 <div className="tab-domains">
399 <div className="tab-header">
400 <DomainStats domains={domains} />
401 <div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
402 <div style={{ display: 'flex', gap: '4px' }}>
403 <input
404 type="text"
405 placeholder="Search domain (e.g., example.com)"
406 value={searchQuery}
407 onChange={(e) => setSearchQuery(e.target.value)}
408 onKeyPress={(e) => e.key === 'Enter' && handleDomainSearch()}
409 style={{ padding: '8px 12px', minWidth: '280px' }}
410 />
411 <button
412 className="btn"
413 onClick={handleDomainSearch}
414 disabled={searching || !searchQuery.trim()}
415 >
416 {searching ? 'Searching...' : 'Search'}
417 </button>
418 </div>
419 {isRootTenant && (
420 <button
421 className="btn"
422 onClick={handleSyncDomains}
423 disabled={syncing}
424 title="Sync all Cloudflare domains and DNS records"
425 style={{ background: '#2196F3', color: 'white' }}
426 >
427 <span className="material-symbols-outlined">sync</span>
428 {syncing ? 'Syncing...' : 'Sync Domains'}
429 </button>
430 )}
431 <button
432 className="btn"
433 onClick={() => openRegistrationModal('dns-only')}
434 style={{ background: '#00BCD4', color: 'white' }}
435 title="Add DNS management for a domain registered elsewhere (free)"
436 >
437 <span className="material-symbols-outlined">dns</span>
438 Add Existing Domain
439 </button>
440 <button className="btn btn-primary" onClick={() => openRegistrationModal('registration')}>
441 <span className="material-symbols-outlined">add</span>
442 Register Domain
443 </button>
444 </div>
445 </div>
446
447 {/* Domain Filter Tabs */}
448 <div style={{
449 borderBottom: '2px solid #e0e0e0',
450 marginBottom: '16px',
451 display: 'flex',
452 gap: '4px',
453 flexWrap: 'wrap'
454 }}>
455 <button
456 onClick={() => setActiveTab('all')}
457 style={{
458 padding: '12px 20px',
459 background: activeTab === 'all' ? '#2196F3' : 'transparent',
460 color: activeTab === 'all' ? 'white' : '#666',
461 border: 'none',
462 borderBottom: activeTab === 'all' ? '3px solid #2196F3' : '3px solid transparent',
463 cursor: 'pointer',
464 fontWeight: activeTab === 'all' ? 'bold' : 'normal',
465 transition: 'all 0.2s'
466 }}
467 >
468 All ({domains.length})
469 </button>
470 <button
471 onClick={() => setActiveTab('registered')}
472 style={{
473 padding: '12px 20px',
474 background: activeTab === 'registered' ? '#4caf50' : 'transparent',
475 color: activeTab === 'registered' ? 'white' : '#666',
476 border: 'none',
477 borderBottom: activeTab === 'registered' ? '3px solid #4caf50' : '3px solid transparent',
478 cursor: 'pointer',
479 fontWeight: activeTab === 'registered' ? 'bold' : 'normal',
480 transition: 'all 0.2s'
481 }}
482 title="Domains with full registration management"
483 >
484 Registered ({domains.filter(d => {
485 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
486 return metadata.domain_type === 'registered';
487 }).length})
488 </button>
489 <button
490 onClick={() => setActiveTab('dns_only')}
491 style={{
492 padding: '12px 20px',
493 background: activeTab === 'dns_only' ? '#00BCD4' : 'transparent',
494 color: activeTab === 'dns_only' ? 'white' : '#666',
495 border: 'none',
496 borderBottom: activeTab === 'dns_only' ? '3px solid #00BCD4' : '3px solid transparent',
497 cursor: 'pointer',
498 fontWeight: activeTab === 'dns_only' ? 'bold' : 'normal',
499 transition: 'all 0.2s'
500 }}
501 title="DNS management only (registered elsewhere)"
502 >
503 DNS Only ({domains.filter(d => {
504 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
505 return metadata.domain_type === 'dns_only';
506 }).length})
507 </button>
508 <button
509 onClick={() => setActiveTab('cloudflare')}
510 style={{
511 padding: '12px 20px',
512 background: activeTab === 'cloudflare' ? '#2196F3' : 'transparent',
513 color: activeTab === 'cloudflare' ? 'white' : '#666',
514 border: 'none',
515 borderBottom: activeTab === 'cloudflare' ? '3px solid #2196F3' : '3px solid transparent',
516 cursor: 'pointer',
517 fontWeight: activeTab === 'cloudflare' ? 'bold' : 'normal',
518 transition: 'all 0.2s'
519 }}
520 title="Using Cloudflare DNS"
521 >
522 ☁️ Cloudflare ({domains.filter(d => {
523 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
524 return metadata.has_cloudflare_dns || metadata.cloudflare_zone_id;
525 }).length})
526 </button>
527 <button
528 onClick={() => setActiveTab('locked')}
529 style={{
530 padding: '12px 20px',
531 background: activeTab === 'locked' ? '#4caf50' : 'transparent',
532 color: activeTab === 'locked' ? 'white' : '#666',
533 border: 'none',
534 borderBottom: activeTab === 'locked' ? '3px solid #4caf50' : '3px solid transparent',
535 cursor: 'pointer',
536 fontWeight: activeTab === 'locked' ? 'bold' : 'normal',
537 transition: 'all 0.2s'
538 }}
539 title="Transfer protected domains"
540 >
541 🔒 Locked ({domains.filter(d => {
542 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
543 return metadata.locked === true;
544 }).length})
545 </button>
546 <button
547 onClick={() => setActiveTab('expiring')}
548 style={{
549 padding: '12px 20px',
550 background: activeTab === 'expiring' ? '#ff9800' : 'transparent',
551 color: activeTab === 'expiring' ? 'white' : '#666',
552 border: 'none',
553 borderBottom: activeTab === 'expiring' ? '3px solid #ff9800' : '3px solid transparent',
554 cursor: 'pointer',
555 fontWeight: activeTab === 'expiring' ? 'bold' : 'normal',
556 transition: 'all 0.2s'
557 }}
558 title="Expiring within 30 days"
559 >
560 ⚠️ Expiring ({(() => {
561 const now = new Date();
562 const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000);
563 return domains.filter(d => {
564 const metadata = typeof d.metadata === 'string' ? JSON.parse(d.metadata) : d.metadata || {};
565 if (metadata.domain_type === 'dns_only') return false;
566 if (!d.expiration_date) return false;
567 const expiryDate = new Date(d.expiration_date);
568 return expiryDate <= thirtyDaysFromNow && expiryDate >= now;
569 }).length;
570 })()})
571 </button>
572 {isRootTenant && (
573 <button
574 onClick={() => setActiveTab('pending')}
575 style={{
576 padding: '12px 20px',
577 background: activeTab === 'pending' ? '#ff9800' : 'transparent',
578 color: activeTab === 'pending' ? 'white' : '#666',
579 border: 'none',
580 borderBottom: activeTab === 'pending' ? '3px solid #ff9800' : '3px solid transparent',
581 cursor: 'pointer',
582 fontWeight: activeTab === 'pending' ? 'bold' : 'normal',
583 transition: 'all 0.2s'
584 }}
585 title="Pending registration requests"
586 >
587 Pending ({domains.filter(d => d.status === 'pending').length})
588 </button>
589 )}
590 </div>
591
592 {searchResult && (
593 <div style={{
594 padding: '16px',
595 marginBottom: '16px',
596 background: searchResult.available && !searchResult.alreadyOwned ? '#e8f5e9' : '#fff3e0',
597 border: `1px solid ${searchResult.available && !searchResult.alreadyOwned ? '#4caf50' : '#ff9800'}`,
598 borderRadius: '8px'
599 }}>
600 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
601 <div>
602 <h3 style={{ margin: '0 0 8px 0' }}>{searchResult.domain}</h3>
603 <p style={{ margin: 0 }}>
604 {searchResult.alreadyOwned ? (
605 <strong>Already registered in your account</strong>
606 ) : searchResult.available ? (
607 <>
608 <strong style={{ color: '#4caf50' }}>Available!</strong>
609 {searchResult.price && ` - $${searchResult.price} ${searchResult.currency}/year`}
610 {searchResult.note && <span style={{ fontSize: '0.9em', color: '#666', display: 'block', marginTop: '4px' }}>{searchResult.note}</span>}
611 </>
612 ) : (
613 <strong style={{ color: '#ff5722' }}>Not Available</strong>
614 )}
615 </p>
616 </div>
617 {searchResult.available && !searchResult.alreadyOwned && (
618 <button
619 className="btn btn-primary"
620 onClick={openRegistrationModal}
621 >
622 Register Now
623 </button>
624 )}
625 </div>
626 </div>
627 )}
628
629 <DomainTable
630 domains={filteredDomains}
631 onEdit={openModal}
632 onDelete={handleDelete}
633 onManageDNS={handleManageDNS}
634 onManage={handleManageDomain}
635 />
636
637 <DomainFormModal
638 showModal={showModal}
639 editingDomain={editingDomain}
640 formData={formData}
641 setFormData={setFormData}
642 customers={customers}
643 contracts={contracts}
644 onSubmit={handleSubmit}
645 onClose={closeModal}
646 />
647
648 <DomainRegistrationModal
649 showModal={showRegistrationModal}
650 formData={registrationData}
651 setFormData={setRegistrationData}
652 customers={customers}
653 contracts={contracts}
654 onSubmit={handleRegistrationRequest}
655 onClose={closeModal}
656 />
657
658 <DomainDNSModal
659 showModal={showDNSModal}
660 domain={dnsEditingDomain}
661 onClose={closeDNSModal}
662 onUpdate={loadDomains}
663 />
664
665 {showManagementModal && managingDomain && (
666 <DomainManagementModal
667 domain={managingDomain}
668 onClose={closeManagementModal}
669 onUpdate={loadDomains}
670 />
671 )}
672 </div>
673 );
674}