commit 0593965305c2b9b205d0096941c16b63e539499f Author: oguz ozturk Date: Sat Jan 10 13:06:24 2026 +0300 Initial commit: Backend API with Cloudflare integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16dd292 --- /dev/null +++ b/.gitignore @@ -0,0 +1,61 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ +*.egg-info/ +dist/ +build/ + +# Flask +instance/ +.webassets-cache + +# Database +*.db +*.sqlite +*.sqlite3 + +# Environment +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +logs/ + +# Node (for frontend) +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Frontend build +frontend/dist/ +frontend/build/ + +# SSH Keys +*.pem +*.key +hosting_platform_temp + +# Temporary files +backend_main.py + diff --git a/README.md b/README.md new file mode 100644 index 0000000..3568734 --- /dev/null +++ b/README.md @@ -0,0 +1,155 @@ +# 🚀 Hosting Platform - Automated DNS & SSL Management + +Modern, otomatik DNS ve SSL yönetim platformu. Cloudflare entegrasyonu ile domain'leri saniyeler içinde yapılandırın. + +## 📋 Özellikler + +- ✅ **Cloudflare Entegrasyonu**: API token ile otomatik DNS yönetimi +- ✅ **DNS Önizleme**: Değişiklikleri uygulamadan önce görüntüleyin +- ✅ **Otomatik SSL**: Cloudflare SSL/TLS yapılandırması +- ✅ **Load Balancer**: Hash-based IP dağıtımı +- ✅ **Modern UI**: React + Vite ile hızlı ve responsive arayüz +- ✅ **Auto-Deploy**: Git push ile otomatik deployment +- ✅ **PostgreSQL**: Güvenilir veri saklama +- ✅ **Redis**: Hızlı cache ve session yönetimi + +## 🏗️ Mimari + +``` +┌─────────────────────────────────────────────────────────┐ +│ Cloudflare CDN │ +│ (SSL/TLS + DDoS Protection) │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Nginx Reverse Proxy │ +│ (176.96.129.77 - Load Balancer) │ +└─────────────────────────────────────────────────────────┘ + │ + ┌──────────────┴──────────────┐ + ▼ ▼ + ┌───────────────┐ ┌───────────────┐ + │ Frontend │ │ Backend │ + │ React + Vite │ │ Flask API │ + │ Port 3001 │ │ Port 5000 │ + └───────────────┘ └───────────────┘ + │ + ┌────────────────┴────────────────┐ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ PostgreSQL │ │ Redis │ + │ Port 5432 │ │ Port 6379 │ + └──────────────┘ └──────────────┘ +``` + +## 🛠️ Teknolojiler + +### Backend +- **Flask 3.0** - Modern Python web framework +- **SQLAlchemy 2.0** - ORM +- **PostgreSQL 16** - Database +- **Redis 7.0** - Cache & Sessions +- **Cloudflare API** - DNS & SSL management + +### Frontend +- **React 18** - UI library +- **Vite** - Build tool +- **TailwindCSS** - Styling +- **Axios** - HTTP client + +### DevOps +- **Gitea** - Git repository +- **Nginx** - Reverse proxy +- **Supervisor** - Process management +- **Systemd** - Service management + +## 📦 Kurulum + +### Gereksinimler +- Ubuntu 22.04+ +- Python 3.12+ +- Node.js 18+ +- PostgreSQL 16 +- Redis 7.0 + +### Backend Kurulumu + +```bash +cd backend +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt + +# Environment variables +cp .env.example .env +# .env dosyasını düzenleyin + +# Database migration +flask db upgrade + +# Başlatma +python app/main.py +``` + +### Frontend Kurulumu + +```bash +cd frontend +npm install +npm run dev +``` + +## 🔧 Yapılandırma + +### Environment Variables + +```bash +# Database +DATABASE_URL=postgresql://user:pass@localhost:5432/hosting_db + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# API +API_HOST=0.0.0.0 +API_PORT=5000 + +# Load Balancer IPs +LB_IPS=176.96.129.77,176.96.129.78,176.96.129.79 + +# Secret +SECRET_KEY=your-secret-key-here +``` + +## 🚀 API Endpoints + +### Health Check +```bash +GET /health +``` + +### DNS Management +```bash +POST /api/dns/validate-token +POST /api/dns/preview-changes +POST /api/dns/apply-changes +``` + +### Domain Management +```bash +GET /api/domains +GET /api/domains/ +POST /api/domains +PUT /api/domains/ +DELETE /api/domains/ +``` + +## 📝 Lisans + +MIT License - Detaylar için LICENSE dosyasına bakın. + +## 👨‍💻 Geliştirici + +Hosting Platform Team + diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..b345b4f --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,29 @@ +import os +from dotenv import load_dotenv + +load_dotenv() + +class Config: + # Flask + SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production") + + # Database + SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_URL", + "postgresql://hosting:hosting_pass_2024@localhost:5432/hosting" + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Redis + REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0") + + # Load Balancer IPs + LB_IPS = os.getenv("LB_IPS", "176.96.129.77").split(",") + + # API + API_HOST = os.getenv("API_HOST", "0.0.0.0") + API_PORT = int(os.getenv("API_PORT", 5000)) + + # Cloudflare Platform Account (opsiyonel) + 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 new file mode 100644 index 0000000..9466fc3 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,151 @@ +from flask import Flask, request, jsonify +from flask_cors import CORS +from flask_migrate import Migrate +import hashlib +import redis + +from app.config import Config +from app.models.domain import db, Domain, DNSRecord +from app.services.cloudflare_service import CloudflareService + +app = Flask(__name__) +app.config.from_object(Config) + +# Extensions +CORS(app) +db.init_app(app) +migrate = Migrate(app, db) + +# Redis +redis_client = redis.from_url(Config.REDIS_URL) + + +# Helper Functions +def select_lb_ip(domain: str) -> str: + """Domain için load balancer IP seç (hash-based)""" + hash_value = int(hashlib.md5(domain.encode()).hexdigest(), 16) + index = hash_value % len(Config.LB_IPS) + return Config.LB_IPS[index] + + +# Routes +@app.route('/health', methods=['GET']) +def health(): + """Health check""" + return jsonify({"status": "ok", "service": "hosting-platform-api"}) + + +@app.route('/api/dns/validate-token', methods=['POST']) +def validate_cf_token(): + """Cloudflare API token doğrula""" + 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) + + +@app.route('/api/dns/preview-changes', methods=['POST']) +def preview_changes(): + """DNS değişiklik önizlemesi""" + data = request.json + domain = data.get('domain') + zone_id = data.get('zone_id') + cf_token = data.get('cf_token') + + 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) + + cf_service = CloudflareService(cf_token) + preview = cf_service.generate_dns_preview(domain, zone_id, new_ip) + + return jsonify(preview) + + +@app.route('/api/dns/apply-changes', methods=['POST']) +def apply_changes(): + """DNS değişikliklerini uygula""" + data = request.json + domain = 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) + customer_id = data.get('customer_id', 1) # Test için + + if not all([domain, zone_id, cf_token, preview]): + return jsonify({"error": "Eksik parametreler"}), 400 + + 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) + + # Veritabanına kaydet + domain_obj = Domain.query.filter_by(domain_name=domain).first() + if not domain_obj: + domain_obj = Domain( + domain_name=domain, + customer_id=customer_id, + use_cloudflare=True, + cf_zone_id=zone_id, + cf_proxy_enabled=proxy_enabled, + lb_ip=preview["new_ip"], + status="active", + dns_configured=True, + ssl_configured=len(ssl_config["errors"]) == 0 + ) + db.session.add(domain_obj) + else: + domain_obj.cf_zone_id = zone_id + domain_obj.cf_proxy_enabled = proxy_enabled + domain_obj.lb_ip = preview["new_ip"] + domain_obj.status = "active" + domain_obj.dns_configured = True + domain_obj.ssl_configured = len(ssl_config["errors"]) == 0 + + db.session.commit() + + return jsonify({ + "status": "success", + "dns_result": result, + "ssl_config": ssl_config, + "domain_id": domain_obj.id + }) + + return jsonify(result), 500 + + +@app.route('/api/domains', methods=['GET']) +def list_domains(): + """Domain listesi""" + customer_id = request.args.get('customer_id', 1, type=int) + domains = Domain.query.filter_by(customer_id=customer_id).all() + return jsonify([d.to_dict() for d in domains]) + + +@app.route('/api/domains/', methods=['GET']) +def get_domain(domain_id): + """Domain detayı""" + domain = Domain.query.get_or_404(domain_id) + return jsonify(domain.to_dict()) + + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + app.run(host=Config.API_HOST, port=Config.API_PORT, debug=True) + diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py new file mode 100644 index 0000000..e744ca9 --- /dev/null +++ b/backend/app/models/domain.py @@ -0,0 +1,84 @@ +from datetime import datetime +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +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 + use_cloudflare = db.Column(db.Boolean, default=True) + cf_zone_id = db.Column(db.String(255), nullable=True) + cf_api_token = db.Column(db.Text, nullable=True) # Encrypted + cf_proxy_enabled = db.Column(db.Boolean, default=True) + + # DNS + current_ip = db.Column(db.String(45), nullable=True) + lb_ip = db.Column(db.String(45), nullable=True) + + # Status + status = db.Column(db.String(50), default="pending") # pending, active, error + dns_configured = db.Column(db.Boolean, default=False) + ssl_configured = db.Column(db.Boolean, default=False) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Relationships + dns_records = db.relationship("DNSRecord", backref="domain", lazy=True, cascade="all, delete-orphan") + + def to_dict(self): + return { + "id": self.id, + "domain_name": self.domain_name, + "customer_id": self.customer_id, + "use_cloudflare": self.use_cloudflare, + "cf_zone_id": self.cf_zone_id, + "cf_proxy_enabled": self.cf_proxy_enabled, + "current_ip": self.current_ip, + "lb_ip": self.lb_ip, + "status": self.status, + "dns_configured": self.dns_configured, + "ssl_configured": self.ssl_configured, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + + +class DNSRecord(db.Model): + __tablename__ = "dns_records" + + id = db.Column(db.Integer, primary_key=True) + domain_id = db.Column(db.Integer, db.ForeignKey("domains.id"), nullable=False) + + record_type = db.Column(db.String(10), nullable=False) # A, CNAME, MX, TXT, etc. + name = db.Column(db.String(255), nullable=False) + content = db.Column(db.Text, nullable=False) + ttl = db.Column(db.Integer, default=300) + proxied = db.Column(db.Boolean, default=False) + + # Cloudflare + cf_record_id = db.Column(db.String(255), nullable=True) + + # Timestamps + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + def to_dict(self): + return { + "id": self.id, + "domain_id": self.domain_id, + "record_type": self.record_type, + "name": self.name, + "content": self.content, + "ttl": self.ttl, + "proxied": self.proxied, + "cf_record_id": self.cf_record_id, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/cloudflare_service.py b/backend/app/services/cloudflare_service.py new file mode 100644 index 0000000..1ef0570 --- /dev/null +++ b/backend/app/services/cloudflare_service.py @@ -0,0 +1,285 @@ +import hashlib +from typing import Dict, List, Optional +import CloudFlare + + +class CloudflareService: + """Cloudflare API işlemleri""" + + def __init__(self, api_token: str): + self.cf = CloudFlare.CloudFlare(token=api_token) + self.api_token = api_token + + def validate_token_and_get_zone(self, domain: str) -> Dict: + """ + API token doğrula ve zone bilgilerini al + """ + try: + # Zone ara + zones = self.cf.zones.get(params={"name": domain}) + + if not zones: + return { + "status": "error", + "message": f"{domain} zone bulunamadı. Domain Cloudflare hesabınızda olduğundan emin olun." + } + + zone = zones[0] + zone_id = zone["id"] + + # Mevcut DNS kayıtlarını al + dns_records = self.cf.zones.dns_records.get(zone_id) + + return { + "status": "success", + "zone_id": zone_id, + "zone_name": zone["name"], + "zone_status": zone["status"], + "nameservers": zone.get("name_servers", []), + "account_email": zone.get("account", {}).get("email", "N/A"), + "current_dns_records": [ + { + "type": r["type"], + "name": r["name"], + "content": r["content"], + "proxied": r.get("proxied", False), + "ttl": r["ttl"], + "id": r["id"] + } + for r in dns_records + ] + } + + except CloudFlare.exceptions.CloudFlareAPIError as e: + return { + "status": "error", + "message": f"Cloudflare API hatası: {str(e)}" + } + except Exception as e: + return { + "status": "error", + "message": f"Beklenmeyen hata: {str(e)}" + } + + def generate_dns_preview(self, domain: str, zone_id: str, new_ip: str) -> Dict: + """ + DNS değişiklik önizlemesi oluştur + """ + try: + # Mevcut A kayıtlarını al + dns_records = self.cf.zones.dns_records.get( + zone_id, + params={"type": "A"} + ) + + current_root = None + current_www = None + + for record in dns_records: + if record["name"] == domain: + current_root = record + elif record["name"] == f"www.{domain}": + current_www = record + + # Önizleme oluştur + preview = { + "domain": domain, + "new_ip": new_ip, + "changes": [] + } + + # Root domain (@) değişikliği + if current_root: + preview["changes"].append({ + "record_type": "A", + "name": "@", + "current": { + "value": current_root["content"], + "proxied": current_root.get("proxied", False), + "ttl": current_root["ttl"] + }, + "new": { + "value": new_ip, + "proxied": current_root.get("proxied", True), + "ttl": "auto" + }, + "action": "update", + "record_id": current_root["id"] + }) + else: + preview["changes"].append({ + "record_type": "A", + "name": "@", + "current": None, + "new": { + "value": new_ip, + "proxied": True, + "ttl": "auto" + }, + "action": "create" + }) + + # www subdomain değişikliği + if current_www: + preview["changes"].append({ + "record_type": "A", + "name": "www", + "current": { + "value": current_www["content"], + "proxied": current_www.get("proxied", False), + "ttl": current_www["ttl"] + }, + "new": { + "value": new_ip, + "proxied": current_www.get("proxied", True), + "ttl": "auto" + }, + "action": "update", + "record_id": current_www["id"] + }) + else: + preview["changes"].append({ + "record_type": "A", + "name": "www", + "current": None, + "new": { + "value": new_ip, + "proxied": True, + "ttl": "auto" + }, + "action": "create" + }) + + # Diğer kayıtlar (değişmeyecek) + all_records = self.cf.zones.dns_records.get(zone_id) + other_records = [ + r for r in all_records + if r["type"] != "A" or (r["name"] != domain and r["name"] != f"www.{domain}") + ] + + preview["preserved_records"] = [ + { + "type": r["type"], + "name": r["name"], + "content": r["content"] + } + for r in other_records[:10] # İlk 10 kayıt + ] + + preview["preserved_count"] = len(other_records) + + return preview + + except Exception as e: + return { + "status": "error", + "message": f"Önizleme oluşturma hatası: {str(e)}" + } + + def apply_dns_changes(self, zone_id: str, preview: Dict, proxy_enabled: bool = True) -> Dict: + """ + DNS değişikliklerini uygula + """ + results = { + "domain": preview["domain"], + "applied_changes": [], + "errors": [] + } + + for change in preview["changes"]: + try: + if change["action"] == "update": + # Mevcut kaydı güncelle + self.cf.zones.dns_records.patch( + zone_id, + change["record_id"], + data={ + "type": "A", + "name": change["name"], + "content": change["new"]["value"], + "proxied": proxy_enabled, + "ttl": 1 if proxy_enabled else 300 + } + ) + results["applied_changes"].append({ + "name": change["name"], + "action": "updated", + "new_value": change["new"]["value"] + }) + + elif change["action"] == "create": + # Yeni kayıt oluştur + self.cf.zones.dns_records.post( + zone_id, + data={ + "type": "A", + "name": change["name"], + "content": change["new"]["value"], + "proxied": proxy_enabled, + "ttl": 1 if proxy_enabled else 300 + } + ) + results["applied_changes"].append({ + "name": change["name"], + "action": "created", + "new_value": change["new"]["value"] + }) + + except Exception as e: + results["errors"].append({ + "name": change["name"], + "error": str(e) + }) + + if results["errors"]: + results["status"] = "partial" + else: + results["status"] = "success" + + return results + + def configure_ssl(self, zone_id: str) -> Dict: + """ + Cloudflare SSL ayarlarını yapılandır + """ + ssl_config = { + "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 diff --git a/backend/app/utils/__init__.py b/backend/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..319f78e --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,30 @@ +# Web Framework +Flask==3.0.0 +Flask-CORS==4.0.0 +Flask-SQLAlchemy==3.1.1 +Flask-Migrate==4.0.5 + +# Database +psycopg2-binary==2.9.9 +SQLAlchemy==2.0.23 + +# Redis +redis==5.0.1 + +# Cloudflare +cloudflare==2.19.4 +requests==2.31.0 + +# Utilities +python-dotenv==1.0.0 +pydantic==2.5.2 +python-dateutil==2.8.2 + +# Security +cryptography==41.0.7 + +# Development +pytest==7.4.3 +pytest-cov==4.1.0 +black==23.12.1 +flake8==6.1.0