diff --git a/backend/app/routes/customer.py b/backend/app/routes/customer.py index c7584fb..dde86de 100644 --- a/backend/app/routes/customer.py +++ b/backend/app/routes/customer.py @@ -313,7 +313,7 @@ def update_domain(current_user, domain_id): @customer_bp.route('/domains/', methods=['DELETE']) @token_required def delete_domain(current_user, domain_id): - """Delete domain""" + """Delete domain and cleanup Cloudflare zone""" try: customer = current_user.customer if not customer: @@ -328,11 +328,49 @@ def delete_domain(current_user, domain_id): if not domain: return jsonify({'error': 'Domain not found'}), 404 - # Store CF account info before deletion + # Store info before deletion cf_account_id = domain.cf_account_id cf_account_type = domain.cf_account_type + cf_zone_id = domain.cf_zone_id - # Delete domain + cleanup_errors = [] + + # Delete Cloudflare zone if exists + if cf_zone_id: + try: + # Get CF API token + if cf_account_type == 'company' and cf_account_id: + admin_api = AdminAPIService() + account_result = admin_api.get_cf_account(cf_account_id) + + if account_result['status'] == 'success': + api_token = account_result['account'].get('api_token') + else: + api_token = None + cleanup_errors.append('Failed to get CF account API token') + + elif cf_account_type == 'own': + api_token = domain.get_cf_api_token() + else: + api_token = None + + if api_token: + from app.services.cloudflare_service import CloudflareService + cf_service = CloudflareService(api_token) + + # Delete zone from Cloudflare + delete_result = cf_service.delete_zone(cf_zone_id) + + if delete_result['status'] != 'success': + cleanup_errors.append(f"Failed to delete CF zone: {delete_result.get('error')}") + + except Exception as e: + cleanup_errors.append(f"Error deleting CF zone: {str(e)}") + + # Delete DNS records from database + DNSRecord.query.filter_by(domain_id=domain.id).delete() + + # Delete domain from database db.session.delete(domain) db.session.commit() @@ -342,11 +380,14 @@ def delete_domain(current_user, domain_id): decrement_result = admin_api.decrement_domain_count(cf_account_id) if decrement_result['status'] != 'success': - # Log warning but don't fail the request - # Domain is already deleted from customer DB - print(f"Warning: Failed to decrement domain count in Admin Panel: {decrement_result.get('error')}") + cleanup_errors.append(f"Failed to decrement domain count: {decrement_result.get('error')}") - return jsonify({'message': 'Domain deleted successfully'}), 200 + response = {'message': 'Domain deleted successfully'} + + if cleanup_errors: + response['warnings'] = cleanup_errors + + return jsonify(response), 200 except Exception as e: db.session.rollback() @@ -439,3 +480,336 @@ def get_customer_stats(current_user): return jsonify({'error': str(e)}), 500 +@customer_bp.route('/domains//dns-records', methods=['POST']) +@token_required +def create_dns_record(current_user, domain_id): + """Create a new DNS record for a domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + if not domain.cf_zone_id: + return jsonify({'error': 'Domain does not have a Cloudflare zone'}), 400 + + data = request.get_json() + + # Validate required fields + required = ['record_type', 'name', 'content'] + for field in required: + if not data.get(field): + return jsonify({'error': f'{field} is required'}), 400 + + # Get CF API token + if domain.cf_account_type == 'company' and domain.cf_account_id: + admin_api = AdminAPIService() + account_result = admin_api.get_cf_account(domain.cf_account_id) + + if account_result['status'] != 'success': + return jsonify({'error': 'Failed to get CF account'}), 500 + + api_token = account_result['account'].get('api_token') + elif domain.cf_account_type == 'own': + api_token = domain.get_cf_api_token() + else: + return jsonify({'error': 'No CF account configured'}), 400 + + if not api_token: + return jsonify({'error': 'CF API token not found'}), 500 + + # Create DNS record in Cloudflare + from app.services.cloudflare_service import CloudflareService + cf_service = CloudflareService(api_token) + + result = cf_service.create_dns_record( + zone_id=domain.cf_zone_id, + record_type=data['record_type'], + name=data['name'], + content=data['content'], + ttl=data.get('ttl', 1), + proxied=data.get('proxied', False) + ) + + if result['status'] != 'success': + return jsonify(result), 500 + + # Save to database + dns_record = DNSRecord( + domain_id=domain.id, + record_type=data['record_type'], + name=data['name'], + content=data['content'], + ttl=data.get('ttl', 1), + proxied=data.get('proxied', False), + cf_record_id=result['record']['id'] + ) + + db.session.add(dns_record) + db.session.commit() + + return jsonify({ + 'message': 'DNS record created successfully', + 'record': dns_record.to_dict() + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains//dns-records/', methods=['PUT']) +@token_required +def update_dns_record(current_user, domain_id, record_id): + """Update a DNS record""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + # Get DNS record + dns_record = DNSRecord.query.filter_by( + id=record_id, + domain_id=domain.id + ).first() + + if not dns_record: + return jsonify({'error': 'DNS record not found'}), 404 + + data = request.get_json() + + # Get CF API token + if domain.cf_account_type == 'company' and domain.cf_account_id: + admin_api = AdminAPIService() + account_result = admin_api.get_cf_account(domain.cf_account_id) + + if account_result['status'] != 'success': + return jsonify({'error': 'Failed to get CF account'}), 500 + + api_token = account_result['account'].get('api_token') + elif domain.cf_account_type == 'own': + api_token = domain.get_cf_api_token() + else: + return jsonify({'error': 'No CF account configured'}), 400 + + if not api_token: + return jsonify({'error': 'CF API token not found'}), 500 + + # Update DNS record in Cloudflare + from app.services.cloudflare_service import CloudflareService + cf_service = CloudflareService(api_token) + + result = cf_service.update_dns_record( + zone_id=domain.cf_zone_id, + record_id=dns_record.cf_record_id, + record_type=data.get('record_type', dns_record.record_type), + name=data.get('name', dns_record.name), + content=data.get('content', dns_record.content), + ttl=data.get('ttl', dns_record.ttl), + proxied=data.get('proxied', dns_record.proxied) + ) + + if result['status'] != 'success': + return jsonify(result), 500 + + # Update database + if 'record_type' in data: + dns_record.record_type = data['record_type'] + if 'name' in data: + dns_record.name = data['name'] + if 'content' in data: + dns_record.content = data['content'] + if 'ttl' in data: + dns_record.ttl = data['ttl'] + if 'proxied' in data: + dns_record.proxied = data['proxied'] + + db.session.commit() + + return jsonify({ + 'message': 'DNS record updated successfully', + 'record': dns_record.to_dict() + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains//dns-records/', methods=['DELETE']) +@token_required +def delete_dns_record(current_user, domain_id, record_id): + """Delete a DNS record""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + # Get DNS record + dns_record = DNSRecord.query.filter_by( + id=record_id, + domain_id=domain.id + ).first() + + if not dns_record: + return jsonify({'error': 'DNS record not found'}), 404 + + # Get CF API token + if domain.cf_account_type == 'company' and domain.cf_account_id: + admin_api = AdminAPIService() + account_result = admin_api.get_cf_account(domain.cf_account_id) + + if account_result['status'] != 'success': + return jsonify({'error': 'Failed to get CF account'}), 500 + + api_token = account_result['account'].get('api_token') + elif domain.cf_account_type == 'own': + api_token = domain.get_cf_api_token() + else: + return jsonify({'error': 'No CF account configured'}), 400 + + if not api_token: + return jsonify({'error': 'CF API token not found'}), 500 + + # Delete DNS record from Cloudflare + from app.services.cloudflare_service import CloudflareService + cf_service = CloudflareService(api_token) + + result = cf_service.delete_dns_record( + zone_id=domain.cf_zone_id, + record_id=dns_record.cf_record_id + ) + + if result['status'] != 'success': + return jsonify(result), 500 + + # Delete from database + db.session.delete(dns_record) + db.session.commit() + + return jsonify({ + 'message': 'DNS record deleted successfully' + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains//sync-dns', methods=['POST']) +@token_required +def sync_dns_records(current_user, domain_id): + """Sync DNS records from Cloudflare to database""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domain with customer isolation + domain = Domain.query.filter_by( + id=domain_id, + customer_id=customer.id + ).first() + + if not domain: + return jsonify({'error': 'Domain not found'}), 404 + + if not domain.cf_zone_id: + return jsonify({'error': 'Domain does not have a Cloudflare zone'}), 400 + + # Get CF API token + if domain.cf_account_type == 'company' and domain.cf_account_id: + admin_api = AdminAPIService() + account_result = admin_api.get_cf_account(domain.cf_account_id) + + if account_result['status'] != 'success': + return jsonify({'error': 'Failed to get CF account'}), 500 + + api_token = account_result['account'].get('api_token') + elif domain.cf_account_type == 'own': + api_token = domain.get_cf_api_token() + else: + return jsonify({'error': 'No CF account configured'}), 400 + + if not api_token: + return jsonify({'error': 'CF API token not found'}), 500 + + # Get DNS records from Cloudflare + from app.services.cloudflare_service import CloudflareService + cf_service = CloudflareService(api_token) + + result = cf_service.get_dns_records(domain.cf_zone_id) + + if result['status'] != 'success': + return jsonify(result), 500 + + # Sync to database + synced = 0 + for cf_record in result['records']: + # Check if record exists + existing = DNSRecord.query.filter_by( + domain_id=domain.id, + cf_record_id=cf_record['id'] + ).first() + + if existing: + # Update existing record + existing.record_type = cf_record['type'] + existing.name = cf_record['name'] + existing.content = cf_record['content'] + existing.ttl = cf_record['ttl'] + existing.proxied = cf_record['proxied'] + else: + # Create new record + new_record = DNSRecord( + domain_id=domain.id, + record_type=cf_record['type'], + name=cf_record['name'], + content=cf_record['content'], + ttl=cf_record['ttl'], + proxied=cf_record['proxied'], + cf_record_id=cf_record['id'] + ) + db.session.add(new_record) + + synced += 1 + + db.session.commit() + + return jsonify({ + 'message': 'DNS records synced successfully', + 'synced': synced, + 'total': result['total'] + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + diff --git a/backend/app/services/cloudflare_service.py b/backend/app/services/cloudflare_service.py index 38cb2d0..d21cab9 100644 --- a/backend/app/services/cloudflare_service.py +++ b/backend/app/services/cloudflare_service.py @@ -295,40 +295,204 @@ class CloudflareService: "steps": [], "errors": [] } - + try: # 1. SSL/TLS Mode: Full (strict) self.cf.zones.settings.ssl.patch(zone_id, data={"value": "full"}) ssl_config["steps"].append({"name": "ssl_mode", "status": "success", "value": "full"}) except Exception as e: ssl_config["errors"].append({"step": "ssl_mode", "error": str(e)}) - + try: # 2. Always Use HTTPS self.cf.zones.settings.always_use_https.patch(zone_id, data={"value": "on"}) ssl_config["steps"].append({"name": "always_https", "status": "success"}) except Exception as e: ssl_config["errors"].append({"step": "always_https", "error": str(e)}) - + try: # 3. Automatic HTTPS Rewrites self.cf.zones.settings.automatic_https_rewrites.patch(zone_id, data={"value": "on"}) ssl_config["steps"].append({"name": "auto_https_rewrites", "status": "success"}) except Exception as e: ssl_config["errors"].append({"step": "auto_https_rewrites", "error": str(e)}) - + try: # 4. Minimum TLS Version self.cf.zones.settings.min_tls_version.patch(zone_id, data={"value": "1.2"}) ssl_config["steps"].append({"name": "min_tls", "status": "success", "value": "1.2"}) except Exception as e: ssl_config["errors"].append({"step": "min_tls", "error": str(e)}) - + try: # 5. TLS 1.3 self.cf.zones.settings.tls_1_3.patch(zone_id, data={"value": "on"}) ssl_config["steps"].append({"name": "tls_1_3", "status": "success"}) except Exception as e: ssl_config["errors"].append({"step": "tls_1_3", "error": str(e)}) - + return ssl_config + + def get_dns_records(self, zone_id: str, record_type: Optional[str] = None) -> Dict: + """ + Get all DNS records for a zone + """ + try: + params = {} + if record_type: + params['type'] = record_type + + records = self.cf.zones.dns_records.get(zone_id, params=params) + + return { + 'status': 'success', + 'records': [ + { + 'id': r['id'], + 'type': r['type'], + 'name': r['name'], + 'content': r['content'], + 'proxied': r.get('proxied', False), + 'ttl': r['ttl'], + 'created_on': r.get('created_on'), + 'modified_on': r.get('modified_on') + } + for r in records + ], + 'total': len(records) + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + 'status': 'error', + 'error': f'Cloudflare API error: {str(e)}' + } + except Exception as e: + return { + 'status': 'error', + 'error': f'Unexpected error: {str(e)}' + } + + def create_dns_record(self, zone_id: str, record_type: str, name: str, + content: str, ttl: int = 1, proxied: bool = False) -> Dict: + """ + Create a new DNS record + """ + try: + data = { + 'type': record_type, + 'name': name, + 'content': content, + 'ttl': ttl, + 'proxied': proxied + } + + record = self.cf.zones.dns_records.post(zone_id, data=data) + + return { + 'status': 'success', + 'record': { + 'id': record['id'], + 'type': record['type'], + 'name': record['name'], + 'content': record['content'], + 'proxied': record.get('proxied', False), + 'ttl': record['ttl'] + } + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + 'status': 'error', + 'error': f'Cloudflare API error: {str(e)}' + } + except Exception as e: + return { + 'status': 'error', + 'error': f'Unexpected error: {str(e)}' + } + + def update_dns_record(self, zone_id: str, record_id: str, record_type: str, + name: str, content: str, ttl: int = 1, proxied: bool = False) -> Dict: + """ + Update an existing DNS record + """ + try: + data = { + 'type': record_type, + 'name': name, + 'content': content, + 'ttl': ttl, + 'proxied': proxied + } + + record = self.cf.zones.dns_records.put(zone_id, record_id, data=data) + + return { + 'status': 'success', + 'record': { + 'id': record['id'], + 'type': record['type'], + 'name': record['name'], + 'content': record['content'], + 'proxied': record.get('proxied', False), + 'ttl': record['ttl'] + } + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + 'status': 'error', + 'error': f'Cloudflare API error: {str(e)}' + } + except Exception as e: + return { + 'status': 'error', + 'error': f'Unexpected error: {str(e)}' + } + + def delete_dns_record(self, zone_id: str, record_id: str) -> Dict: + """ + Delete a DNS record + """ + try: + self.cf.zones.dns_records.delete(zone_id, record_id) + + return { + 'status': 'success', + 'message': 'DNS record deleted successfully' + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + 'status': 'error', + 'error': f'Cloudflare API error: {str(e)}' + } + except Exception as e: + return { + 'status': 'error', + 'error': f'Unexpected error: {str(e)}' + } + + def delete_zone(self, zone_id: str) -> Dict: + """ + Delete a Cloudflare zone + """ + try: + self.cf.zones.delete(zone_id) + + return { + 'status': 'success', + 'message': 'Zone deleted successfully' + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + 'status': 'error', + 'error': f'Cloudflare API error: {str(e)}' + } + except Exception as e: + return { + 'status': 'error', + 'error': f'Unexpected error: {str(e)}' + }