Add CF account auto-selection and verification toggle
This commit is contained in:
parent
2a7dd9a348
commit
84c8cb728e
|
|
@ -16,6 +16,7 @@ class CloudflareAccount(db.Model):
|
||||||
|
|
||||||
# Limits & Status
|
# Limits & Status
|
||||||
is_active = db.Column(db.Boolean, default=True)
|
is_active = db.Column(db.Boolean, default=True)
|
||||||
|
use_for_verification = db.Column(db.Boolean, default=True) # Use for customer domain verification
|
||||||
max_domains = db.Column(db.Integer, default=100) # Bu hesapta max kaç domain olabilir
|
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
|
current_domain_count = db.Column(db.Integer, default=0) # Şu an kaç domain var
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ class CloudflareAccount(db.Model):
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"email": self.email,
|
"email": self.email,
|
||||||
"is_active": self.is_active,
|
"is_active": self.is_active,
|
||||||
|
"use_for_verification": self.use_for_verification,
|
||||||
"max_domains": self.max_domains,
|
"max_domains": self.max_domains,
|
||||||
"current_domain_count": self.current_domain_count,
|
"current_domain_count": self.current_domain_count,
|
||||||
"notes": self.notes,
|
"notes": self.notes,
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ def create_cf_account():
|
||||||
email=data['email'],
|
email=data['email'],
|
||||||
max_domains=data.get('max_domains', 100),
|
max_domains=data.get('max_domains', 100),
|
||||||
notes=data.get('notes', ''),
|
notes=data.get('notes', ''),
|
||||||
is_active=True
|
is_active=True,
|
||||||
|
use_for_verification=data.get('use_for_verification', True)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Token'ı şifrele ve kaydet
|
# Token'ı şifrele ve kaydet
|
||||||
|
|
@ -174,6 +175,9 @@ def update_cf_account(account_id):
|
||||||
if 'is_active' in data:
|
if 'is_active' in data:
|
||||||
account.is_active = data['is_active']
|
account.is_active = data['is_active']
|
||||||
|
|
||||||
|
if 'use_for_verification' in data:
|
||||||
|
account.use_for_verification = data['use_for_verification']
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
@ -271,6 +275,134 @@ def test_cf_account(account_id):
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/cf-accounts/<int:account_id>/permissions', methods=['GET'])
|
||||||
|
def get_cf_account_permissions(account_id):
|
||||||
|
"""Cloudflare hesabının yetkilerini kontrol 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:
|
||||||
|
# Token verification endpoint'i kullan
|
||||||
|
verify_response = cf_service.cf.user.tokens.verify.get()
|
||||||
|
|
||||||
|
# Zone listesini al
|
||||||
|
zones = cf_service.cf.zones.get(params={'per_page': 1})
|
||||||
|
|
||||||
|
# Permissions bilgilerini topla
|
||||||
|
permissions = {
|
||||||
|
"token_status": verify_response.get("status", "unknown"),
|
||||||
|
"token_id": verify_response.get("id", ""),
|
||||||
|
"permissions": [],
|
||||||
|
"zone_access": False,
|
||||||
|
"dns_write": False,
|
||||||
|
"ssl_write": False,
|
||||||
|
"zone_count": len(zones) if zones else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Token policies'den permissions çıkar
|
||||||
|
if "policies" in verify_response:
|
||||||
|
for policy in verify_response["policies"]:
|
||||||
|
if "permission_groups" in policy:
|
||||||
|
for perm in policy["permission_groups"]:
|
||||||
|
perm_name = perm.get("name", "")
|
||||||
|
permissions["permissions"].append(perm_name)
|
||||||
|
|
||||||
|
# Önemli yetkileri flag'le
|
||||||
|
if "Zone" in perm_name:
|
||||||
|
permissions["zone_access"] = True
|
||||||
|
if "DNS" in perm_name and "Write" in perm_name:
|
||||||
|
permissions["dns_write"] = True
|
||||||
|
if "SSL" in perm_name and "Write" in perm_name:
|
||||||
|
permissions["ssl_write"] = True
|
||||||
|
|
||||||
|
# Özet bilgi
|
||||||
|
permissions["summary"] = {
|
||||||
|
"can_manage_zones": permissions["zone_access"],
|
||||||
|
"can_manage_dns": permissions["dns_write"],
|
||||||
|
"can_manage_ssl": permissions["ssl_write"],
|
||||||
|
"total_zones": permissions["zone_count"],
|
||||||
|
"available_domains": account.max_domains - account.current_domain_count
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"account": {
|
||||||
|
"id": account.id,
|
||||||
|
"name": account.name,
|
||||||
|
"email": account.email
|
||||||
|
},
|
||||||
|
"permissions": permissions
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"❌ Yetki kontrolü hatası: {str(e)}"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Yetki kontrolü sırasında hata: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@admin_bp.route('/cf-accounts/auto-select', methods=['POST'])
|
||||||
|
def auto_select_cf_account():
|
||||||
|
"""
|
||||||
|
Müşteri için otomatik CF hesap seçimi
|
||||||
|
En az dolu, aktif ve verification için kullanılabilir hesabı seçer
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Aktif, verification için kullanılabilir ve dolu olmayan hesapları getir
|
||||||
|
available_accounts = CloudflareAccount.query.filter(
|
||||||
|
CloudflareAccount.is_active == True,
|
||||||
|
CloudflareAccount.use_for_verification == True,
|
||||||
|
CloudflareAccount.current_domain_count < CloudflareAccount.max_domains
|
||||||
|
).order_by(
|
||||||
|
# En az dolu olanı seç
|
||||||
|
CloudflareAccount.current_domain_count.asc()
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if not available_accounts:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": "Kullanılabilir Cloudflare hesabı bulunamadı. Lütfen yönetici ile iletişime geçin."
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
# En uygun hesabı seç (ilk sıradaki)
|
||||||
|
selected_account = available_accounts[0]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": "success",
|
||||||
|
"account": {
|
||||||
|
"id": selected_account.id,
|
||||||
|
"name": selected_account.name,
|
||||||
|
"available_capacity": selected_account.max_domains - selected_account.current_domain_count,
|
||||||
|
"current_domains": selected_account.current_domain_count,
|
||||||
|
"max_domains": selected_account.max_domains
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": f"Hesap seçimi sırasında hata: {str(e)}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# CUSTOMER MANAGEMENT ENDPOINTS
|
# CUSTOMER MANAGEMENT ENDPOINTS
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
|
|
@ -115,11 +115,28 @@ def create_domain(current_user):
|
||||||
# Validate CF account if using company account
|
# Validate CF account if using company account
|
||||||
cf_account_id = data.get('cf_account_id')
|
cf_account_id = data.get('cf_account_id')
|
||||||
cf_account_type = data.get('cf_account_type', 'company')
|
cf_account_type = data.get('cf_account_type', 'company')
|
||||||
|
cf_account = None
|
||||||
|
|
||||||
if cf_account_type == 'company':
|
if cf_account_type == 'company':
|
||||||
|
# Otomatik hesap seçimi - cf_account_id verilmemişse en uygun hesabı seç
|
||||||
if not cf_account_id:
|
if not cf_account_id:
|
||||||
return jsonify({'error': 'cf_account_id is required for company account'}), 400
|
# En az dolu, aktif ve verification için kullanılabilir hesabı seç
|
||||||
|
cf_account = CloudflareAccount.query.filter(
|
||||||
|
CloudflareAccount.is_active == True,
|
||||||
|
CloudflareAccount.use_for_verification == True,
|
||||||
|
CloudflareAccount.current_domain_count < CloudflareAccount.max_domains
|
||||||
|
).order_by(
|
||||||
|
CloudflareAccount.current_domain_count.asc()
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not cf_account:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'No available Cloudflare account found. Please contact administrator.'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
cf_account_id = cf_account.id
|
||||||
|
else:
|
||||||
|
# Manuel seçim yapılmışsa o hesabı kullan
|
||||||
cf_account = CloudflareAccount.query.get(cf_account_id)
|
cf_account = CloudflareAccount.query.get(cf_account_id)
|
||||||
if not cf_account:
|
if not cf_account:
|
||||||
return jsonify({'error': 'Cloudflare account not found'}), 404
|
return jsonify({'error': 'Cloudflare account not found'}), 404
|
||||||
|
|
@ -127,6 +144,9 @@ def create_domain(current_user):
|
||||||
if not cf_account.is_active:
|
if not cf_account.is_active:
|
||||||
return jsonify({'error': 'Cloudflare account is not active'}), 400
|
return jsonify({'error': 'Cloudflare account is not active'}), 400
|
||||||
|
|
||||||
|
if not cf_account.use_for_verification:
|
||||||
|
return jsonify({'error': 'This Cloudflare account is not available for domain verification'}), 400
|
||||||
|
|
||||||
# Check CF account capacity
|
# Check CF account capacity
|
||||||
if cf_account.current_domain_count >= cf_account.max_domains:
|
if cf_account.current_domain_count >= cf_account.max_domains:
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Migration: Add use_for_verification field to cloudflare_accounts table
|
||||||
|
-- Date: 2026-01-12
|
||||||
|
|
||||||
|
-- Add use_for_verification column
|
||||||
|
ALTER TABLE cloudflare_accounts
|
||||||
|
ADD COLUMN IF NOT EXISTS use_for_verification BOOLEAN DEFAULT TRUE;
|
||||||
|
|
||||||
|
-- Update existing records to TRUE (default behavior)
|
||||||
|
UPDATE cloudflare_accounts
|
||||||
|
SET use_for_verification = TRUE
|
||||||
|
WHERE use_for_verification IS NULL;
|
||||||
|
|
||||||
|
-- Add comment
|
||||||
|
COMMENT ON COLUMN cloudflare_accounts.use_for_verification IS 'Whether this account can be used for customer domain verification';
|
||||||
|
|
||||||
|
|
@ -94,21 +94,17 @@ const AddDomainWizard = ({ onClose, onSuccess, customer }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfAccountType === 'company' && !selectedCompanyAccount) {
|
|
||||||
setError('Please select a company Cloudflare account');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// If company account, create domain immediately and skip to step 4
|
// If company account, create domain immediately with auto-selection
|
||||||
if (cfAccountType === 'company') {
|
if (cfAccountType === 'company') {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// Backend will automatically select the best available CF account
|
||||||
const response = await api.post('/api/customer/domains', {
|
const response = await api.post('/api/customer/domains', {
|
||||||
domain_name: domainName,
|
domain_name: domainName,
|
||||||
cf_account_type: 'company',
|
cf_account_type: 'company',
|
||||||
cf_account_id: selectedCompanyAccount.id,
|
// No cf_account_id - backend will auto-select
|
||||||
});
|
});
|
||||||
|
|
||||||
setDomainId(response.data.domain.id);
|
setDomainId(response.data.domain.id);
|
||||||
|
|
@ -324,33 +320,18 @@ const AddDomainWizard = ({ onClose, onSuccess, customer }) => {
|
||||||
className="mt-1 mr-3"
|
className="mt-1 mr-3"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h4 className="font-semibold text-gray-900">Use Company Cloudflare Account</h4>
|
<h4 className="font-semibold text-gray-900">✨ Use Company Cloudflare Account (Recommended)</h4>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
We'll manage your DNS using our Cloudflare account. Easier setup, no API token needed.
|
We'll automatically manage your DNS using our Cloudflare account. Easier setup, no API token needed.
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-2 flex items-center gap-2 text-xs text-green-700">
|
||||||
{cfAccountType === 'company' && companyAccounts.length > 0 && (
|
<CheckCircleIcon className="w-4 h-4" />
|
||||||
<div className="mt-3">
|
<span>Automatic account selection</span>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
</div>
|
||||||
Select Account:
|
<div className="mt-1 flex items-center gap-2 text-xs text-green-700">
|
||||||
</label>
|
<CheckCircleIcon className="w-4 h-4" />
|
||||||
<select
|
<span>No configuration required</span>
|
||||||
value={selectedCompanyAccount?.id || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
const account = companyAccounts.find(a => a.id === parseInt(e.target.value));
|
|
||||||
setSelectedCompanyAccount(account);
|
|
||||||
}}
|
|
||||||
className="input-field w-full"
|
|
||||||
>
|
|
||||||
<option value="">Choose an account...</option>
|
|
||||||
{companyAccounts.map((account) => (
|
|
||||||
<option key={account.id} value={account.id}>
|
|
||||||
{account.name} ({account.email})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
||||||
max_domains: 100,
|
max_domains: 100,
|
||||||
notes: '',
|
notes: '',
|
||||||
is_active: true,
|
is_active: true,
|
||||||
|
use_for_verification: true,
|
||||||
})
|
})
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
|
|
@ -22,6 +23,7 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
||||||
max_domains: account.max_domains,
|
max_domains: account.max_domains,
|
||||||
notes: account.notes || '',
|
notes: account.notes || '',
|
||||||
is_active: account.is_active,
|
is_active: account.is_active,
|
||||||
|
use_for_verification: account.use_for_verification !== undefined ? account.use_for_verification : true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [account])
|
}, [account])
|
||||||
|
|
@ -172,6 +174,23 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Use for Verification */}
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="use_for_verification"
|
||||||
|
checked={formData.use_for_verification}
|
||||||
|
onChange={(e) => setFormData({ ...formData, use_for_verification: e.target.checked })}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<label htmlFor="use_for_verification" className="text-sm font-medium">
|
||||||
|
Domain doğrulamada kullan
|
||||||
|
</label>
|
||||||
|
<span className="ml-2 text-xs text-gray-500">
|
||||||
|
(Müşteri "Bizim CF" seçtiğinde bu hesap kullanılabilir)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ function AdminCFAccounts() {
|
||||||
const [success, setSuccess] = useState(null)
|
const [success, setSuccess] = useState(null)
|
||||||
const [showAddModal, setShowAddModal] = useState(false)
|
const [showAddModal, setShowAddModal] = useState(false)
|
||||||
const [editingAccount, setEditingAccount] = useState(null)
|
const [editingAccount, setEditingAccount] = useState(null)
|
||||||
|
const [showPermissions, setShowPermissions] = useState(null)
|
||||||
|
const [permissions, setPermissions] = useState(null)
|
||||||
|
const [loadingPermissions, setLoadingPermissions] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAccounts()
|
loadAccounts()
|
||||||
|
|
@ -58,6 +61,42 @@ function AdminCFAccounts() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCheckPermissions = async (accountId) => {
|
||||||
|
setLoadingPermissions(true)
|
||||||
|
setShowPermissions(accountId)
|
||||||
|
setPermissions(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await adminAPI.getCFAccountPermissions(accountId)
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
setPermissions(response.data.permissions)
|
||||||
|
} else {
|
||||||
|
setError(response.data.message)
|
||||||
|
setShowPermissions(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Yetki kontrolü başarısız: ' + (err.response?.data?.message || err.message))
|
||||||
|
setShowPermissions(null)
|
||||||
|
} finally {
|
||||||
|
setLoadingPermissions(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleVerification = async (account) => {
|
||||||
|
try {
|
||||||
|
const response = await adminAPI.updateCFAccount(account.id, {
|
||||||
|
use_for_verification: !account.use_for_verification
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
setSuccess(`Domain doğrulama ${!account.use_for_verification ? 'aktif' : 'pasif'} edildi`)
|
||||||
|
loadAccounts()
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Güncelleme başarısız: ' + (err.response?.data?.message || err.message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto p-6">
|
<div className="max-w-6xl mx-auto p-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
|
@ -141,6 +180,23 @@ function AdminCFAccounts() {
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Domain Verification Toggle */}
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t">
|
||||||
|
<span className="text-gray-600">Domain Doğrulama:</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleVerification(account)}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||||
|
account.use_for_verification ? 'bg-green-600' : 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||||
|
account.use_for_verification ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{account.notes && (
|
{account.notes && (
|
||||||
|
|
@ -149,13 +205,19 @@ function AdminCFAccounts() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTest(account.id)}
|
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"
|
className="flex-1 px-3 py-2 border border-blue-600 text-blue-600 rounded hover:bg-blue-50 text-sm"
|
||||||
>
|
>
|
||||||
🧪 Test
|
🧪 Test
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleCheckPermissions(account.id)}
|
||||||
|
className="flex-1 px-3 py-2 border border-purple-600 text-purple-600 rounded hover:bg-purple-50 text-sm"
|
||||||
|
>
|
||||||
|
🔐 Yetkiler
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingAccount(account)}
|
onClick={() => setEditingAccount(account)}
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 text-sm"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 text-sm"
|
||||||
|
|
@ -170,6 +232,66 @@ function AdminCFAccounts() {
|
||||||
🗑️
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Permissions Panel */}
|
||||||
|
{showPermissions === account.id && (
|
||||||
|
<div className="mt-4 p-4 bg-gray-50 rounded border border-gray-200">
|
||||||
|
{loadingPermissions ? (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600 mx-auto"></div>
|
||||||
|
<p className="mt-2 text-sm text-gray-600">Yetkiler kontrol ediliyor...</p>
|
||||||
|
</div>
|
||||||
|
) : permissions ? (
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between items-center mb-3">
|
||||||
|
<h4 className="font-bold text-gray-800">🔐 Cloudflare Yetkileri</h4>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowPermissions(null)}
|
||||||
|
className="text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-3 h-3 rounded-full ${permissions.summary.can_manage_zones ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
||||||
|
<span className="text-sm">Zone Yönetimi</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-3 h-3 rounded-full ${permissions.summary.can_manage_dns ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
||||||
|
<span className="text-sm">DNS Yönetimi</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`w-3 h-3 rounded-full ${permissions.summary.can_manage_ssl ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
||||||
|
<span className="text-sm">SSL Yönetimi</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm font-medium">Toplam Zone: {permissions.summary.total_zones}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Permissions List */}
|
||||||
|
{permissions.permissions.length > 0 && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-300">
|
||||||
|
<p className="text-xs font-medium text-gray-600 mb-2">Tüm Yetkiler:</p>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{permissions.permissions.map((perm, idx) => (
|
||||||
|
<span
|
||||||
|
key={idx}
|
||||||
|
className="text-xs px-2 py-1 bg-purple-100 text-purple-800 rounded"
|
||||||
|
>
|
||||||
|
{perm}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,12 @@ export const adminAPI = {
|
||||||
|
|
||||||
testCFAccount: (accountId) =>
|
testCFAccount: (accountId) =>
|
||||||
api.post(`/api/admin/cf-accounts/${accountId}/test`),
|
api.post(`/api/admin/cf-accounts/${accountId}/test`),
|
||||||
|
|
||||||
|
getCFAccountPermissions: (accountId) =>
|
||||||
|
api.get(`/api/admin/cf-accounts/${accountId}/permissions`),
|
||||||
|
|
||||||
|
autoSelectCFAccount: () =>
|
||||||
|
api.post('/api/admin/cf-accounts/auto-select'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export default api
|
export default api
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue