fix width
This commit is contained in:
99
README.md
99
README.md
@@ -16,9 +16,17 @@ A complete solution for managing RFC 8805 compliant IP geolocation feeds (geofee
|
||||
- **Mobile Optimized** - Full mobile Safari support with PWA capabilities
|
||||
- **CSRF Protection** - Secure form submissions
|
||||
- **Developer Tools** - Database backup/restore, schema sync, and error log viewer
|
||||
- **PTR Record Management** - AWS Route53 integration for checking reverse DNS records
|
||||
|
||||
## What's New
|
||||
|
||||
### PTR Records Tab
|
||||
- **AWS Route53 Integration** - Connect to your AWS account to list A records from hosted zones
|
||||
- **PTR Lookup** - Check if PTR (reverse DNS) records match your forward DNS hostnames
|
||||
- **Bulk Checking** - Check all PTR records in a zone with one click
|
||||
- **Status Indicators** - Visual indicators for MATCH, MISMATCH, and MISSING PTR records
|
||||
- **Zone Selector** - Easily switch between multiple hosted zones
|
||||
|
||||
### Developer Mode Tab
|
||||
- **Database Backup** - Export full database backup as JSON (entries, settings, logos, audit log)
|
||||
- **Database Import** - Restore from a previously exported backup file
|
||||
@@ -189,6 +197,38 @@ When enabled, new IP entries are automatically enriched with:
|
||||
- Timezone and coordinates
|
||||
- Security flags (proxy, VPN, Tor, threat, etc.)
|
||||
|
||||
### AWS Route53 Integration
|
||||
|
||||
Configure AWS credentials in the Advanced tab to enable PTR record checking:
|
||||
|
||||
1. Create an IAM user in AWS with `route53:ListHostedZones` and `route53:ListResourceRecordSets` permissions
|
||||
2. Generate an access key and secret key for the IAM user
|
||||
3. In the Advanced tab, enter:
|
||||
- **AWS Access Key ID** - Your IAM access key
|
||||
- **AWS Secret Access Key** - Your IAM secret key
|
||||
- **AWS Region** - Select your preferred region (Route53 is global, but a region is required for API signing)
|
||||
- **Hosted Zone IDs** - Comma-separated list of Route53 hosted zone IDs (e.g., `Z1234567890ABC, Z0987654321DEF`)
|
||||
4. Click "Test Connection" to verify credentials
|
||||
5. Go to the PTR tab to view A records and check PTR status
|
||||
|
||||
**IAM Policy Example:**
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"route53:ListHostedZones",
|
||||
"route53:ListResourceRecordSets",
|
||||
"route53:GetHostedZone"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook Integration
|
||||
|
||||
Configure webhooks in the Advanced tab to notify n8n when data changes:
|
||||
@@ -408,6 +448,49 @@ Content-Type: application/json
|
||||
GET api.php?action=system_info
|
||||
```
|
||||
|
||||
### AWS Settings
|
||||
```
|
||||
GET api.php?action=aws_settings_get
|
||||
```
|
||||
Returns current AWS Route53 configuration.
|
||||
|
||||
```
|
||||
POST api.php?action=aws_settings_save
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"aws_access_key_id": "AKIAIOSFODNN7EXAMPLE",
|
||||
"aws_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
"aws_region": "us-east-1",
|
||||
"aws_hosted_zones": "Z1234567890ABC, Z0987654321DEF",
|
||||
"csrf_token": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### Test AWS Connection
|
||||
```
|
||||
GET api.php?action=aws_test
|
||||
```
|
||||
Tests AWS credentials and returns hosted zone count.
|
||||
|
||||
### List AWS Hosted Zones
|
||||
```
|
||||
GET api.php?action=aws_zones
|
||||
```
|
||||
Returns list of configured hosted zones with their names and IDs.
|
||||
|
||||
### List A Records
|
||||
```
|
||||
GET api.php?action=aws_records&zone_id=Z1234567890ABC
|
||||
```
|
||||
Returns all A records from the specified hosted zone.
|
||||
|
||||
### PTR Lookup
|
||||
```
|
||||
GET api.php?action=ptr_lookup&ip=192.168.1.1
|
||||
```
|
||||
Performs reverse DNS lookup and returns the PTR record for the given IP.
|
||||
|
||||
## Geofeed Format (RFC 8805)
|
||||
|
||||
Each line in the exported CSV follows this format:
|
||||
@@ -481,6 +564,22 @@ Ensure your IP prefixes are in valid CIDR notation (e.g., `192.168.1.0/24`)
|
||||
- Ensure your browser/OS has dark mode enabled
|
||||
- Try clearing browser cache
|
||||
|
||||
### AWS Route53 connection fails
|
||||
- Verify your AWS Access Key ID and Secret Access Key are correct
|
||||
- Ensure the IAM user has the required permissions (ListHostedZones, ListResourceRecordSets)
|
||||
- Check that the hosted zone IDs are correct (format: Z followed by alphanumeric characters)
|
||||
- Review error logs in the Developer tab for detailed error messages
|
||||
|
||||
### PTR records showing as MISSING
|
||||
- PTR records are managed by your IP provider (e.g., IPXO, your ISP, or cloud provider)
|
||||
- Contact your IP provider to set up reverse DNS for your IP addresses
|
||||
- PTR lookups use the server's DNS resolver - results may vary based on DNS propagation
|
||||
|
||||
### PTR records showing as MISMATCH
|
||||
- The PTR record exists but doesn't match the expected hostname
|
||||
- Verify the PTR is set correctly with your IP provider
|
||||
- Note: Trailing dots in hostnames are normalized during comparison
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Feel free to use and modify as needed.
|
||||
|
||||
@@ -126,7 +126,11 @@ INSERT INTO geofeed_settings (setting_key, setting_value) VALUES
|
||||
('n8n_webhook_enabled', '0'),
|
||||
('n8n_webhook_delay_minutes', '3'),
|
||||
('ipregistry_api_key', ''),
|
||||
('ipregistry_enabled', '0')
|
||||
('ipregistry_enabled', '0'),
|
||||
('aws_access_key_id', ''),
|
||||
('aws_secret_access_key', ''),
|
||||
('aws_region', 'us-east-1'),
|
||||
('aws_hosted_zones', '')
|
||||
ON DUPLICATE KEY UPDATE setting_key = setting_key;
|
||||
|
||||
-- ============================================
|
||||
|
||||
393
webapp/api.php
393
webapp/api.php
@@ -170,6 +170,30 @@ try {
|
||||
handleErrorLogsClear($db);
|
||||
break;
|
||||
|
||||
case 'aws_settings_get':
|
||||
handleAwsSettingsGet($db);
|
||||
break;
|
||||
|
||||
case 'aws_settings_save':
|
||||
handleAwsSettingsSave($db);
|
||||
break;
|
||||
|
||||
case 'aws_test':
|
||||
handleAwsTest($db);
|
||||
break;
|
||||
|
||||
case 'aws_zones':
|
||||
handleAwsZones($db);
|
||||
break;
|
||||
|
||||
case 'aws_records':
|
||||
handleAwsRecords($db);
|
||||
break;
|
||||
|
||||
case 'ptr_lookup':
|
||||
handlePtrLookup($db);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Invalid action'], 400);
|
||||
}
|
||||
@@ -2171,3 +2195,372 @@ function formatBytes($bytes, $precision = 2) {
|
||||
$bytes /= pow(1024, $pow);
|
||||
return round($bytes, $precision) . ' ' . $units[$pow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AWS settings
|
||||
*/
|
||||
function handleAwsSettingsGet($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'aws_access_key_id' => getSetting($db, 'aws_access_key_id', ''),
|
||||
'aws_secret_access_key' => getSetting($db, 'aws_secret_access_key', '') ? '••••••••' : '',
|
||||
'aws_region' => getSetting($db, 'aws_region', 'us-east-1'),
|
||||
'aws_hosted_zones' => getSetting($db, 'aws_hosted_zones', ''),
|
||||
'is_configured' => !empty(getSetting($db, 'aws_access_key_id', '')) && !empty(getSetting($db, 'aws_secret_access_key', ''))
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save AWS settings
|
||||
*/
|
||||
function handleAwsSettingsSave($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
|
||||
jsonResponse(['error' => 'Invalid CSRF token'], 403);
|
||||
}
|
||||
|
||||
saveSetting($db, 'aws_access_key_id', $input['aws_access_key_id'] ?? '');
|
||||
|
||||
// Only update secret if it's not masked
|
||||
if (!empty($input['aws_secret_access_key']) && strpos($input['aws_secret_access_key'], '••••') === false) {
|
||||
saveSetting($db, 'aws_secret_access_key', $input['aws_secret_access_key']);
|
||||
}
|
||||
|
||||
saveSetting($db, 'aws_region', $input['aws_region'] ?? 'us-east-1');
|
||||
saveSetting($db, 'aws_hosted_zones', $input['aws_hosted_zones'] ?? '');
|
||||
|
||||
jsonResponse(['success' => true, 'message' => 'AWS settings saved successfully']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test AWS connection
|
||||
*/
|
||||
function handleAwsTest($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!validateCSRFToken($input['csrf_token'] ?? '')) {
|
||||
jsonResponse(['error' => 'Invalid CSRF token'], 403);
|
||||
}
|
||||
|
||||
$accessKeyId = getSetting($db, 'aws_access_key_id', '');
|
||||
$secretAccessKey = getSetting($db, 'aws_secret_access_key', '');
|
||||
$region = getSetting($db, 'aws_region', 'us-east-1');
|
||||
|
||||
if (empty($accessKeyId) || empty($secretAccessKey)) {
|
||||
jsonResponse(['error' => 'AWS credentials not configured'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = awsRoute53Request('GET', '2013-04-01/hostedzone', [], $accessKeyId, $secretAccessKey, $region);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
jsonResponse(['success' => false, 'error' => $result['error']]);
|
||||
}
|
||||
|
||||
$zoneCount = 0;
|
||||
if (isset($result['HostedZones']['HostedZone'])) {
|
||||
$zones = $result['HostedZones']['HostedZone'];
|
||||
$zoneCount = isset($zones['Id']) ? 1 : count($zones);
|
||||
}
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'message' => "Connection successful! Found {$zoneCount} hosted zone(s)."
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AWS hosted zones with details
|
||||
*/
|
||||
function handleAwsZones($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$accessKeyId = getSetting($db, 'aws_access_key_id', '');
|
||||
$secretAccessKey = getSetting($db, 'aws_secret_access_key', '');
|
||||
$region = getSetting($db, 'aws_region', 'us-east-1');
|
||||
$configuredZones = array_map('trim', explode(',', getSetting($db, 'aws_hosted_zones', '')));
|
||||
|
||||
if (empty($accessKeyId) || empty($secretAccessKey)) {
|
||||
jsonResponse(['error' => 'AWS credentials not configured'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = awsRoute53Request('GET', '2013-04-01/hostedzone', [], $accessKeyId, $secretAccessKey, $region);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
jsonResponse(['success' => false, 'error' => $result['error']]);
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
if (isset($result['HostedZones']['HostedZone'])) {
|
||||
$hostedZones = $result['HostedZones']['HostedZone'];
|
||||
// Handle single zone vs array of zones
|
||||
if (isset($hostedZones['Id'])) {
|
||||
$hostedZones = [$hostedZones];
|
||||
}
|
||||
|
||||
foreach ($hostedZones as $zone) {
|
||||
$zoneId = str_replace('/hostedzone/', '', $zone['Id']);
|
||||
// Only include configured zones if any are specified
|
||||
if (empty($configuredZones[0]) || in_array($zoneId, $configuredZones)) {
|
||||
$zones[] = [
|
||||
'id' => $zoneId,
|
||||
'name' => rtrim($zone['Name'], '.'),
|
||||
'record_count' => $zone['ResourceRecordSetCount'] ?? 0,
|
||||
'private' => ($zone['Config']['PrivateZone'] ?? 'false') === 'true'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(['success' => true, 'zones' => $zones]);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get A records from a hosted zone
|
||||
*/
|
||||
function handleAwsRecords($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$zoneId = $_GET['zone_id'] ?? '';
|
||||
if (empty($zoneId)) {
|
||||
jsonResponse(['error' => 'Zone ID required'], 400);
|
||||
}
|
||||
|
||||
$accessKeyId = getSetting($db, 'aws_access_key_id', '');
|
||||
$secretAccessKey = getSetting($db, 'aws_secret_access_key', '');
|
||||
$region = getSetting($db, 'aws_region', 'us-east-1');
|
||||
|
||||
if (empty($accessKeyId) || empty($secretAccessKey)) {
|
||||
jsonResponse(['error' => 'AWS credentials not configured'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = awsRoute53Request(
|
||||
'GET',
|
||||
"2013-04-01/hostedzone/{$zoneId}/rrset?type=A&maxitems=300",
|
||||
[],
|
||||
$accessKeyId,
|
||||
$secretAccessKey,
|
||||
$region
|
||||
);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
jsonResponse(['success' => false, 'error' => $result['error']]);
|
||||
}
|
||||
|
||||
$records = [];
|
||||
if (isset($result['ResourceRecordSets']['ResourceRecordSet'])) {
|
||||
$recordSets = $result['ResourceRecordSets']['ResourceRecordSet'];
|
||||
// Handle single record vs array
|
||||
if (isset($recordSets['Name'])) {
|
||||
$recordSets = [$recordSets];
|
||||
}
|
||||
|
||||
foreach ($recordSets as $record) {
|
||||
if (($record['Type'] ?? '') !== 'A') continue;
|
||||
|
||||
$ips = [];
|
||||
if (isset($record['ResourceRecords']['ResourceRecord'])) {
|
||||
$rrs = $record['ResourceRecords']['ResourceRecord'];
|
||||
if (isset($rrs['Value'])) {
|
||||
$ips[] = $rrs['Value'];
|
||||
} else {
|
||||
foreach ($rrs as $rr) {
|
||||
$ips[] = $rr['Value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($ips as $ip) {
|
||||
$records[] = [
|
||||
'hostname' => rtrim($record['Name'], '.'),
|
||||
'ip' => $ip,
|
||||
'ttl' => $record['TTL'] ?? 300,
|
||||
'ptr' => null,
|
||||
'ptr_status' => 'unknown'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(['success' => true, 'records' => $records]);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup PTR record for an IP
|
||||
*/
|
||||
function handlePtrLookup($db) {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
|
||||
$ip = $_GET['ip'] ?? '';
|
||||
$expectedHostname = $_GET['hostname'] ?? '';
|
||||
|
||||
if (empty($ip)) {
|
||||
jsonResponse(['error' => 'IP address required'], 400);
|
||||
}
|
||||
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
jsonResponse(['error' => 'Invalid IPv4 address'], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// Perform reverse DNS lookup
|
||||
$ptr = gethostbyaddr($ip);
|
||||
|
||||
// If gethostbyaddr returns the IP, no PTR was found
|
||||
if ($ptr === $ip) {
|
||||
$ptr = null;
|
||||
}
|
||||
|
||||
// Determine status
|
||||
$status = 'unknown';
|
||||
if ($ptr === null) {
|
||||
$status = 'missing';
|
||||
} elseif (!empty($expectedHostname)) {
|
||||
// Normalize hostnames for comparison (remove trailing dots, lowercase)
|
||||
$normalizedPtr = strtolower(rtrim($ptr, '.'));
|
||||
$normalizedExpected = strtolower(rtrim($expectedHostname, '.'));
|
||||
|
||||
if ($normalizedPtr === $normalizedExpected) {
|
||||
$status = 'match';
|
||||
} else {
|
||||
$status = 'mismatch';
|
||||
}
|
||||
} else {
|
||||
$status = 'found';
|
||||
}
|
||||
|
||||
jsonResponse([
|
||||
'success' => true,
|
||||
'ip' => $ip,
|
||||
'ptr' => $ptr,
|
||||
'status' => $status,
|
||||
'expected' => $expectedHostname
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make AWS Route53 API request with Signature Version 4
|
||||
*/
|
||||
function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey, $region) {
|
||||
$service = 'route53';
|
||||
$host = 'route53.amazonaws.com';
|
||||
$endpoint = "https://{$host}/{$path}";
|
||||
|
||||
$algorithm = 'AWS4-HMAC-SHA256';
|
||||
$timestamp = gmdate('Ymd\THis\Z');
|
||||
$datestamp = gmdate('Ymd');
|
||||
|
||||
// Create canonical request
|
||||
$canonicalUri = '/' . $path;
|
||||
$canonicalQuerystring = '';
|
||||
|
||||
// Parse query string if present
|
||||
if (strpos($path, '?') !== false) {
|
||||
list($canonicalUri, $queryString) = explode('?', '/' . $path, 2);
|
||||
parse_str($queryString, $queryParams);
|
||||
ksort($queryParams);
|
||||
$canonicalQuerystring = http_build_query($queryParams);
|
||||
}
|
||||
|
||||
$payloadHash = hash('sha256', is_array($body) ? json_encode($body) : '');
|
||||
|
||||
$canonicalHeaders = "host:{$host}\n" .
|
||||
"x-amz-date:{$timestamp}\n";
|
||||
$signedHeaders = 'host;x-amz-date';
|
||||
|
||||
$canonicalRequest = "{$method}\n{$canonicalUri}\n{$canonicalQuerystring}\n{$canonicalHeaders}\n{$signedHeaders}\n{$payloadHash}";
|
||||
|
||||
// Create string to sign
|
||||
$credentialScope = "{$datestamp}/{$region}/{$service}/aws4_request";
|
||||
$stringToSign = "{$algorithm}\n{$timestamp}\n{$credentialScope}\n" . hash('sha256', $canonicalRequest);
|
||||
|
||||
// Calculate signature
|
||||
$kSecret = 'AWS4' . $secretAccessKey;
|
||||
$kDate = hash_hmac('sha256', $datestamp, $kSecret, true);
|
||||
$kRegion = hash_hmac('sha256', $region, $kDate, true);
|
||||
$kService = hash_hmac('sha256', $service, $kRegion, true);
|
||||
$kSigning = hash_hmac('sha256', 'aws4_request', $kService, true);
|
||||
$signature = hash_hmac('sha256', $stringToSign, $kSigning);
|
||||
|
||||
// Create authorization header
|
||||
$authorizationHeader = "{$algorithm} Credential={$accessKeyId}/{$credentialScope}, SignedHeaders={$signedHeaders}, Signature={$signature}";
|
||||
|
||||
// Make request
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $endpoint,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
"Host: {$host}",
|
||||
"X-Amz-Date: {$timestamp}",
|
||||
"Authorization: {$authorizationHeader}",
|
||||
"Content-Type: application/xml"
|
||||
]
|
||||
]);
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($body) ? json_encode($body) : $body);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
return ['error' => "cURL error: {$error}"];
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
// Parse error from XML response
|
||||
if (preg_match('/<Message>(.+?)<\/Message>/s', $response, $matches)) {
|
||||
return ['error' => $matches[1]];
|
||||
}
|
||||
return ['error' => "HTTP {$httpCode}: {$response}"];
|
||||
}
|
||||
|
||||
// Parse XML response
|
||||
$xml = simplexml_load_string($response);
|
||||
if ($xml === false) {
|
||||
return ['error' => 'Failed to parse XML response'];
|
||||
}
|
||||
|
||||
return json_decode(json_encode($xml), true);
|
||||
}
|
||||
|
||||
470
webapp/index.php
470
webapp/index.php
@@ -1556,6 +1556,12 @@ if (function_exists('requireAuth')) {
|
||||
</svg>
|
||||
Advanced
|
||||
</button>
|
||||
<button class="tab" onclick="switchTab('ptr')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
PTR Records
|
||||
</button>
|
||||
<button class="tab" onclick="switchTab('developer')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
@@ -1894,6 +1900,69 @@ if (function_exists('requireAuth')) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AWS Route53 Settings Section -->
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
AWS Route53 Settings
|
||||
</h2>
|
||||
<p class="advanced-section-desc">Configure AWS credentials and hosted zones for PTR record management.</p>
|
||||
|
||||
<div class="form-grid" style="margin-top: 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">AWS Access Key ID</label>
|
||||
<input type="text" class="form-input" id="awsAccessKeyId" placeholder="AKIAIOSFODNN7EXAMPLE">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">AWS Secret Access Key</label>
|
||||
<input type="password" class="form-input" id="awsSecretAccessKey" placeholder="••••••••••••••••">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">AWS Region</label>
|
||||
<select class="form-select" id="awsRegion">
|
||||
<option value="us-east-1">US East (N. Virginia)</option>
|
||||
<option value="us-east-2">US East (Ohio)</option>
|
||||
<option value="us-west-1">US West (N. California)</option>
|
||||
<option value="us-west-2">US West (Oregon)</option>
|
||||
<option value="eu-west-1">EU (Ireland)</option>
|
||||
<option value="eu-west-2">EU (London)</option>
|
||||
<option value="eu-central-1">EU (Frankfurt)</option>
|
||||
<option value="ap-southeast-1">Asia Pacific (Singapore)</option>
|
||||
<option value="ap-southeast-2">Asia Pacific (Sydney)</option>
|
||||
<option value="ap-northeast-1">Asia Pacific (Tokyo)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Route53 Hosted Zone IDs (comma separated)</label>
|
||||
<input type="text" class="form-input" id="awsHostedZones" placeholder="Z1234567890ABC, Z0987654321DEF">
|
||||
<small style="color: var(--text-tertiary); font-size: 12px; margin-top: 4px; display: block;">Enter the hosted zone IDs for your forward DNS zones (A records)</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px;">
|
||||
<button class="btn btn-primary" onclick="saveAwsSettings()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>
|
||||
Save AWS Settings
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="testAwsConnection()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
Test Connection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="awsTestResult" style="margin-top: 16px; display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">Danger Zone</h2>
|
||||
<p class="advanced-section-desc">Irreversible actions - please proceed with caution.</p>
|
||||
@@ -1908,6 +1977,109 @@ if (function_exists('requireAuth')) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PTR Records Tab -->
|
||||
<div class="tab-content" id="tab-ptr">
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
PTR Record Management
|
||||
</h2>
|
||||
<p class="advanced-section-desc">View A records from your Route53 hosted zones and check their PTR records at IPXO.</p>
|
||||
|
||||
<div id="ptrNotConfigured" style="display: none;">
|
||||
<div class="alert alert-warning">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span>AWS Route53 is not configured. Please configure your AWS credentials and hosted zones in the <a href="#" onclick="switchTab('advanced'); return false;" style="color: var(--purple-primary);">Advanced tab</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ptrConfigured">
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; align-items: center;">
|
||||
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
|
||||
<label class="form-label">Select Hosted Zone</label>
|
||||
<select class="form-select" id="ptrZoneSelect" onchange="loadPtrRecords()">
|
||||
<option value="">Select a zone...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="loadPtrRecords()" style="margin-top: 24px;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="checkAllPtrs()" style="margin-top: 24px;" id="checkAllPtrsBtn" disabled>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
Check All PTRs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="ptrRecordsContainer">
|
||||
<div class="table-container">
|
||||
<div class="table-header">
|
||||
<h3 class="table-title">A Records</h3>
|
||||
<span id="ptrRecordCount" style="color: var(--text-secondary); font-size: 13px;"></span>
|
||||
</div>
|
||||
<div class="table-scroll">
|
||||
<table style="min-width: 900px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="min-width: 250px;">Hostname</th>
|
||||
<th style="width: 140px;">IP Address</th>
|
||||
<th style="width: 80px;">TTL</th>
|
||||
<th style="min-width: 250px;">Current PTR</th>
|
||||
<th style="width: 100px;">PTR Status</th>
|
||||
<th style="width: 100px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ptrRecordsBody">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
|
||||
Select a hosted zone to view A records
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PTR Legend -->
|
||||
<div class="advanced-section">
|
||||
<h2 class="advanced-section-title">PTR Status Legend</h2>
|
||||
<div style="display: flex; gap: 24px; flex-wrap: wrap; margin-top: 12px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--success); color: white;">MATCH</span>
|
||||
<span style="font-size: 13px; color: var(--text-secondary);">PTR matches hostname</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--warning); color: white;">MISMATCH</span>
|
||||
<span style="font-size: 13px; color: var(--text-secondary);">PTR exists but doesn't match</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">MISSING</span>
|
||||
<span style="font-size: 13px; color: var(--text-secondary);">No PTR record found</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--text-tertiary); color: white;">UNKNOWN</span>
|
||||
<span style="font-size: 13px; color: var(--text-secondary);">Not checked yet</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Developer Tab -->
|
||||
<div class="tab-content" id="tab-developer">
|
||||
<!-- Database Backup Section -->
|
||||
@@ -2246,6 +2418,7 @@ if (function_exists('requireAuth')) {
|
||||
loadWebhookSettings();
|
||||
loadWebhookQueueStatus();
|
||||
loadIpRegistrySettings();
|
||||
loadAwsSettings();
|
||||
}
|
||||
|
||||
// Load data for developer tab
|
||||
@@ -2253,6 +2426,11 @@ if (function_exists('requireAuth')) {
|
||||
loadSystemInfo();
|
||||
loadErrorLogs();
|
||||
}
|
||||
|
||||
// Load data for PTR tab
|
||||
if (tab === 'ptr') {
|
||||
loadPtrZones();
|
||||
}
|
||||
}
|
||||
|
||||
// API Helper
|
||||
@@ -4036,6 +4214,298 @@ if (function_exists('requireAuth')) {
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================
|
||||
// AWS Route53 / PTR Record Functions
|
||||
// =============================================
|
||||
|
||||
// Load AWS settings
|
||||
async function loadAwsSettings() {
|
||||
try {
|
||||
const result = await api('aws_settings_get');
|
||||
if (result.success && result.data) {
|
||||
document.getElementById('awsAccessKeyId').value = result.data.aws_access_key_id || '';
|
||||
document.getElementById('awsSecretAccessKey').value = result.data.aws_secret_access_key || '';
|
||||
document.getElementById('awsRegion').value = result.data.aws_region || 'us-east-1';
|
||||
document.getElementById('awsHostedZones').value = result.data.aws_hosted_zones || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load AWS settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Save AWS settings
|
||||
async function saveAwsSettings() {
|
||||
const data = {
|
||||
aws_access_key_id: document.getElementById('awsAccessKeyId').value,
|
||||
aws_secret_access_key: document.getElementById('awsSecretAccessKey').value,
|
||||
aws_region: document.getElementById('awsRegion').value,
|
||||
aws_hosted_zones: document.getElementById('awsHostedZones').value
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await api('aws_settings_save', {}, 'POST', data);
|
||||
if (result.success) {
|
||||
showToast('AWS settings saved successfully', 'success');
|
||||
} else {
|
||||
showToast(result.error || 'Failed to save AWS settings', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Failed to save AWS settings: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Test AWS connection
|
||||
async function testAwsConnection() {
|
||||
const resultDiv = document.getElementById('awsTestResult');
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Testing connection...</div>';
|
||||
|
||||
try {
|
||||
const result = await api('aws_test');
|
||||
if (result.success) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<span>Connection successful! Found ${result.zones_count} hosted zone(s).</span>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<span>${escapeHtml(result.error || 'Connection failed')}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<span>Connection failed: ${escapeHtml(error.message)}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Load PTR zones into dropdown
|
||||
async function loadPtrZones() {
|
||||
const select = document.getElementById('ptrZoneSelect');
|
||||
const notConfigured = document.getElementById('ptrNotConfigured');
|
||||
const configured = document.getElementById('ptrConfigured');
|
||||
|
||||
try {
|
||||
const result = await api('aws_zones');
|
||||
if (result.success && result.zones && result.zones.length > 0) {
|
||||
notConfigured.style.display = 'none';
|
||||
configured.style.display = 'block';
|
||||
|
||||
select.innerHTML = '<option value="">Select a zone...</option>';
|
||||
result.zones.forEach(zone => {
|
||||
const option = document.createElement('option');
|
||||
option.value = zone.id;
|
||||
option.textContent = zone.name + ' (' + zone.id + ')';
|
||||
select.appendChild(option);
|
||||
});
|
||||
} else {
|
||||
notConfigured.style.display = 'block';
|
||||
configured.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
notConfigured.style.display = 'block';
|
||||
configured.style.display = 'none';
|
||||
console.error('Failed to load PTR zones:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load A records from selected zone
|
||||
async function loadPtrRecords() {
|
||||
const zoneId = document.getElementById('ptrZoneSelect').value;
|
||||
const tbody = document.getElementById('ptrRecordsBody');
|
||||
const countSpan = document.getElementById('ptrRecordCount');
|
||||
const checkAllBtn = document.getElementById('checkAllPtrsBtn');
|
||||
|
||||
if (!zoneId) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
|
||||
Select a hosted zone to view A records
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
countSpan.textContent = '';
|
||||
checkAllBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; padding: 40px;">
|
||||
<div class="loading"><div class="spinner"></div></div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await api('aws_records', { zone_id: zoneId });
|
||||
if (result.success) {
|
||||
const records = result.records || [];
|
||||
countSpan.textContent = records.length + ' record(s)';
|
||||
checkAllBtn.disabled = records.length === 0;
|
||||
|
||||
if (records.length === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
|
||||
No A records found in this zone
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = records.map((record, index) => `
|
||||
<tr data-ip="${escapeHtml(record.ip)}" data-hostname="${escapeHtml(record.hostname)}">
|
||||
<td><span class="cell-truncate" title="${escapeHtml(record.hostname)}">${escapeHtml(record.hostname)}</span></td>
|
||||
<td><code style="font-size: 12px;">${escapeHtml(record.ip)}</code></td>
|
||||
<td style="color: var(--text-secondary);">${record.ttl || '-'}</td>
|
||||
<td class="ptr-value" style="color: var(--text-tertiary);">-</td>
|
||||
<td class="ptr-status">
|
||||
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--text-tertiary); color: white;">UNKNOWN</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-sm" onclick="checkPtrRecord('${escapeHtml(record.ip)}', '${escapeHtml(record.hostname)}', this)" title="Check PTR">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
Check
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
} else {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--error); padding: 40px;">
|
||||
${escapeHtml(result.error || 'Failed to load records')}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
countSpan.textContent = '';
|
||||
checkAllBtn.disabled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
tbody.innerHTML = `
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center; color: var(--error); padding: 40px;">
|
||||
Failed to load records: ${escapeHtml(error.message)}
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
countSpan.textContent = '';
|
||||
checkAllBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check single PTR record
|
||||
async function checkPtrRecord(ip, expectedHostname, button) {
|
||||
const row = button.closest('tr');
|
||||
const ptrValueCell = row.querySelector('.ptr-value');
|
||||
const ptrStatusCell = row.querySelector('.ptr-status');
|
||||
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div>';
|
||||
|
||||
try {
|
||||
const result = await api('ptr_lookup', { ip: ip });
|
||||
if (result.success) {
|
||||
const ptr = result.ptr || '';
|
||||
ptrValueCell.textContent = ptr || '(none)';
|
||||
ptrValueCell.style.color = ptr ? 'var(--text-primary)' : 'var(--text-tertiary)';
|
||||
|
||||
// Normalize hostnames for comparison (remove trailing dot, lowercase)
|
||||
const normalizedPtr = ptr.toLowerCase().replace(/\.$/, '');
|
||||
const normalizedExpected = expectedHostname.toLowerCase().replace(/\.$/, '');
|
||||
|
||||
let status, statusColor;
|
||||
if (!ptr) {
|
||||
status = 'MISSING';
|
||||
statusColor = 'var(--error)';
|
||||
} else if (normalizedPtr === normalizedExpected) {
|
||||
status = 'MATCH';
|
||||
statusColor = 'var(--success)';
|
||||
} else {
|
||||
status = 'MISMATCH';
|
||||
statusColor = 'var(--warning)';
|
||||
}
|
||||
|
||||
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${status}</span>`;
|
||||
} else {
|
||||
ptrValueCell.textContent = 'Error';
|
||||
ptrValueCell.style.color = 'var(--error)';
|
||||
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">ERROR</span>`;
|
||||
}
|
||||
} catch (error) {
|
||||
ptrValueCell.textContent = 'Error';
|
||||
ptrValueCell.style.color = 'var(--error)';
|
||||
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--error); color: white;">ERROR</span>`;
|
||||
}
|
||||
|
||||
button.disabled = false;
|
||||
button.innerHTML = `
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
Check
|
||||
`;
|
||||
}
|
||||
|
||||
// Check all PTR records
|
||||
async function checkAllPtrs() {
|
||||
const rows = document.querySelectorAll('#ptrRecordsBody tr[data-ip]');
|
||||
const checkAllBtn = document.getElementById('checkAllPtrsBtn');
|
||||
|
||||
if (rows.length === 0) {
|
||||
showToast('No records to check', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
checkAllBtn.disabled = true;
|
||||
checkAllBtn.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div> Checking...';
|
||||
|
||||
let checked = 0;
|
||||
for (const row of rows) {
|
||||
const ip = row.getAttribute('data-ip');
|
||||
const hostname = row.getAttribute('data-hostname');
|
||||
const button = row.querySelector('button');
|
||||
|
||||
await checkPtrRecord(ip, hostname, button);
|
||||
checked++;
|
||||
|
||||
// Small delay to avoid overwhelming the server
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
checkAllBtn.disabled = false;
|
||||
checkAllBtn.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
Check All PTRs
|
||||
`;
|
||||
|
||||
showToast(`Checked ${checked} PTR record(s)`, 'success');
|
||||
}
|
||||
|
||||
// Clear error logs
|
||||
async function clearErrorLogs() {
|
||||
if (!confirm('Are you sure you want to clear the error log? This action cannot be undone.')) {
|
||||
|
||||
Reference in New Issue
Block a user