mirror of
https://github.com/myronblair/novacpx
synced 2026-06-30 17:50:41 -05:00
6fdccc6dbd
#9 auth.php: add self-service change-password action (current+new+confirm) accounts.php: fix admin change-password — accept account_id, fetch username for chpasswd (was using int ID), add Auth::require('admin') guard user.js: add Change Password page + navItem + submitChangePassword() #10 EmailManager: store AES-256-CBC enc_password alongside SHA512-CRYPT hash webmail.php: rewrite login-url to use webmail_sso_tokens table novacpx-sso.php: Roundcube SSO bridge (validate token, decrypt, autosubmit) Migration 005: add enc_password column + webmail_sso_tokens table #11 opendkim: installed, configured (/etc/opendkim.conf, signing.table, key.table, trusted.hosts), socket at /var/spool/postfix/opendkim/, Postfix milter wired, service enabled+running, key generation verified #12 files.php: fix safe_path() for non-existent paths (write/mkdir), add safe_path_new() helper using parent-dir realpath check, fix delete guard (block deleting account root dirs), fix rename destination, clamp chmod to 0777 #13 nova.js: api() handles network errors, 429 rate-limit with retry-after, non-JSON responses (PHP fatal pages) — graceful error instead of throw admin/user/reseller index.php: filemtime-based cache-busting on all assets 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);
|
|
}
|
|
}
|