EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
cloudflare-dns.js
Go to the documentation of this file.
1/**
2 * @file routes/cloudflare-dns.js
3 * @module routes/cloudflare-dns
4 * @description
5 * Cloudflare DNS management API for zone and record CRUD operations.
6 * Provides comprehensive DNS record management with Redis caching for performance.
7 *
8 * **Core Features:**
9 * - Zone listing and zone ID retrieval
10 * - DNS record CRUD (Create, Read, Update, Delete)
11 * - Redis cache with 5-minute sync interval
12 * - Cache invalidation on mutations
13 * - Support for all DNS record types (A, AAAA, CNAME, MX, TXT, etc.)
14 * - Cloudflare proxy toggle support
15 * - Audit logging for all changes
16 *
17 * **Caching Strategy:**
18 * - GET requests use Redis cache (5-min TTL)
19 * - Force refresh with ?refresh=true query parameter
20 * - Cache automatically invalidated on POST/PUT/DELETE
21 * - Background worker syncs cache periodically
22 *
23 * **Security:**
24 * - All routes require authentication
25 * - User actions logged with username
26 * - Cloudflare API token from environment
27 * @requires express
28 * @requires ../middleware/auth
29 * @requires ../services/cloudflare
30 * @requires ../dnsRecordsSyncWorker
31 * @author RMM-PSA Development Team
32 * @date 2026-03-12
33 * @since 1.0.0
34 */
35const express = require('express');
36const router = express.Router();
37const authenticateToken = require('../middleware/auth');
38const { getTenantFilter } = require('../middleware/tenant');
39const pool = require('../services/db');
40const {
41 getZoneId,
42 listDNSRecords,
43 createDNSRecord,
44 updateDNSRecord,
45 deleteDNSRecord,
46 listZones
47} = require('../services/cloudflare');
48const { getCachedDNSRecords, invalidateDNSCache } = require('../dnsRecordsSyncWorker');
49
50// Apply authentication to all routes
51router.use(authenticateToken);
52
53/**
54 * @api {get} /cloudflare-dns/zones List Cloudflare zones
55 * @apiName ListCloudflareZones
56 * @apiGroup CloudflareDNS
57 * @apiDescription
58 * Retrieve all Cloudflare zones (domains) managed under the account.
59 * Returns zone metadata including names, IDs, and status.
60 * @apiHeader {string} Authorization Bearer JWT token
61 * @apiSuccess {boolean} success Operation success flag
62 * @apiSuccess {object[]} zones Array of zone objects
63 * @apiSuccess {string} zones.id Zone ID
64 * @apiSuccess {string} zones.name Domain name
65 * @apiSuccess {string} zones.status Zone status (active, pending, moved, etc.)
66 * @apiSuccess {DateTime} zones.created_on Zone creation date
67 * @apiError (500) {Boolean} success=false Operation failed
68 * @apiError (500) {String} error Error message
69 * @apiError (500) {String} details Detailed error information
70 * @apiExample {curl} Example usage:
71 * curl -X GET https://api.example.com/cloudflare-dns/zones \\
72 * -H "Authorization: Bearer YOUR_TOKEN"
73 * @apiSuccessExample {json} Success-Response:
74 * HTTP/1.1 200 OK
75 * {
76 * "success": true,
77 * "zones": [
78 * {
79 * "id": "abc123",
80 * "name": "example.com",
81 * "status": "active",
82 * "created_on": "2026-01-15T10:00:00Z"
83 * }
84 * ]
85 * }
86 */
87router.get('/zones', async (req, res) => {
88 try {
89 const zones = await listZones();
90 res.json({ success: true, zones });
91 } catch (err) {
92 console.error('Error fetching Cloudflare zones:', err);
93 res.status(500).json({
94 success: false,
95 error: 'Failed to fetch zones',
96 details: err.message
97 });
98 }
99});
100
101/**
102 * @api {get} /cloudflare-dns/:domain/zone-id Get zone ID
103 * @apiName GetZoneId
104 * @apiGroup CloudflareDNS
105 * @apiDescription
106 * Retrieve Cloudflare zone ID for a specific domain.
107 * Zone IDs are required for DNS record operations.
108 * @apiHeader {string} Authorization Bearer JWT token
109 * @apiParam {string} domain Domain name (e.g., "example.com")
110 * @apiSuccess {boolean} success Operation success flag
111 * @apiSuccess {string} zone_id Cloudflare zone identifier
112 * @apiSuccess {string} domain Domain name
113 * @apiError (404) {Boolean} success=false Zone not found
114 * @apiError (404) {String} error="Zone not found in Cloudflare"
115 * @apiError (500) {Boolean} success=false Operation failed
116 * @apiError (500) {String} error Error message
117 * @apiError (500) {String} details Detailed error information
118 * @apiExample {curl} Example usage:
119 * curl -X GET https://api.example.com/cloudflare-dns/example.com/zone-id \\
120 * -H "Authorization: Bearer YOUR_TOKEN"
121 * @apiSuccessExample {json} Success-Response:
122 * HTTP/1.1 200 OK
123 * {
124 * "success": true,
125 * "zone_id": "abc123xyz",
126 * "domain": "example.com"
127 * }
128 */
129router.get('/:domain/zone-id', async (req, res) => {
130 const { domain } = req.params;
131
132 try {
133 const zoneId = await getZoneId(domain);
134
135 if (!zoneId) {
136 return res.status(404).json({
137 success: false,
138 error: 'Zone not found in Cloudflare'
139 });
140 }
141
142 res.json({ success: true, zone_id: zoneId, domain });
143 } catch (err) {
144 console.error(`Error getting zone ID for ${domain}:`, err);
145 res.status(500).json({
146 success: false,
147 error: 'Failed to get zone ID',
148 details: err.message
149 });
150 }
151});
152
153/**
154 * @api {get} /cloudflare-dns/:domain/records List DNS records
155 * @apiName ListDNSRecords
156 * @apiGroup CloudflareDNS
157 * @apiDescription
158 * Retrieve all DNS records for a domain with Redis caching.
159 * First checks Redis cache (5-min TTL), falls back to Cloudflare API on cache miss.
160 * Returns all record types: A, AAAA, CNAME, MX, TXT, SRV, etc.
161 * @apiHeader {string} Authorization Bearer JWT token
162 * @apiParam {string} domain Domain name (e.g., "example.com")
163 * @apiParam {boolean} [refresh=false] Force refresh from Cloudflare API (bypass cache)
164 * @apiSuccess {boolean} success Operation success flag
165 * @apiSuccess {string} domain Domain name
166 * @apiSuccess {string} [zone_id] Cloudflare zone ID (if fetched from API)
167 * @apiSuccess {object[]} records Array of DNS record objects
168 * @apiSuccess {string} records.id Record ID
169 * @apiSuccess {string} records.type Record type (A, AAAA, CNAME, MX, TXT, etc.)
170 * @apiSuccess {string} records.name Record name/hostname
171 * @apiSuccess {string} records.content Record value/target
172 * @apiSuccess {number} records.ttl Time-to-live in seconds
173 * @apiSuccess {boolean} records.proxied Cloudflare proxy status (orange cloud)
174 * @apiSuccess {number} [records.priority] Priority (MX/SRV records)
175 * @apiSuccess {DateTime} last_synced Last sync timestamp
176 * @apiSuccess {boolean} from_cache Whether data came from Redis cache
177 * @apiError (404) {Boolean} success=false Zone not found
178 * @apiError (404) {String} error="Zone not found in Cloudflare"
179 * @apiError (500) {Boolean} success=false Operation failed
180 * @apiError (500) {String} error Error message
181 * @apiError (500) {String} details Detailed error information
182 * @apiExample {curl} Example usage (cached):
183 * curl -X GET https://api.example.com/cloudflare-dns/example.com/records \\
184 * -H "Authorization: Bearer YOUR_TOKEN"
185 *
186 * @apiExample {curl} Force refresh from API:
187 * curl -X GET https://api.example.com/cloudflare-dns/example.com/records?refresh=true \\
188 * -H "Authorization: Bearer YOUR_TOKEN"
189 *
190 * @apiSuccessExample {json} Success-Response (cached):
191 * HTTP/1.1 200 OK
192 * {
193 * "success": true,
194 * "domain": "example.com",
195 * "records": [
196 * {
197 * "id": "rec123",
198 * "type": "A",
199 * "name": "example.com",
200 * "content": "192.0.2.1",
201 * "ttl": 3600,
202 * "proxied": true
203 * },
204 * {
205 * "id": "rec456",
206 * "type": "CNAME",
207 * "name": "www.example.com",
208 * "content": "example.com",
209 * "ttl": 3600,
210 * "proxied": true
211 * }
212 * ],
213 * "last_synced": "2026-03-12T10:05:00Z",
214 * "from_cache": true
215 * }
216 */
217router.get('/:domain/records', async (req, res) => {
218 const { domain } = req.params;
219 const { refresh } = req.query; // Optional: force refresh from API
220
221 try {
222 // Try cache first (unless refresh requested)
223 if (!refresh) {
224 const cached = await getCachedDNSRecords(domain);
225
226 if (cached) {
227 console.log(`[DNS] Cache hit for ${domain} (${cached.records.length} records, synced: ${cached.last_synced})`);
228 return res.json({
229 success: true,
230 domain,
231 records: cached.records,
232 last_synced: cached.last_synced,
233 from_cache: true
234 });
235 }
236 }
237
238 // Cache miss or refresh requested - fetch from Cloudflare API
239 console.log(`[DNS] Cache miss for ${domain}, fetching from Cloudflare...`);
240
241 // Get zone ID first
242 const zoneId = await getZoneId(domain);
243
244 if (!zoneId) {
245 return res.status(404).json({
246 success: false,
247 error: 'Zone not found in Cloudflare'
248 });
249 }
250
251 // Get all DNS records from API
252 const records = await listDNSRecords(zoneId);
253
254 res.json({
255 success: true,
256 domain,
257 zone_id: zoneId,
258 records,
259 last_synced: new Date().toISOString(),
260 from_cache: false
261 });
262 } catch (err) {
263 console.error(`Error fetching DNS records for ${domain}:`, err);
264 res.status(500).json({
265 success: false,
266 error: 'Failed to fetch DNS records',
267 details: err.message
268 });
269 }
270});
271
272/**
273 * @api {post} /cloudflare-dns/:domain/records Create DNS record
274 * @apiName CreateDNSRecord
275 * @apiGroup CloudflareDNS
276 * @apiDescription
277 * Create a new DNS record in Cloudflare zone.
278 * Supports all DNS record types with optional priority and proxy settings.
279 * Automatically invalidates Redis cache on success.
280 * @apiHeader {string} Authorization Bearer JWT token
281 * @apiHeader {string} Content-Type application/json
282 * @apiParam {string} domain Domain name (URL parameter)
283 * @apiParam {string} type Record type (A, AAAA, CNAME, MX, TXT, SRV, etc.)
284 * @apiParam {string} name Record name/hostname (e.g., "www" or "@" for root)
285 * @apiParam {string} content Record value (IP address, hostname, or text)
286 * @apiParam {number} [ttl=3600] Time-to-live in seconds (1=auto)
287 * @apiParam {number} [priority] Priority (required for MX/SRV records)
288 * @apiParam {boolean} [proxied=false] Enable Cloudflare proxy (orange cloud)
289 * @apiSuccess {boolean} success=true Operation succeeded
290 * @apiSuccess {string} message="DNS record created successfully"
291 * @apiSuccess {Object} record Created DNS record object with Cloudflare metadata
292 * @apiError (400) {Boolean} success=false Validation failed
293 * @apiError (400) {String} error="Missing required fields: type, name, content"
294 * @apiError (404) {Boolean} success=false Zone not found
295 * @apiError (404) {String} error="Zone not found in Cloudflare"
296 * @apiError (409) {Boolean} success=false Record already exists
297 * @apiError (409) {String} error="DNS record already exists"
298 * @apiError (500) {Boolean} success=false Operation failed
299 * @apiError (500) {String} error Error message
300 * @apiError (500) {String} details Detailed error information
301 * @apiExample {curl} Create A record:
302 * curl -X POST https://api.example.com/cloudflare-dns/example.com/records \\
303 * -H "Authorization: Bearer YOUR_TOKEN" \\
304 * -H "Content-Type: application/json" \\
305 * -d '{
306 * "type": "A",
307 * "name": "www",
308 * "content": "192.0.2.1",
309 * "ttl": 3600,
310 * "proxied": true
311 * }'
312 * @apiExample {curl} Create MX record:
313 * curl -X POST https://api.example.com/cloudflare-dns/example.com/records \\
314 * -H "Authorization: Bearer YOUR_TOKEN" \\
315 * -H "Content-Type: application/json" \\
316 * -d '{
317 * "type": "MX",
318 * "name": "@",
319 * "content": "mail.example.com",
320 * "ttl": 3600,
321 * "priority": 10
322 * }'
323 * @apiSuccessExample {json} Success-Response:
324 * HTTP/1.1 200 OK
325 * {
326 * "success": true,
327 * "message": "DNS record created successfully",
328 * "record": {
329 * "id": "newrec789",
330 * "type": "A",
331 * "name": "www.example.com",
332 * "content": "192.0.2.1",
333 * "ttl": 3600,
334 * "proxied": true
335 * }
336 * }
337 */
338router.post('/:domain/records', async (req, res) => {
339 const { domain } = req.params;
340 const { type, name, content, ttl, priority, proxied } = req.body;
341
342 try {
343 // Validate required fields
344 if (!type || !name || !content) {
345 return res.status(400).json({
346 success: false,
347 error: 'Missing required fields: type, name, content'
348 });
349 }
350
351 // Get zone ID
352 const zoneId = await getZoneId(domain);
353
354 if (!zoneId) {
355 return res.status(404).json({
356 success: false,
357 error: 'Zone not found in Cloudflare'
358 });
359 }
360
361 // Create DNS record
362 const record = await createDNSRecord(zoneId, {
363 type,
364 name,
365 content,
366 ttl: ttl || 3600,
367 priority,
368 proxied: proxied !== undefined ? proxied : false
369 });
370
371 // Invalidate cache so next fetch gets fresh data
372 await invalidateDNSCache(domain);
373
374 // Log the change
375 console.log(`[DNS] User ${req.user.username} created ${type} record ${name} for ${domain}`);
376
377 res.json({
378 success: true,
379 message: 'DNS record created successfully',
380 record
381 });
382 } catch (err) {
383 console.error(`Error creating DNS record for ${domain}:`, err);
384
385 if (err.code === 'RECORD_EXISTS') {
386 return res.status(409).json({
387 success: false,
388 error: 'DNS record already exists'
389 });
390 }
391
392 res.status(500).json({
393 success: false,
394 error: 'Failed to create DNS record',
395 details: err.message
396 });
397 }
398});
399
400/**
401 * @api {put} /cloudflare-dns/:domain/records/:recordId Update DNS record
402 * @apiName UpdateDNSRecord
403 * @apiGroup CloudflareDNS
404 * @apiDescription
405 * Update an existing DNS record in Cloudflare zone.
406 * Requires all fields (partial updates not supported by Cloudflare API).
407 * Automatically invalidates Redis cache on success.
408 * @apiHeader {string} Authorization Bearer JWT token
409 * @apiHeader {string} Content-Type application/json
410 * @apiParam {string} domain Domain name (URL parameter)
411 * @apiParam {string} recordId Cloudflare record ID (URL parameter)
412 * @apiParam {string} type Record type (A, AAAA, CNAME, MX, TXT, etc.)
413 * @apiParam {string} name Record name/hostname
414 * @apiParam {string} content Record value
415 * @apiParam {number} [ttl=3600] Time-to-live in seconds
416 * @apiParam {number} [priority] Priority (for MX/SRV records)
417 * @apiParam {boolean} [proxied=false] Cloudflare proxy status
418 * @apiSuccess {boolean} success=true Operation succeeded
419 * @apiSuccess {string} message="DNS record updated successfully"
420 * @apiSuccess {Object} record Updated DNS record object
421 * @apiError (400) {Boolean} success=false Validation failed
422 * @apiError (400) {String} error="Missing required fields: type, name, content"
423 * @apiError (404) {Boolean} success=false Zone or record not found
424 * @apiError (404) {String} error="Zone not found in Cloudflare"
425 * @apiError (500) {Boolean} success=false Operation failed
426 * @apiError (500) {String} error Error message
427 * @apiError (500) {String} details Detailed error information
428 * @apiExample {curl} Update A record:
429 * curl -X PUT https://api.example.com/cloudflare-dns/example.com/records/rec123 \\
430 * -H "Authorization: Bearer YOUR_TOKEN" \\
431 * -H "Content-Type: application/json" \\
432 * -d '{
433 * "type": "A",
434 * "name": "www",
435 * "content": "192.0.2.2",
436 * "ttl": 1800,
437 * "proxied": false
438 * }'
439 * @apiSuccessExample {json} Success-Response:
440 * HTTP/1.1 200 OK
441 * {
442 * "success": true,
443 * "message": "DNS record updated successfully",
444 * "record": {
445 * "id": "rec123",
446 * "type": "A",
447 * "name": "www.example.com",
448 * "content": "192.0.2.2",
449 * "ttl": 1800,
450 * "proxied": false
451 * }
452 * }
453 */
454router.put('/:domain/records/:recordId', async (req, res) => {
455 const { domain, recordId } = req.params;
456 const { type, name, content, ttl, priority, proxied } = req.body;
457
458 try {
459 // Validate required fields
460 if (!type || !name || !content) {
461 return res.status(400).json({
462 success: false,
463 error: 'Missing required fields: type, name, content'
464 });
465 }
466
467 // Get zone ID
468 const zoneId = await getZoneId(domain);
469
470 if (!zoneId) {
471 return res.status(404).json({
472 success: false,
473 error: 'Zone not found in Cloudflare'
474 });
475 }
476
477 // Update DNS record
478 const record = await updateDNSRecord(zoneId, recordId, {
479 type,
480 name,
481 content,
482 ttl: ttl || 3600,
483 priority,
484 proxied: proxied !== undefined ? proxied : false
485 });
486
487 // Invalidate cache so next fetch gets fresh data
488 await invalidateDNSCache(domain);
489
490 // Log the change
491 console.log(`[DNS] User ${req.user.username} updated ${type} record ${name} for ${domain}`);
492
493 res.json({
494 success: true,
495 message: 'DNS record updated successfully',
496 record
497 });
498 } catch (err) {
499 console.error(`Error updating DNS record ${recordId} for ${domain}:`, err);
500 res.status(500).json({
501 success: false,
502 error: 'Failed to update DNS record',
503 details: err.message
504 });
505 }
506});
507
508/**
509 * @api {delete} /cloudflare-dns/:domain/records/:recordId Delete DNS record
510 * @apiName DeleteDNSRecord
511 * @apiGroup CloudflareDNS
512 * @apiDescription
513 * Delete a DNS record from Cloudflare zone.
514 * Permanently removes the record and invalidates Redis cache.
515 * Action is logged with username for audit trail.
516 * @apiHeader {string} Authorization Bearer JWT token
517 * @apiParam {string} domain Domain name (URL parameter)
518 * @apiParam {string} recordId Cloudflare record ID (URL parameter)
519 * @apiSuccess {boolean} success=true Operation succeeded
520 * @apiSuccess {string} message="DNS record deleted successfully"
521 * @apiError (404) {Boolean} success=false Zone or record not found
522 * @apiError (404) {String} error="Zone not found in Cloudflare"
523 * @apiError (500) {Boolean} success=false Operation failed
524 * @apiError (500) {String} error Error message
525 * @apiError (500) {String} details Detailed error information
526 * @apiExample {curl} Delete record:
527 * curl -X DELETE https://api.example.com/cloudflare-dns/example.com/records/rec123 \\
528 * -H "Authorization: Bearer YOUR_TOKEN"
529 * @apiSuccessExample {json} Success-Response:
530 * HTTP/1.1 200 OK
531 * {
532 * "success": true,
533 * "message": "DNS record deleted successfully"
534 * }
535 */
536router.delete('/:domain/records/:recordId', async (req, res) => {
537 const { domain, recordId } = req.params;
538
539 try {
540 // Get zone ID
541 const zoneId = await getZoneId(domain);
542
543 if (!zoneId) {
544 return res.status(404).json({
545 success: false,
546 error: 'Zone not found in Cloudflare'
547 });
548 }
549
550 // Delete DNS record
551 await deleteDNSRecord(zoneId, recordId);
552
553 // Invalidate cache so next fetch gets fresh data
554 await invalidateDNSCache(domain);
555
556 // Log the change
557 console.log(`[DNS] User ${req.user.username} deleted record ${recordId} for ${domain}`);
558
559 res.json({
560 success: true,
561 message: 'DNS record deleted successfully'
562 });
563 } catch (err) {
564 console.error(`Error deleting DNS record ${recordId} for ${domain}:`, err);
565 res.status(500).json({
566 success: false,
567 error: 'Failed to delete DNS record',
568 details: err.message
569 });
570 }
571});
572
573module.exports = router;