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
|
||||
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
|
||||
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,
|
||||
"email": self.email,
|
||||
"is_active": self.is_active,
|
||||
"use_for_verification": self.use_for_verification,
|
||||
"max_domains": self.max_domains,
|
||||
"current_domain_count": self.current_domain_count,
|
||||
"notes": self.notes,
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@ def create_cf_account():
|
|||
email=data['email'],
|
||||
max_domains=data.get('max_domains', 100),
|
||||
notes=data.get('notes', ''),
|
||||
is_active=True
|
||||
is_active=True,
|
||||
use_for_verification=data.get('use_for_verification', True)
|
||||
)
|
||||
|
||||
# Token'ı şifrele ve kaydet
|
||||
|
|
@ -174,6 +175,9 @@ def update_cf_account(account_id):
|
|||
if 'is_active' in data:
|
||||
account.is_active = data['is_active']
|
||||
|
||||
if 'use_for_verification' in data:
|
||||
account.use_for_verification = data['use_for_verification']
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
|
|
@ -271,6 +275,134 @@ def test_cf_account(account_id):
|
|||
}), 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
|
||||
# ============================================
|
||||
|
|
|
|||
|
|
@ -115,18 +115,38 @@ def create_domain(current_user):
|
|||
# Validate CF account if using company account
|
||||
cf_account_id = data.get('cf_account_id')
|
||||
cf_account_type = data.get('cf_account_type', 'company')
|
||||
|
||||
cf_account = None
|
||||
|
||||
if cf_account_type == 'company':
|
||||
# Otomatik hesap seçimi - cf_account_id verilmemişse en uygun hesabı seç
|
||||
if not cf_account_id:
|
||||
return jsonify({'error': 'cf_account_id is required for company account'}), 400
|
||||
|
||||
cf_account = CloudflareAccount.query.get(cf_account_id)
|
||||
if not cf_account:
|
||||
return jsonify({'error': 'Cloudflare account not found'}), 404
|
||||
|
||||
# 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)
|
||||
if not cf_account:
|
||||
return jsonify({'error': 'Cloudflare account not found'}), 404
|
||||
|
||||
if not cf_account.is_active:
|
||||
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
|
||||
if cf_account.current_domain_count >= cf_account.max_domains:
|
||||
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;
|
||||
}
|
||||
|
||||
if (cfAccountType === 'company' && !selectedCompanyAccount) {
|
||||
setError('Please select a company Cloudflare account');
|
||||
return;
|
||||
}
|
||||
|
||||
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') {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Backend will automatically select the best available CF account
|
||||
const response = await api.post('/api/customer/domains', {
|
||||
domain_name: domainName,
|
||||
cf_account_type: 'company',
|
||||
cf_account_id: selectedCompanyAccount.id,
|
||||
// No cf_account_id - backend will auto-select
|
||||
});
|
||||
|
||||
setDomainId(response.data.domain.id);
|
||||
|
|
@ -324,33 +320,18 @@ const AddDomainWizard = ({ onClose, onSuccess, customer }) => {
|
|||
className="mt-1 mr-3"
|
||||
/>
|
||||
<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">
|
||||
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>
|
||||
|
||||
{cfAccountType === 'company' && companyAccounts.length > 0 && (
|
||||
<div className="mt-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Select Account:
|
||||
</label>
|
||||
<select
|
||||
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 className="mt-2 flex items-center gap-2 text-xs text-green-700">
|
||||
<CheckCircleIcon className="w-4 h-4" />
|
||||
<span>Automatic account selection</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-green-700">
|
||||
<CheckCircleIcon className="w-4 h-4" />
|
||||
<span>No configuration required</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
|||
max_domains: 100,
|
||||
notes: '',
|
||||
is_active: true,
|
||||
use_for_verification: true,
|
||||
})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
|
@ -22,6 +23,7 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
|||
max_domains: account.max_domains,
|
||||
notes: account.notes || '',
|
||||
is_active: account.is_active,
|
||||
use_for_verification: account.use_for_verification !== undefined ? account.use_for_verification : true,
|
||||
})
|
||||
}
|
||||
}, [account])
|
||||
|
|
@ -172,6 +174,23 @@ function CFAccountModal({ account, onClose, onSuccess }) {
|
|||
</label>
|
||||
</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 */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ function AdminCFAccounts() {
|
|||
const [success, setSuccess] = useState(null)
|
||||
const [showAddModal, setShowAddModal] = useState(false)
|
||||
const [editingAccount, setEditingAccount] = useState(null)
|
||||
const [showPermissions, setShowPermissions] = useState(null)
|
||||
const [permissions, setPermissions] = useState(null)
|
||||
const [loadingPermissions, setLoadingPermissions] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<div className="max-w-6xl mx-auto p-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
|
|
@ -141,6 +180,23 @@ function AdminCFAccounts() {
|
|||
}}
|
||||
></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>
|
||||
|
||||
{account.notes && (
|
||||
|
|
@ -149,13 +205,19 @@ function AdminCFAccounts() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<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={() => 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
|
||||
onClick={() => setEditingAccount(account)}
|
||||
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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,12 @@ export const adminAPI = {
|
|||
|
||||
testCFAccount: (accountId) =>
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue