EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
cloudflare.js
Go to the documentation of this file.
1// backend/services/cloudflare.js
2const axios = require('axios');
3
4const CLOUDFLARE_BASE_URL = process.env.CLOUDFLARE_BASE_URL || 'https://api.cloudflare.com/client/v4';
5const CLOUDFLARE_ZONE_ID = process.env.CLOUDFLARE_ZONE_ID;
6const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || process.env.CLOUDFLARE_TOKEN;
7const CLOUDFLARE_ACCOUNT_ID = process.env.CLOUDFLARE_ACCOUNT_ID;
8
9// ============================================================================
10// DNS Management
11// ============================================================================
12
13// Create DNS A or CNAME record for new tenant
14/**
15 *
16 * @param subdomain
17 * @param target
18 */
19async function createSubdomain(subdomain, target) {
20 if (!CLOUDFLARE_ZONE_ID || !CLOUDFLARE_API_TOKEN) {
21 throw new Error('Cloudflare credentials not configured');
22 }
23
24 const dnsName = `${subdomain}.everydaytech.au`;
25
26 try {
27 const res = await axios.post(
28 `${CLOUDFLARE_BASE_URL}/zones/${CLOUDFLARE_ZONE_ID}/dns_records`,
29 {
30 type: 'CNAME',
31 name: dnsName,
32 content: target, // e.g. "app.everydaytech.au"
33 ttl: 120,
34 proxied: true
35 },
36 {
37 headers: {
38 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
39 'Content-Type': 'application/json'
40 }
41 }
42 );
43
44 if (res.data.success) {
45 console.log(`[Cloudflare] Created subdomain ${dnsName} → ${target}`);
46 return dnsName;
47 } else {
48 console.error('[Cloudflare] Failed:', res.data.errors);
49 throw new Error('Cloudflare DNS creation failed');
50 }
51 } catch (err) {
52 // Check for duplicate record error
53 const message = err.response?.data?.errors?.[0]?.message || err.message;
54 if (message && message.includes('record already exists')) {
55 console.warn(`[Cloudflare] Subdomain ${dnsName} already exists.`);
56 const error = new Error('Subdomain already exists');
57 error.code = 'SUBDOMAIN_EXISTS';
58 throw error;
59 }
60 console.error('[Cloudflare] Error creating subdomain:', err.response?.data || err.message);
61 throw err;
62 }
63}
64
65// ============================================================================
66// Domain Registrar API
67// ============================================================================
68
69/**
70 * Search for domain availability on Cloudflare Registrar
71 * @param {string} domain - Domain name to search (e.g., 'example.com')
72 * @returns {Promise<object>} - Domain availability and pricing info
73 */
74async function searchDomain(domain) {
75 if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_API_TOKEN) {
76 throw new Error('Cloudflare Registrar credentials not configured');
77 }
78
79 try {
80 const res = await axios.get(
81 `${CLOUDFLARE_BASE_URL}/accounts/${CLOUDFLARE_ACCOUNT_ID}/registrar/domains/${domain}`,
82 {
83 headers: {
84 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
85 'Content-Type': 'application/json'
86 }
87 }
88 );
89
90 if (res.data.success) {
91 const result = res.data.result;
92 return {
93 domain: domain,
94 available: result.available,
95 price: result.current_year_cost || 0,
96 currency: 'USD',
97 registrar: 'cloudflare',
98 canRegister: result.can_register,
99 supportedTlds: result.supported_tld
100 };
101 } else {
102 console.error('[Cloudflare Registrar] Search failed:', res.data.errors);
103 throw new Error('Domain search failed');
104 }
105 } catch (err) {
106 console.error('[Cloudflare Registrar] Error searching domain:', err.response?.data || err.message);
107
108 // If API not available, return placeholder
109 if (err.response?.status === 404 || err.response?.status === 403) {
110 return {
111 domain: domain,
112 available: true,
113 price: 10.00,
114 currency: 'USD',
115 registrar: 'cloudflare',
116 canRegister: false,
117 note: 'Cloudflare Registrar API not available - manual registration required'
118 };
119 }
120
121 throw err;
122 }
123}
124
125/**
126 * Register a domain on Cloudflare Registrar
127 * @param {object} params - Registration parameters
128 * @returns {Promise<object>} - Registration result
129 */
130async function registerDomain(params) {
131 const {
132 domain,
133 years = 1,
134 privacy = true,
135 auto_renew = true,
136 registrant_contact
137 } = params;
138
139 if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_API_TOKEN) {
140 throw new Error('Cloudflare Registrar credentials not configured');
141 }
142
143 if (!registrant_contact) {
144 throw new Error('Registrant contact information is required');
145 }
146
147 try {
148 const res = await axios.post(
149 `${CLOUDFLARE_BASE_URL}/accounts/${CLOUDFLARE_ACCOUNT_ID}/registrar/domains/${domain}`,
150 {
151 years: years,
152 privacy: privacy,
153 auto_renew: auto_renew,
154 registrant: registrant_contact
155 },
156 {
157 headers: {
158 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
159 'Content-Type': 'application/json'
160 }
161 }
162 );
163
164 if (res.data.success) {
165 const result = res.data.result;
166 console.log(`[Cloudflare Registrar] Registered domain ${domain}`);
167 return {
168 domain: domain,
169 status: 'active',
170 registration_date: result.created_on,
171 expiration_date: result.expires_on,
172 auto_renew: auto_renew,
173 privacy: privacy,
174 registrar: 'cloudflare'
175 };
176 } else {
177 console.error('[Cloudflare Registrar] Registration failed:', res.data.errors);
178 throw new Error(res.data.errors?.[0]?.message || 'Domain registration failed');
179 }
180 } catch (err) {
181 console.error('[Cloudflare Registrar] Error registering domain:', err.response?.data || err.message);
182
183 // If API not available, throw descriptive error
184 if (err.response?.status === 404 || err.response?.status === 403) {
185 const error = new Error('Cloudflare Registrar API not available. Please register domain manually and add it to the system.');
186 error.code = 'API_NOT_AVAILABLE';
187 throw error;
188 }
189
190 throw err;
191 }
192}
193
194/**
195 * Get domain details from Cloudflare Registrar
196 * @param {string} domain - Domain name
197 * @returns {Promise<object>} - Domain details
198 */
199async function getDomainDetails(domain) {
200 if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_API_TOKEN) {
201 throw new Error('Cloudflare Registrar credentials not configured');
202 }
203
204 try {
205 const res = await axios.get(
206 `${CLOUDFLARE_BASE_URL}/accounts/${CLOUDFLARE_ACCOUNT_ID}/registrar/domains/${domain}`,
207 {
208 headers: {
209 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
210 'Content-Type': 'application/json'
211 }
212 }
213 );
214
215 if (res.data.success) {
216 return res.data.result;
217 } else {
218 throw new Error('Failed to get domain details');
219 }
220 } catch (err) {
221 console.error('[Cloudflare Registrar] Error getting domain details:', err.response?.data || err.message);
222 throw err;
223 }
224}
225
226/**
227 * Get Cloudflare zone ID for a domain
228 * @param {string} domain - Domain name (e.g., 'example.com')
229 * @returns {Promise<string|null>} - Zone ID if found
230 */
231async function getZoneId(domain) {
232 if (!CLOUDFLARE_API_TOKEN) {
233 console.error('[Cloudflare] API token not configured');
234 return null;
235 }
236
237 try {
238 const res = await axios.get(
239 `${CLOUDFLARE_BASE_URL}/zones?name=${domain}`,
240 {
241 headers: {
242 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
243 'Content-Type': 'application/json'
244 }
245 }
246 );
247
248 if (res.data.success && res.data.result.length > 0) {
249 return res.data.result[0].id;
250 }
251 return null;
252 } catch (err) {
253 console.error('[Cloudflare] Error getting zone ID:', err.response?.data || err.message);
254 return null;
255 }
256}
257
258/**
259 * List all zones in Cloudflare account
260 * @returns {Promise<Array>} - Array of zones
261 */
262async function listZones() {
263 if (!CLOUDFLARE_API_TOKEN) {
264 throw new Error('Cloudflare API token not configured');
265 }
266
267 try {
268 const res = await axios.get(
269 `${CLOUDFLARE_BASE_URL}/zones`,
270 {
271 headers: {
272 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
273 'Content-Type': 'application/json'
274 }
275 }
276 );
277
278 if (res.data.success) {
279 return res.data.result;
280 }
281 return [];
282 } catch (err) {
283 console.error('[Cloudflare] Error listing zones:', err.response?.data || err.message);
284 return [];
285 }
286}
287
288// ============================================================================
289// DNS Zone & Record Management
290// ============================================================================
291
292/**
293 * Get zone details by domain name
294 * @param {string} domain - Domain name
295 * @returns {Promise<object | null>} - Zone object or null if not found
296 */
297async function getZone(domain) {
298 if (!CLOUDFLARE_API_TOKEN) {
299 throw new Error('Cloudflare API token not configured');
300 }
301
302 try {
303 const res = await axios.get(
304 `${CLOUDFLARE_BASE_URL}/zones?name=${domain}`,
305 {
306 headers: {
307 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
308 'Content-Type': 'application/json'
309 }
310 }
311 );
312
313 if (res.data.success && res.data.result.length > 0) {
314 return res.data.result[0];
315 }
316 return null;
317 } catch (err) {
318 console.error('[Cloudflare] Error getting zone:', err.response?.data || err.message);
319 return null;
320 }
321}
322
323/**
324 * Create a DNS zone in Cloudflare
325 * @param {string} domain - Domain name (e.g., 'example.com')
326 * @param {object} options - Zone options
327 * @returns {Promise<object>} - Zone creation result
328 */
329async function createZone(domain, options = {}) {
330 if (!CLOUDFLARE_API_TOKEN) {
331 throw new Error('Cloudflare API token not configured');
332 }
333
334 if (!CLOUDFLARE_ACCOUNT_ID) {
335 console.warn('[Cloudflare] Account ID not set - zone will be created without account association');
336 }
337
338 try {
339 const payload = {
340 name: domain,
341 jump_start: options.jump_start !== false // Auto-fetch DNS records from domain
342 };
343
344 // Add account if available
345 if (CLOUDFLARE_ACCOUNT_ID) {
346 payload.account = { id: CLOUDFLARE_ACCOUNT_ID };
347 }
348
349 const res = await axios.post(
350 `${CLOUDFLARE_BASE_URL}/zones`,
351 payload,
352 {
353 headers: {
354 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
355 'Content-Type': 'application/json'
356 }
357 }
358 );
359
360 if (res.data.success) {
361 const zone = res.data.result;
362 console.log(`[Cloudflare] Created zone ${domain} (ID: ${zone.id})`);
363 return {
364 zone_id: zone.id,
365 domain: zone.name,
366 status: zone.status,
367 nameservers: zone.name_servers,
368 created_on: zone.created_on
369 };
370 } else {
371 console.error('[Cloudflare] Zone creation failed:', res.data.errors);
372 throw new Error(res.data.errors?.[0]?.message || 'Zone creation failed');
373 }
374 } catch (err) {
375 // Handle zone already exists error
376 if (err.response?.data?.errors?.[0]?.code === 1061) {
377 console.warn(`[Cloudflare] Zone ${domain} already exists`);
378 // Try to get the existing zone
379 const zoneId = await getZoneId(domain);
380 if (zoneId) {
381 const zones = await listZones();
382 const zone = zones.find(z => z.id === zoneId);
383 if (zone) {
384 return {
385 zone_id: zone.id,
386 domain: zone.name,
387 status: zone.status,
388 nameservers: zone.name_servers,
389 created_on: zone.created_on,
390 already_exists: true
391 };
392 }
393 }
394 }
395
396 console.error('[Cloudflare] Error creating zone:', err.response?.data || err.message);
397 throw err;
398 }
399}
400
401/**
402 * Create a DNS record in Cloudflare
403 * @param {string} zoneId - Zone ID
404 * @param {object} record - DNS record details
405 * @returns {Promise<object>} - Created record
406 */
407async function createDNSRecord(zoneId, record) {
408 if (!CLOUDFLARE_API_TOKEN) {
409 throw new Error('Cloudflare API token not configured');
410 }
411
412 const { type, name, content, ttl = 3600, priority, proxied = false } = record;
413
414 if (!type || !name || !content) {
415 throw new Error('DNS record type, name, and content are required');
416 }
417
418 try {
419 const payload = {
420 type,
421 name,
422 content,
423 ttl,
424 proxied: type === 'A' || type === 'AAAA' || type === 'CNAME' ? proxied : undefined
425 };
426
427 // Add priority for MX records
428 if (type === 'MX' && priority !== undefined) {
429 payload.priority = priority;
430 }
431
432 const res = await axios.post(
433 `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records`,
434 payload,
435 {
436 headers: {
437 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
438 'Content-Type': 'application/json'
439 }
440 }
441 );
442
443 if (res.data.success) {
444 console.log(`[Cloudflare] Created ${type} record ${name} -> ${content}`);
445 return res.data.result;
446 } else {
447 console.error('[Cloudflare] DNS record creation failed:', res.data.errors);
448 throw new Error(res.data.errors?.[0]?.message || 'DNS record creation failed');
449 }
450 } catch (err) {
451 // Handle duplicate record error
452 if (err.response?.data?.errors?.[0]?.code === 81057) {
453 console.warn(`[Cloudflare] DNS record ${type} ${name} already exists`);
454 const error = new Error('DNS record already exists');
455 error.code = 'RECORD_EXISTS';
456 throw error;
457 }
458
459 console.error('[Cloudflare] Error creating DNS record:', err.response?.data || err.message);
460 throw err;
461 }
462}
463
464/**
465 * Update a DNS record in Cloudflare
466 * @param {string} zoneId - Zone ID
467 * @param {string} recordId - DNS record ID
468 * @param {object} record - Updated DNS record details
469 * @returns {Promise<object>} - Updated record
470 */
471async function updateDNSRecord(zoneId, recordId, record) {
472 if (!CLOUDFLARE_API_TOKEN) {
473 throw new Error('Cloudflare API token not configured');
474 }
475
476 const { type, name, content, ttl = 3600, priority, proxied = false } = record;
477
478 try {
479 const payload = {
480 type,
481 name,
482 content,
483 ttl,
484 proxied
485 };
486
487 // Add priority for MX records
488 if (type === 'MX' && priority !== undefined) {
489 payload.priority = priority;
490 }
491
492 const res = await axios.put(
493 `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records/${recordId}`,
494 payload,
495 {
496 headers: {
497 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
498 'Content-Type': 'application/json'
499 }
500 }
501 );
502
503 if (res.data.success) {
504 console.log(`[Cloudflare] Updated ${type} record ${name}`);
505 return res.data.result;
506 } else {
507 console.error('[Cloudflare] DNS record update failed:', res.data.errors);
508 throw new Error(res.data.errors?.[0]?.message || 'DNS record update failed');
509 }
510 } catch (err) {
511 console.error('[Cloudflare] Error updating DNS record:', err.response?.data || err.message);
512 throw err;
513 }
514}
515
516/**
517 * Delete a DNS record from Cloudflare
518 * @param {string} zoneId - Zone ID
519 * @param {string} recordId - DNS record ID
520 * @returns {Promise<boolean>} - Success status
521 */
522async function deleteDNSRecord(zoneId, recordId) {
523 if (!CLOUDFLARE_API_TOKEN) {
524 throw new Error('Cloudflare API token not configured');
525 }
526
527 try {
528 const res = await axios.delete(
529 `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records/${recordId}`,
530 {
531 headers: {
532 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
533 'Content-Type': 'application/json'
534 }
535 }
536 );
537
538 if (res.data.success) {
539 console.log(`[Cloudflare] Deleted DNS record ${recordId}`);
540 return true;
541 } else {
542 console.error('[Cloudflare] DNS record deletion failed:', res.data.errors);
543 throw new Error(res.data.errors?.[0]?.message || 'DNS record deletion failed');
544 }
545 } catch (err) {
546 console.error('[Cloudflare] Error deleting DNS record:', err.response?.data || err.message);
547 throw err;
548 }
549}
550
551/**
552 * List all DNS records for a zone
553 * @param {string} zoneId - Zone ID
554 * @returns {Promise<Array>} - Array of DNS records
555 */
556async function listDNSRecords(zoneId) {
557 if (!CLOUDFLARE_API_TOKEN) {
558 throw new Error('Cloudflare API token not configured');
559 }
560
561 try {
562 const res = await axios.get(
563 `${CLOUDFLARE_BASE_URL}/zones/${zoneId}/dns_records?per_page=100`,
564 {
565 headers: {
566 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
567 'Content-Type': 'application/json'
568 }
569 }
570 );
571
572 if (res.data.success) {
573 return res.data.result;
574 }
575 return [];
576 } catch (err) {
577 console.error('[Cloudflare] Error listing DNS records:', err.response?.data || err.message);
578 return [];
579 }
580}
581
582/**
583 * Update nameservers for a domain (for registered domains only)
584 * @param {string} domain - Domain name
585 * @param {Array<string>} nameservers - Array of nameserver hostnames
586 * @returns {Promise<object>} - Update result
587 */
588async function updateNameservers(domain, nameservers) {
589 if (!CLOUDFLARE_ACCOUNT_ID || !CLOUDFLARE_API_TOKEN) {
590 throw new Error('Cloudflare Registrar credentials required to update nameservers');
591 }
592
593 try {
594 const res = await axios.put(
595 `${CLOUDFLARE_BASE_URL}/accounts/${CLOUDFLARE_ACCOUNT_ID}/registrar/domains/${domain}`,
596 {
597 nameservers: nameservers
598 },
599 {
600 headers: {
601 Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
602 'Content-Type': 'application/json'
603 }
604 }
605 );
606
607 if (res.data.success) {
608 console.log(`[Cloudflare] Updated nameservers for ${domain}`);
609 return res.data.result;
610 } else {
611 throw new Error(res.data.errors?.[0]?.message || 'Nameserver update failed');
612 }
613 } catch (err) {
614 console.error('[Cloudflare] Error updating nameservers:', err.response?.data || err.message);
615 throw err;
616 }
617}
618
619module.exports = {
620 createSubdomain,
621 searchDomain,
622 registerDomain,
623 getDomainDetails,
624 getZoneId,
625 getZone,
626 listZones,
627 createZone,
628 createDNSRecord,
629 updateDNSRecord,
630 deleteDNSRecord,
631 listDNSRecords,
632 updateNameservers
633};
634
635