EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
domainRequests.js
Go to the documentation of this file.
1/**
2 * @file Domain Registration Request Management API Routes
3 * @description
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
7 * provisioning.
8 *
9 * Key features:
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)
17 *
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
23 *
24 * Security:
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
29 *
30 * Database tables:
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
34 * @requires express
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}
45 */
46
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');
55
56// Apply authentication and tenant context to all routes
57router.use(authenticateToken, setTenantContext);
58
59/**
60 * @api {post} /domain-requests Submit Domain Registration Request
61 * @apiName SubmitDomainRequest
62 * @apiGroup DomainRequests
63 * @apiDescription
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.
67 *
68 * Request body example:
69 * ```json
70 * {
71 * "domain_name": "example.com",
72 * "domain_type": "registration", // or "dns-only"
73 * "customer_id": 123,
74 * "contract_id": 456,
75 * "years": 1,
76 * "price": 12.99,
77 * "currency": "USD",
78 * "nameservers": ["ns1.example.com", "ns2.example.com"],
79 * "registrant_contact": {
80 * "firstName": "John",
81 * "lastName": "Doe",
82 * "email": "john@example.com",
83 * "phone": "+1.5555551234",
84 * "address": "123 Main St",
85 * "city": "San Francisco",
86 * "state": "CA",
87 * "postalCode": "94105",
88 * "country": "US",
89 * "organization": "Example Corp"
90 * },
91 * "admin_contact": { ... }, // Optional, defaults to registrant_contact
92 * "tech_contact": { ... }, // Optional, defaults to registrant_contact
93 * "billing_contact": { ... } // Optional, defaults to registrant_contact
94 * }
95 * ```
96 * @apiPermission authenticated
97 * @apiUse AuthHeader
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":{...}}'
121 */
122
123// POST /domain-requests - Submit domain registration request
124router.post('/', async (req, res) => {
125 const requestTimestamp = new Date().toISOString();
126 try {
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,
135 role: req.user?.role
136 });
137 console.log('[Domain Request] Tenant:', {
138 id: req.tenant?.id,
139 isMsp: req.tenant?.isMsp,
140 subdomain: req.tenant?.subdomain
141 });
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')
146 });
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');
149
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',
154 debug: {
155 contentType: req.get('Content-Type'),
156 hasBody: !!req.body,
157 bodyKeys: req.body ? Object.keys(req.body) : []
158 }
159 });
160 }
161
162 const {
163 customer_id,
164 contract_id,
165 domain_name,
166 domain_type = 'registration',
167 years = 1,
168 price,
169 currency = 'USD',
170 nameservers,
171 registrant_contact,
172 admin_contact,
173 tech_contact,
174 billing_contact
175 } = req.body;
176
177 // Validation
178 if (!domain_name || !registrant_contact) {
179 return res.status(400).json({ error: 'Domain name and registrant contact are required' });
180 }
181
182 const tenantId = req.tenant?.id;
183 const userId = req.user?.user_id;
184
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,
190 requested_by
191 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
192 RETURNING *`,
193 [
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),
200 userId
201 ]
202 );
203
204 const request = result.rows[0];
205
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
214 });
215
216 // Send notification to root tenant admins
217 try {
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`
221 );
222
223 if (rootTenantResult.rows.length > 0) {
224 const rootTenantId = rootTenantResult.rows[0].tenant_id;
225
226 // Create notification for root tenant admins
227 await pool.query(
228 `INSERT INTO notifications (tenant_id, title, message, type, customer_id)
229 VALUES ($1, $2, $3, $4, $5)`,
230 [
231 rootTenantId,
232 'New Domain Registration Request',
233 `${domain_name} registration requested by ${registrant_contact.firstName} ${registrant_contact.lastName}`,
234 'info',
235 customer_id
236 ]
237 );
238 console.log('[Domain Request] ✅ Notification sent to root tenant:', rootTenantId);
239 } else {
240 console.log('[Domain Request] ⚠️ No root tenant found for notification');
241 }
242 } catch (notifErr) {
243 console.error('[Domain Request] ❌ Error sending notification:', notifErr.message);
244 // Don't fail the request if notification fails
245 }
246
247 console.log('[Domain Request] ✅ Request completed successfully');
248 console.log('========================================');
249 res.status(201).json(request);
250 } catch (err) {
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
264 });
265 }
266});
267
268/**
269 * @api {get} /domain-requests List Domain Registration Requests
270 * @apiName ListDomainRequests
271 * @apiGroup DomainRequests
272 * @apiDescription
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
277 * @apiUse AuthHeader
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
308 *
309 * @apiError {String} error Error message
310 * @apiError {String} details Detailed error description
311 *
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"
315 *
316 * @apiExample {json} Example Response:
317 * {
318 * "requests": [
319 * {
320 * "request_id": 42,
321 * "tenant_id": 5,
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",
328 * "years": 1,
329 * "price": 12.99,
330 * "currency": "USD",
331 * "requested_by": 7,
332 * "requested_by_name": "John Doe",
333 * "requested_at": "2025-01-15T10:30:00Z"
334 * }
335 * ],
336 * "total": 1,
337 * "page": 1,
338 * "limit": 50
339 * }
340 */
341
342// GET /domain-requests - List domain registration requests
343router.get('/', async (req, res) => {
344 try {
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;
350
351 console.log('[Domain Requests GET] Tenant filter:', {
352 clause,
353 params,
354 isMsp: req.tenant?.isMsp,
355 tenantId: req.tenant?.id,
356 status,
357 userRole: req.user?.role
358 });
359
360 let query = `
361 SELECT dr.*,
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
373 `;
374 let countQuery = 'SELECT COUNT(*) FROM domain_registration_requests dr';
375 const where = [];
376 let paramIndex = nextParamIndex;
377
378 // Tenant filter
379 if (clause) {
380 where.push(clause);
381 }
382
383 // Status filter
384 if (status) {
385 where.push(`dr.status = $${paramIndex}`);
386 params.push(status);
387 paramIndex++;
388 }
389
390 if (where.length > 0) {
391 query += ' WHERE ' + where.join(' AND ');
392 countQuery += ' WHERE ' + where.join(' AND ');
393 }
394
395 query += ` ORDER BY dr.requested_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
396 params.push(limit, offset);
397
398 console.log('[Domain Requests GET] Final query:', query);
399 console.log('[Domain Requests GET] Query params:', params);
400
401 const [requestsResult, countResult] = await Promise.all([
402 pool.query(query, params),
403 pool.query(countQuery, clause || status ? params.slice(0, params.length - 2) : [])
404 ]);
405
406 console.log('[Domain Requests GET] Found', requestsResult.rows.length, 'requests');
407
408 res.json({
409 requests: requestsResult.rows,
410 total: parseInt(countResult.rows[0].count, 10),
411 page,
412 limit
413 });
414 } catch (err) {
415 console.error('Error fetching domain registration requests:', err);
416 res.status(500).json({ error: 'Server error', details: err.message });
417 }
418});
419
420/**
421 * @api {get} /domain-requests/:id Get Domain Registration Request Details
422 * @apiName GetDomainRequest
423 * @apiGroup DomainRequests
424 * @apiDescription
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
429 * @apiUse AuthHeader
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")
461 *
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
465 *
466 * @apiExample {curl} Example Request:
467 * curl -X GET https://api.ibghub.com/api/domain-requests/42 \
468 * -H "Authorization: Bearer YOUR_JWT_TOKEN"
469 */
470
471// GET /domain-requests/:id - Get domain registration request details
472router.get('/:id', async (req, res) => {
473 const { id } = req.params;
474 try {
475 const { clause, params } = getTenantFilter(req, 'dr');
476 let query = `
477 SELECT 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
490 `;
491 const queryParams = [id];
492
493 if (clause) {
494 query += ` AND ${clause}`;
495 queryParams.push(...params);
496 }
497
498 const result = await pool.query(query, queryParams);
499
500 if (result.rows.length === 0) {
501 return res.status(404).json({ error: 'Domain registration request not found' });
502 }
503
504 res.json(result.rows[0]);
505 } catch (err) {
506 console.error('Error fetching domain registration request:', err);
507 res.status(500).json({ error: 'Server error', details: err.message });
508 }
509});
510
511/**
512 * @api {post} /domain-requests/:id/approve Approve Domain Registration Request
513 * @apiName ApproveDomainRequest
514 * @apiGroup DomainRequests
515 * @apiDescription
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:
519 *
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
524 *
525 * Workflow:
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
533 *
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
539 * @apiUse AuthHeader
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):
567 * {
568 * "success": true,
569 * "message": "Domain DNS zone created successfully via external",
570 * "domain_id": 789,
571 * "registrar": "external",
572 * "zone_id": "a1b2c3d4e5f6g7h8i9j0",
573 * "nameservers": [
574 * "clara.ns.cloudflare.com",
575 * "ivan.ns.cloudflare.com"
576 * ]
577 * }
578 * @apiExample {json} Example Response (Moniker):
579 * {
580 * "success": true,
581 * "message": "Domain registered successfully via moniker",
582 * "domain_id": 790,
583 * "registrar": "moniker"
584 * }
585 */
586
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;
592
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
601 });
602
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);
606
607 if (!isRootAdmin) {
608 return res.status(403).json({ error: 'Only root tenant admins can approve domain registration requests' });
609 }
610
611 const client = await pool.connect();
612 try {
613 await client.query('BEGIN');
614
615 // Get request details
616 const requestResult = await client.query(
617 'SELECT * FROM domain_registration_requests WHERE request_id = $1 FOR UPDATE',
618 [id]
619 );
620
621 if (requestResult.rows.length === 0) {
622 await client.query('ROLLBACK');
623 return res.status(404).json({ error: 'Domain registration request not found' });
624 }
625
626 const request = requestResult.rows[0];
627
628 if (request.status !== 'pending') {
629 await client.query('ROLLBACK');
630 return res.status(400).json({ error: `Cannot approve request with status: ${request.status}` });
631 }
632
633 // Update request to approved status (pre-registration)
634 await client.query(
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]
639 );
640
641 await client.query('COMMIT');
642
643 console.log('[Domain Approve] Request approved, starting domain creation for:', request.domain_name);
644 console.log('[Domain Approve] Domain type:', request.domain_type);
645
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');
649
650 try {
651 // Create Cloudflare zone
652 console.log('[Domain Approve] Creating Cloudflare zone for:', request.domain_name);
653 const zoneResult = await createZone(request.domain_name);
654
655 if (!zoneResult || !zoneResult.id) {
656 throw new Error('Failed to create Cloudflare zone');
657 }
658
659 console.log('[Domain Approve] Cloudflare zone created:', zoneResult.id);
660
661 // Extract nameservers from Cloudflare response
662 const cloudflareNameservers = zoneResult.name_servers || [];
663
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`,
672 [
673 request.tenant_id,
674 request.customer_id,
675 request.contract_id,
676 request.domain_name,
677 'external', // Registrar is external for DNS-only domains
678 'active',
679 JSON.stringify(cloudflareNameservers),
680 'cloudflare',
681 request.price || 0,
682 request.currency || 'USD',
683 `DNS-only domain. Cloudflare Zone ID: ${zoneResult.id}. Nameservers: ${cloudflareNameservers.join(', ')}`
684 ]
685 );
686
687 const domainId = domainResult.rows[0].domain_id;
688
689 // Update request to registered status
690 await pool.query(
691 `UPDATE domain_registration_requests
692 SET status = 'registered', domain_id = $1, updated_at = NOW()
693 WHERE request_id = $2`,
694 [domainId, id]
695 );
696
697 // Notify customer of success
698 if (request.customer_id) {
699 await pool.query(
700 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
701 VALUES ($1, $2, $3, $4, $5)`,
702 [
703 request.tenant_id,
704 request.customer_id,
705 'DNS-Only Domain Approved',
706 `Your domain ${request.domain_name} has been added to Cloudflare. Update nameservers to: ${cloudflareNameservers.join(', ')}`,
707 'success'
708 ]
709 );
710 }
711
712 return res.json({
713 success: true,
714 message: 'DNS-only domain approved and Cloudflare zone created successfully',
715 domain_id: domainId,
716 zone_id: zoneResult.id,
717 nameservers: cloudflareNameservers
718 });
719
720 } catch (cloudflareErr) {
721 console.error('[Domain Approve] Cloudflare zone creation error:', cloudflareErr);
722
723 // Update request with error
724 await pool.query(
725 `UPDATE domain_registration_requests
726 SET status = 'failed', registration_error = $1, updated_at = NOW()
727 WHERE request_id = $2`,
728 [cloudflareErr.message, id]
729 );
730
731 // Notify customer of failure
732 if (request.customer_id) {
733 await pool.query(
734 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
735 VALUES ($1, $2, $3, $4, $5)`,
736 [
737 request.tenant_id,
738 request.customer_id,
739 'DNS-Only Domain Setup Failed',
740 `Failed to set up DNS for ${request.domain_name}: ${cloudflareErr.message}`,
741 'error'
742 ]
743 );
744 }
745
746 return res.status(500).json({
747 error: 'DNS-only domain approved but Cloudflare zone creation failed',
748 details: cloudflareErr.message
749 });
750 }
751 }
752
753 // Handle regular domain registration based on selected registrar
754 let registrationResult;
755 let actualRegistrar = registrar;
756
757 // Route to appropriate registrar API
758 if (registrar === 'cloudflare-registrar') {
759 console.log('[Domain Approve] CLOUDFLARE REGISTRAR MODE: Registering domain via Cloudflare');
760
761 try {
762 const registrant = request.registrant_contact;
763
764 // Call Cloudflare registrar API
765 registrationResult = await registerCloudflare({
766 domain: request.domain_name,
767 years: request.years,
768 privacy: true,
769 auto_renew: true,
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
781 }
782 });
783
784 console.log('[Domain Approve] Cloudflare registration result:', registrationResult);
785
786 // Mark as successful
787 registrationResult.success = true;
788 actualRegistrar = 'cloudflare';
789
790 console.log('[Domain Approve] ✅ Cloudflare registration successful');
791 } catch (cloudflareErr) {
792 console.error('[Domain Approve] Cloudflare registration error:', cloudflareErr);
793
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
799 });
800 }
801
802 registrationResult = {
803 success: false,
804 error: cloudflareErr.message
805 };
806 }
807 } else if (registrar === 'cloudflare') {
808 console.log('[Domain Approve] CLOUDFLARE MODE: Creating DNS zone only (no registration)');
809
810 try {
811 // Create Cloudflare zone
812 console.log('[Domain Approve] Creating Cloudflare zone for:', request.domain_name);
813 const zoneResult = await createZone(request.domain_name);
814
815 if (!zoneResult || !zoneResult.id) {
816 throw new Error('Failed to create Cloudflare zone');
817 }
818
819 console.log('[Domain Approve] Cloudflare zone created:', zoneResult.id);
820
821 // Extract nameservers from Cloudflare response
822 const cloudflareNameservers = zoneResult.name_servers || [];
823
824 registrationResult = {
825 success: true,
826 domainId: null,
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'
831 };
832 actualRegistrar = 'external'; // Mark as external since we don't register
833 } catch (cloudflareErr) {
834 console.error('[Domain Approve] Cloudflare error:', cloudflareErr);
835 registrationResult = {
836 success: false,
837 error: cloudflareErr.message
838 };
839 }
840 } else if (registrar === 'enom') {
841 console.log('[Domain Approve] ENOM MODE: Registering domain via eNom API');
842
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'
848 });
849 }
850
851 try {
852 // Prepare registrant contact for eNom
853 const registrant = request.registrant_contact;
854
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
869 });
870
871 console.log('[Domain Approve] eNom registration result:', registrationResult);
872
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);
877 } else {
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');
880 }
881 } catch (enumErr) {
882 console.error('[Domain Approve] eNom registration error:', enumErr);
883 registrationResult = {
884 success: false,
885 error: enumErr.message
886 };
887 }
888 } else if (registrar === 'moniker') {
889 console.log('[Domain Approve] MONIKER MODE: Registering domain via Moniker API');
890
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');
897
898 if (isTestMode) {
899 console.log('[Domain Approve] TEST MODE: Skipping Moniker API, creating domain directly');
900 // In test mode, simulate successful registration
901 registrationResult = {
902 success: true,
903 domainId: null,
904 expirationDate: new Date(Date.now() + request.years * 365 * 24 * 60 * 60 * 1000),
905 message: 'Test mode - domain created without actual registration'
906 };
907 } else {
908 try {
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;
914
915 // Call Moniker registration
916 registrationResult = await moniker.registerDomain({
917 domain: request.domain_name,
918 years: request.years,
919 nameservers: request.nameservers || [],
920 ownercontact: {
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
931 },
932 admincontact: admin ? {
933 FirstName: admin.firstName,
934 LastName: admin.lastName,
935 Organization: admin.organization || '',
936 Email: admin.email,
937 Phone: admin.phone,
938 Address: admin.address,
939 City: admin.city,
940 State: admin.state,
941 PostalCode: admin.postalCode,
942 Country: admin.country
943 } : null,
944 techcontact: tech ? {
945 FirstName: tech.firstName,
946 LastName: tech.lastName,
947 Organization: tech.organization || '',
948 Email: tech.email,
949 Phone: tech.phone,
950 Address: tech.address,
951 City: tech.city,
952 State: tech.state,
953 PostalCode: tech.postalCode,
954 Country: tech.country
955 } : null,
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,
963 City: billing.city,
964 State: billing.state,
965 PostalCode: billing.postalCode,
966 Country: billing.country
967 } : null
968 });
969
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 = {
975 success: false,
976 error: monikerErr.message
977 };
978 }
979 }
980 } else {
981 return res.status(400).json({ error: 'Invalid registrar. Must be cloudflare, cloudflare-registrar, enom, or moniker.' });
982 }
983
984 // Process registration result (works for all registrars)
985 try {
986 // If successful, create domain record
987 if (registrationResult.success) {
988 const registrant = request.registrant_contact;
989
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(', ')}`;
994 }
995
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`,
1003 [
1004 request.tenant_id,
1005 request.customer_id,
1006 request.contract_id,
1007 request.domain_name,
1008 actualRegistrar,
1009 registrationResult.domainId || registrationResult.order_id || null,
1010 'active',
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),
1014 request.price || 0,
1015 request.currency || 'USD',
1016 actualRegistrar === 'external' ? 'cloudflare' : actualRegistrar,
1017 domainNotes || null
1018 ]
1019 );
1020
1021 const domainId = domainResult.rows[0].domain_id;
1022
1023 // Update request to registered status
1024 await pool.query(
1025 `UPDATE domain_registration_requests
1026 SET status = 'registered', domain_id = $1, updated_at = NOW()
1027 WHERE request_id = $2`,
1028 [domainId, id]
1029 );
1030
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(', ')}`;
1036 } else {
1037 notificationMessage = `Your domain ${request.domain_name} has been successfully registered via ${actualRegistrar}`;
1038 }
1039
1040 await pool.query(
1041 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1042 VALUES ($1, $2, $3, $4, $5)`,
1043 [
1044 request.tenant_id,
1045 request.customer_id,
1046 actualRegistrar === 'external' ? 'DNS Zone Created' : 'Domain Registration Successful',
1047 notificationMessage,
1048 'success'
1049 ]
1050 );
1051 }
1052
1053 res.json({
1054 success: true,
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
1060 });
1061 } else {
1062 // Registration failed - update request with error
1063 await pool.query(
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]
1068 );
1069
1070 // Notify customer of failure
1071 if (request.customer_id) {
1072 await pool.query(
1073 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1074 VALUES ($1, $2, $3, $4, $5)`,
1075 [
1076 request.tenant_id,
1077 request.customer_id,
1078 'Domain Registration Failed',
1079 `Registration of ${request.domain_name} failed: ${registrationResult.error}`,
1080 'error'
1081 ]
1082 );
1083 }
1084
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`
1088 );
1089 if (rootTenantResult.rows.length > 0) {
1090 await pool.query(
1091 `INSERT INTO notifications (tenant_id, title, message, type)
1092 VALUES ($1, $2, $3, $4)`,
1093 [
1094 rootTenantResult.rows[0].tenant_id,
1095 'Domain Registration Failed',
1096 `Failed to register ${request.domain_name}: ${registrationResult.error}`,
1097 'error'
1098 ]
1099 );
1100 }
1101
1102 res.status(500).json({
1103 error: `Domain registration approved but ${registrar} registration failed`,
1104 details: registrationResult.error
1105 });
1106 }
1107 } catch (regErr) {
1108 console.error(`${registrar} registration error:`, regErr);
1109
1110 // Update request with error
1111 await pool.query(
1112 `UPDATE domain_registration_requests
1113 SET status = 'failed', registration_error = $1, updated_at = NOW()
1114 WHERE request_id = $2`,
1115 [regErr.message, id]
1116 );
1117
1118 // Notify customer of failure
1119 if (request.customer_id) {
1120 await pool.query(
1121 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1122 VALUES ($1, $2, $3, $4, $5)`,
1123 [
1124 request.tenant_id,
1125 request.customer_id,
1126 'Domain Registration Failed',
1127 `Registration of ${request.domain_name} failed: ${regErr.message}`,
1128 'error'
1129 ]
1130 );
1131 }
1132
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`
1136 );
1137 if (rootTenantResult.rows.length > 0) {
1138 await pool.query(
1139 `INSERT INTO notifications (tenant_id, title, message, type)
1140 VALUES ($1, $2, $3, $4)`,
1141 [
1142 rootTenantResult.rows[0].tenant_id,
1143 'Domain Registration Error',
1144 `Error registering ${request.domain_name}: ${regErr.message}`,
1145 'error'
1146 ]
1147 );
1148 }
1149
1150 res.status(500).json({
1151 error: `Domain registration approved but ${registrar} API error occurred`,
1152 details: regErr.message
1153 });
1154 }
1155 } catch (err) {
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 });
1159 } finally {
1160 client.release();
1161 }
1162});
1163
1164/**
1165 * @api {post} /domain-requests/:id/deny Deny Domain Registration Request
1166 * @apiName DenyDomainRequest
1167 * @apiGroup DomainRequests
1168 * @apiDescription
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:
1190 * {
1191 * "success": true,
1192 * "message": "Domain registration request denied"
1193 * }
1194 */
1195
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;
1201
1202 // Only root tenant admins can deny
1203 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.isMsp === true || req.user?.is_msp === true;
1204 if (!isRootAdmin) {
1205 return res.status(403).json({ error: 'Only root tenant admins can deny domain registration requests' });
1206 }
1207
1208 try {
1209 // Get request details for notification
1210 const requestResult = await pool.query(
1211 'SELECT * FROM domain_registration_requests WHERE request_id = $1',
1212 [id]
1213 );
1214
1215 if (requestResult.rows.length === 0) {
1216 return res.status(404).json({ error: 'Domain registration request not found' });
1217 }
1218
1219 const request = requestResult.rows[0];
1220
1221 if (request.status !== 'pending') {
1222 return res.status(400).json({ error: `Cannot deny request with status: ${request.status}` });
1223 }
1224
1225 // Update request to denied status
1226 await pool.query(
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]
1231 );
1232
1233 // Notify customer of denial
1234 if (request.customer_id) {
1235 await pool.query(
1236 `INSERT INTO notifications (tenant_id, customer_id, title, message, type)
1237 VALUES ($1, $2, $3, $4, $5)`,
1238 [
1239 request.tenant_id,
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}` : ''}`,
1243 'warning'
1244 ]
1245 );
1246 }
1247
1248 res.json({ success: true, message: 'Domain registration request denied' });
1249 } catch (err) {
1250 console.error('Error denying domain registration request:', err);
1251 res.status(500).json({ error: 'Failed to deny domain registration request', details: err.message });
1252 }
1253});
1254
1255/**
1256 * @api {put} /domain-requests/:id Update Domain Registration Request
1257 * @apiName UpdateDomainRequest
1258 * @apiGroup DomainRequests
1259 * @apiDescription
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.
1263 *
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
1296 *
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
1302 *
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",...}}'
1308 *
1309 * @apiExample {json} Example Response:
1310 * {
1311 * "request_id": 42,
1312 * "tenant_id": 5,
1313 * "customer_id": 123,
1314 * "contract_id": 456,
1315 * "domain_name": "example.com",
1316 * "domain_type": "registration",
1317 * "status": "pending",
1318 * "years": 2,
1319 * "price": 24.99,
1320 * "currency": "USD",
1321 * "registrant_contact": {"firstName": "Jane", "lastName": "Doe", ...},
1322 * "updated_at": "2025-01-15T14:22:00Z"
1323 * }
1324 */
1325
1326// PUT /domain-requests/:id - Update domain registration request (admin edits before approval)
1327router.put('/:id', async (req, res) => {
1328 const { id } = req.params;
1329 const {
1330 customer_id,
1331 contract_id,
1332 years,
1333 price,
1334 currency,
1335 nameservers,
1336 registrant_contact,
1337 admin_contact,
1338 tech_contact,
1339 billing_contact
1340 } = req.body;
1341
1342 // Only root tenant admins can update
1343 const isRootAdmin = req.tenant?.subdomain === 'admin' || req.tenant?.is_msp === true;
1344 if (!isRootAdmin) {
1345 return res.status(403).json({ error: 'Only root tenant admins can update domain registration requests' });
1346 }
1347
1348 try {
1349 const updates = [];
1350 const params = [];
1351 let paramIndex = 1;
1352
1353 if (customer_id !== undefined) {
1354 updates.push(`customer_id = $${paramIndex++}`);
1355 params.push(customer_id);
1356 }
1357 if (contract_id !== undefined) {
1358 updates.push(`contract_id = $${paramIndex++}`);
1359 params.push(contract_id);
1360 }
1361 if (years !== undefined) {
1362 updates.push(`years = $${paramIndex++}`);
1363 params.push(years);
1364 }
1365 if (price !== undefined) {
1366 updates.push(`price = $${paramIndex++}`);
1367 params.push(price);
1368 }
1369 if (currency !== undefined) {
1370 updates.push(`currency = $${paramIndex++}`);
1371 params.push(currency);
1372 }
1373 if (nameservers !== undefined) {
1374 updates.push(`nameservers = $${paramIndex++}`);
1375 params.push(JSON.stringify(nameservers));
1376 }
1377 if (registrant_contact !== undefined) {
1378 updates.push(`registrant_contact = $${paramIndex++}`);
1379 params.push(JSON.stringify(registrant_contact));
1380 }
1381 if (admin_contact !== undefined) {
1382 updates.push(`admin_contact = $${paramIndex++}`);
1383 params.push(JSON.stringify(admin_contact));
1384 }
1385 if (tech_contact !== undefined) {
1386 updates.push(`tech_contact = $${paramIndex++}`);
1387 params.push(JSON.stringify(tech_contact));
1388 }
1389 if (billing_contact !== undefined) {
1390 updates.push(`billing_contact = $${paramIndex++}`);
1391 params.push(JSON.stringify(billing_contact));
1392 }
1393
1394 if (updates.length === 0) {
1395 return res.status(400).json({ error: 'No fields to update' });
1396 }
1397
1398 updates.push(`updated_at = NOW()`);
1399 params.push(id);
1400
1401 const query = `
1402 UPDATE domain_registration_requests
1403 SET ${updates.join(', ')}
1404 WHERE request_id = $${paramIndex}
1405 RETURNING *
1406 `;
1407
1408 const result = await pool.query(query, params);
1409
1410 if (result.rows.length === 0) {
1411 return res.status(404).json({ error: 'Domain registration request not found' });
1412 }
1413
1414 res.json(result.rows[0]);
1415 } catch (err) {
1416 console.error('Error updating domain registration request:', err);
1417 res.status(500).json({ error: 'Failed to update domain registration request', details: err.message });
1418 }
1419});
1420
1421/**
1422 * @api {post} /domain-requests/backfill-notifications Backfill Notifications for Domain Requests
1423 * @apiName BackfillDomainRequestNotifications
1424 * @apiGroup DomainRequests
1425 * @apiDescription
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.
1430 *
1431 * Creates notifications with:
1432 * - Title: "New Domain Registration Request"
1433 * - Message: "[domain_name] registration requested by [requester_name]"
1434 * - Type: "info"
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:
1452 * {
1453 * "message": "Successfully created 3 notifications",
1454 * "created": [
1455 * {"domain": "example.com", "notification_id": 101},
1456 * {"domain": "test.org", "notification_id": 102},
1457 * {"domain": "demo.net", "notification_id": 103}
1458 * ]
1459 * }
1460 */
1461
1462// POST /domain-requests/backfill-notifications - Backfill notifications for existing requests (admin only)
1463router.post('/backfill-notifications', async (req, res) => {
1464 try {
1465 // Admin only
1466 if (!req.user || req.user.role !== 'admin') {
1467 return res.status(403).json({ error: 'Admin access required' });
1468 }
1469
1470 // Get root tenant
1471 const rootTenantResult = await pool.query(
1472 `SELECT tenant_id FROM tenants WHERE subdomain = 'admin' OR is_msp = true LIMIT 1`
1473 );
1474
1475 if (rootTenantResult.rows.length === 0) {
1476 return res.status(404).json({ error: 'Root tenant not found' });
1477 }
1478
1479 const rootTenantId = rootTenantResult.rows[0].tenant_id;
1480
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
1486 WHERE NOT EXISTS (
1487 SELECT 1 FROM notifications n
1488 WHERE n.message LIKE '%' || dr.domain_name || '%'
1489 )
1490 ORDER BY dr.requested_at DESC
1491 `);
1492
1493 // Create notifications for each request
1494 const created = [];
1495 for (const request of requestsResult.rows) {
1496 const registrant = request.registrant_contact;
1497 const fullName = `${registrant.firstName || ''} ${registrant.lastName || ''}`.trim() || 'Unknown';
1498
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`,
1503 [
1504 rootTenantId,
1505 'New Domain Registration Request',
1506 `${request.domain_name} registration requested by ${fullName}`,
1507 'info',
1508 request.customer_id,
1509 request.requested_at
1510 ]
1511 );
1512
1513 created.push({
1514 domain: request.domain_name,
1515 notification_id: notifResult.rows[0].notification_id
1516 });
1517 }
1518
1519 res.json({
1520 message: `Successfully created ${created.length} notifications`,
1521 created
1522 });
1523
1524 } catch (err) {
1525 console.error('Error backfilling notifications:', err);
1526 res.status(500).json({ error: 'Failed to backfill notifications', details: err.message });
1527 }
1528});
1529
1530module.exports = router;
1531