Add CF account auto-selection and verification toggle

This commit is contained in:
oguz ozturk 2026-01-12 16:32:43 +03:00
parent 2a7dd9a348
commit 84c8cb728e
8 changed files with 339 additions and 42 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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