feat: Yeni DNS akışı - CF hesap seçimi, NS yönlendirme, Admin panel

- 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ı
This commit is contained in:
oguz ozturk 2026-01-10 14:14:29 +03:00
parent 60d1362013
commit f54467436a
19 changed files with 2485 additions and 12 deletions

23
backend/.env.example Normal file
View File

@ -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

View File

@ -24,6 +24,9 @@ class Config:
API_HOST = os.getenv("API_HOST", "0.0.0.0") API_HOST = os.getenv("API_HOST", "0.0.0.0")
API_PORT = int(os.getenv("API_PORT", 5000)) 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_API_TOKEN = os.getenv("PLATFORM_CF_API_TOKEN")
PLATFORM_CF_ACCOUNT_ID = os.getenv("PLATFORM_CF_ACCOUNT_ID") PLATFORM_CF_ACCOUNT_ID = os.getenv("PLATFORM_CF_ACCOUNT_ID")

View File

@ -3,19 +3,32 @@ from flask_cors import CORS
from flask_migrate import Migrate from flask_migrate import Migrate
import hashlib import hashlib
import redis import redis
import os
from app.config import Config 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 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 = Flask(__name__)
app.config.from_object(Config) app.config.from_object(Config)
# Set ENCRYPTION_KEY environment variable
if Config.ENCRYPTION_KEY:
os.environ['ENCRYPTION_KEY'] = Config.ENCRYPTION_KEY
# Extensions # Extensions
CORS(app) CORS(app)
db.init_app(app) db.init_app(app)
migrate = Migrate(app, db) migrate = Migrate(app, db)
# Register blueprints
app.register_blueprint(admin_bp)
app.register_blueprint(dns_bp)
# Redis # Redis
redis_client = redis.from_url(Config.REDIS_URL) redis_client = redis.from_url(Config.REDIS_URL)

View File

@ -0,0 +1,3 @@
from app.models.domain import db, CloudflareAccount, Domain, DNSRecord
__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord']

View File

@ -1,20 +1,88 @@
from datetime import datetime from datetime import datetime
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from app.utils.encryption import encrypt_text, decrypt_text
db = SQLAlchemy() 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): class Domain(db.Model):
__tablename__ = "domains" __tablename__ = "domains"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
domain_name = db.Column(db.String(255), unique=True, nullable=False, index=True) domain_name = db.Column(db.String(255), unique=True, nullable=False, index=True)
customer_id = db.Column(db.Integer, 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) 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_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) 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 # DNS
current_ip = db.Column(db.String(45), nullable=True) current_ip = db.Column(db.String(45), nullable=True)
@ -31,15 +99,33 @@ class Domain(db.Model):
# Relationships # Relationships
dns_records = db.relationship("DNSRecord", backref="domain", lazy=True, cascade="all, delete-orphan") 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): def to_dict(self):
return { return {
"id": self.id, "id": self.id,
"domain_name": self.domain_name, "domain_name": self.domain_name,
"customer_id": self.customer_id, "customer_id": self.customer_id,
"use_cloudflare": self.use_cloudflare, "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_zone_id": self.cf_zone_id,
"cf_proxy_enabled": self.cf_proxy_enabled, "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, "current_ip": self.current_ip,
"lb_ip": self.lb_ip, "lb_ip": self.lb_ip,
"status": self.status, "status": self.status,

270
backend/app/routes/admin.py Normal file
View File

@ -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/<int:account_id>', 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/<int:account_id>', 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/<int:account_id>', 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/<int:account_id>/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

167
backend/app/routes/dns.py Normal file
View File

@ -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

View File

@ -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": []
}

View File

@ -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)

View File

@ -5,7 +5,7 @@ Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5 Flask-Migrate==4.0.5
# Database # Database
psycopg2-binary==2.9.9 # psycopg2-binary==2.9.9 # Commented out for SQLite testing
SQLAlchemy==2.0.23 SQLAlchemy==2.0.23
# Redis # Redis
@ -19,6 +19,7 @@ requests==2.31.0
python-dotenv==1.0.0 python-dotenv==1.0.0
pydantic==2.5.2 pydantic==2.5.2
python-dateutil==2.8.2 python-dateutil==2.8.2
dnspython==2.4.2
# Security # Security
cryptography==41.0.7 cryptography==41.0.7

115
deploy.sh Normal file
View File

@ -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"

View File

@ -1,10 +1,12 @@
import { useState } from 'react' import { useState } from 'react'
import DomainSetup from './pages/DomainSetup' import DomainSetup from './pages/DomainSetup'
import DomainSetupNew from './pages/DomainSetupNew'
import DomainList from './pages/DomainList' import DomainList from './pages/DomainList'
import AdminCFAccounts from './pages/AdminCFAccounts'
import './App.css' import './App.css'
function App() { function App() {
const [currentPage, setCurrentPage] = useState('setup') const [currentPage, setCurrentPage] = useState('setup-new')
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
@ -23,6 +25,16 @@ function App() {
</div> </div>
<nav className="flex space-x-4"> <nav className="flex space-x-4">
<button
onClick={() => setCurrentPage('setup-new')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'setup-new'
? 'bg-blue-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
🆕 Add Domain (New)
</button>
<button <button
onClick={() => setCurrentPage('setup')} onClick={() => setCurrentPage('setup')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${ className={`px-4 py-2 rounded-lg font-medium transition-colors ${
@ -31,7 +43,7 @@ function App() {
: 'text-gray-600 hover:bg-gray-100' : 'text-gray-600 hover:bg-gray-100'
}`} }`}
> >
Add Domain Add Domain (Old)
</button> </button>
<button <button
onClick={() => setCurrentPage('list')} onClick={() => setCurrentPage('list')}
@ -43,6 +55,16 @@ function App() {
> >
My Domains My Domains
</button> </button>
<button
onClick={() => setCurrentPage('admin')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
currentPage === 'admin'
? 'bg-purple-500 text-white'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
Admin
</button>
</nav> </nav>
</div> </div>
</div> </div>
@ -50,7 +72,10 @@ function App() {
{/* Main Content */} {/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{currentPage === 'setup' ? <DomainSetup /> : <DomainList />} {currentPage === 'setup-new' && <DomainSetupNew />}
{currentPage === 'setup' && <DomainSetup />}
{currentPage === 'list' && <DomainList />}
{currentPage === 'admin' && <AdminCFAccounts />}
</main> </main>
{/* Footer */} {/* Footer */}

View File

@ -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 (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">
{account ? 'Hesap Düzenle' : 'Yeni Cloudflare Hesabı Ekle'}
</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-2xl"
>
×
</button>
</div>
{error && (
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Name */}
<div>
<label className="block text-sm font-medium mb-2">
Hesap Adı *
</label>
<input
type="text"
value={formData.name}
onChange={(e) => 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
/>
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium mb-2">
Cloudflare Email *
</label>
<input
type="email"
value={formData.email}
onChange={(e) => 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
/>
</div>
{/* API Token */}
<div>
<label className="block text-sm font-medium mb-2">
API Token {account ? '(Değiştirmek için girin)' : '*'}
</label>
<input
type="password"
value={formData.api_token}
onChange={(e) => 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}
/>
<p className="mt-1 text-sm text-gray-600">
Token şifreli olarak saklanacaktır
</p>
</div>
{/* Max Domains */}
<div>
<label className="block text-sm font-medium mb-2">
Maksimum Domain Sayısı *
</label>
<input
type="number"
value={formData.max_domains}
onChange={(e) => 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
/>
<p className="mt-1 text-sm text-gray-600">
Bu hesapta maksimum kaç domain barındırılabilir
</p>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium mb-2">
Notlar
</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
placeholder="Hesap hakkında notlar..."
rows="3"
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
{/* Active Status */}
<div className="flex items-center">
<input
type="checkbox"
id="is_active"
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
className="mr-2"
/>
<label htmlFor="is_active" className="text-sm font-medium">
Hesap aktif
</label>
</div>
{/* Actions */}
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
İptal
</button>
<button
type="submit"
disabled={loading}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Kaydediliyor...' : account ? 'Güncelle' : 'Ekle'}
</button>
</div>
</form>
</div>
</div>
</div>
)
}
export default CFAccountModal

View File

@ -0,0 +1,145 @@
function CFTokenGuide({ onClose }) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">Cloudflare API Token Oluşturma Rehberi</h2>
<button
onClick={onClose}
className="text-gray-500 hover:text-gray-700 text-2xl"
>
×
</button>
</div>
<div className="space-y-6">
{/* Step 1 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">1. Cloudflare Dashboard'a Giriş Yapın</h3>
<p className="text-gray-700 mb-2">
<a
href="https://dash.cloudflare.com"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline"
>
https://dash.cloudflare.com
</a> adresine gidin ve giriş yapın.
</p>
</div>
{/* Step 2 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">2. API Tokens Sayfasına Gidin</h3>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li>Sağ üst köşedeki profil ikonuna tıklayın</li>
<li>"My Profile" seçeneğini seçin</li>
<li>Sol menüden "API Tokens" sekmesine tıklayın</li>
</ul>
</div>
{/* Step 3 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">3. Yeni Token Oluşturun</h3>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li>"Create Token" butonuna tıklayın</li>
<li>"Edit zone DNS" template'ini seçin</li>
<li>Veya "Create Custom Token" ile özel token oluşturun</li>
</ul>
</div>
{/* Step 4 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">4. Token İzinlerini Ayarlayın</h3>
<div className="bg-gray-50 p-4 rounded-lg">
<p className="font-medium mb-2">Gerekli İzinler:</p>
<ul className="space-y-2 text-sm">
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span><strong>Zone - DNS - Edit:</strong> DNS kayıtlarını düzenlemek için</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span><strong>Zone - Zone - Read:</strong> Zone bilgilerini okumak için</span>
</li>
<li className="flex items-start">
<span className="text-green-600 mr-2"></span>
<span><strong>Zone - Zone Settings - Edit:</strong> SSL ayarları için</span>
</li>
</ul>
</div>
</div>
{/* Step 5 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">5. Zone Seçimi</h3>
<p className="text-gray-700 mb-2">
"Zone Resources" bölümünde:
</p>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li><strong>Specific zone:</strong> Sadece belirli bir domain için</li>
<li><strong>All zones:</strong> Tüm domain'leriniz için (önerilen)</li>
</ul>
</div>
{/* Step 6 */}
<div className="border-l-4 border-blue-500 pl-4">
<h3 className="font-bold text-lg mb-2">6. Token'ı Oluşturun ve Kopyalayın</h3>
<ul className="list-disc list-inside text-gray-700 space-y-1">
<li>"Continue to summary" butonuna tıklayın</li>
<li>"Create Token" butonuna tıklayın</li>
<li>Oluşturulan token'ı kopyalayın</li>
</ul>
<div className="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
<strong> Önemli:</strong> Token sadece bir kez gösterilir!
Mutlaka güvenli bir yere kaydedin.
</p>
</div>
</div>
{/* Example Token */}
<div className="border-l-4 border-green-500 pl-4">
<h3 className="font-bold text-lg mb-2">Token Örneği</h3>
<div className="bg-gray-900 text-green-400 p-3 rounded font-mono text-sm overflow-x-auto">
y_12345abcdefghijklmnopqrstuvwxyz1234567890ABCD
</div>
<p className="text-sm text-gray-600 mt-2">
Token bu formatta olacaktır (genellikle 40 karakter)
</p>
</div>
{/* Security Tips */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 className="font-bold mb-2 flex items-center">
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Güvenlik İpuçları
</h3>
<ul className="text-sm text-blue-800 space-y-1">
<li> Token'ı kimseyle paylaşmayın</li>
<li> Token'ı güvenli bir şifre yöneticisinde saklayın</li>
<li> Kullanılmayan token'ları silin</li>
<li> Token'ı düzenli olarak yenileyin</li>
</ul>
</div>
</div>
<div className="mt-6 flex justify-end">
<button
onClick={onClose}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Anladım
</button>
</div>
</div>
</div>
</div>
)
}
export default CFTokenGuide

View File

@ -0,0 +1,161 @@
import { useState, useEffect } from 'react'
function NSInstructions({
domain,
nsInstructions,
nsStatus,
onCheck,
onContinue,
loading
}) {
const [autoCheck, setAutoCheck] = useState(true)
useEffect(() => {
if (autoCheck && nsStatus && !nsStatus.is_cloudflare) {
const interval = setInterval(() => {
onCheck()
}, 30000) // Check every 30 seconds
return () => clearInterval(interval)
}
}, [autoCheck, nsStatus, onCheck])
const isConfigured = nsStatus?.is_cloudflare
return (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Nameserver Yönlendirmesi</h2>
<div className="mb-6">
<p className="text-gray-600 mb-2">
Domain: <strong>{domain}</strong>
</p>
{/* NS Status */}
{nsStatus && (
<div className={`p-4 rounded-lg mb-4 ${
isConfigured
? 'bg-green-50 border border-green-200'
: 'bg-yellow-50 border border-yellow-200'
}`}>
<div className="flex items-center">
{isConfigured ? (
<>
<svg className="w-6 h-6 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span className="text-green-800 font-medium">
Nameserver'lar Cloudflare'e yönlendirilmiş!
</span>
</>
) : (
<>
<svg className="w-6 h-6 text-yellow-600 mr-2 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span className="text-yellow-800 font-medium">
Nameserver yönlendirmesi bekleniyor...
</span>
</>
)}
</div>
{nsStatus.current_nameservers && nsStatus.current_nameservers.length > 0 && (
<div className="mt-3 text-sm">
<p className="font-medium text-gray-700">Mevcut Nameserver'lar:</p>
<ul className="mt-1 space-y-1">
{nsStatus.current_nameservers.map((ns, idx) => (
<li key={idx} className="text-gray-600 font-mono text-xs">
{ns}
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
{/* Instructions */}
{!isConfigured && nsInstructions && (
<div className="mb-6">
<h3 className="font-bold mb-3">Nameserver Değiştirme Talimatları:</h3>
<div className="bg-gray-50 p-4 rounded-lg mb-4">
<p className="font-medium text-gray-700 mb-2">Cloudflare Nameserver'ları:</p>
<div className="space-y-1">
{nsInstructions.cloudflare_nameservers.map((ns, idx) => (
<div key={idx} className="flex items-center justify-between bg-white p-2 rounded border">
<code className="text-sm font-mono">{ns}</code>
<button
onClick={() => navigator.clipboard.writeText(ns)}
className="text-blue-600 hover:text-blue-800 text-sm"
>
📋 Kopyala
</button>
</div>
))}
</div>
</div>
<div className="space-y-2 text-sm text-gray-700">
{nsInstructions.instructions.map((instruction, idx) => (
<div key={idx} className="flex items-start">
<span className="mr-2">{instruction.startsWith(' -') ? ' •' : ''}</span>
<span>{instruction.replace(' - ', '')}</span>
</div>
))}
</div>
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
<strong>💡 İpucu:</strong> DNS propagation genellikle 1-2 saat içinde tamamlanır,
ancak bazı durumlarda 24-48 saat sürebilir.
</p>
</div>
</div>
)}
{/* Auto-check toggle */}
{!isConfigured && (
<div className="mb-4 flex items-center">
<input
type="checkbox"
id="autoCheck"
checked={autoCheck}
onChange={(e) => setAutoCheck(e.target.checked)}
className="mr-2"
/>
<label htmlFor="autoCheck" className="text-sm text-gray-700">
Otomatik kontrol (30 saniyede bir)
</label>
</div>
)}
{/* Actions */}
<div className="flex gap-3">
{!isConfigured && (
<button
onClick={onCheck}
disabled={loading}
className="px-6 py-2 border border-blue-600 text-blue-600 rounded-lg hover:bg-blue-50 disabled:opacity-50"
>
{loading ? 'Kontrol ediliyor...' : '🔄 Manuel Kontrol Et'}
</button>
)}
{isConfigured && (
<button
onClick={onContinue}
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700"
>
Devam Et
</button>
)}
</div>
</div>
)
}
export default NSInstructions

View File

@ -0,0 +1,199 @@
import { useState, useEffect } from 'react'
import { adminAPI } from '../services/api'
import CFAccountModal from '../components/CFAccountModal'
function AdminCFAccounts() {
const [accounts, setAccounts] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(null)
const [showAddModal, setShowAddModal] = useState(false)
const [editingAccount, setEditingAccount] = useState(null)
useEffect(() => {
loadAccounts()
}, [])
const loadAccounts = async () => {
setLoading(true)
try {
const response = await adminAPI.listCFAccounts()
if (response.data.status === 'success') {
setAccounts(response.data.accounts)
}
} catch (err) {
setError('Hesaplar yüklenemedi: ' + err.message)
} finally {
setLoading(false)
}
}
const handleTest = async (accountId) => {
try {
const response = await adminAPI.testCFAccount(accountId)
if (response.data.status === 'success') {
setSuccess(`${response.data.message}`)
setTimeout(() => setSuccess(null), 3000)
} else {
setError(`${response.data.message}`)
}
} catch (err) {
setError('Test başarısız: ' + err.message)
}
}
const handleDelete = async (accountId) => {
if (!confirm('Bu hesabı devre dışı bırakmak istediğinizden emin misiniz?')) {
return
}
try {
const response = await adminAPI.deleteCFAccount(accountId)
if (response.data.status === 'success') {
setSuccess('Hesap devre dışı bırakıldı')
loadAccounts()
}
} catch (err) {
setError(err.response?.data?.message || 'Silme başarısız')
}
}
return (
<div className="max-w-6xl mx-auto p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Cloudflare Hesap Yönetimi</h1>
<button
onClick={() => setShowAddModal(true)}
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
>
+ Yeni Hesap Ekle
</button>
</div>
{/* Messages */}
{error && (
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
<button onClick={() => setError(null)} className="float-right">×</button>
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
{success}
<button onClick={() => setSuccess(null)} className="float-right">×</button>
</div>
)}
{/* Accounts List */}
{loading ? (
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Yükleniyor...</p>
</div>
) : accounts.length === 0 ? (
<div className="bg-white p-12 rounded-lg shadow text-center">
<p className="text-gray-600 mb-4">Henüz Cloudflare hesabı eklenmemiş</p>
<button
onClick={() => setShowAddModal(true)}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
İlk Hesabı Ekle
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{accounts.map((account) => (
<div
key={account.id}
className={`bg-white p-6 rounded-lg shadow border-2 ${
account.is_active ? 'border-green-200' : 'border-gray-200'
}`}
>
<div className="flex justify-between items-start mb-4">
<div>
<h3 className="text-xl font-bold">{account.name}</h3>
<p className="text-sm text-gray-600">{account.email}</p>
</div>
<span
className={`px-3 py-1 rounded text-sm font-medium ${
account.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-600'
}`}
>
{account.is_active ? 'Aktif' : 'Pasif'}
</span>
</div>
<div className="mb-4 space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Domain Sayısı:</span>
<span className="font-medium">
{account.current_domain_count} / {account.max_domains}
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{
width: `${(account.current_domain_count / account.max_domains) * 100}%`,
}}
></div>
</div>
</div>
{account.notes && (
<div className="mb-4 p-3 bg-gray-50 rounded text-sm text-gray-700">
<strong>Not:</strong> {account.notes}
</div>
)}
<div className="flex gap-2">
<button
onClick={() => handleTest(account.id)}
className="flex-1 px-3 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50 text-sm"
>
🧪 Test
</button>
<button
onClick={() => setEditingAccount(account)}
className="flex-1 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 text-sm"
>
Düzenle
</button>
<button
onClick={() => handleDelete(account.id)}
className="px-3 py-2 border border-red-600 text-red-600 rounded hover:bg-red-50 text-sm"
disabled={account.current_domain_count > 0}
>
🗑
</button>
</div>
</div>
))}
</div>
)}
{/* Add/Edit Modal */}
{(showAddModal || editingAccount) && (
<CFAccountModal
account={editingAccount}
onClose={() => {
setShowAddModal(false)
setEditingAccount(null)
}}
onSuccess={() => {
setShowAddModal(false)
setEditingAccount(null)
loadAccounts()
setSuccess(editingAccount ? 'Hesap güncellendi' : 'Hesap eklendi')
}}
/>
)}
</div>
)
}
export default AdminCFAccounts

View File

@ -0,0 +1,710 @@
import { useState, useEffect } from 'react'
import { dnsAPI, adminAPI } from '../services/api'
import NSInstructions from '../components/NSInstructions'
import CFTokenGuide from '../components/CFTokenGuide'
function DomainSetupNew() {
// State management
const [step, setStep] = useState(1)
const [domain, setDomain] = useState('')
const [cfAccountType, setCfAccountType] = useState(null) // 'own' or 'company'
// Company CF account selection
const [companyCFAccounts, setCompanyCFAccounts] = useState([])
const [selectedCFAccount, setSelectedCFAccount] = useState(null)
// Own CF token
const [cfToken, setCfToken] = useState('')
const [showTokenGuide, setShowTokenGuide] = useState(false)
// Zone info
const [zoneInfo, setZoneInfo] = useState(null)
// NS status
const [nsStatus, setNsStatus] = useState(null)
const [nsInstructions, setNsInstructions] = useState(null)
const [nsPolling, setNsPolling] = useState(false)
// DNS preview
const [preview, setPreview] = useState(null)
// Proxy setting
const [proxyEnabled, setProxyEnabled] = useState(true)
// UI state
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(null)
// Load company CF accounts on mount
useEffect(() => {
loadCompanyCFAccounts()
}, [])
// NS polling effect
useEffect(() => {
let interval
if (nsPolling && zoneInfo) {
interval = setInterval(() => {
checkNameservers()
}, 30000) // Check every 30 seconds
}
return () => clearInterval(interval)
}, [nsPolling, zoneInfo])
const loadCompanyCFAccounts = async () => {
try {
const response = await adminAPI.listCFAccounts()
if (response.data.status === 'success') {
setCompanyCFAccounts(response.data.accounts)
}
} catch (err) {
console.error('Failed to load CF accounts:', err)
}
}
const handleDomainSubmit = (e) => {
e.preventDefault()
if (!domain) {
setError('Lütfen domain adı girin')
return
}
setError(null)
setStep(2)
}
const handleCFAccountTypeSelect = (type) => {
setCfAccountType(type)
setError(null)
if (type === 'own') {
setStep(3) // Go to token input
} else {
setStep(4) // Go to company account selection
}
}
const handleOwnTokenValidate = async (e) => {
e.preventDefault()
setLoading(true)
setError(null)
try {
const response = await dnsAPI.validateToken(domain, cfToken)
if (response.data.status === 'success') {
setZoneInfo(response.data)
// Check if NS is already configured
await checkNameservers()
setStep(5) // Go to NS check/instructions
} else {
setError(response.data.message)
}
} catch (err) {
setError(err.response?.data?.error || 'Token doğrulama başarısız')
} finally {
setLoading(false)
}
}
const handleCompanyAccountSelect = async (accountId) => {
setLoading(true)
setError(null)
try {
const response = await dnsAPI.selectCompanyAccount(domain, accountId)
if (response.data.status === 'success') {
setSelectedCFAccount(accountId)
setZoneInfo(response.data)
// Get NS instructions
await getNSInstructions(response.data.zone_id, accountId)
setStep(5) // Go to NS instructions
} else if (response.data.status === 'pending') {
setError('Zone bulunamadı. Cloudflare\'de zone oluşturulması gerekiyor.')
} else {
setError(response.data.message)
}
} catch (err) {
setError(err.response?.data?.error || 'Hesap seçimi başarısız')
} finally {
setLoading(false)
}
}
const checkNameservers = async () => {
try {
const response = await dnsAPI.checkNameservers(domain)
setNsStatus(response.data)
if (response.data.is_cloudflare) {
// NS configured, move to next step
setNsPolling(false)
setStep(6) // Go to DNS preview
}
} catch (err) {
console.error('NS check failed:', err)
}
}
const getNSInstructions = async (zoneId, accountId) => {
try {
// Get API token for the account
const accountResponse = await adminAPI.getCFAccount(accountId, true)
const apiToken = accountResponse.data.account.api_token
const response = await dnsAPI.getNSInstructions(domain, zoneId, apiToken)
if (response.data.status === 'success') {
setNsInstructions(response.data)
setNsPolling(true) // Start polling
}
} catch (err) {
console.error('Failed to get NS instructions:', err)
}
}
const startNSPolling = () => {
setNsPolling(true)
checkNameservers()
}
const handlePreviewChanges = async () => {
setLoading(true)
setError(null)
try {
const apiToken = cfAccountType === 'own'
? cfToken
: (await adminAPI.getCFAccount(selectedCFAccount, true)).data.account.api_token
const response = await dnsAPI.previewChanges(
domain,
zoneInfo.zone_id,
apiToken
)
if (response.data.status !== 'error') {
setPreview(response.data)
setStep(7) // Go to preview + proxy selection
} else {
setError(response.data.message)
}
} catch (err) {
setError(err.response?.data?.error || 'Önizleme oluşturulamadı')
} finally {
setLoading(false)
}
}
const handleApplyChanges = async () => {
setLoading(true)
setError(null)
try {
const apiToken = cfAccountType === 'own'
? cfToken
: (await adminAPI.getCFAccount(selectedCFAccount, true)).data.account.api_token
const response = await dnsAPI.applyChanges(
domain,
zoneInfo.zone_id,
apiToken,
preview,
proxyEnabled
)
if (response.data.status === 'success') {
setSuccess('Domain başarıyla yapılandırıldı!')
setStep(8) // Success screen
} else {
setError('Değişiklikler uygulanamadı')
}
} catch (err) {
setError(err.response?.data?.error || 'Değişiklikler uygulanamadı')
} finally {
setLoading(false)
}
}
const resetForm = () => {
setStep(1)
setDomain('')
setCfAccountType(null)
setSelectedCFAccount(null)
setCfToken('')
setZoneInfo(null)
setNsStatus(null)
setNsInstructions(null)
setNsPolling(false)
setPreview(null)
setProxyEnabled(true)
setError(null)
setSuccess(null)
}
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">Domain Kurulumu</h1>
{/* Progress Steps */}
<div className="mb-8">
<div className="flex items-center justify-between">
{[
{ num: 1, label: 'Domain' },
{ num: 2, label: 'CF Hesabı' },
{ num: 3, label: 'NS Yönlendirme' },
{ num: 4, label: 'DNS Önizleme' },
{ num: 5, label: 'Tamamla' },
].map((s, idx) => (
<div key={s.num} className="flex items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${
step >= s.num
? 'bg-blue-600 text-white'
: 'bg-gray-300 text-gray-600'
}`}
>
{s.num}
</div>
<span className="ml-2 text-sm font-medium">{s.label}</span>
{idx < 4 && (
<div
className={`w-16 h-1 mx-2 ${
step > s.num ? 'bg-blue-600' : 'bg-gray-300'
}`}
/>
)}
</div>
))}
</div>
</div>
{/* Error/Success Messages */}
{error && (
<div className="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded">
{error}
</div>
)}
{success && (
<div className="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded">
{success}
</div>
)}
{/* Step 1: Domain Input */}
{step === 1 && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Domain Adınızı Girin</h2>
<form onSubmit={handleDomainSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
Domain Adı
</label>
<input
type="text"
value={domain}
onChange={(e) => setDomain(e.target.value)}
placeholder="example.com"
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
required
/>
<p className="mt-2 text-sm text-gray-600">
Domain'iniz herhangi bir sağlayıcıdan (GoDaddy, Namecheap, vb.) alınmış olabilir.
</p>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
>
Devam Et
</button>
</form>
</div>
)}
{/* Step 2: CF Account Type Selection */}
{step === 2 && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Cloudflare Hesabı Seçimi</h2>
<p className="mb-6 text-gray-600">
Domain: <strong>{domain}</strong>
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Own CF Account */}
<button
onClick={() => handleCFAccountTypeSelect('own')}
className="p-6 border-2 border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 transition text-left"
>
<div className="flex items-center mb-3">
<svg className="w-8 h-8 text-blue-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<h3 className="text-lg font-bold">Kendi Cloudflare Hesabım</h3>
</div>
<p className="text-sm text-gray-600">
Kendi Cloudflare hesabınızı kullanın. API token oluşturmanız gerekecek.
</p>
<ul className="mt-3 text-sm text-gray-600 space-y-1">
<li> Tam kontrol</li>
<li> Kendi hesap ayarlarınız</li>
<li> API token gerekli</li>
</ul>
</button>
{/* Company CF Account */}
<button
onClick={() => handleCFAccountTypeSelect('company')}
className="p-6 border-2 border-gray-300 rounded-lg hover:border-green-500 hover:bg-green-50 transition text-left"
>
<div className="flex items-center mb-3">
<svg className="w-8 h-8 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<h3 className="text-lg font-bold">Sizin Cloudflare Hesabınız</h3>
</div>
<p className="text-sm text-gray-600">
Bizim yönettiğimiz Cloudflare hesabını kullanın. API token gerekmez.
</p>
<ul className="mt-3 text-sm text-gray-600 space-y-1">
<li> Kolay kurulum</li>
<li> API token gerekmez</li>
<li> Yönetilen servis</li>
</ul>
</button>
</div>
<button
onClick={() => setStep(1)}
className="mt-6 text-gray-600 hover:text-gray-800"
>
Geri
</button>
</div>
)}
{/* Step 3: Own CF Token Input */}
{step === 3 && cfAccountType === 'own' && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Cloudflare API Token</h2>
<div className="mb-4 p-4 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
<strong>Not:</strong> Cloudflare API token'ınız yoksa, aşağıdaki butona tıklayarak nasıl oluşturacağınızı öğrenebilirsiniz.
</p>
<button
onClick={() => setShowTokenGuide(true)}
className="mt-2 text-blue-600 hover:text-blue-800 font-medium text-sm"
>
📖 API Token Oluşturma Rehberi
</button>
</div>
<form onSubmit={handleOwnTokenValidate}>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">
API Token
</label>
<input
type="text"
value={cfToken}
onChange={(e) => setCfToken(e.target.value)}
placeholder="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
/>
</div>
<div className="flex gap-3">
<button
type="button"
onClick={() => setStep(2)}
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Geri
</button>
<button
type="submit"
disabled={loading}
className="flex-1 bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Doğrulanıyor...' : 'Token\'ı Doğrula'}
</button>
</div>
</form>
</div>
)}
{/* Step 4: Company CF Account Selection */}
{step === 4 && cfAccountType === 'company' && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">Cloudflare Hesabı Seçin</h2>
{companyCFAccounts.length === 0 ? (
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-yellow-800">
Henüz tanımlı Cloudflare hesabı yok. Lütfen admin panelinden hesap ekleyin.
</p>
</div>
) : (
<div className="space-y-3">
{companyCFAccounts.map((account) => (
<button
key={account.id}
onClick={() => handleCompanyAccountSelect(account.id)}
disabled={loading || !account.is_active}
className={`w-full p-4 border-2 rounded-lg text-left transition ${
account.is_active
? 'border-gray-300 hover:border-blue-500 hover:bg-blue-50'
: 'border-gray-200 bg-gray-100 cursor-not-allowed'
}`}
>
<div className="flex justify-between items-start">
<div>
<h3 className="font-bold">{account.name}</h3>
<p className="text-sm text-gray-600">{account.email}</p>
</div>
<div className="text-right text-sm">
<span className={`px-2 py-1 rounded ${
account.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-200 text-gray-600'
}`}>
{account.is_active ? 'Aktif' : 'Pasif'}
</span>
<p className="mt-1 text-gray-600">
{account.current_domain_count}/{account.max_domains} domain
</p>
</div>
</div>
</button>
))}
</div>
)}
<button
onClick={() => setStep(2)}
className="mt-6 text-gray-600 hover:text-gray-800"
>
Geri
</button>
</div>
)}
{/* Step 5: NS Instructions/Check */}
{step === 5 && (
<NSInstructions
domain={domain}
nsInstructions={nsInstructions}
nsStatus={nsStatus}
onCheck={checkNameservers}
onContinue={handlePreviewChanges}
loading={loading}
/>
)}
{/* Step 6-7: DNS Preview + Proxy Selection */}
{(step === 6 || step === 7) && preview && (
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-xl font-bold mb-4">DNS Değişiklik Önizlemesi</h2>
<div className="mb-6">
<p className="text-gray-600 mb-4">
Domain: <strong>{domain}</strong>
</p>
{/* DNS Changes */}
<div className="space-y-4">
{preview.changes && preview.changes.map((change, idx) => (
<div key={idx} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="font-bold">
{change.record_type} Kaydı: {change.name === '@' ? domain : `${change.name}.${domain}`}
</h3>
<span className={`px-3 py-1 rounded text-sm font-medium ${
change.action === 'create'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{change.action === 'create' ? 'Yeni Kayıt' : 'Güncelleme'}
</span>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Current */}
{change.current && (
<div className="bg-red-50 p-3 rounded">
<p className="text-sm font-medium text-red-800 mb-2">Mevcut Durum</p>
<p className="text-sm text-gray-700">IP: <code>{change.current.value}</code></p>
<p className="text-sm text-gray-700">
Proxy: {change.current.proxied ? '✅ Açık' : '❌ Kapalı'}
</p>
<p className="text-sm text-gray-700">TTL: {change.current.ttl}</p>
</div>
)}
{/* New */}
<div className={`p-3 rounded ${change.current ? 'bg-green-50' : 'bg-blue-50'}`}>
<p className={`text-sm font-medium mb-2 ${
change.current ? 'text-green-800' : 'text-blue-800'
}`}>
Yeni Durum
</p>
<p className="text-sm text-gray-700">IP: <code>{change.new.value}</code></p>
<p className="text-sm text-gray-700">
Proxy: {change.new.proxied ? '✅ Açık' : '❌ Kapalı'}
</p>
<p className="text-sm text-gray-700">TTL: {change.new.ttl}</p>
</div>
</div>
</div>
))}
</div>
{/* Preserved Records */}
{preview.preserved_count > 0 && (
<div className="mt-4 p-4 bg-gray-50 rounded-lg">
<p className="text-sm text-gray-700">
<strong>{preview.preserved_count}</strong> adet diğer DNS kaydı korunacak
(MX, TXT, CNAME, vb.)
</p>
</div>
)}
</div>
{/* Proxy Selection */}
<div className="mb-6 p-4 border-2 border-blue-200 rounded-lg bg-blue-50">
<h3 className="font-bold mb-3 flex items-center">
<svg className="w-5 h-5 mr-2 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
Cloudflare Proxy Ayarı
</h3>
<div className="space-y-3">
<label className="flex items-start p-3 bg-white rounded border-2 border-green-500 cursor-pointer">
<input
type="radio"
name="proxy"
checked={proxyEnabled}
onChange={() => setProxyEnabled(true)}
className="mt-1 mr-3"
/>
<div>
<p className="font-medium text-gray-900"> Proxy ık (Önerilen)</p>
<p className="text-sm text-gray-600 mt-1">
DDoS koruması<br/>
CDN hızlandırma<br/>
SSL/TLS şifreleme<br/>
IP gizleme
</p>
</div>
</label>
<label className="flex items-start p-3 bg-white rounded border-2 border-gray-300 cursor-pointer">
<input
type="radio"
name="proxy"
checked={!proxyEnabled}
onChange={() => setProxyEnabled(false)}
className="mt-1 mr-3"
/>
<div>
<p className="font-medium text-gray-900"> Proxy Kapalı (Sadece DNS)</p>
<p className="text-sm text-gray-600 mt-1">
Sadece DNS yönlendirmesi<br/>
Cloudflare koruması yok<br/>
Sunucu IP'si görünür
</p>
</div>
</label>
</div>
</div>
{/* Confirmation */}
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
<strong> Dikkat:</strong> Bu değişiklikleri uyguladığınızda, domain'inizin DNS kayıtları
güncellenecektir. DNS propagation 1-2 saat sürebilir.
</p>
</div>
{/* Actions */}
<div className="flex gap-3">
<button
onClick={() => setStep(5)}
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Geri
</button>
<button
onClick={handleApplyChanges}
disabled={loading}
className="flex-1 bg-green-600 text-white py-2 px-4 rounded-lg hover:bg-green-700 disabled:opacity-50"
>
{loading ? 'Uygulanıyor...' : '✅ Değişiklikleri Uygula'}
</button>
</div>
</div>
)}
{/* Step 8: Success */}
{step === 8 && (
<div className="bg-white p-6 rounded-lg shadow text-center">
<div className="mb-6">
<svg className="w-20 h-20 text-green-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 className="text-2xl font-bold text-green-600 mb-4">
🎉 Domain Başarıyla Yapılandırıldı!
</h2>
<div className="mb-6 text-left bg-gray-50 p-4 rounded-lg">
<h3 className="font-bold mb-2">Yapılandırma Özeti:</h3>
<ul className="space-y-2 text-sm text-gray-700">
<li> Domain: <strong>{domain}</strong></li>
<li> Cloudflare: {cfAccountType === 'own' ? 'Kendi Hesabınız' : 'Şirket Hesabı'}</li>
<li> DNS Kayıtları: Güncellendi</li>
<li> SSL/TLS: Yapılandırıldı</li>
<li> Proxy: {proxyEnabled ? 'Açık' : 'Kapalı'}</li>
</ul>
</div>
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
<strong>📌 Sonraki Adımlar:</strong><br/>
DNS propagation 1-2 saat içinde tamamlanacak<br/>
SSL sertifikası otomatik olarak oluşturulacak<br/>
Domain'iniz 24 saat içinde aktif olacak
</p>
</div>
<div className="flex gap-3 justify-center">
<button
onClick={resetForm}
className="px-6 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
Yeni Domain Ekle
</button>
<button
onClick={() => window.location.href = '/domains'}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Domain Listesine Git
</button>
</div>
</div>
)}
{/* CF Token Guide Modal */}
{showTokenGuide && (
<CFTokenGuide onClose={() => setShowTokenGuide(false)} />
)}
</div>
)
}
export default DomainSetupNew

View File

@ -13,13 +13,31 @@ export const dnsAPI = {
// Health check // Health check
health: () => api.get('/health'), health: () => api.get('/health'),
// Validate Cloudflare token // Nameserver operations
checkNameservers: (domain) =>
api.post('/api/dns/check-nameservers', { domain }),
getNSInstructions: (domain, zoneId, apiToken) =>
api.post('/api/dns/get-ns-instructions', {
domain,
zone_id: zoneId,
api_token: apiToken,
}),
// Validate Cloudflare token (customer's own token)
validateToken: (domain, cfToken) => validateToken: (domain, cfToken) =>
api.post('/api/dns/validate-token', { api.post('/api/dns/validate-token', {
domain, domain,
cf_token: cfToken, cf_token: cfToken,
}), }),
// Select company CF account
selectCompanyAccount: (domain, cfAccountId) =>
api.post('/api/dns/select-company-account', {
domain,
cf_account_id: cfAccountId,
}),
// Preview DNS changes // Preview DNS changes
previewChanges: (domain, zoneId, cfToken) => previewChanges: (domain, zoneId, cfToken) =>
api.post('/api/dns/preview-changes', { api.post('/api/dns/preview-changes', {
@ -49,5 +67,27 @@ export const dnsAPI = {
getDomain: (domainId) => api.get(`/api/domains/${domainId}`), getDomain: (domainId) => api.get(`/api/domains/${domainId}`),
} }
// Admin API
export const adminAPI = {
// CF Account management
listCFAccounts: () => api.get('/api/admin/cf-accounts'),
getCFAccount: (accountId, includeToken = false) =>
api.get(`/api/admin/cf-accounts/${accountId}`, {
params: { include_token: includeToken },
}),
createCFAccount: (data) => api.post('/api/admin/cf-accounts', data),
updateCFAccount: (accountId, data) =>
api.put(`/api/admin/cf-accounts/${accountId}`, data),
deleteCFAccount: (accountId) =>
api.delete(`/api/admin/cf-accounts/${accountId}`),
testCFAccount: (accountId) =>
api.post(`/api/admin/cf-accounts/${accountId}/test`),
}
export default api export default api

22
hosting-backend.service Normal file
View File

@ -0,0 +1,22 @@
[Unit]
Description=Hosting Platform Backend API
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/hosting-backend
Environment="PATH=/var/www/hosting-backend/venv/bin"
ExecStart=/var/www/hosting-backend/venv/bin/python app/main.py
Restart=always
RestartSec=10
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=hosting-backend
[Install]
WantedBy=multi-user.target