Files
anchorhost-backend/routes/domains.py
T
wfalkowski 2768f5f97d
Build and publish Docker image / build-and-publish (push) Successful in 8s
feat(domains): migrate user namespaces to wildcard CNAMEs
2026-10-20 18:10:23 +00:00

285 lines
10 KiB
Python

"""Domains API routes for AnchorHost."""
from domain_runtime import domain_manager
from domain_service import create_domain_record, delete_domain_record, get_domain_by_id, get_user_domains, link_domain_to_server, unlink_domain_from_server
from flask import Blueprint, jsonify, request
from runtime_config import (
logger,
)
from server_records import get_server_by_id
from user_service import authenticate_user, get_bearer_token, require_admin
from websocket_service import sync_user_domains_with_dns
__all__ = ['create_domain_api', 'delete_domain_api', 'fix_all_dnssec_api', 'get_domains_api', 'link_domain_api', 'sync_domains_api', 'test_domain_api', 'unlink_domain_api', 'verify_dnssec_api']
bp = Blueprint("domains", __name__)
@bp.route('/api/dnssec/fix-all', methods=['POST'])
def fix_all_dnssec_api():
"""Retired: DNSSEC now belongs to the parent zone, not user namespaces."""
try:
token = get_bearer_token()
user = authenticate_user(token)
# Only allow admin users
if not user or user.get('role') != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify({'error': 'Per-user DNSSEC delegation is retired; DNSSEC is managed on the parent zone.'}), 410
except Exception as e:
logger.error(f"Error fixing DNSSEC: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/dnssec/verify/<username>', methods=['GET'])
def verify_dnssec_api(username):
"""Retired: DNSSEC is inherited from the parent zone."""
try:
token = get_bearer_token()
user = authenticate_user(token)
# Users can only check their own DNSSEC, admins can check any
if not user or (user.get('role') != 'admin' and user.get('username') != username):
return jsonify({'error': 'Unauthorized'}), 403
return jsonify({'error': 'Per-user DNSSEC delegation is retired; DNSSEC is managed on the parent zone.'}), 410
except Exception as e:
logger.error(f"Error verifying DNSSEC: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains', methods=['GET'])
def get_domains_api():
"""Get domains API endpoint"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
include_serverless = request.args.get('include_serverless', 'true').lower() == 'true'
domains = get_user_domains(user['id'], include_serverless)
return jsonify({
'success': True,
'domains': domains,
'count': len(domains)
}), 200
except Exception as e:
logger.error(f"Error getting domains: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains/sync', methods=['POST'])
def sync_domains_api():
"""Sync domains between database and DNS"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
success = sync_user_domains_with_dns(user['id'])
if success:
domains = get_user_domains(user['id'], True)
return jsonify({
'success': True,
'message': 'DNS namespace verified and wildcard records are ready',
'domains': domains,
'count': len(domains)
}), 200
else:
return jsonify({'error': 'Failed to sync domains'}), 500
except Exception as e:
logger.error(f"Error syncing domains: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains', methods=['POST'])
def create_domain_api():
"""Create domain API endpoint"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
if not domain_manager:
return jsonify({'error': 'Domain service not available'}), 503
data = request.get_json()
subdomain = data.get('subdomain')
server_id = data.get('serverId')
if not subdomain:
return jsonify({'error': 'Subdomain name required'}), 400
# Check if subdomain is available
availability_result = domain_manager.check_domain_availability(subdomain, user['username'])
if not availability_result.get('available', False):
return jsonify({'error': availability_result.get('message', 'Domain not available')}), 400
# Create the subdomain
result = domain_manager.create_subdomain(user['username'], subdomain)
if result['success'] and result.get('domain_data'):
# Save to database
domain_record = {
'user_id': user['id'],
'subdomain': result['domain_data']['subdomain'],
'full_domain': result['domain_data']['domain'],
'record_type': result['domain_data']['record_type'],
'ip_address': result['domain_data']['ip_address'],
'ttl': result['domain_data']['ttl'],
'server_id': server_id,
'created_at': result['domain_data']['created_at']
}
db_domain = create_domain_record(domain_record)
if db_domain:
return jsonify({
'success': True,
'domain': db_domain,
'message': result['message']
}), 201
else:
return jsonify({'error': 'Domain created in DNS but failed to save to database'}), 500
else:
return jsonify({'error': result.get('message', 'Failed to create domain')}), 500
except Exception as e:
logger.error(f"Error creating domain: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains/<domain_id>', methods=['DELETE'])
def delete_domain_api(domain_id):
"""Delete domain API endpoint"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
if not domain_manager:
return jsonify({'error': 'Domain service not available'}), 503
# Get domain from database
domain = get_domain_by_id(domain_id)
if not domain or domain['user_id'] != user['id']:
return jsonify({'error': 'Domain not found or access denied'}), 404
# Delete from DNS
result = domain_manager.delete_subdomain(user['username'], domain['subdomain'])
if result['success']:
# Delete from database
if delete_domain_record(domain_id):
return jsonify({
'success': True,
'message': result['message']
}), 200
else:
return jsonify({'error': 'Domain deleted from DNS but failed to delete from database'}), 500
else:
return jsonify({'error': result.get('message', 'Failed to delete domain')}), 500
except Exception as e:
logger.error(f"Error deleting domain: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains/<domain_id>/link', methods=['POST'])
def link_domain_api(domain_id):
"""Link domain to server API endpoint"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
data = request.get_json()
server_id = data.get('serverId')
if not server_id:
return jsonify({'error': 'Server ID required'}), 400
# Verify domain belongs to user
domain = get_domain_by_id(domain_id)
if not domain or domain['user_id'] != user['id']:
return jsonify({'error': 'Domain not found or access denied'}), 404
# Verify server belongs to user
server = get_server_by_id(server_id)
if not server or server['owner_id'] != user['id']:
return jsonify({'error': 'Server not found or access denied'}), 404
# Check if server is a website or VPS
if server['type'] not in ['website', 'vps']:
return jsonify({'error': 'Domain can only be linked to Website or VPS servers'}), 400
# Link domain to server (this will create IngressRoute)
if link_domain_to_server(domain_id, server_id):
return jsonify({
'success': True,
'message': f'Domain linked to server {server["name"]} with IngressRoute created'
}), 200
else:
return jsonify({'error': 'Failed to link domain'}), 500
except Exception as e:
logger.error(f"Error linking domain: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains/<domain_id>/unlink', methods=['POST'])
def unlink_domain_api(domain_id):
"""Unlink domain from server API endpoint"""
try:
token = get_bearer_token()
user = authenticate_user(token)
if not user:
return jsonify({'error': 'Unauthorized'}), 401
# Verify domain belongs to user
domain = get_domain_by_id(domain_id)
if not domain or domain['user_id'] != user['id']:
return jsonify({'error': 'Domain not found or access denied'}), 404
# Unlink domain from server (this will delete IngressRoute)
if unlink_domain_from_server(domain_id):
return jsonify({
'success': True,
'message': 'Domain unlinked from server and IngressRoute removed'
}), 200
else:
return jsonify({'error': 'Failed to unlink domain'}), 500
except Exception as e:
logger.error(f"Error unlinking domain: {e}")
return jsonify({'error': str(e)}), 500
@bp.route('/api/domains/test', methods=['GET'])
def test_domain_api():
"""Test domain API connection"""
if not require_admin():
return jsonify({'error': 'Unauthorized'}), 403
try:
if not domain_manager:
return jsonify({'success': False, 'message': 'Domain manager not initialized'}), 503
success = domain_manager.test_connection()
return jsonify({
'success': success,
'message': 'Domain API connection test completed',
'connected': success
}), 200
except Exception as e:
logger.error(f"Error testing domain API: {e}")
return jsonify({'success': False, 'error': str(e)}), 500