mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
135bbcb0b3
- WordPressManager.php: wp-cli wrapper for install/update/clone/delete - BackupManager.php: tar+mysqldump, schedules, retention, rclone - CloudflareManager.php: zone/record management, sync, cache purge - TOTP.php: RFC 6238 pure-PHP with backup codes - Auth.php: TOTP_REQUIRED two-step login flow - 4 new API endpoints: wordpress, backup, cloudflare, totp - DB migration 002: TOTP cols, CF cols, wordpress_installs, backups tables - admin.js: full UI for all 4 features + TOTP login step - admin/index.php: sidebar nav for WordPress, 2FA Manager, Cloudflare Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
151 lines
7.7 KiB
PHP
151 lines
7.7 KiB
PHP
<?php
|
|
class CloudflareManager {
|
|
private const API = 'https://api.cloudflare.com/client/v4/';
|
|
private PDO $db;
|
|
|
|
public function __construct() {
|
|
$this->db = Database::getInstance()->getPDO();
|
|
}
|
|
|
|
// ── Credential management ─────────────────────────────────────────────────
|
|
public function saveCredentials(int $accountId, string $apiKey, string $email): bool {
|
|
$stmt = $this->db->prepare("UPDATE accounts SET cf_api_key=?, cf_api_email=? WHERE id=?");
|
|
return $stmt->execute([$apiKey, $email, $accountId]);
|
|
}
|
|
|
|
public function testCredentials(string $apiKey, string $email): bool {
|
|
$r = $this->req('GET', 'user/tokens/verify', [], $apiKey, $email);
|
|
return $r['success'] ?? false;
|
|
}
|
|
|
|
public function getCredentials(int $accountId): ?array {
|
|
$stmt = $this->db->prepare("SELECT cf_api_key, cf_api_email, cf_zone_id FROM accounts WHERE id=?");
|
|
$stmt->execute([$accountId]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
return ($row && $row['cf_api_key']) ? $row : null;
|
|
}
|
|
|
|
// ── Zones ─────────────────────────────────────────────────────────────────
|
|
public function listZones(string $apiKey, string $email): array {
|
|
$r = $this->req('GET', 'zones?per_page=200&status=active', [], $apiKey, $email);
|
|
return $r['result'] ?? [];
|
|
}
|
|
|
|
public function getZoneId(string $domain, string $apiKey, string $email): ?string {
|
|
$r = $this->req('GET', 'zones?name=' . urlencode($domain), [], $apiKey, $email);
|
|
return $r['result'][0]['id'] ?? null;
|
|
}
|
|
|
|
// ── DNS records ───────────────────────────────────────────────────────────
|
|
public function listRecords(string $zoneId, string $apiKey, string $email): array {
|
|
$r = $this->req('GET', "zones/{$zoneId}/dns_records?per_page=200", [], $apiKey, $email);
|
|
return $r['result'] ?? [];
|
|
}
|
|
|
|
public function createRecord(string $zoneId, array $record, string $apiKey, string $email): array {
|
|
return $this->req('POST', "zones/{$zoneId}/dns_records", $record, $apiKey, $email);
|
|
}
|
|
|
|
public function updateRecord(string $zoneId, string $recordId, array $record, string $apiKey, string $email): array {
|
|
return $this->req('PUT', "zones/{$zoneId}/dns_records/{$recordId}", $record, $apiKey, $email);
|
|
}
|
|
|
|
public function deleteRecord(string $zoneId, string $recordId, string $apiKey, string $email): bool {
|
|
$r = $this->req('DELETE', "zones/{$zoneId}/dns_records/{$recordId}", [], $apiKey, $email);
|
|
return $r['success'] ?? false;
|
|
}
|
|
|
|
public function toggleProxy(string $zoneId, string $recordId, bool $proxied, string $apiKey, string $email): array {
|
|
// Fetch existing record first
|
|
$r = $this->req('GET', "zones/{$zoneId}/dns_records/{$recordId}", [], $apiKey, $email);
|
|
$rec = $r['result'] ?? [];
|
|
$rec['proxied'] = $proxied;
|
|
unset($rec['id'], $rec['zone_id'], $rec['zone_name'], $rec['created_on'], $rec['modified_on'], $rec['meta']);
|
|
return $this->req('PUT', "zones/{$zoneId}/dns_records/{$recordId}", $rec, $apiKey, $email);
|
|
}
|
|
|
|
// ── Sync: push local DNS records to Cloudflare ────────────────────────────
|
|
public function syncToCloudflare(string $domain, string $zoneId, string $apiKey, string $email): array {
|
|
$localRecords = $this->getLocalRecords($domain);
|
|
$cfRecords = $this->listRecords($zoneId, $apiKey, $email);
|
|
$cfIndex = [];
|
|
foreach ($cfRecords as $r) $cfIndex[$r['type'] . '_' . $r['name']] = $r;
|
|
|
|
$created = 0; $updated = 0;
|
|
foreach ($localRecords as $local) {
|
|
$key = $local['type'] . '_' . $local['name'] . '.' . $domain . '.';
|
|
if (isset($cfIndex[$key])) {
|
|
$this->updateRecord($zoneId, $cfIndex[$key]['id'], [
|
|
'type' => $local['type'],
|
|
'name' => $local['name'],
|
|
'content' => $local['content'],
|
|
'ttl' => (int)($local['ttl'] ?? 1),
|
|
'proxied' => in_array($local['type'], ['A','AAAA','CNAME']),
|
|
], $apiKey, $email);
|
|
$updated++;
|
|
} else {
|
|
$this->createRecord($zoneId, [
|
|
'type' => $local['type'],
|
|
'name' => $local['name'],
|
|
'content' => $local['content'],
|
|
'ttl' => (int)($local['ttl'] ?? 1),
|
|
'proxied' => in_array($local['type'], ['A','AAAA','CNAME']),
|
|
], $apiKey, $email);
|
|
$created++;
|
|
}
|
|
}
|
|
return ['created' => $created, 'updated' => $updated];
|
|
}
|
|
|
|
// ── Sync: pull Cloudflare records into local DB ───────────────────────────
|
|
public function syncFromCloudflare(string $domain, string $zoneId, string $apiKey, string $email): int {
|
|
$cfRecords = $this->listRecords($zoneId, $apiKey, $email);
|
|
$count = 0;
|
|
foreach ($cfRecords as $rec) {
|
|
$name = rtrim(str_replace('.' . $domain, '', $rec['name']), '.');
|
|
$this->db->prepare("INSERT INTO dns_records (domain, name, type, content, ttl, priority)
|
|
VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE content=VALUES(content), ttl=VALUES(ttl)")
|
|
->execute([$domain, $name ?: '@', $rec['type'], $rec['content'], $rec['ttl'] ?? 300, $rec['priority'] ?? 0]);
|
|
$count++;
|
|
}
|
|
// Store zone ID on domain
|
|
$this->db->prepare("UPDATE dns_zones SET cf_zone_id=? WHERE domain=?")->execute([$zoneId, $domain]);
|
|
return $count;
|
|
}
|
|
|
|
// ── Purge cache ───────────────────────────────────────────────────────────
|
|
public function purgeCache(string $zoneId, string $apiKey, string $email): bool {
|
|
$r = $this->req('POST', "zones/{$zoneId}/purge_cache", ['purge_everything' => true], $apiKey, $email);
|
|
return $r['success'] ?? false;
|
|
}
|
|
|
|
// ── HTTP helper ───────────────────────────────────────────────────────────
|
|
private function req(string $method, string $path, array $body, string $apiKey, string $email): array {
|
|
$ch = curl_init(self::API . ltrim($path, '/'));
|
|
$headers = [
|
|
"X-Auth-Email: {$email}",
|
|
"X-Auth-Key: {$apiKey}",
|
|
"Content-Type: application/json",
|
|
];
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 15,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
]);
|
|
if ($body && in_array($method, ['POST','PUT','PATCH'])) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
|
|
}
|
|
$result = curl_exec($ch);
|
|
curl_close($ch);
|
|
return json_decode($result, true) ?? ['success' => false, 'errors' => ['curl failed']];
|
|
}
|
|
|
|
private function getLocalRecords(string $domain): array {
|
|
$stmt = $this->db->prepare("SELECT * FROM dns_records WHERE domain=?");
|
|
$stmt->execute([$domain]);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
}
|