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:
parent
60d1362013
commit
f54467436a
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
from app.models.domain import db, CloudflareAccount, Domain, DNSRecord
|
||||||
|
|
||||||
|
__all__ = ['db', 'CloudflareAccount', 'Domain', 'DNSRecord']
|
||||||
|
|
@ -1,8 +1,70 @@
|
||||||
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"
|
||||||
|
|
||||||
|
|
@ -10,12 +72,18 @@ class Domain(db.Model):
|
||||||
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)
|
||||||
lb_ip = db.Column(db.String(45), nullable=True)
|
lb_ip = db.Column(db.String(45), nullable=True)
|
||||||
|
|
@ -32,14 +100,32 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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 Açı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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
Loading…
Reference in New Issue