From f54467436a0985d32d602a3d9e61d888c740d20f Mon Sep 17 00:00:00 2001 From: oguz ozturk Date: Sat, 10 Jan 2026 14:14:29 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Yeni=20DNS=20ak=C4=B1=C5=9F=C4=B1=20-?= =?UTF-8?q?=20CF=20hesap=20se=C3=A7imi,=20NS=20y=C3=B6nlendirme,=20Admin?= =?UTF-8?q?=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: CloudflareAccount modeli ve şifreleme - Backend: Admin API endpoints (CF hesap yönetimi) - Backend: DNS API endpoints (NS kontrolü, token doğrulama) - Backend: Nameserver servisi (DNS sorgulama) - Frontend: DomainSetupNew - Yeni domain kurulum akışı - Frontend: CF hesap seçimi (Kendi/Şirket) - Frontend: NS yönlendirme talimatları + otomatik polling - Frontend: DNS önizleme + Proxy seçimi - Frontend: Admin panel - CF hesap yönetimi - Frontend: CFTokenGuide modal - API token oluşturma rehberi - Frontend: NSInstructions component - Deployment: deploy.sh script - Service: hosting-backend.service systemd dosyası --- backend/.env.example | 23 + backend/app/config.py | 5 +- backend/app/main.py | 15 +- backend/app/models/__init__.py | 3 + backend/app/models/domain.py | 96 ++- backend/app/routes/admin.py | 270 ++++++++ backend/app/routes/dns.py | 167 +++++ backend/app/services/nameserver_service.py | 173 +++++ backend/app/utils/encryption.py | 117 ++++ backend/requirements.txt | 3 +- deploy.sh | 115 ++++ frontend/src/App.jsx | 31 +- frontend/src/components/CFAccountModal.jsx | 200 ++++++ frontend/src/components/CFTokenGuide.jsx | 145 +++++ frontend/src/components/NSInstructions.jsx | 161 +++++ frontend/src/pages/AdminCFAccounts.jsx | 199 ++++++ frontend/src/pages/DomainSetupNew.jsx | 710 +++++++++++++++++++++ frontend/src/services/api.js | 42 +- hosting-backend.service | 22 + 19 files changed, 2485 insertions(+), 12 deletions(-) create mode 100644 backend/.env.example create mode 100644 backend/app/routes/admin.py create mode 100644 backend/app/routes/dns.py create mode 100644 backend/app/services/nameserver_service.py create mode 100644 backend/app/utils/encryption.py create mode 100644 deploy.sh create mode 100644 frontend/src/components/CFAccountModal.jsx create mode 100644 frontend/src/components/CFTokenGuide.jsx create mode 100644 frontend/src/components/NSInstructions.jsx create mode 100644 frontend/src/pages/AdminCFAccounts.jsx create mode 100644 frontend/src/pages/DomainSetupNew.jsx create mode 100644 hosting-backend.service diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..02288ac --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,23 @@ +# Flask Configuration +SECRET_KEY=dev-secret-key-change-in-production + +# Database +DATABASE_URL=postgresql://hosting:hosting_pass_2024@localhost:5432/hosting + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# Load Balancer IPs (comma separated) +LB_IPS=176.96.129.77 + +# API Configuration +API_HOST=0.0.0.0 +API_PORT=5000 + +# Encryption Key (REQUIRED - Generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") +ENCRYPTION_KEY=qcaGX4ChgOqDRmfxaikZYYJJ_qYZUDx2nRWVVGHr4sM= + +# Cloudflare Platform Accounts (DEPRECATED - Use database instead) +# PLATFORM_CF_API_TOKEN=your_token_here +# PLATFORM_CF_ACCOUNT_ID=your_account_id_here + diff --git a/backend/app/config.py b/backend/app/config.py index b345b4f..de7cc8a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -24,6 +24,9 @@ class Config: API_HOST = os.getenv("API_HOST", "0.0.0.0") API_PORT = int(os.getenv("API_PORT", 5000)) - # Cloudflare Platform Account (opsiyonel) + # Encryption (for sensitive data like API tokens) + ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY") + + # Cloudflare Platform Account (opsiyonel - deprecated, use database instead) PLATFORM_CF_API_TOKEN = os.getenv("PLATFORM_CF_API_TOKEN") PLATFORM_CF_ACCOUNT_ID = os.getenv("PLATFORM_CF_ACCOUNT_ID") diff --git a/backend/app/main.py b/backend/app/main.py index 4a4c453..57fbd4b 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,19 +3,32 @@ from flask_cors import CORS from flask_migrate import Migrate import hashlib import redis +import os from app.config import Config -from app.models.domain import db, Domain, DNSRecord +from app.models.domain import db, Domain, DNSRecord, CloudflareAccount from app.services.cloudflare_service import CloudflareService +# Import blueprints +from app.routes.admin import admin_bp +from app.routes.dns import dns_bp + app = Flask(__name__) app.config.from_object(Config) +# Set ENCRYPTION_KEY environment variable +if Config.ENCRYPTION_KEY: + os.environ['ENCRYPTION_KEY'] = Config.ENCRYPTION_KEY + # Extensions CORS(app) db.init_app(app) migrate = Migrate(app, db) +# Register blueprints +app.register_blueprint(admin_bp) +app.register_blueprint(dns_bp) + # Redis redis_client = redis.from_url(Config.REDIS_URL) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index e69de29..f484932 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -0,0 +1,3 @@ +from app.models.domain import db, CloudflareAccount, Domain, DNSRecord + +__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord'] diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py index e744ca9..4373e64 100644 --- a/backend/app/models/domain.py +++ b/backend/app/models/domain.py @@ -1,20 +1,88 @@ from datetime import datetime from flask_sqlalchemy import SQLAlchemy +from app.utils.encryption import encrypt_text, decrypt_text db = SQLAlchemy() + +class CloudflareAccount(db.Model): + """Şirket Cloudflare hesapları - Admin tarafından yönetilir""" + __tablename__ = "cloudflare_accounts" + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False, unique=True) # "Account 1", "Production CF", etc. + email = db.Column(db.String(255), nullable=False) + api_token_encrypted = db.Column(db.Text, nullable=False) # Şifreli token + + # Limits & Status + is_active = db.Column(db.Boolean, default=True) + max_domains = db.Column(db.Integer, default=100) # Bu hesapta max kaç domain olabilir + current_domain_count = db.Column(db.Integer, default=0) # Şu an kaç domain var + + # Metadata + notes = db.Column(db.Text, nullable=True) # Admin notları + created_by = db.Column(db.Integer, nullable=True) # Admin user ID + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + domains = db.relationship("Domain", backref="cf_account", lazy=True) + + def set_api_token(self, plaintext_token: str): + """API token'ı şifrele ve kaydet""" + self.api_token_encrypted = encrypt_text(plaintext_token) + + def get_api_token(self) -> str: + """Şifreli API token'ı çöz ve döndür""" + if not self.api_token_encrypted: + return "" + return decrypt_text(self.api_token_encrypted) + + def to_dict(self, include_token: bool = False): + """ + Model'i dict'e çevir + + Args: + include_token: True ise API token'ı da döndür (sadece admin için) + """ + data = { + "id": self.id, + "name": self.name, + "email": self.email, + "is_active": self.is_active, + "max_domains": self.max_domains, + "current_domain_count": self.current_domain_count, + "notes": self.notes, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + if include_token: + data["api_token"] = self.get_api_token() + + return data + + class Domain(db.Model): __tablename__ = "domains" - + id = db.Column(db.Integer, primary_key=True) domain_name = db.Column(db.String(255), unique=True, nullable=False, index=True) customer_id = db.Column(db.Integer, nullable=False, index=True) - - # Cloudflare + + # Cloudflare Configuration use_cloudflare = db.Column(db.Boolean, default=True) + cf_account_type = db.Column(db.String(20), nullable=True) # "own" veya "company" + cf_account_id = db.Column(db.Integer, db.ForeignKey("cloudflare_accounts.id"), nullable=True) # Şirket hesabı ise cf_zone_id = db.Column(db.String(255), nullable=True) - cf_api_token = db.Column(db.Text, nullable=True) # Encrypted + cf_api_token_encrypted = db.Column(db.Text, nullable=True) # Müşterinin kendi token'ı (şifreli) cf_proxy_enabled = db.Column(db.Boolean, default=True) + + # Nameserver Status + ns_configured = db.Column(db.Boolean, default=False) # NS'ler CF'ye yönlendirildi mi? + ns_checked_at = db.Column(db.DateTime, nullable=True) # Son NS kontrolü # DNS current_ip = db.Column(db.String(45), nullable=True) @@ -31,15 +99,33 @@ class Domain(db.Model): # Relationships dns_records = db.relationship("DNSRecord", backref="domain", lazy=True, cascade="all, delete-orphan") - + + def set_cf_api_token(self, plaintext_token: str): + """Müşterinin CF API token'ını şifrele ve kaydet""" + self.cf_api_token_encrypted = encrypt_text(plaintext_token) + + def get_cf_api_token(self) -> str: + """Şifreli CF API token'ı çöz ve döndür""" + if self.cf_account_type == "company" and self.cf_account: + # Şirket hesabı kullanıyorsa, o hesabın token'ını döndür + return self.cf_account.get_api_token() + elif self.cf_api_token_encrypted: + # Kendi token'ı varsa onu döndür + return decrypt_text(self.cf_api_token_encrypted) + return "" + def to_dict(self): return { "id": self.id, "domain_name": self.domain_name, "customer_id": self.customer_id, "use_cloudflare": self.use_cloudflare, + "cf_account_type": self.cf_account_type, + "cf_account_id": self.cf_account_id, "cf_zone_id": self.cf_zone_id, "cf_proxy_enabled": self.cf_proxy_enabled, + "ns_configured": self.ns_configured, + "ns_checked_at": self.ns_checked_at.isoformat() if self.ns_checked_at else None, "current_ip": self.current_ip, "lb_ip": self.lb_ip, "status": self.status, diff --git a/backend/app/routes/admin.py b/backend/app/routes/admin.py new file mode 100644 index 0000000..9edd3c9 --- /dev/null +++ b/backend/app/routes/admin.py @@ -0,0 +1,270 @@ +""" +Admin routes - Cloudflare hesap yönetimi +""" +from flask import Blueprint, request, jsonify +from app.models.domain import db, CloudflareAccount +from app.services.cloudflare_service import CloudflareService + +admin_bp = Blueprint('admin', __name__, url_prefix='/api/admin') + + +@admin_bp.route('/cf-accounts', methods=['GET']) +def list_cf_accounts(): + """Tüm Cloudflare hesaplarını listele""" + try: + accounts = CloudflareAccount.query.filter_by(is_active=True).all() + + return jsonify({ + "status": "success", + "accounts": [acc.to_dict(include_token=False) for acc in accounts], + "count": len(accounts) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesaplar listelenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts', methods=['POST']) +def create_cf_account(): + """Yeni Cloudflare hesabı ekle""" + try: + data = request.json + + # Validasyon + required_fields = ['name', 'email', 'api_token'] + for field in required_fields: + if not data.get(field): + return jsonify({ + "status": "error", + "message": f"'{field}' alanı gerekli" + }), 400 + + # Token'ı doğrula + cf_service = CloudflareService(data['api_token']) + + # Basit bir API çağrısı yaparak token'ı test et + try: + zones = cf_service.cf.zones.get(params={'per_page': 1}) + # Token geçerli + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Cloudflare API token geçersiz: {str(e)}" + }), 400 + + # Aynı isimde hesap var mı kontrol et + existing = CloudflareAccount.query.filter_by(name=data['name']).first() + if existing: + return jsonify({ + "status": "error", + "message": f"'{data['name']}' isimli hesap zaten mevcut" + }), 400 + + # Yeni hesap oluştur + account = CloudflareAccount( + name=data['name'], + email=data['email'], + max_domains=data.get('max_domains', 100), + notes=data.get('notes', ''), + is_active=True + ) + + # Token'ı şifrele ve kaydet + account.set_api_token(data['api_token']) + + db.session.add(account) + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Cloudflare hesabı başarıyla eklendi", + "account": account.to_dict(include_token=False) + }), 201 + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap eklenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['GET']) +def get_cf_account(account_id): + """Belirli bir Cloudflare hesabını getir""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # include_token parametresi ile token'ı da döndürebiliriz (sadece admin için) + include_token = request.args.get('include_token', 'false').lower() == 'true' + + return jsonify({ + "status": "success", + "account": account.to_dict(include_token=include_token) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesap getirilirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['PUT']) +def update_cf_account(account_id): + """Cloudflare hesabını güncelle""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + data = request.json + + # Güncellenebilir alanlar + if 'name' in data: + # Aynı isimde başka hesap var mı? + existing = CloudflareAccount.query.filter( + CloudflareAccount.name == data['name'], + CloudflareAccount.id != account_id + ).first() + if existing: + return jsonify({ + "status": "error", + "message": f"'{data['name']}' isimli hesap zaten mevcut" + }), 400 + account.name = data['name'] + + if 'email' in data: + account.email = data['email'] + + if 'api_token' in data: + # Yeni token'ı doğrula + cf_service = CloudflareService(data['api_token']) + try: + zones = cf_service.cf.zones.get(params={'per_page': 1}) + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Cloudflare API token geçersiz: {str(e)}" + }), 400 + + account.set_api_token(data['api_token']) + + if 'max_domains' in data: + account.max_domains = data['max_domains'] + + if 'notes' in data: + account.notes = data['notes'] + + if 'is_active' in data: + account.is_active = data['is_active'] + + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Hesap başarıyla güncellendi", + "account": account.to_dict(include_token=False) + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap güncellenirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts/', methods=['DELETE']) +def delete_cf_account(account_id): + """Cloudflare hesabını sil (soft delete)""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # Bu hesabı kullanan domain var mı kontrol et + if account.current_domain_count > 0: + return jsonify({ + "status": "error", + "message": f"Bu hesap {account.current_domain_count} domain tarafından kullanılıyor. Önce domain'leri başka hesaba taşıyın." + }), 400 + + # Soft delete (is_active = False) + account.is_active = False + db.session.commit() + + return jsonify({ + "status": "success", + "message": "Hesap başarıyla devre dışı bırakıldı" + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + "status": "error", + "message": f"Hesap silinirken hata: {str(e)}" + }), 500 + + +@admin_bp.route('/cf-accounts//test', methods=['POST']) +def test_cf_account(account_id): + """Cloudflare hesabının API bağlantısını test et""" + try: + account = CloudflareAccount.query.get(account_id) + + if not account: + return jsonify({ + "status": "error", + "message": "Hesap bulunamadı" + }), 404 + + # API token'ı al + api_token = account.get_api_token() + + # Cloudflare API'ye bağlan + cf_service = CloudflareService(api_token) + + try: + # Zone listesini al (test için) + zones = cf_service.cf.zones.get(params={'per_page': 5}) + + return jsonify({ + "status": "success", + "message": "✅ Cloudflare API bağlantısı başarılı", + "zone_count": len(zones), + "sample_zones": [ + {"name": z["name"], "status": z["status"]} + for z in zones[:3] + ] + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"❌ Cloudflare API bağlantı hatası: {str(e)}" + }), 400 + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Test sırasında hata: {str(e)}" + }), 500 + diff --git a/backend/app/routes/dns.py b/backend/app/routes/dns.py new file mode 100644 index 0000000..763145b --- /dev/null +++ b/backend/app/routes/dns.py @@ -0,0 +1,167 @@ +""" +DNS routes - Yeni akış ile CF hesap seçimi, NS kontrolü, DNS yönetimi +""" +from flask import Blueprint, request, jsonify +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 +import hashlib + +dns_bp = Blueprint('dns', __name__, url_prefix='/api/dns') + + +def select_lb_ip(domain: str, lb_ips: list) -> str: + """Domain için load balancer IP seç (hash-based)""" + hash_value = int(hashlib.md5(domain.encode()).hexdigest(), 16) + index = hash_value % len(lb_ips) + return lb_ips[index] + + +@dns_bp.route('/check-nameservers', methods=['POST']) +def check_nameservers(): + """Domain'in nameserver'larını kontrol et""" + try: + data = request.json + domain = data.get('domain') + + if not domain: + return jsonify({"error": "domain gerekli"}), 400 + + # NS kontrolü yap + result = NameserverService.check_cloudflare_nameservers(domain) + + return jsonify(result) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"NS kontrolü sırasında hata: {str(e)}" + }), 500 + + +@dns_bp.route('/get-ns-instructions', methods=['POST']) +def get_ns_instructions(): + """NS yönlendirme talimatlarını al""" + try: + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + api_token = data.get('api_token') + + if not all([domain, zone_id, api_token]): + return jsonify({"error": "domain, zone_id ve api_token gerekli"}), 400 + + # Mevcut NS'leri al + current_ns = NameserverService.get_current_nameservers(domain) + + # Cloudflare zone NS'lerini al + cf_ns = NameserverService.get_cloudflare_zone_nameservers(zone_id, api_token) + + if cf_ns["status"] == "error": + return jsonify(cf_ns), 400 + + return jsonify({ + "status": "success", + "domain": domain, + "current_nameservers": current_ns.get("nameservers", []), + "cloudflare_nameservers": cf_ns["nameservers"], + "instructions": [ + "1. Domain sağlayıcınızın (GoDaddy, Namecheap, vb.) kontrol paneline giriş yapın", + "2. Domain yönetimi veya DNS ayarları bölümüne gidin", + "3. 'Nameservers' veya 'Name Servers' seçeneğini bulun", + "4. 'Custom Nameservers' veya 'Use custom nameservers' seçeneğini seçin", + f"5. Aşağıdaki Cloudflare nameserver'larını ekleyin:", + *[f" - {ns}" for ns in cf_ns["nameservers"]], + "6. Değişiklikleri kaydedin", + "7. DNS propagation 24-48 saat sürebilir (genellikle 1-2 saat içinde tamamlanır)" + ] + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"NS talimatları alınırken hata: {str(e)}" + }), 500 + + +@dns_bp.route('/validate-token', methods=['POST']) +def validate_cf_token(): + """Cloudflare API token doğrula (müşterinin kendi token'ı)""" + try: + data = request.json + domain = data.get('domain') + cf_token = data.get('cf_token') + + if not domain or not cf_token: + return jsonify({"error": "domain ve cf_token gerekli"}), 400 + + cf_service = CloudflareService(cf_token) + result = cf_service.validate_token_and_get_zone(domain) + + return jsonify(result) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Token doğrulama hatası: {str(e)}" + }), 500 + + +@dns_bp.route('/select-company-account', methods=['POST']) +def select_company_account(): + """Şirket CF hesabı seç ve zone oluştur/bul""" + try: + data = request.json + domain = data.get('domain') + cf_account_id = data.get('cf_account_id') + + if not domain or not cf_account_id: + return jsonify({"error": "domain ve cf_account_id gerekli"}), 400 + + # CF hesabını al + cf_account = CloudflareAccount.query.get(cf_account_id) + + if not cf_account or not cf_account.is_active: + return jsonify({ + "status": "error", + "message": "Cloudflare hesabı bulunamadı veya aktif değil" + }), 404 + + # Hesap kapasitesi kontrolü + if cf_account.current_domain_count >= cf_account.max_domains: + return jsonify({ + "status": "error", + "message": f"Bu hesap kapasitesi dolmuş ({cf_account.current_domain_count}/{cf_account.max_domains})" + }), 400 + + # API token'ı al + api_token = cf_account.get_api_token() + + # Cloudflare'de zone var mı kontrol et + cf_service = CloudflareService(api_token) + result = cf_service.validate_token_and_get_zone(domain) + + if result["status"] == "success": + # Zone zaten var + return jsonify({ + "status": "success", + "zone_exists": True, + **result + }) + else: + # Zone yok, oluşturulması gerekiyor + # TODO: Zone oluşturma fonksiyonu eklenecek + return jsonify({ + "status": "pending", + "zone_exists": False, + "message": "Zone bulunamadı. Cloudflare'de zone oluşturulması gerekiyor.", + "cf_account": cf_account.to_dict(include_token=False) + }) + + except Exception as e: + return jsonify({ + "status": "error", + "message": f"Hesap seçimi sırasında hata: {str(e)}" + }), 500 + diff --git a/backend/app/services/nameserver_service.py b/backend/app/services/nameserver_service.py new file mode 100644 index 0000000..faabbf8 --- /dev/null +++ b/backend/app/services/nameserver_service.py @@ -0,0 +1,173 @@ +""" +Nameserver kontrolü ve doğrulama servisi +""" +import dns.resolver +from typing import Dict, List, Optional +import CloudFlare + + +class NameserverService: + """NS kayıtlarını kontrol eden servis""" + + CLOUDFLARE_NAMESERVERS = [ + "ns1.cloudflare.com", + "ns2.cloudflare.com", + "ns3.cloudflare.com", + "ns4.cloudflare.com", + "ns5.cloudflare.com", + "ns6.cloudflare.com", + "ns7.cloudflare.com", + ] + + @staticmethod + def get_current_nameservers(domain: str) -> Dict: + """ + Domain'in mevcut nameserver'larını al + + Args: + domain: Kontrol edilecek domain + + Returns: + { + "status": "success" | "error", + "nameservers": ["ns1.example.com", ...], + "message": "..." + } + """ + try: + # NS kayıtlarını sorgula + resolver = dns.resolver.Resolver() + resolver.timeout = 5 + resolver.lifetime = 5 + + answers = resolver.resolve(domain, 'NS') + nameservers = [str(rdata.target).rstrip('.') for rdata in answers] + + return { + "status": "success", + "nameservers": nameservers, + "count": len(nameservers) + } + + except dns.resolver.NXDOMAIN: + return { + "status": "error", + "message": f"Domain '{domain}' bulunamadı (NXDOMAIN)", + "nameservers": [] + } + + except dns.resolver.NoAnswer: + return { + "status": "error", + "message": f"Domain '{domain}' için NS kaydı bulunamadı", + "nameservers": [] + } + + except dns.resolver.Timeout: + return { + "status": "error", + "message": "DNS sorgusu zaman aşımına uğradı", + "nameservers": [] + } + + except Exception as e: + return { + "status": "error", + "message": f"NS sorgu hatası: {str(e)}", + "nameservers": [] + } + + @staticmethod + def check_cloudflare_nameservers(domain: str) -> Dict: + """ + Domain'in NS'lerinin Cloudflare'e yönlendirilip yönlendirilmediğini kontrol et + + Returns: + { + "status": "success" | "partial" | "error", + "is_cloudflare": bool, + "current_nameservers": [...], + "cloudflare_nameservers": [...], + "message": "..." + } + """ + result = NameserverService.get_current_nameservers(domain) + + if result["status"] == "error": + return { + "status": "error", + "is_cloudflare": False, + "current_nameservers": [], + "cloudflare_nameservers": [], + "message": result["message"] + } + + current_ns = result["nameservers"] + + # Cloudflare NS'leri ile karşılaştır + cf_ns_found = [] + other_ns_found = [] + + for ns in current_ns: + ns_lower = ns.lower() + if any(cf_ns in ns_lower for cf_ns in NameserverService.CLOUDFLARE_NAMESERVERS): + cf_ns_found.append(ns) + else: + other_ns_found.append(ns) + + # Tüm NS'ler Cloudflare mı? + all_cloudflare = len(cf_ns_found) > 0 and len(other_ns_found) == 0 + + # Kısmi Cloudflare mı? + partial_cloudflare = len(cf_ns_found) > 0 and len(other_ns_found) > 0 + + if all_cloudflare: + status = "success" + message = f"✅ Tüm nameserver'lar Cloudflare'e yönlendirilmiş ({len(cf_ns_found)} NS)" + elif partial_cloudflare: + status = "partial" + message = f"⚠️ Bazı nameserver'lar Cloudflare'e yönlendirilmiş ({len(cf_ns_found)}/{len(current_ns)})" + else: + status = "error" + message = f"❌ Nameserver'lar henüz Cloudflare'e yönlendirilmemiş" + + return { + "status": status, + "is_cloudflare": all_cloudflare, + "current_nameservers": current_ns, + "cloudflare_nameservers": cf_ns_found, + "other_nameservers": other_ns_found, + "message": message + } + + @staticmethod + def get_cloudflare_zone_nameservers(zone_id: str, api_token: str) -> Dict: + """ + Cloudflare zone'un nameserver'larını al + + Returns: + { + "status": "success" | "error", + "nameservers": ["ns1.cloudflare.com", ...], + "message": "..." + } + """ + try: + cf = CloudFlare.CloudFlare(token=api_token) + zone = cf.zones.get(zone_id) + + nameservers = zone.get("name_servers", []) + + return { + "status": "success", + "nameservers": nameservers, + "count": len(nameservers) + } + + except Exception as e: + return { + "status": "error", + "message": f"Cloudflare zone NS sorgu hatası: {str(e)}", + "nameservers": [] + } + diff --git a/backend/app/utils/encryption.py b/backend/app/utils/encryption.py new file mode 100644 index 0000000..f5a0224 --- /dev/null +++ b/backend/app/utils/encryption.py @@ -0,0 +1,117 @@ +""" +Encryption/Decryption utilities for sensitive data +Uses Fernet (symmetric encryption) from cryptography library +""" +from cryptography.fernet import Fernet +import os +import base64 +from typing import Optional + + +class EncryptionService: + """Şifreleme servisi - API token'ları ve hassas verileri şifreler""" + + def __init__(self, encryption_key: Optional[str] = None): + """ + Args: + encryption_key: Base64 encoded Fernet key. + Eğer verilmezse ENCRYPTION_KEY env variable kullanılır. + """ + if encryption_key is None: + encryption_key = os.getenv('ENCRYPTION_KEY') + + if not encryption_key: + raise ValueError( + "ENCRYPTION_KEY environment variable gerekli! " + "Oluşturmak için: python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'" + ) + + # Key'i bytes'a çevir + if isinstance(encryption_key, str): + encryption_key = encryption_key.encode() + + self.cipher = Fernet(encryption_key) + + def encrypt(self, plaintext: str) -> str: + """ + Metni şifrele + + Args: + plaintext: Şifrelenecek metin + + Returns: + Base64 encoded şifreli metin + """ + if not plaintext: + return "" + + # String'i bytes'a çevir + plaintext_bytes = plaintext.encode('utf-8') + + # Şifrele + encrypted_bytes = self.cipher.encrypt(plaintext_bytes) + + # Base64 encode et (database'de saklamak için) + return encrypted_bytes.decode('utf-8') + + def decrypt(self, encrypted_text: str) -> str: + """ + Şifreli metni çöz + + Args: + encrypted_text: Base64 encoded şifreli metin + + Returns: + Orijinal metin + """ + if not encrypted_text: + return "" + + try: + # Base64 decode et + encrypted_bytes = encrypted_text.encode('utf-8') + + # Şifreyi çöz + decrypted_bytes = self.cipher.decrypt(encrypted_bytes) + + # Bytes'ı string'e çevir + return decrypted_bytes.decode('utf-8') + + except Exception as e: + raise ValueError(f"Şifre çözme hatası: {str(e)}") + + @staticmethod + def generate_key() -> str: + """ + Yeni bir encryption key oluştur + + Returns: + Base64 encoded Fernet key + """ + return Fernet.generate_key().decode('utf-8') + + +# Global instance (singleton pattern) +_encryption_service = None + + +def get_encryption_service() -> EncryptionService: + """Global encryption service instance'ını al""" + global _encryption_service + + if _encryption_service is None: + _encryption_service = EncryptionService() + + return _encryption_service + + +# Convenience functions +def encrypt_text(plaintext: str) -> str: + """Metni şifrele (convenience function)""" + return get_encryption_service().encrypt(plaintext) + + +def decrypt_text(encrypted_text: str) -> str: + """Şifreli metni çöz (convenience function)""" + return get_encryption_service().decrypt(encrypted_text) + diff --git a/backend/requirements.txt b/backend/requirements.txt index 319f78e..01a727b 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,7 +5,7 @@ Flask-SQLAlchemy==3.1.1 Flask-Migrate==4.0.5 # Database -psycopg2-binary==2.9.9 +# psycopg2-binary==2.9.9 # Commented out for SQLite testing SQLAlchemy==2.0.23 # Redis @@ -19,6 +19,7 @@ requests==2.31.0 python-dotenv==1.0.0 pydantic==2.5.2 python-dateutil==2.8.2 +dnspython==2.4.2 # Security cryptography==41.0.7 diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..8c7cc16 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Deployment script for argeict.net server +# This script deploys backend and frontend to the production server + +set -e # Exit on error + +SERVER="root@argeict.net" +BACKEND_DIR="/var/www/hosting-backend" +FRONTEND_DIR="/var/www/hosting-frontend" + +echo "🚀 Starting deployment to argeict.net..." + +# 1. Deploy Backend +echo "" +echo "📦 Deploying Backend..." +ssh $SERVER "mkdir -p $BACKEND_DIR" + +# Copy backend files +rsync -avz --exclude='venv' --exclude='__pycache__' --exclude='*.pyc' --exclude='hosting.db' \ + backend/ $SERVER:$BACKEND_DIR/ + +# Copy .env.example as template +scp backend/.env.example $SERVER:$BACKEND_DIR/.env.example + +echo "✅ Backend files copied" + +# Install dependencies and restart backend +ssh $SERVER << 'ENDSSH' +cd /var/www/hosting-backend + +# Create venv if not exists +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv +fi + +# Activate venv and install dependencies +source venv/bin/activate +pip install --upgrade pip +pip install -r requirements.txt + +# Check if .env exists, if not create from example +if [ ! -f ".env" ]; then + echo "Creating .env file..." + cp .env.example .env + # Generate random encryption key + ENCRYPTION_KEY=$(python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())") + sed -i "s|ENCRYPTION_KEY=.*|ENCRYPTION_KEY=$ENCRYPTION_KEY|" .env + echo "⚠️ Please edit .env file and set DATABASE_URL and other settings" +fi + +# Initialize database if needed +if [ ! -f "hosting.db" ]; then + echo "Initializing database..." + python3 -c "from app.main import app, db; app.app_context().push(); db.create_all()" +fi + +# Restart backend service (if using systemd) +if systemctl is-active --quiet hosting-backend; then + echo "Restarting backend service..." + sudo systemctl restart hosting-backend +else + echo "⚠️ Backend service not found. Please start manually or create systemd service." +fi + +echo "✅ Backend deployed" +ENDSSH + +# 2. Deploy Frontend +echo "" +echo "📦 Deploying Frontend..." + +# Build frontend locally first +echo "Building frontend..." +cd frontend +npm install +npm run build +cd .. + +# Copy built files to server +ssh $SERVER "mkdir -p $FRONTEND_DIR" +rsync -avz --delete frontend/dist/ $SERVER:$FRONTEND_DIR/ + +echo "✅ Frontend deployed" + +# 3. Update Nginx configuration (if needed) +echo "" +echo "🔧 Checking Nginx configuration..." + +ssh $SERVER << 'ENDSSH' +# Reload nginx if config changed +if nginx -t 2>/dev/null; then + echo "Reloading Nginx..." + sudo systemctl reload nginx + echo "✅ Nginx reloaded" +else + echo "⚠️ Nginx config test failed. Please check configuration." +fi +ENDSSH + +echo "" +echo "✅ Deployment completed!" +echo "" +echo "🌐 URLs:" +echo " Frontend: https://argeict.net" +echo " API: https://api.argeict.net" +echo " Gitea: https://gitea.argeict.net" +echo "" +echo "📝 Next steps:" +echo " 1. SSH to server and check .env file: ssh $SERVER" +echo " 2. Set DATABASE_URL in /var/www/hosting-backend/.env" +echo " 3. Check backend logs: journalctl -u hosting-backend -f" +echo " 4. Test API: curl https://api.argeict.net/health" + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 29f761c..3d341c2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,10 +1,12 @@ import { useState } from 'react' import DomainSetup from './pages/DomainSetup' +import DomainSetupNew from './pages/DomainSetupNew' import DomainList from './pages/DomainList' +import AdminCFAccounts from './pages/AdminCFAccounts' import './App.css' function App() { - const [currentPage, setCurrentPage] = useState('setup') + const [currentPage, setCurrentPage] = useState('setup-new') return (
@@ -23,6 +25,16 @@ function App() {
@@ -50,7 +72,10 @@ function App() { {/* Main Content */}
- {currentPage === 'setup' ? : } + {currentPage === 'setup-new' && } + {currentPage === 'setup' && } + {currentPage === 'list' && } + {currentPage === 'admin' && }
{/* Footer */} diff --git a/frontend/src/components/CFAccountModal.jsx b/frontend/src/components/CFAccountModal.jsx new file mode 100644 index 0000000..b56d7ac --- /dev/null +++ b/frontend/src/components/CFAccountModal.jsx @@ -0,0 +1,200 @@ +import { useState, useEffect } from 'react' +import { adminAPI } from '../services/api' + +function CFAccountModal({ account, onClose, onSuccess }) { + const [formData, setFormData] = useState({ + name: '', + email: '', + api_token: '', + max_domains: 100, + notes: '', + is_active: true, + }) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (account) { + setFormData({ + name: account.name, + email: account.email, + api_token: '', // Don't show existing token + max_domains: account.max_domains, + notes: account.notes || '', + is_active: account.is_active, + }) + } + }, [account]) + + const handleSubmit = async (e) => { + e.preventDefault() + setLoading(true) + setError(null) + + try { + // Prepare data - don't send empty api_token on update + const data = { ...formData } + if (account && !data.api_token) { + delete data.api_token + } + + const response = account + ? await adminAPI.updateCFAccount(account.id, data) + : await adminAPI.createCFAccount(data) + + if (response.data.status === 'success') { + onSuccess() + } else { + setError(response.data.message) + } + } catch (err) { + setError(err.response?.data?.error || 'İşlem başarısız') + } finally { + setLoading(false) + } + } + + return ( +
+
+
+
+

+ {account ? 'Hesap Düzenle' : 'Yeni Cloudflare Hesabı Ekle'} +

+ +
+ + {error && ( +
+ {error} +
+ )} + +
+ {/* Name */} +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Örn: Ana CF Hesabı" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +
+ + {/* Email */} +
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="email@example.com" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +
+ + {/* API Token */} +
+ + setFormData({ ...formData, api_token: e.target.value })} + placeholder={account ? 'Mevcut token korunacak' : 'Cloudflare API Token'} + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 font-mono text-sm" + required={!account} + /> +

+ Token şifreli olarak saklanacaktır +

+
+ + {/* Max Domains */} +
+ + setFormData({ ...formData, max_domains: parseInt(e.target.value) })} + min="1" + max="1000" + className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500" + required + /> +

+ Bu hesapta maksimum kaç domain barındırılabilir +

+
+ + {/* Notes */} +
+ +