feat: Add complete DNS management system with Cloudflare integration
- Add DNS record CRUD operations (Create, Read, Update, Delete) - Add Cloudflare zone management (create, delete) - Add DNS record sync from Cloudflare - Add domain deletion with cleanup (zone + DNS records) - Add SSL auto-configuration - Support for A, AAAA, CNAME, MX, TXT, NS, SRV records - Add proxy (CDN) support for DNS records - Integrate with Admin Panel for CF account management - Add customer isolation for all DNS operations
This commit is contained in:
parent
61dc963b28
commit
c62a1afd6f
|
|
@ -313,7 +313,7 @@ def update_domain(current_user, domain_id):
|
|||
@customer_bp.route('/domains/<int:domain_id>', 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/<int:domain_id>/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/<int:domain_id>/dns-records/<int:record_id>', 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/<int:domain_id>/dns-records/<int:record_id>', 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/<int:domain_id>/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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue