2 * @file Domain Registration Request Management API Routes
4 * This module provides API endpoints for managing domain registration requests,
5 * supporting full registration workflow from submission to approval/denial, with
6 * multi-registrar integration (Moniker, eNom, Cloudflare) and automated DNS zone
10 * - **Multi-registrar support**: Moniker, eNom, Cloudflare DNS-only
11 * - **Approval workflow**: Submit, review, approve/deny with audit trail
12 * - **DNS automation**: Automatic Cloudflare zone creation upon approval
13 * - **Contact management**: Full WHOIS contact data (registrant, admin, tech, billing)
14 * - **Tenant isolation**: Multi-tenant with MSP/root tenant approval permissions
15 * - **Notification system**: Automated notifications for request lifecycle events
16 * - **Domain types**: Registration (with registrar APIs) or DNS-only (external registrar)
18 * Registrar integration:
19 * - **Moniker**: Full domain registration with WHOIS contacts via Moniker API
20 * - **eNom**: Full domain registration via eNom reseller API
21 * - **Cloudflare**: DNS-only mode (zone creation, customer updates nameservers)
22 * - **External**: DNS-only with Cloudflare zone for domains registered elsewhere
25 * - All routes require authentication via JWT tokens
26 * - Tenant context applied for data isolation
27 * - Only root tenant/MSP admins can approve/deny/update requests
28 * - Standard users can submit and view their own requests
31 * - `domain_registration_requests`: Request lifecycle (pending, approved, denied, registered, failed)
32 * - `domains`: Registered/active domains linked to customers and contracts
33 * - `notifications`: Request status notifications for customers and admins
35 * @requires ../services/db - PostgreSQL database pool
36 * @requires ../middleware/auth - JWT authentication
37 * @requires ../middleware/tenant - Multi-tenant context and filtering
38 * @requires ../services/moniker - Moniker domain registrar API client
39 * @requires ../services/enom - eNom domain registrar API client
40 * @requires ../services/cloudflare - Cloudflare DNS zone management API client
41 * @module routes/domainRequests
42 * @see {@link module:services/moniker}
43 * @see {@link module:services/enom}
44 * @see {@link module:services/cloudflare}
47const express = require('express');
48const router = express.Router();
49const pool = require('../services/db');
50const authenticateToken = require('../middleware/auth');
51const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
52const moniker = require('../services/moniker');
53const enom = require('../services/enom');
54const { createZone, registerDomain: registerCloudflare } = require('../services/cloudflare');
56// Apply authentication and tenant context to all routes
57router.use(authenticateToken, setTenantContext);
60 * @api {post} /domain-requests Submit Domain Registration Request
61 * @apiName SubmitDomainRequest
62 * @apiGroup DomainRequests
64 * Creates a new domain registration request with complete WHOIS contact information.
65 * Automatically sends notification to root tenant admins for approval. Supports both
66 * full registration via Moniker/eNom or DNS-only mode for externally-registered domains.
68 * Request body example:
71 * "domain_name": "example.com",
72 * "domain_type": "registration", // or "dns-only"
78 * "nameservers": ["ns1.example.com", "ns2.example.com"],
79 * "registrant_contact": {
80 * "firstName": "John",
82 * "email": "john@example.com",
83 * "phone": "+1.5555551234",
84 * "address": "123 Main St",
85 * "city": "San Francisco",
87 * "postalCode": "94105",
89 * "organization": "Example Corp"
91 * "admin_contact": { ... }, // Optional, defaults to registrant_contact
92 * "tech_contact": { ... }, // Optional, defaults to registrant_contact
93 * "billing_contact": { ... } // Optional, defaults to registrant_contact
96 * @apiPermission authenticated
98 * @apiBody {string} domain_name Domain name to register (e.g., "example.com")
99 * @apiBody {String="registration","dns-only"} domain_type Registration type
100 * @apiBody {number} customer_id Customer ID from customers table
101 * @apiBody {number} [contract_id] Associated contract ID
102 * @apiBody {number} years Registration duration (typically 1-10 years)
103 * @apiBody {number} price Domain registration/renewal price
104 * @apiBody {String="USD","EUR","GBP"} currency Price currency code
105 * @apiBody {string[]} [nameservers] Custom nameservers (optional, Cloudflare used if omitted)
106 * @apiBody {object} registrant_contact WHOIS registrant contact information
107 * @apiBody {object} [admin_contact] WHOIS admin contact (defaults to registrant)
108 * @apiBody {Object} [tech_contact] WHOIS technical contact (defaults to registrant)
109 * @apiBody {Object} [billing_contact] WHOIS billing contact (defaults to registrant)
110 * @apiSuccess {boolean} success Operation success status (true)
111 * @apiSuccess {number} request_id Created domain registration request ID
112 * @apiSuccess {string} message Success message
113 * @apiError {string} error Error message
114 * @apiError {string} details Detailed error description
115 * @apiError {string} timestamp ISO timestamp of error occurrence
116 * @apiExample {curl} Example Request:
117 * curl -X POST https://api.ibghub.com/api/domain-requests \
118 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
119 * -H "Content-Type: application/json" \
120 * -d '{"domain_name":"example.com","domain_type":"registration","customer_id":123,"years":1,"price":12.99,"currency":"USD","registrant_contact":{...}}'
123// POST /domain-requests - Submit domain registration request
124router.post('/', async (req, res) => {
125 const requestTimestamp = new Date().toISOString();
127 // Enhanced debug logging for analysis
128 console.log('========================================');
129 console.log('[Domain Request] NEW DOMAIN REGISTRATION REQUEST');
130 console.log('[Domain Request] Timestamp:', requestTimestamp);
131 console.log('[Domain Request] User:', {
132 userId: req.user?.user_id,
133 email: req.user?.email,
134 tenantId: req.user?.tenantId,
137 console.log('[Domain Request] Tenant:', {
139 isMsp: req.tenant?.isMsp,
140 subdomain: req.tenant?.subdomain
142 console.log('[Domain Request] Request details:', {
143 contentType: req.get('Content-Type'),
144 ip: req.ip || req.connection.remoteAddress,
145 userAgent: req.get('User-Agent')
147 console.log('[Domain Request] Body:', JSON.stringify(req.body, null, 2));
148 console.log('[Domain Request] Body keys:', req.body ? Object.keys(req.body) : 'undefined');
150 // Check if body exists
151 if (!req.body || Object.keys(req.body).length === 0) {
152 return res.status(400).json({
153 error: 'Request body is required',
155 contentType: req.get('Content-Type'),
157 bodyKeys: req.body ? Object.keys(req.body) : []
166 domain_type = 'registration',
178 if (!domain_name || !registrant_contact) {
179 return res.status(400).json({ error: 'Domain name and registrant contact are required' });
182 const tenantId = req.tenant?.id;
183 const userId = req.user?.user_id;
185 // Insert domain registration request
186 const result = await pool.query(
187 `INSERT INTO domain_registration_requests (
188 tenant_id, customer_id, contract_id, domain_name, domain_type, years, price, currency,
189 nameservers, registrant_contact, admin_contact, tech_contact, billing_contact,
191 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
194 tenantId, customer_id, contract_id, domain_name, domain_type, years, price, currency,
195 JSON.stringify(nameservers || []),
196 JSON.stringify(registrant_contact),
197 JSON.stringify(admin_contact || registrant_contact),
198 JSON.stringify(tech_contact || registrant_contact),
199 JSON.stringify(billing_contact || registrant_contact),
204 const request = result.rows[0];
206 console.log('[Domain Request] ✅ Request created successfully:', {
207 requestId: request.request_id,
208 domainName: request.domain_name,
209 status: request.status,
210 years: request.years,
211 price: request.price,
212 currency: request.currency,
213 createdAt: request.created_at
216 // Send notification to root tenant admins
218 // Find root tenant (subdomain = 'admin' or is_msp = true)
219 const rootTenantResult = await pool.query(
220 `SELECT tenant_id FROM tenants WHERE subdomain = 'admin' OR is_msp = true LIMIT 1`
223 if (rootTenantResult.rows.length > 0) {
224 const rootTenantId = rootTenantResult.rows[0].tenant_id;
226 // Create notification for root tenant admins
228 `INSERT INTO notifications (tenant_id, title, message, type, customer_id)
229 VALUES ($1, $2, $3, $4, $5)`,
232 'New Domain Registration Request',
233 `${domain_name} registration requested by ${registrant_contact.firstName} ${registrant_contact.lastName}`,
238 console.log('[Domain Request] ✅ Notification sent to root tenant:', rootTenantId);
240 console.log('[Domain Request] ⚠️ No root tenant found for notification');
243 console.error('[Domain Request] ❌ Error sending notification:', notifErr.message);
244 // Don't fail the request if notification fails
247 console.log('[Domain Request] ✅ Request completed successfully');
248 console.log('========================================');
249 res.status(201).json(request);
251 console.error('========================================');
252 console.error('[Domain Request] ❌ ERROR creating domain registration request');
253 console.error('[Domain Request] Error time:', new Date().toISOString());
254 console.error('[Domain Request] Error message:', err.message);
255 console.error('[Domain Request] Error stack:', err.stack);
256 console.error('[Domain Request] Request body:', JSON.stringify(req.body, null, 2));
257 console.error('[Domain Request] Tenant:', req.tenant);
258 console.error('[Domain Request] User:', req.user);
259 console.error('========================================');
260 res.status(500).json({
261 error: 'Failed to submit domain registration request',
262 details: err.message,
263 timestamp: requestTimestamp
269 * @api {get} /domain-requests List Domain Registration Requests
270 * @apiName ListDomainRequests
271 * @apiGroup DomainRequests
273 * Retrieves paginated list of domain registration requests with tenant isolation.
274 * MSP/root tenant sees all requests; standard tenants see only their own requests.
275 * Supports filtering by status and includes related customer, contract, and user data.
276 * @apiPermission authenticated
278 * @apiQuery {String="pending","approved","denied","registered","failed"} [status] Filter by request status
279 * @apiQuery {number} [page=1] Page number (1-indexed)
280 * @apiQuery {number} [limit=50] Results per page (max 100)
281 * @apiSuccess {object[]} requests Array of domain registration requests
282 * @apiSuccess {number} requests.request_id Unique request identifier
283 * @apiSuccess {number} requests.tenant_id Tenant ID that owns the request
284 * @apiSuccess {string} requests.tenant_subdomain Tenant subdomain
285 * @apiSuccess {number} requests.customer_id Customer ID
286 * @apiSuccess {string} requests.customer_name Customer name
287 * @apiSuccess {number} requests.contract_id Associated contract ID
288 * @apiSuccess {string} requests.contract_title Contract title
289 * @apiSuccess {string} requests.domain_name Requested domain name
290 * @apiSuccess {string} requests.domain_type "registration" or "dns-only"
291 * @apiSuccess {string} requests.status Request status (pending/approved/denied/registered/failed)
292 * @apiSuccess {number} requests.years Registration years
293 * @apiSuccess {number} requests.price Domain cost
294 * @apiSuccess {string} requests.currency Currency code (USD, EUR, etc.)
295 * @apiSuccess {Object} requests.registrant_contact WHOIS registrant contact data
296 * @apiSuccess {number} requests.requested_by User ID that submitted request
297 * @apiSuccess {String} requests.requested_by_name User name that submitted request
298 * @apiSuccess {String} requests.requested_at ISO timestamp of submission
299 * @apiSuccess {Number} requests.reviewed_by User ID that reviewed (approved/denied)
300 * @apiSuccess {String} requests.reviewed_by_name Reviewer user name
301 * @apiSuccess {String} requests.reviewed_at ISO timestamp of review
302 * @apiSuccess {String} requests.approval_notes Admin notes/reason for decision
303 * @apiSuccess {Number} requests.domain_id Created domain ID (if registered)
304 * @apiSuccess {String} requests.registration_error Error message (if failed)
305 * @apiSuccess {Number} total Total count of matching requests
306 * @apiSuccess {Number} page Current page number
307 * @apiSuccess {Number} limit Results per page
309 * @apiError {String} error Error message
310 * @apiError {String} details Detailed error description
312 * @apiExample {curl} Example Request:
313 * curl -X GET "https://api.ibghub.com/api/domain-requests?status=pending&page=1&limit=50" \
314 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
316 * @apiExample {json} Example Response:
322 * "tenant_subdomain": "acmecorp",
323 * "customer_id": 123,
324 * "customer_name": "ACME Corp",
325 * "domain_name": "example.com",
326 * "domain_type": "registration",
327 * "status": "pending",
332 * "requested_by_name": "John Doe",
333 * "requested_at": "2025-01-15T10:30:00Z"
342// GET /domain-requests - List domain registration requests
343router.get('/', async (req, res) => {
345 const { clause, params, nextParamIndex } = getTenantFilter(req, 'dr');
346 const status = req.query.status;
347 const page = parseInt(req.query.page, 10) || 1;
348 const limit = parseInt(req.query.limit, 10) || 50;
349 const offset = (page - 1) * limit;
351 console.log('[Domain Requests GET] Tenant filter:', {
354 isMsp: req.tenant?.isMsp,
355 tenantId: req.tenant?.id,
357 userRole: req.user?.role
362 c.name as customer_name,
363 ct.title as contract_title,
364 u1.name as requested_by_name,
365 u2.name as reviewed_by_name,
366 t.subdomain as tenant_subdomain
367 FROM domain_registration_requests dr
368 LEFT JOIN customers c ON dr.customer_id = c.customer_id
369 LEFT JOIN contracts ct ON dr.contract_id = ct.contract_id
370 LEFT JOIN users u1 ON dr.requested_by = u1.user_id
371 LEFT JOIN users u2 ON dr.reviewed_by = u2.user_id
372 LEFT JOIN tenants t ON dr.tenant_id = t.tenant_id
374 let countQuery = 'SELECT COUNT(*) FROM domain_registration_requests dr';
376 let paramIndex = nextParamIndex;
385 where.push(`dr.status = $${paramIndex}`);
390 if (where.length > 0) {
391 query += ' WHERE ' + where.join(' AND ');
392 countQuery += ' WHERE ' + where.join(' AND ');
395 query += ` ORDER BY dr.requested_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
396 params.push(limit, offset);
398 console.log('[Domain Requests GET] Final query:', query);
399 console.log('[Domain Requests GET] Query params:', params);
401 const [requestsResult, countResult] = await Promise.all([
402 pool.query(query, params),
403 pool.query(countQuery, clause || status ? params.slice(0, params.length - 2) : [])
406 console.log('[Domain Requests GET] Found', requestsResult.rows.length, 'requests');
409 requests: requestsResult.rows,
410 total: parseInt(countResult.rows[0].count, 10),
415 console.error('Error fetching domain registration requests:', err);
416 res.status(500).json({ error: 'Server error', details: err.message });
421 * @api {get} /domain-requests/:id Get Domain Registration Request Details
422 * @apiName GetDomainRequest
423 * @apiGroup DomainRequests
425 * Retrieves complete details of a single domain registration request including all
426 * contact information, customer/contract data, and review status. Tenant isolation
427 * ensures users can only access requests within their tenant scope.
428 * @apiPermission authenticated
430 * @apiParam {number} id Domain registration request ID
431 * @apiSuccess {number} request_id Unique request identifier
432 * @apiSuccess {number} tenant_id Tenant ID that owns the request
433 * @apiSuccess {string} tenant_subdomain Tenant subdomain
434 * @apiSuccess {number} customer_id Customer ID
435 * @apiSuccess {string} customer_name Customer name
436 * @apiSuccess {string} customer_email Customer email address
437 * @apiSuccess {number} contract_id Associated contract ID
438 * @apiSuccess {string} contract_title Contract title
439 * @apiSuccess {string} contract.billing_interval Contract billing frequency
440 * @apiSuccess {string} domain_name Requested domain name
441 * @apiSuccess {string} domain_type "registration" or "dns-only"
442 * @apiSuccess {string} status Request status (pending/approved/denied/registered/failed)
443 * @apiSuccess {number} years Registration duration
444 * @apiSuccess {number} price Domain cost
445 * @apiSuccess {string} currency Currency code
446 * @apiSuccess {string[]} nameservers Requested nameservers
447 * @apiSuccess {Object} registrant_contact WHOIS registrant contact (firstName, lastName, email, phone, address, city, state, postalCode, country)
448 * @apiSuccess {Object} admin_contact WHOIS admin contact
449 * @apiSuccess {Object} tech_contact WHOIS technical contact
450 * @apiSuccess {Object} billing_contact WHOIS billing contact
451 * @apiSuccess {Number} requested_by User ID that submitted request
452 * @apiSuccess {String} requested_by_name User name that submitted request
453 * @apiSuccess {String} requested_by_email User email that submitted request
454 * @apiSuccess {String} requested_at ISO timestamp of submission
455 * @apiSuccess {Number} reviewed_by User ID that reviewed (null if pending)
456 * @apiSuccess {String} reviewed_by_name Reviewer user name
457 * @apiSuccess {String} reviewed_at ISO timestamp of review
458 * @apiSuccess {String} approval_notes Admin notes/reason for approval/denial
459 * @apiSuccess {Number} domain_id Created domain ID (if status = "registered")
460 * @apiSuccess {String} registration_error Error message (if status = "failed")
462 * @apiError (404) {String} error "Domain registration request not found"
463 * @apiError (500) {String} error "Server error"
464 * @apiError (500) {String} details Detailed error description
466 * @apiExample {curl} Example Request:
467 * curl -X GET https://api.ibghub.com/api/domain-requests/42 \
468 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
471// GET /domain-requests/:id - Get domain registration request details
472router.get('/:id', async (req, res) => {
473 const { id } = req.params;
475 const { clause, params } = getTenantFilter(req, 'dr');
478 c.name as customer_name, c.email as customer_email,
479 ct.title as contract_title, ct.billing_interval,
480 u1.name as requested_by_name, u1.email as requested_by_email,
481 u2.name as reviewed_by_name,
482 t.subdomain as tenant_subdomain
483 FROM domain_registration_requests dr
484 LEFT JOIN customers c ON dr.customer_id = c.customer_id
485 LEFT JOIN contracts ct ON dr.contract_id = ct.contract_id
486 LEFT JOIN users u1 ON dr.requested_by = u1.user_id
487 LEFT JOIN users u2 ON dr.reviewed_by = u2.user_id
488 LEFT JOIN tenants t ON dr.tenant_id = t.tenant_id
489 WHERE dr.request_id = $1
491 const queryParams = [id];
494 query += ` AND ${clause}`;
495 queryParams.push(...params);
498 const result = await pool.query(query, queryParams);
500 if (result.rows.length === 0) {
501 return res.status(404).json({ error: 'Domain registration request not found' });
504 res.json(result.rows[0]);
506 console.error('Error fetching domain registration request:', err);
507 res.status(500).json({ error: 'Server error', details: err.message });
512 * @api {post} /domain-requests/:id/approve Approve Domain Registration Request
513 * @apiName ApproveDomainRequest
514 * @apiGroup DomainRequests
516 * Approves a pending domain registration request and initiates domain registration or
517 * DNS zone creation based on selected registrar. This is the most complex endpoint,
518 * handling three registration modes:
520 * 1. **Moniker Registration**: Full domain registration via Moniker API with WHOIS contacts
521 * 2. **eNom Registration**: Full domain registration via eNom reseller API
522 * 3. **Cloudflare DNS-only**: Create Cloudflare zone without registering (customer updates NS)
523 * 4. **External DNS-only**: Domain registered elsewhere, just provision Cloudflare zone
526 * - Validates request is in "pending" status
527 * - Updates status to "approved" with admin notes
528 * - Routes to appropriate registrar API based on `registrar` parameter
529 * - On success: Creates domain record in database, updates status to "registered"
530 * - On failure: Updates status to "failed" with error message
531 * - Sends notifications to customer and root tenant admins
532 * - For Cloudflare/external: Creates DNS zone, provides nameservers to customer
534 * Transaction handling:
535 * - Uses database transaction for status updates
536 * - Separate try/catch blocks for registration API calls
537 * - Rolls back approval if registration fails completely
538 * @apiPermission rootAdmin
540 * @apiNote Only root tenant/MSP administrators can approve domain requests
541 * @apiParam {number} id Domain registration request ID
542 * @apiBody {string} [approval_notes] Admin notes explaining approval decision
543 * @apiBody {String="moniker","enom","cloudflare"} [registrar=cloudflare] Registrar to use for registration
544 * @apiSuccess {boolean} success Operation success status
545 * @apiSuccess {string} message Success message describing action taken
546 * @apiSuccess {number} domain_id Created domain record ID
547 * @apiSuccess {string} registrar Registrar used (moniker/enom/external for Cloudflare)
548 * @apiSuccess {string[]} [nameservers] Cloudflare nameservers (for DNS-only modes)
549 * @apiSuccess {string} [zone_id] Cloudflare zone ID (for DNS-only modes)
550 * @apiError (400) {String} error "Cannot approve request with status: X" (if not pending)
551 * @apiError (400) {String} error "Invalid registrar. Must be cloudflare, enom, or moniker."
552 * @apiError (403) {String} error "Only root tenant admins can approve domain registration requests"
553 * @apiError (404) {String} error "Domain registration request not found"
554 * @apiError (500) {String} error "Domain registration approved but [registrar] registration failed"
555 * @apiError (500) {String} details Detailed error from registrar API
556 * @apiExample {curl} Example Request (Moniker):
557 * curl -X POST https://api.ibghub.com/api/domain-requests/42/approve \
558 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
559 * -H "Content-Type: application/json" \
560 * -d '{"approval_notes":"Approved for Q1 2025 campaign","registrar":"moniker"}'
561 * @apiExample {curl} Example Request (Cloudflare DNS-only):
562 * curl -X POST https://api.ibghub.com/api/domain-requests/42/approve \
563 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
564 * -H "Content-Type: application/json" \
565 * -d '{"approval_notes":"DNS-only for external domain","registrar":"cloudflare"}'
566 * @apiExample {json} Example Response (Cloudflare):
569 * "message": "Domain DNS zone created successfully via external",
571 * "registrar": "external",
572 * "zone_id": "a1b2c3d4e5f6g7h8i9j0",
574 * "clara.ns.cloudflare.com",
575 * "ivan.ns.cloudflare.com"
578 * @apiExample {json} Example Response (Moniker):
581 * "message": "Domain registered successfully via moniker",
583 * "registrar": "moniker"
587// POST /domain-requests/:id/approve - Approve domain registration request
588router.post('/:id/approve', async (req, res) => {
589 const { id } = req.params;
590 const { approval_notes, registrar = 'cloudflare' } = req.body;
591 const userId = req.user?.user_id;
593 console.log('[Domain Approve] Auth check:', {
594 subdomain: req.tenant?.subdomain,
595 is_msp: req.tenant?.is_msp,
596 isMsp: req.tenant?.isMsp,
597 userRole: req.user?.role,
598 userId: req.user?.user_id,
599 tenantId: req.tenant?.id,
600 selectedRegistrar: registrar
603 // Only root tenant admins can approve
604 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.isMsp === true || req.user?.is_msp === true;
605 console.log('[Domain Approve] isRootAdmin:', isRootAdmin);
608 return res.status(403).json({ error: 'Only root tenant admins can approve domain registration requests' });
611 const client = await pool.connect();
613 await client.query('BEGIN');
615 // Get request details
616 const requestResult = await client.query(
617 'SELECT * FROM domain_registration_requests WHERE request_id = $1 FOR UPDATE',
621 if (requestResult.rows.length === 0) {
622 await client.query('ROLLBACK');
623 return res.status(404).json({ error: 'Domain registration request not found' });
626 const request = requestResult.rows[0];
628 if (request.status !== 'pending') {
629 await client.query('ROLLBACK');
630 return res.status(400).json({ error: `Cannot approve request with status: ${request.status}` });
633 // Update request to approved status (pre-registration)
635 `UPDATE domain_registration_requests
636 SET status = 'approved', reviewed_by = $1, reviewed_at = NOW(), approval_notes = $2, updated_at = NOW()
637 WHERE request_id = $3`,
638 [userId, approval_notes, id]
641 await client.query('COMMIT');
643 console.log('[Domain Approve] Request approved, starting domain creation for:', request.domain_name);
644 console.log('[Domain Approve] Domain type:', request.domain_type);
646 // Handle DNS-only domains differently (no registration, just Cloudflare zone creation)
647 if (request.domain_type === 'dns-only') {
648 console.log('[Domain Approve] DNS-ONLY mode: Creating Cloudflare zone without registration');
651 // Create Cloudflare zone
652 console.log('[Domain Approve] Creating Cloudflare zone for:', request.domain_name);
653 const zoneResult = await createZone(request.domain_name);
655 if (!zoneResult || !zoneResult.id) {
656 throw new Error('Failed to create Cloudflare zone');
659 console.log('[Domain Approve] Cloudflare zone created:', zoneResult.id);
661 // Extract nameservers from Cloudflare response
662 const cloudflareNameservers = zoneResult.name_servers || [];
664 // Create domain record in database
665 const domainResult = await pool.query(
666 `INSERT INTO public.domains (
667 tenant_id, customer_id, contract_id, domain_name, registrar,
668 status, registration_date, nameservers, dns_provider,
669 purchase_price, currency, notes
670 ) VALUES ($1, $2, $3, $4, $5, $6, NOW(), $7, $8, $9, $10, $11)
671 RETURNING domain_id`,
677 'external', // Registrar is external for DNS-only domains
679 JSON.stringify(cloudflareNameservers),
682 request.currency || 'USD',
683 `DNS-only domain. Cloudflare Zone ID: ${zoneResult.id}. Nameservers: ${cloudflareNameservers.join(', ')}`
687 const domainId = domainResult.rows[0].domain_id;
689 // Update request to registered status
691 `UPDATE domain_registration_requests
692 SET status = 'registered', domain_id = $1, updated_at = NOW()
693 WHERE request_id = $2`,
697 // Notify customer of success
698 if (request.customer_id) {
700 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
701 VALUES ($1, $2, $3, $4, $5)`,
705 'DNS-Only Domain Approved',
706 `Your domain ${request.domain_name} has been added to Cloudflare. Update nameservers to: ${cloudflareNameservers.join(', ')}`,
714 message: 'DNS-only domain approved and Cloudflare zone created successfully',
716 zone_id: zoneResult.id,
717 nameservers: cloudflareNameservers
720 } catch (cloudflareErr) {
721 console.error('[Domain Approve] Cloudflare zone creation error:', cloudflareErr);
723 // Update request with error
725 `UPDATE domain_registration_requests
726 SET status = 'failed', registration_error = $1, updated_at = NOW()
727 WHERE request_id = $2`,
728 [cloudflareErr.message, id]
731 // Notify customer of failure
732 if (request.customer_id) {
734 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
735 VALUES ($1, $2, $3, $4, $5)`,
739 'DNS-Only Domain Setup Failed',
740 `Failed to set up DNS for ${request.domain_name}: ${cloudflareErr.message}`,
746 return res.status(500).json({
747 error: 'DNS-only domain approved but Cloudflare zone creation failed',
748 details: cloudflareErr.message
753 // Handle regular domain registration based on selected registrar
754 let registrationResult;
755 let actualRegistrar = registrar;
757 // Route to appropriate registrar API
758 if (registrar === 'cloudflare-registrar') {
759 console.log('[Domain Approve] CLOUDFLARE REGISTRAR MODE: Registering domain via Cloudflare');
762 const registrant = request.registrant_contact;
764 // Call Cloudflare registrar API
765 registrationResult = await registerCloudflare({
766 domain: request.domain_name,
767 years: request.years,
770 registrant_contact: {
771 first_name: registrant.firstName,
772 last_name: registrant.lastName,
773 organization: registrant.organization || '',
774 email: registrant.email,
775 phone: registrant.phone,
776 address: registrant.address,
777 city: registrant.city,
778 state: registrant.state,
779 postal_code: registrant.postalCode,
780 country: registrant.country
784 console.log('[Domain Approve] Cloudflare registration result:', registrationResult);
786 // Mark as successful
787 registrationResult.success = true;
788 actualRegistrar = 'cloudflare';
790 console.log('[Domain Approve] ✅ Cloudflare registration successful');
791 } catch (cloudflareErr) {
792 console.error('[Domain Approve] Cloudflare registration error:', cloudflareErr);
794 // Check if it's API not available
795 if (cloudflareErr.code === 'API_NOT_AVAILABLE') {
796 return res.status(503).json({
797 error: 'Cloudflare Registrar API not available',
798 message: cloudflareErr.message
802 registrationResult = {
804 error: cloudflareErr.message
807 } else if (registrar === 'cloudflare') {
808 console.log('[Domain Approve] CLOUDFLARE MODE: Creating DNS zone only (no registration)');
811 // Create Cloudflare zone
812 console.log('[Domain Approve] Creating Cloudflare zone for:', request.domain_name);
813 const zoneResult = await createZone(request.domain_name);
815 if (!zoneResult || !zoneResult.id) {
816 throw new Error('Failed to create Cloudflare zone');
819 console.log('[Domain Approve] Cloudflare zone created:', zoneResult.id);
821 // Extract nameservers from Cloudflare response
822 const cloudflareNameservers = zoneResult.name_servers || [];
824 registrationResult = {
827 expirationDate: null, // No expiry for DNS-only
828 nameservers: cloudflareNameservers,
829 zoneId: zoneResult.id,
830 message: 'Cloudflare DNS zone created - customer must update nameservers at their registrar'
832 actualRegistrar = 'external'; // Mark as external since we don't register
833 } catch (cloudflareErr) {
834 console.error('[Domain Approve] Cloudflare error:', cloudflareErr);
835 registrationResult = {
837 error: cloudflareErr.message
840 } else if (registrar === 'enom') {
841 console.log('[Domain Approve] ENOM MODE: Registering domain via eNom API');
843 // Check if eNom API credentials are configured
844 if (!process.env.ENOM_UID || !process.env.ENOM_PASSWORD) {
845 return res.status(500).json({
846 error: 'eNom API credentials not configured',
847 message: 'Please set ENOM_UID and ENOM_PASSWORD environment variables'
852 // Prepare registrant contact for eNom
853 const registrant = request.registrant_contact;
855 // Call eNom registration
856 registrationResult = await enom.registerDomain({
857 domain: request.domain_name,
858 years: request.years,
859 nameservers: request.nameservers || [],
860 registrantFirstName: registrant.firstName,
861 registrantLastName: registrant.lastName,
862 registrantEmail: registrant.email,
863 registrantPhone: registrant.phone,
864 registrantAddress1: registrant.address,
865 registrantCity: registrant.city,
866 registrantStateProvince: registrant.state,
867 registrantPostalCode: registrant.postalCode,
868 registrantCountry: registrant.country
871 console.log('[Domain Approve] eNom registration result:', registrationResult);
873 // Verify registration was successful (check for order_id)
874 if (registrationResult && registrationResult.order_id) {
875 registrationResult.success = true;
876 console.log('[Domain Approve] ✅ eNom registration successful, Order ID:', registrationResult.order_id);
878 console.error('[Domain Approve] ❌ eNom registration returned without Order ID:', registrationResult);
879 throw new Error('eNom registration did not return a valid Order ID - registration may have failed');
882 console.error('[Domain Approve] eNom registration error:', enumErr);
883 registrationResult = {
885 error: enumErr.message
888 } else if (registrar === 'moniker') {
889 console.log('[Domain Approve] MONIKER MODE: Registering domain via Moniker API');
891 // Check if Moniker API credentials are configured
892 const isTestMode = !process.env.MONIKER_USER_KEY ||
893 !process.env.MONIKER_API_PASSWORD ||
894 process.env.MONIKER_USER_KEY === 'monikerapi' ||
895 process.env.MONIKER_API_PASSWORD === 'monikerpass' ||
896 process.env.MONIKER_BASE_URL?.includes('testapi');
899 console.log('[Domain Approve] TEST MODE: Skipping Moniker API, creating domain directly');
900 // In test mode, simulate successful registration
901 registrationResult = {
904 expirationDate: new Date(Date.now() + request.years * 365 * 24 * 60 * 60 * 1000),
905 message: 'Test mode - domain created without actual registration'
909 // Prepare contacts (use admin/tech/billing = registrant if not specified)
910 const registrant = request.registrant_contact;
911 const admin = request.admin_contact || registrant;
912 const tech = request.tech_contact || registrant;
913 const billing = request.billing_contact || registrant;
915 // Call Moniker registration
916 registrationResult = await moniker.registerDomain({
917 domain: request.domain_name,
918 years: request.years,
919 nameservers: request.nameservers || [],
921 FirstName: registrant.firstName,
922 LastName: registrant.lastName,
923 Organization: registrant.organization || '',
924 Email: registrant.email,
925 Phone: registrant.phone,
926 Address: registrant.address,
927 City: registrant.city,
928 State: registrant.state,
929 PostalCode: registrant.postalCode,
930 Country: registrant.country
932 admincontact: admin ? {
933 FirstName: admin.firstName,
934 LastName: admin.lastName,
935 Organization: admin.organization || '',
938 Address: admin.address,
941 PostalCode: admin.postalCode,
942 Country: admin.country
944 techcontact: tech ? {
945 FirstName: tech.firstName,
946 LastName: tech.lastName,
947 Organization: tech.organization || '',
950 Address: tech.address,
953 PostalCode: tech.postalCode,
954 Country: tech.country
956 billingcontact: billing ? {
957 FirstName: billing.firstName,
958 LastName: billing.lastName,
959 Organization: billing.organization || '',
960 Email: billing.email,
961 Phone: billing.phone,
962 Address: billing.address,
964 State: billing.state,
965 PostalCode: billing.postalCode,
966 Country: billing.country
970 console.log('[Domain Approve] Moniker registration result:', registrationResult);
971 registrationResult.success = registrationResult.status !== 'FAILURE';
972 } catch (monikerErr) {
973 console.error('[Domain Approve] Moniker registration error:', monikerErr);
974 registrationResult = {
976 error: monikerErr.message
981 return res.status(400).json({ error: 'Invalid registrar. Must be cloudflare, cloudflare-registrar, enom, or moniker.' });
984 // Process registration result (works for all registrars)
986 // If successful, create domain record
987 if (registrationResult.success) {
988 const registrant = request.registrant_contact;
990 // Prepare domain notes based on registrar
991 let domainNotes = '';
992 if (actualRegistrar === 'external') {
993 domainNotes = `Cloudflare DNS-only. Zone ID: ${registrationResult.zoneId}. Nameservers: ${registrationResult.nameservers?.join(', ')}`;
996 const domainResult = await pool.query(
997 `INSERT INTO public.domains (
998 tenant_id, customer_id, contract_id, domain_name, registrar, registrar_domain_id,
999 status, registration_date, expiration_date, nameservers, registrant_contact,
1000 purchase_price, currency, dns_provider, notes
1001 ) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10, $11, $12, $13, $14)
1002 RETURNING domain_id`,
1005 request.customer_id,
1006 request.contract_id,
1007 request.domain_name,
1009 registrationResult.domainId || registrationResult.order_id || null,
1011 registrationResult.expirationDate || (actualRegistrar !== 'external' ? new Date(Date.now() + request.years * 365 * 24 * 60 * 60 * 1000) : null),
1012 JSON.stringify(registrationResult.nameservers || request.nameservers || []),
1013 JSON.stringify(registrant),
1015 request.currency || 'USD',
1016 actualRegistrar === 'external' ? 'cloudflare' : actualRegistrar,
1021 const domainId = domainResult.rows[0].domain_id;
1023 // Update request to registered status
1025 `UPDATE domain_registration_requests
1026 SET status = 'registered', domain_id = $1, updated_at = NOW()
1027 WHERE request_id = $2`,
1031 // Notify customer of success
1032 if (request.customer_id) {
1033 let notificationMessage = '';
1034 if (actualRegistrar === 'external') {
1035 notificationMessage = `Your domain ${request.domain_name} has been added to Cloudflare. Update nameservers to: ${registrationResult.nameservers.join(', ')}`;
1037 notificationMessage = `Your domain ${request.domain_name} has been successfully registered via ${actualRegistrar}`;
1041 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1042 VALUES ($1, $2, $3, $4, $5)`,
1045 request.customer_id,
1046 actualRegistrar === 'external' ? 'DNS Zone Created' : 'Domain Registration Successful',
1047 notificationMessage,
1055 message: `Domain ${actualRegistrar === 'external' ? 'DNS zone created' : 'registered'} successfully via ${actualRegistrar}`,
1056 domain_id: domainId,
1057 registrar: actualRegistrar,
1058 nameservers: registrationResult.nameservers,
1059 zone_id: registrationResult.zoneId
1062 // Registration failed - update request with error
1064 `UPDATE domain_registration_requests
1065 SET status = 'failed', registration_error = $1, updated_at = NOW()
1066 WHERE request_id = $2`,
1067 [registrationResult.error || 'Unknown registration error', id]
1070 // Notify customer of failure
1071 if (request.customer_id) {
1073 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1074 VALUES ($1, $2, $3, $4, $5)`,
1077 request.customer_id,
1078 'Domain Registration Failed',
1079 `Registration of ${request.domain_name} failed: ${registrationResult.error}`,
1085 // Notify root tenant admin of failure
1086 const rootTenantResult = await pool.query(
1087 `SELECT tenant_id FROM tenants WHERE subdomain = 'admin' OR is_msp = true LIMIT 1`
1089 if (rootTenantResult.rows.length > 0) {
1091 `INSERT INTO notifications (tenant_id, title, message, type)
1092 VALUES ($1, $2, $3, $4)`,
1094 rootTenantResult.rows[0].tenant_id,
1095 'Domain Registration Failed',
1096 `Failed to register ${request.domain_name}: ${registrationResult.error}`,
1102 res.status(500).json({
1103 error: `Domain registration approved but ${registrar} registration failed`,
1104 details: registrationResult.error
1108 console.error(`${registrar} registration error:`, regErr);
1110 // Update request with error
1112 `UPDATE domain_registration_requests
1113 SET status = 'failed', registration_error = $1, updated_at = NOW()
1114 WHERE request_id = $2`,
1115 [regErr.message, id]
1118 // Notify customer of failure
1119 if (request.customer_id) {
1121 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1122 VALUES ($1, $2, $3, $4, $5)`,
1125 request.customer_id,
1126 'Domain Registration Failed',
1127 `Registration of ${request.domain_name} failed: ${regErr.message}`,
1133 // Notify root tenant admin of failure
1134 const rootTenantResult = await pool.query(
1135 `SELECT tenant_id FROM tenants WHERE subdomain = 'admin' OR is_msp = true LIMIT 1`
1137 if (rootTenantResult.rows.length > 0) {
1139 `INSERT INTO notifications (tenant_id, title, message, type)
1140 VALUES ($1, $2, $3, $4)`,
1142 rootTenantResult.rows[0].tenant_id,
1143 'Domain Registration Error',
1144 `Error registering ${request.domain_name}: ${regErr.message}`,
1150 res.status(500).json({
1151 error: `Domain registration approved but ${registrar} API error occurred`,
1152 details: regErr.message
1156 await client.query('ROLLBACK');
1157 console.error('Error approving domain registration request:', err);
1158 res.status(500).json({ error: 'Failed to approve domain registration request', details: err.message });
1165 * @api {post} /domain-requests/:id/deny Deny Domain Registration Request
1166 * @apiName DenyDomainRequest
1167 * @apiGroup DomainRequests
1169 * Denies a pending domain registration request with optional explanation notes.
1170 * Updates request status to "denied" and sends notification to requesting customer.
1171 * Only root tenant/MSP administrators have permission to deny requests.
1172 * @apiPermission rootAdmin
1173 * @apiUse AuthHeader
1174 * @apiNote Only root tenant/MSP administrators can deny domain requests
1175 * @apiParam {number} id Domain registration request ID
1176 * @apiBody {string} [approval_notes] Admin notes explaining reason for denial
1177 * @apiSuccess {boolean} success Operation success status (true)
1178 * @apiSuccess {string} message "Domain registration request denied"
1179 * @apiError (400) {String} error "Cannot deny request with status: X" (if not pending)
1180 * @apiError (403) {String} error "Only root tenant admins can deny domain registration requests"
1181 * @apiError (404) {String} error "Domain registration request not found"
1182 * @apiError (500) {String} error "Failed to deny domain registration request"
1183 * @apiError (500) {String} details Detailed error description
1184 * @apiExample {curl} Example Request:
1185 * curl -X POST https://api.ibghub.com/api/domain-requests/42/deny \
1186 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
1187 * -H "Content-Type: application/json" \
1188 * -d '{"approval_notes":"Domain name conflicts with trademark policy"}'
1189 * @apiExample {json} Example Response:
1192 * "message": "Domain registration request denied"
1196// POST /domain-requests/:id/deny - Deny domain registration request
1197router.post('/:id/deny', async (req, res) => {
1198 const { id } = req.params;
1199 const { approval_notes } = req.body;
1200 const userId = req.user?.user_id;
1202 // Only root tenant admins can deny
1203 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.isMsp === true || req.user?.is_msp === true;
1205 return res.status(403).json({ error: 'Only root tenant admins can deny domain registration requests' });
1209 // Get request details for notification
1210 const requestResult = await pool.query(
1211 'SELECT * FROM domain_registration_requests WHERE request_id = $1',
1215 if (requestResult.rows.length === 0) {
1216 return res.status(404).json({ error: 'Domain registration request not found' });
1219 const request = requestResult.rows[0];
1221 if (request.status !== 'pending') {
1222 return res.status(400).json({ error: `Cannot deny request with status: ${request.status}` });
1225 // Update request to denied status
1227 `UPDATE domain_registration_requests
1228 SET status = 'denied', reviewed_by = $1, reviewed_at = NOW(), approval_notes = $2, updated_at = NOW()
1229 WHERE request_id = $3`,
1230 [userId, approval_notes, id]
1233 // Notify customer of denial
1234 if (request.customer_id) {
1236 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1237 VALUES ($1, $2, $3, $4, $5)`,
1240 request.customer_id,
1241 'Domain Registration Request Denied',
1242 `Your domain registration request for ${request.domain_name} has been denied${approval_notes ? `: ${approval_notes}` : ''}`,
1248 res.json({ success: true, message: 'Domain registration request denied' });
1250 console.error('Error denying domain registration request:', err);
1251 res.status(500).json({ error: 'Failed to deny domain registration request', details: err.message });
1256 * @api {put} /domain-requests/:id Update Domain Registration Request
1257 * @apiName UpdateDomainRequest
1258 * @apiGroup DomainRequests
1260 * Updates fields of a domain registration request before approval. Typically used by
1261 * root tenant administrators to correct information, adjust pricing, update contacts,
1262 * or change registration parameters prior to processing with registrar APIs.
1264 * All fields are optional - only provided fields will be updated. Contact objects
1265 * are stored as JSON and completely replace existing contact data when updated.
1266 * @apiPermission rootAdmin
1267 * @apiUse AuthHeader
1268 * @apiNote Only root tenant/MSP administrators can update domain requests
1269 * @apiParam {number} id Domain registration request ID
1270 * @apiBody {number} [customer_id] Update customer association
1271 * @apiBody {number} [contract_id] Update contract association
1272 * @apiBody {number} [years] Update registration duration
1273 * @apiBody {number} [price] Update domain cost
1274 * @apiBody {string} [currency] Update currency code (USD, EUR, GBP, etc.)
1275 * @apiBody {string[]} [nameservers] Update custom nameservers
1276 * @apiBody {Object} [registrant_contact] Update WHOIS registrant contact (full object required)
1277 * @apiBody {Object} [admin_contact] Update WHOIS admin contact (full object required)
1278 * @apiBody {Object} [tech_contact] Update WHOIS technical contact (full object required)
1279 * @apiBody {Object} [billing_contact] Update WHOIS billing contact (full object required)
1280 * @apiSuccess {number} request_id Updated request ID
1281 * @apiSuccess {number} tenant_id Tenant ID
1282 * @apiSuccess {number} customer_id Customer ID
1283 * @apiSuccess {number} contract_id Contract ID
1284 * @apiSuccess {string} domain_name Domain name (immutable)
1285 * @apiSuccess {string} domain_type Domain type (immutable)
1286 * @apiSuccess {string} status Request status
1287 * @apiSuccess {number} years Registration years
1288 * @apiSuccess {number} price Domain cost
1289 * @apiSuccess {String} currency Currency code
1290 * @apiSuccess {String[]} nameservers Nameservers array
1291 * @apiSuccess {Object} registrant_contact Updated registrant contact
1292 * @apiSuccess {Object} admin_contact Updated admin contact
1293 * @apiSuccess {Object} tech_contact Updated tech contact
1294 * @apiSuccess {Object} billing_contact Updated billing contact
1295 * @apiSuccess {String} updated_at ISO timestamp of last update
1297 * @apiError (400) {String} error "No fields to update" (if request body is empty)
1298 * @apiError (403) {String} error "Only root tenant admins can update domain registration requests"
1299 * @apiError (404) {String} error "Domain registration request not found"
1300 * @apiError (500) {String} error "Failed to update domain registration request"
1301 * @apiError (500) {String} details Detailed error description
1303 * @apiExample {curl} Example Request:
1304 * curl -X PUT https://api.ibghub.com/api/domain-requests/42 \
1305 * -H "Authorization: Bearer YOUR_JWT_TOKEN" \
1306 * -H "Content-Type: application/json" \
1307 * -d '{"years":2,"price":24.99,"registrant_contact":{"firstName":"Jane","lastName":"Doe",...}}'
1309 * @apiExample {json} Example Response:
1313 * "customer_id": 123,
1314 * "contract_id": 456,
1315 * "domain_name": "example.com",
1316 * "domain_type": "registration",
1317 * "status": "pending",
1320 * "currency": "USD",
1321 * "registrant_contact": {"firstName": "Jane", "lastName": "Doe", ...},
1322 * "updated_at": "2025-01-15T14:22:00Z"
1326// PUT /domain-requests/:id - Update domain registration request (admin edits before approval)
1327router.put('/:id', async (req, res) => {
1328 const { id } = req.params;
1342 // Only root tenant admins can update
1343 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.is_msp === true;
1345 return res.status(403).json({ error: 'Only root tenant admins can update domain registration requests' });
1353 if (customer_id !== undefined) {
1354 updates.push(`customer_id = $${paramIndex++}`);
1355 params.push(customer_id);
1357 if (contract_id !== undefined) {
1358 updates.push(`contract_id = $${paramIndex++}`);
1359 params.push(contract_id);
1361 if (years !== undefined) {
1362 updates.push(`years = $${paramIndex++}`);
1365 if (price !== undefined) {
1366 updates.push(`price = $${paramIndex++}`);
1369 if (currency !== undefined) {
1370 updates.push(`currency = $${paramIndex++}`);
1371 params.push(currency);
1373 if (nameservers !== undefined) {
1374 updates.push(`nameservers = $${paramIndex++}`);
1375 params.push(JSON.stringify(nameservers));
1377 if (registrant_contact !== undefined) {
1378 updates.push(`registrant_contact = $${paramIndex++}`);
1379 params.push(JSON.stringify(registrant_contact));
1381 if (admin_contact !== undefined) {
1382 updates.push(`admin_contact = $${paramIndex++}`);
1383 params.push(JSON.stringify(admin_contact));
1385 if (tech_contact !== undefined) {
1386 updates.push(`tech_contact = $${paramIndex++}`);
1387 params.push(JSON.stringify(tech_contact));
1389 if (billing_contact !== undefined) {
1390 updates.push(`billing_contact = $${paramIndex++}`);
1391 params.push(JSON.stringify(billing_contact));
1394 if (updates.length === 0) {
1395 return res.status(400).json({ error: 'No fields to update' });
1398 updates.push(`updated_at = NOW()`);
1402 UPDATE domain_registration_requests
1403 SET ${updates.join(', ')}
1404 WHERE request_id = $${paramIndex}
1408 const result = await pool.query(query, params);
1410 if (result.rows.length === 0) {
1411 return res.status(404).json({ error: 'Domain registration request not found' });
1414 res.json(result.rows[0]);
1416 console.error('Error updating domain registration request:', err);
1417 res.status(500).json({ error: 'Failed to update domain registration request', details: err.message });
1422 * @api {post} /domain-requests/backfill-notifications Backfill Notifications for Domain Requests
1423 * @apiName BackfillDomainRequestNotifications
1424 * @apiGroup DomainRequests
1426 * Administrative utility endpoint to create notifications for existing domain registration
1427 * requests that don't have associated notifications. Useful after database migrations or
1428 * when notification system was temporarily unavailable. Only processes requests that don't
1429 * already have notifications mentioning their domain name.
1431 * Creates notifications with:
1432 * - Title: "New Domain Registration Request"
1433 * - Message: "[domain_name] registration requested by [requester_name]"
1435 * - Created timestamp: Matches original request timestamp
1436 * - Target: Root tenant notification area
1437 * @apiPermission admin
1438 * @apiUse AuthHeader
1439 * @apiNote Only system administrators with role='admin' can run this utility
1440 * @apiSuccess {string} message Summary of operation
1441 * @apiSuccess {object[]} created Array of created notification records
1442 * @apiSuccess {string} created.domain Domain name that received notification
1443 * @apiSuccess {number} created.notification_id Created notification ID
1444 * @apiError (403) {String} error "Admin access required"
1445 * @apiError (404) {String} error "Root tenant not found"
1446 * @apiError (500) {String} error "Failed to backfill notifications"
1447 * @apiError (500) {String} details Detailed error description
1448 * @apiExample {curl} Example Request:
1449 * curl -X POST https://api.ibghub.com/api/domain-requests/backfill-notifications \
1450 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
1451 * @apiExample {json} Example Response:
1453 * "message": "Successfully created 3 notifications",
1455 * {"domain": "example.com", "notification_id": 101},
1456 * {"domain": "test.org", "notification_id": 102},
1457 * {"domain": "demo.net", "notification_id": 103}
1462// POST /domain-requests/backfill-notifications - Backfill notifications for existing requests (admin only)
1463router.post('/backfill-notifications', async (req, res) => {
1466 if (!req.user || req.user.role !== 'admin') {
1467 return res.status(403).json({ error: 'Admin access required' });
1471 const rootTenantResult = await pool.query(
1472 `SELECT tenant_id FROM tenants WHERE subdomain = 'admin' OR is_msp = true LIMIT 1`
1475 if (rootTenantResult.rows.length === 0) {
1476 return res.status(404).json({ error: 'Root tenant not found' });
1479 const rootTenantId = rootTenantResult.rows[0].tenant_id;
1481 // Get all domain requests that don't have notifications
1482 const requestsResult = await pool.query(`
1483 SELECT dr.request_id, dr.domain_name, dr.customer_id, dr.requested_at,
1484 dr.registrant_contact
1485 FROM domain_registration_requests dr
1487 SELECT 1 FROM notifications n
1488 WHERE n.message LIKE '%' || dr.domain_name || '%'
1490 ORDER BY dr.requested_at DESC
1493 // Create notifications for each request
1495 for (const request of requestsResult.rows) {
1496 const registrant = request.registrant_contact;
1497 const fullName = `${registrant.firstName || ''} ${registrant.lastName || ''}`.trim() || 'Unknown';
1499 const notifResult = await pool.query(
1500 `INSERT INTO notifications (tenant_id, title, message, type, customer_id, created_at)
1501 VALUES ($1, $2, $3, $4, $5, $6)
1502 RETURNING notification_id`,
1505 'New Domain Registration Request',
1506 `${request.domain_name} registration requested by ${fullName}`,
1508 request.customer_id,
1509 request.requested_at
1514 domain: request.domain_name,
1515 notification_id: notifResult.rows[0].notification_id
1520 message: `Successfully created ${created.length} notifications`,
1525 console.error('Error backfilling notifications:', err);
1526 res.status(500).json({ error: 'Failed to backfill notifications', details: err.message });
1530module.exports = router;