From 0190a8e0a03bf798ba29c206448c4cc4d64c2b97 Mon Sep 17 00:00:00 2001 From: oguz ozturk Date: Sat, 10 Jan 2026 16:51:27 +0300 Subject: [PATCH] Add customer routes and update domain model with project_name --- backend/app/main.py | 2 + backend/app/models/domain.py | 6 + backend/app/routes/customer.py | 328 +++++++++++++++++++++++++++++++++ backend/app/routes/dns.py | 126 +++++++++++++ 4 files changed, 462 insertions(+) create mode 100644 backend/app/routes/customer.py diff --git a/backend/app/main.py b/backend/app/main.py index 3094841..f7d1c12 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -13,6 +13,7 @@ from app.services.cloudflare_service import CloudflareService from app.routes.auth import auth_bp from app.routes.admin import admin_bp from app.routes.dns import dns_bp +from app.routes.customer import customer_bp app = Flask(__name__) app.config.from_object(Config) @@ -30,6 +31,7 @@ migrate = Migrate(app, db) app.register_blueprint(auth_bp) app.register_blueprint(admin_bp) app.register_blueprint(dns_bp) +app.register_blueprint(customer_bp) # Redis redis_client = redis.from_url(Config.REDIS_URL) diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py index 5f04ce3..5a56446 100644 --- a/backend/app/models/domain.py +++ b/backend/app/models/domain.py @@ -72,6 +72,10 @@ class Domain(db.Model): domain_name = db.Column(db.String(255), unique=True, nullable=False, index=True) customer_id = db.Column(db.Integer, db.ForeignKey('customers.id'), nullable=False, index=True) + # Project Information + project_name = db.Column(db.String(255), nullable=True) # "My WordPress Site" + created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) # User who created this domain + # Cloudflare Configuration use_cloudflare = db.Column(db.Boolean, default=True) cf_account_type = db.Column(db.String(20), nullable=True) # "own" veya "company" @@ -119,6 +123,8 @@ class Domain(db.Model): "id": self.id, "domain_name": self.domain_name, "customer_id": self.customer_id, + "project_name": self.project_name, + "created_by": self.created_by, "use_cloudflare": self.use_cloudflare, "cf_account_type": self.cf_account_type, "cf_account_id": self.cf_account_id, diff --git a/backend/app/routes/customer.py b/backend/app/routes/customer.py new file mode 100644 index 0000000..f00a813 --- /dev/null +++ b/backend/app/routes/customer.py @@ -0,0 +1,328 @@ +""" +Customer Routes - Domain Management +Customer-specific endpoints with isolation +""" +from flask import Blueprint, request, jsonify +from app.models.domain import db, Domain, DNSRecord, CloudflareAccount +from app.models.user import Customer +from app.middleware.auth import token_required +from datetime import datetime + +customer_bp = Blueprint('customer', __name__, url_prefix='/api/customer') + + +@customer_bp.route('/domains', methods=['GET']) +@token_required +def get_domains(current_user): + """Get all domains for the current customer""" + try: + # Get customer + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Get domains with customer isolation + domains = Domain.query.filter_by(customer_id=customer.id).all() + + # Add CF account info + result = [] + for domain in domains: + domain_dict = domain.to_dict() + + # Add CF account name if using company account + if domain.cf_account_type == 'company' and domain.cf_account: + domain_dict['cf_account_name'] = domain.cf_account.name + else: + domain_dict['cf_account_name'] = 'Own Account' + + # Add DNS record count + domain_dict['dns_record_count'] = len(domain.dns_records) + + result.append(domain_dict) + + return jsonify({ + 'domains': result, + 'total': len(result), + 'limit': customer.max_domains + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['GET']) +@token_required +def get_domain(current_user, domain_id): + """Get specific domain details""" + 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 + + domain_dict = domain.to_dict() + + # Add CF account info + if domain.cf_account_type == 'company' and domain.cf_account: + domain_dict['cf_account_name'] = domain.cf_account.name + + # Add DNS records + domain_dict['dns_records'] = [record.to_dict() for record in domain.dns_records] + + return jsonify(domain_dict), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains', methods=['POST']) +@token_required +def create_domain(current_user): + """Create a new domain""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.get_json() + + # Validate required fields + if not data.get('domain_name'): + return jsonify({'error': 'domain_name is required'}), 400 + + domain_name = data['domain_name'].lower().strip() + + # Check domain limit + current_count = Domain.query.filter_by(customer_id=customer.id).count() + if current_count >= customer.max_domains: + return jsonify({ + 'error': f'Domain limit reached. Maximum {customer.max_domains} domains allowed.' + }), 403 + + # Check if domain already exists + existing = Domain.query.filter_by(domain_name=domain_name).first() + if existing: + return jsonify({'error': 'Domain already exists'}), 409 + + # Validate CF account if using company account + cf_account_id = data.get('cf_account_id') + cf_account_type = data.get('cf_account_type', 'company') + + if cf_account_type == 'company': + if not cf_account_id: + return jsonify({'error': 'cf_account_id is required for company account'}), 400 + + cf_account = CloudflareAccount.query.get(cf_account_id) + if not cf_account: + return jsonify({'error': 'Cloudflare account not found'}), 404 + + if not cf_account.is_active: + return jsonify({'error': 'Cloudflare account is not active'}), 400 + + # Check CF account capacity + if cf_account.current_domain_count >= cf_account.max_domains: + return jsonify({ + 'error': f'Cloudflare account is full ({cf_account.max_domains} domains max)' + }), 400 + + # Create domain + domain = Domain( + domain_name=domain_name, + customer_id=customer.id, + created_by=current_user.id, + project_name=data.get('project_name'), + use_cloudflare=data.get('use_cloudflare', True), + cf_account_type=cf_account_type, + cf_account_id=cf_account_id if cf_account_type == 'company' else None, + cf_zone_id=data.get('cf_zone_id'), + cf_proxy_enabled=data.get('cf_proxy_enabled', True), + status='pending' + ) + + # If using own CF account, save encrypted token + if cf_account_type == 'own' and data.get('cf_api_token'): + domain.set_cf_api_token(data['cf_api_token']) + + db.session.add(domain) + + # Update CF account domain count if using company account + if cf_account_type == 'company' and cf_account: + cf_account.current_domain_count += 1 + + db.session.commit() + + return jsonify({ + 'message': 'Domain created successfully', + 'domain': domain.to_dict() + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['PUT']) +@token_required +def update_domain(current_user, domain_id): + """Update 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 + + data = request.get_json() + + # Update allowed fields + if 'project_name' in data: + domain.project_name = data['project_name'] + + if 'cf_proxy_enabled' in data: + domain.cf_proxy_enabled = data['cf_proxy_enabled'] + + if 'status' in data and data['status'] in ['pending', 'active', 'suspended', 'error']: + domain.status = data['status'] + + domain.updated_at = datetime.utcnow() + db.session.commit() + + return jsonify({ + 'message': 'Domain updated successfully', + 'domain': domain.to_dict() + }), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains/', methods=['DELETE']) +@token_required +def delete_domain(current_user, domain_id): + """Delete 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 + + # Update CF account count if using company account + if domain.cf_account_type == 'company' and domain.cf_account: + domain.cf_account.current_domain_count = max(0, domain.cf_account.current_domain_count - 1) + + db.session.delete(domain) + db.session.commit() + + return jsonify({'message': 'Domain deleted successfully'}), 200 + + except Exception as e: + db.session.rollback() + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/domains//dns', methods=['GET']) +@token_required +def get_domain_dns(current_user, domain_id): + """Get DNS records 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 + + records = [record.to_dict() for record in domain.dns_records] + + return jsonify({ + 'domain': domain.domain_name, + 'records': records, + 'total': len(records) + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/cloudflare-accounts', methods=['GET']) +@token_required +def get_cloudflare_accounts(current_user): + """Get available Cloudflare accounts (company accounts only)""" + try: + # Get active company CF accounts + accounts = CloudflareAccount.query.filter_by(is_active=True).all() + + result = [] + for account in accounts: + account_dict = account.to_dict(include_token=False) + # Calculate available capacity + account_dict['available_capacity'] = account.max_domains - account.current_domain_count + account_dict['is_full'] = account.current_domain_count >= account.max_domains + result.append(account_dict) + + return jsonify({ + 'accounts': result, + 'total': len(result) + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@customer_bp.route('/stats', methods=['GET']) +@token_required +def get_customer_stats(current_user): + """Get customer statistics""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + # Count domains by status + total_domains = Domain.query.filter_by(customer_id=customer.id).count() + active_domains = Domain.query.filter_by(customer_id=customer.id, status='active').count() + pending_domains = Domain.query.filter_by(customer_id=customer.id, status='pending').count() + + return jsonify({ + 'total_domains': total_domains, + 'active_domains': active_domains, + 'pending_domains': pending_domains, + 'max_domains': customer.max_domains, + 'available_slots': customer.max_domains - total_domains, + 'subscription_plan': customer.subscription_plan + }), 200 + + except Exception as e: + return jsonify({'error': str(e)}), 500 + + diff --git a/backend/app/routes/dns.py b/backend/app/routes/dns.py index 763145b..2e2d56f 100644 --- a/backend/app/routes/dns.py +++ b/backend/app/routes/dns.py @@ -6,6 +6,7 @@ from datetime import datetime from app.models.domain import db, CloudflareAccount, Domain from app.services.cloudflare_service import CloudflareService from app.services.nameserver_service import NameserverService +from app.middleware.auth import token_required import hashlib dns_bp = Blueprint('dns', __name__, url_prefix='/api/dns') @@ -165,3 +166,128 @@ def select_company_account(): "message": f"Hesap seçimi sırasında hata: {str(e)}" }), 500 + +@dns_bp.route('/preview-changes', methods=['POST']) +@token_required +def preview_changes(current_user): + """DNS değişiklik önizlemesi""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + lb_ips = data.get('lb_ips', ['176.96.129.77']) # Default server IP + + if not all([domain, zone_id, cf_token]): + return jsonify({"error": "domain, zone_id ve cf_token gerekli"}), 400 + + # Load balancer IP seç + new_ip = select_lb_ip(domain, lb_ips) + + cf_service = CloudflareService(cf_token) + preview = cf_service.generate_dns_preview(domain, zone_id, new_ip) + + return jsonify(preview) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Preview oluşturma hatası: {str(e)}" + }), 500 + + +@dns_bp.route('/apply-changes', methods=['POST']) +@token_required +def apply_changes(current_user): + """DNS değişikliklerini uygula ve domain'i kaydet""" + try: + customer = current_user.customer + if not customer: + return jsonify({'error': 'Customer profile not found'}), 404 + + data = request.json + domain_name = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + preview = data.get('preview') + proxy_enabled = data.get('proxy_enabled', True) + cf_account_id = data.get('cf_account_id') + cf_account_type = data.get('cf_account_type', 'company') + project_name = data.get('project_name') + + if not all([domain_name, zone_id, cf_token, preview]): + return jsonify({"error": "Eksik parametreler"}), 400 + + # Check domain limit + current_count = Domain.query.filter_by(customer_id=customer.id).count() + if current_count >= customer.max_domains: + return jsonify({ + 'error': f'Domain limit reached ({customer.max_domains})' + }), 403 + + # Check if domain already exists + existing = Domain.query.filter_by(domain_name=domain_name).first() + if existing: + return jsonify({'error': 'Domain already exists'}), 409 + + cf_service = CloudflareService(cf_token) + + # DNS değişikliklerini uygula + result = cf_service.apply_dns_changes(zone_id, preview, proxy_enabled) + + if result["status"] == "success": + # SSL yapılandır + ssl_config = cf_service.configure_ssl(zone_id) + + # Domain'i veritabanına kaydet + domain_obj = Domain( + domain_name=domain_name, + customer_id=customer.id, + created_by=current_user.id, + project_name=project_name, + use_cloudflare=True, + cf_account_type=cf_account_type, + cf_account_id=cf_account_id if cf_account_type == 'company' else None, + cf_zone_id=zone_id, + cf_proxy_enabled=proxy_enabled, + lb_ip=preview.get("new_ip"), + status="active", + dns_configured=True, + ssl_configured=len(ssl_config.get("errors", [])) == 0 + ) + + # If using own CF account, save encrypted token + if cf_account_type == 'own': + domain_obj.set_cf_api_token(cf_token) + + db.session.add(domain_obj) + + # Update CF account domain count if using company account + if cf_account_type == 'company' and cf_account_id: + cf_account = CloudflareAccount.query.get(cf_account_id) + if cf_account: + cf_account.current_domain_count += 1 + + db.session.commit() + + return jsonify({ + "status": "success", + "dns_result": result, + "ssl_config": ssl_config, + "domain_id": domain_obj.id, + "domain": domain_obj.to_dict() + }), 201 + + return jsonify(result), 500 + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"DNS uygulama hatası: {str(e)}" + }), 500 +