2 * @file routes/cloudflare-dns.js
3 * @module routes/cloudflare-dns
5 * Cloudflare DNS management API for zone and record CRUD operations.
6 * Provides comprehensive DNS record management with Redis caching for performance.
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
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
24 * - All routes require authentication
25 * - User actions logged with username
26 * - Cloudflare API token from environment
28 * @requires ../middleware/auth
29 * @requires ../services/cloudflare
30 * @requires ../dnsRecordsSyncWorker
31 * @author RMM-PSA Development Team
35const express = require('express');
36const router = express.Router();
37const authenticateToken = require('../middleware/auth');
38const { getTenantFilter } = require('../middleware/tenant');
39const pool = require('../services/db');
47} = require('../services/cloudflare');
48const { getCachedDNSRecords, invalidateDNSCache } = require('../dnsRecordsSyncWorker');
50// Apply authentication to all routes
51router.use(authenticateToken);
54 * @api {get} /cloudflare-dns/zones List Cloudflare zones
55 * @apiName ListCloudflareZones
56 * @apiGroup CloudflareDNS
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:
80 * "name": "example.com",
82 * "created_on": "2026-01-15T10:00:00Z"
87router.get('/zones', async (req, res) => {
89 const zones = await listZones();
90 res.json({ success: true, zones });
92 console.error('Error fetching Cloudflare zones:', err);
93 res.status(500).json({
95 error: 'Failed to fetch zones',
102 * @api {get} /cloudflare-dns/:domain/zone-id Get zone ID
104 * @apiGroup CloudflareDNS
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:
125 * "zone_id": "abc123xyz",
126 * "domain": "example.com"
129router.get('/:domain/zone-id', async (req, res) => {
130 const { domain } = req.params;
133 const zoneId = await getZoneId(domain);
136 return res.status(404).json({
138 error: 'Zone not found in Cloudflare'
142 res.json({ success: true, zone_id: zoneId, domain });
144 console.error(`Error getting zone ID for ${domain}:`, err);
145 res.status(500).json({
147 error: 'Failed to get zone ID',
154 * @api {get} /cloudflare-dns/:domain/records List DNS records
155 * @apiName ListDNSRecords
156 * @apiGroup CloudflareDNS
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"
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"
190 * @apiSuccessExample {json} Success-Response (cached):
194 * "domain": "example.com",
199 * "name": "example.com",
200 * "content": "192.0.2.1",
207 * "name": "www.example.com",
208 * "content": "example.com",
213 * "last_synced": "2026-03-12T10:05:00Z",
217router.get('/:domain/records', async (req, res) => {
218 const { domain } = req.params;
219 const { refresh } = req.query; // Optional: force refresh from API
222 // Try cache first (unless refresh requested)
224 const cached = await getCachedDNSRecords(domain);
227 console.log(`[DNS] Cache hit for ${domain} (${cached.records.length} records, synced: ${cached.last_synced})`);
231 records: cached.records,
232 last_synced: cached.last_synced,
238 // Cache miss or refresh requested - fetch from Cloudflare API
239 console.log(`[DNS] Cache miss for ${domain}, fetching from Cloudflare...`);
242 const zoneId = await getZoneId(domain);
245 return res.status(404).json({
247 error: 'Zone not found in Cloudflare'
251 // Get all DNS records from API
252 const records = await listDNSRecords(zoneId);
259 last_synced: new Date().toISOString(),
263 console.error(`Error fetching DNS records for ${domain}:`, err);
264 res.status(500).json({
266 error: 'Failed to fetch DNS records',
273 * @api {post} /cloudflare-dns/:domain/records Create DNS record
274 * @apiName CreateDNSRecord
275 * @apiGroup CloudflareDNS
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" \\
308 * "content": "192.0.2.1",
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" \\
319 * "content": "mail.example.com",
323 * @apiSuccessExample {json} Success-Response:
327 * "message": "DNS record created successfully",
331 * "name": "www.example.com",
332 * "content": "192.0.2.1",
338router.post('/:domain/records', async (req, res) => {
339 const { domain } = req.params;
340 const { type, name, content, ttl, priority, proxied } = req.body;
343 // Validate required fields
344 if (!type || !name || !content) {
345 return res.status(400).json({
347 error: 'Missing required fields: type, name, content'
352 const zoneId = await getZoneId(domain);
355 return res.status(404).json({
357 error: 'Zone not found in Cloudflare'
362 const record = await createDNSRecord(zoneId, {
368 proxied: proxied !== undefined ? proxied : false
371 // Invalidate cache so next fetch gets fresh data
372 await invalidateDNSCache(domain);
375 console.log(`[DNS] User ${req.user.username} created ${type} record ${name} for ${domain}`);
379 message: 'DNS record created successfully',
383 console.error(`Error creating DNS record for ${domain}:`, err);
385 if (err.code === 'RECORD_EXISTS') {
386 return res.status(409).json({
388 error: 'DNS record already exists'
392 res.status(500).json({
394 error: 'Failed to create DNS record',
401 * @api {put} /cloudflare-dns/:domain/records/:recordId Update DNS record
402 * @apiName UpdateDNSRecord
403 * @apiGroup CloudflareDNS
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" \\
435 * "content": "192.0.2.2",
439 * @apiSuccessExample {json} Success-Response:
443 * "message": "DNS record updated successfully",
447 * "name": "www.example.com",
448 * "content": "192.0.2.2",
454router.put('/:domain/records/:recordId', async (req, res) => {
455 const { domain, recordId } = req.params;
456 const { type, name, content, ttl, priority, proxied } = req.body;
459 // Validate required fields
460 if (!type || !name || !content) {
461 return res.status(400).json({
463 error: 'Missing required fields: type, name, content'
468 const zoneId = await getZoneId(domain);
471 return res.status(404).json({
473 error: 'Zone not found in Cloudflare'
478 const record = await updateDNSRecord(zoneId, recordId, {
484 proxied: proxied !== undefined ? proxied : false
487 // Invalidate cache so next fetch gets fresh data
488 await invalidateDNSCache(domain);
491 console.log(`[DNS] User ${req.user.username} updated ${type} record ${name} for ${domain}`);
495 message: 'DNS record updated successfully',
499 console.error(`Error updating DNS record ${recordId} for ${domain}:`, err);
500 res.status(500).json({
502 error: 'Failed to update DNS record',
509 * @api {delete} /cloudflare-dns/:domain/records/:recordId Delete DNS record
510 * @apiName DeleteDNSRecord
511 * @apiGroup CloudflareDNS
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:
533 * "message": "DNS record deleted successfully"
536router.delete('/:domain/records/:recordId', async (req, res) => {
537 const { domain, recordId } = req.params;
541 const zoneId = await getZoneId(domain);
544 return res.status(404).json({
546 error: 'Zone not found in Cloudflare'
551 await deleteDNSRecord(zoneId, recordId);
553 // Invalidate cache so next fetch gets fresh data
554 await invalidateDNSCache(domain);
557 console.log(`[DNS] User ${req.user.username} deleted record ${recordId} for ${domain}`);
561 message: 'DNS record deleted successfully'
564 console.error(`Error deleting DNS record ${recordId} for ${domain}:`, err);
565 res.status(500).json({
567 error: 'Failed to delete DNS record',
573module.exports = router;