This commit is contained in:
42
public/dashboard.php
Normal file
42
public/dashboard.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
session_start();
|
||||
if(!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true){
|
||||
header('location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the current domain of the hosted page
|
||||
$domain = $_SERVER['HTTP_HOST']; // This will automatically get the domain of the hosted page
|
||||
|
||||
// Construct the cURL command
|
||||
$curlCommand = "https://[USERNAME]:[PASSWORD]@$domain/update.php?hostname=[DOMAIN]&myip=[IP]";
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<h1>Welcome, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h1>
|
||||
<div class="flex gap-2 mt-4">
|
||||
<a href="manage_users.php" class="btn">Manage Users</a>
|
||||
<a href="manage_aws.php" class="btn">Manage AWS Credentials</a>
|
||||
<a href="manage_ddns.php" class="btn">Manage DDNS Entries</a>
|
||||
<a href="view_logs.php" class="btn">View All Logs</a>
|
||||
<a href="index.php?logout=true" class="btn btn-danger">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>DDNS Update cURL Command</h2>
|
||||
<p>Use the following cURL command to update your DDNS entry:</p>
|
||||
<pre style="background: rgba(0,0,0,0.3); padding: 1rem; border-radius: 0.5rem; overflow-x: auto;"><?php echo htmlspecialchars($curlCommand); ?></pre>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
145
public/index.php
Normal file
145
public/index.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
function handleFatalError()
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
die("A fatal error occurred. Please check your configuration.");
|
||||
}
|
||||
}
|
||||
register_shutdown_function('handleFatalError');
|
||||
|
||||
include '../dbconfig.php';
|
||||
|
||||
if ($link === null || $link->connect_error) {
|
||||
die("Database connection failed.");
|
||||
}
|
||||
|
||||
// Helper to check table existence
|
||||
function tableExists($link, $tableName)
|
||||
{
|
||||
$sql = "SHOW TABLES LIKE '$tableName'";
|
||||
$result = $link->query($sql);
|
||||
return $result->num_rows > 0;
|
||||
}
|
||||
|
||||
if (!tableExists($link, 'users')) {
|
||||
header('Location: setup.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// --- Keycloak SSO Logic ---
|
||||
|
||||
// 1. Handle Logout
|
||||
if (isset($_GET['logout'])) {
|
||||
session_destroy();
|
||||
$logoutUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/logout?redirect_uri=" . urlencode(KEYCLOAK_REDIRECT_URI);
|
||||
header("Location: $logoutUrl");
|
||||
exit;
|
||||
}
|
||||
|
||||
// 2. Handle OAuth Callback
|
||||
if (isset($_GET['code'])) {
|
||||
$code = $_GET['code'];
|
||||
$tokenUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/token";
|
||||
|
||||
$data = [
|
||||
'grant_type' => 'authorization_code',
|
||||
'client_id' => KEYCLOAK_CLIENT_ID,
|
||||
'client_secret' => KEYCLOAK_CLIENT_SECRET,
|
||||
'code' => $code,
|
||||
'redirect_uri' => KEYCLOAK_REDIRECT_URI
|
||||
];
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $tokenUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$response = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$tokenData = json_decode($response, true);
|
||||
|
||||
if (isset($tokenData['access_token'])) {
|
||||
// Get User Info
|
||||
$userInfoUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/userinfo";
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $userInfoUrl);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $tokenData['access_token']]);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$userInfoResponse = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
|
||||
$userInfo = json_decode($userInfoResponse, true);
|
||||
$email = $userInfo['email'] ?? $userInfo['preferred_username'] ?? null;
|
||||
|
||||
if ($email) {
|
||||
// Check if user exists in local DB
|
||||
$stmt = $link->prepare("SELECT id, username FROM users WHERE username = ?");
|
||||
$stmt->bind_param("s", $email);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 1) {
|
||||
$user = $result->fetch_assoc();
|
||||
$_SESSION['loggedin'] = true;
|
||||
$_SESSION['id'] = $user['id'];
|
||||
$_SESSION['username'] = $user['username'];
|
||||
header("Location: dashboard.php");
|
||||
exit;
|
||||
} else {
|
||||
$error = "Access Denied: User not found in local database.";
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error = "Could not retrieve email from identity provider.";
|
||||
}
|
||||
} else {
|
||||
$error = "Failed to authenticate with SSO.";
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate Login URL
|
||||
$loginUrl = KEYCLOAK_BASE_URL . "/realms/" . KEYCLOAK_REALM . "/protocol/openid-connect/auth" .
|
||||
"?client_id=" . KEYCLOAK_CLIENT_ID .
|
||||
"&response_type=code" .
|
||||
"&redirect_uri=" . urlencode(KEYCLOAK_REDIRECT_URI) .
|
||||
"&scope=openid email profile";
|
||||
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - DDNS Manager</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="card login-card text-center">
|
||||
<h1>DDNS Manager</h1>
|
||||
<p class="mb-4" style="color: #94a3b8;">Secure Access Control</p>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-error">
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo htmlspecialchars($loginUrl); ?>" class="btn"
|
||||
style="width: 100%; box-sizing: border-box;">
|
||||
Login with SSO
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
126
public/manage_aws.php
Normal file
126
public/manage_aws.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../dbconfig.php';
|
||||
|
||||
// Handle form submission to update AWS credentials
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$region = $_POST['region'];
|
||||
$access_key_id = $_POST['access_key_id'];
|
||||
$secret_access_key = $_POST['secret_access_key'];
|
||||
$hosted_zone_id = $_POST['hosted_zone_id'];
|
||||
$approved_fqdn = $_POST['approved_fqdn'];
|
||||
|
||||
// Check if there's already data in the table
|
||||
$check_sql = "SELECT id FROM aws_credentials LIMIT 1";
|
||||
$check_result = $link->query($check_sql);
|
||||
|
||||
if ($check_result->num_rows > 0) {
|
||||
// Update existing record
|
||||
$sql = "UPDATE aws_credentials SET region = ?, access_key_id = ?, secret_access_key = ?, hosted_zone_id = ?, approved_fqdn = ? WHERE id = 1";
|
||||
} else {
|
||||
// Insert new record
|
||||
$sql = "INSERT INTO aws_credentials (region, access_key_id, secret_access_key, hosted_zone_id, approved_fqdn) VALUES (?, ?, ?, ?, ?)";
|
||||
}
|
||||
|
||||
if ($stmt = $link->prepare($sql)) {
|
||||
$stmt->bind_param("sssss", $region, $access_key_id, $secret_access_key, $hosted_zone_id, $approved_fqdn);
|
||||
if ($stmt->execute()) {
|
||||
$success = "AWS credentials updated successfully!";
|
||||
} else {
|
||||
$error = "Error updating AWS credentials: " . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
$error = "Error preparing SQL statement: " . $link->error;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch current AWS credentials from the database
|
||||
$sql = "SELECT region, access_key_id, secret_access_key, hosted_zone_id, approved_fqdn FROM aws_credentials LIMIT 1";
|
||||
$current_credentials = [];
|
||||
if ($result = $link->query($sql)) {
|
||||
if ($result->num_rows > 0) {
|
||||
$current_credentials = $result->fetch_assoc();
|
||||
}
|
||||
$result->free();
|
||||
}
|
||||
|
||||
$link->close();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage AWS Credentials</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Manage AWS Credentials</h1>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($success)): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<?php if (!empty($current_credentials)): ?>
|
||||
<h2>Current AWS Credentials</h2>
|
||||
<ul>
|
||||
<li><strong>Region:</strong> <?php echo htmlspecialchars($current_credentials['region']); ?></li>
|
||||
<li><strong>Access Key ID:</strong>
|
||||
<?php echo htmlspecialchars($current_credentials['access_key_id']); ?></li>
|
||||
<li><strong>Secret Access Key:</strong>
|
||||
<?php echo htmlspecialchars($current_credentials['secret_access_key']); ?></li>
|
||||
<li><strong>Hosted Zone ID:</strong>
|
||||
<?php echo htmlspecialchars($current_credentials['hosted_zone_id']); ?></li>
|
||||
<li><strong>Approved FQDN:</strong>
|
||||
<?php echo htmlspecialchars($current_credentials['approved_fqdn']); ?></li>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p>No AWS credentials found in the database.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Update AWS Credentials</h2>
|
||||
<form method="post">
|
||||
<label>Region:</label>
|
||||
<input type="text" name="region"
|
||||
value="<?php echo htmlspecialchars($current_credentials['region'] ?? ''); ?>" required>
|
||||
|
||||
<label>Access Key ID:</label>
|
||||
<input type="text" name="access_key_id"
|
||||
value="<?php echo htmlspecialchars($current_credentials['access_key_id'] ?? ''); ?>" required>
|
||||
|
||||
<label>Secret Access Key:</label>
|
||||
<input type="text" name="secret_access_key"
|
||||
value="<?php echo htmlspecialchars($current_credentials['secret_access_key'] ?? ''); ?>" required>
|
||||
|
||||
<label>Hosted Zone ID:</label>
|
||||
<input type="text" name="hosted_zone_id"
|
||||
value="<?php echo htmlspecialchars($current_credentials['hosted_zone_id'] ?? ''); ?>" required>
|
||||
|
||||
<label>Approved FQDN:</label>
|
||||
<input type="text" name="approved_fqdn"
|
||||
value="<?php echo htmlspecialchars($current_credentials['approved_fqdn'] ?? ''); ?>" required>
|
||||
|
||||
<input type="submit" value="Update Credentials">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
397
public/manage_ddns.php
Normal file
397
public/manage_ddns.php
Normal file
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../dbconfig.php';
|
||||
require '../vendor/aws.phar';
|
||||
|
||||
use Aws\Route53\Route53Client;
|
||||
use Aws\Exception\AwsException;
|
||||
|
||||
// Clean up logs older than 30 days
|
||||
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||
$cleanup_stmt->execute();
|
||||
$cleanup_stmt->close();
|
||||
}
|
||||
|
||||
// Fetch the approved FQDN from the database
|
||||
$approved_fqdn = '';
|
||||
$aws_sql = "SELECT approved_fqdn, region, access_key_id, secret_access_key, hosted_zone_id FROM aws_credentials LIMIT 1";
|
||||
if ($aws_result = $link->query($aws_sql)) {
|
||||
if ($aws_result->num_rows > 0) {
|
||||
$row = $aws_result->fetch_assoc();
|
||||
$approved_fqdn = $row['approved_fqdn'];
|
||||
$region = $row['region'];
|
||||
$access_key_id = $row['access_key_id'];
|
||||
$secret_access_key = $row['secret_access_key'];
|
||||
$hosted_zone_id = $row['hosted_zone_id'];
|
||||
} else {
|
||||
die("No AWS credentials found in the database.");
|
||||
}
|
||||
$aws_result->free();
|
||||
} else {
|
||||
die("Error fetching AWS credentials: " . $link->error);
|
||||
}
|
||||
|
||||
// Initialize the Route53 client
|
||||
try {
|
||||
$route53 = new Route53Client([
|
||||
'version' => 'latest',
|
||||
'region' => $region,
|
||||
'credentials' => [
|
||||
'key' => $access_key_id,
|
||||
'secret' => $secret_access_key,
|
||||
],
|
||||
]);
|
||||
} catch (AwsException $e) {
|
||||
die("Error initializing Route53 client: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Handle form submission to add a new DDNS entry
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['add_ddns'])) {
|
||||
$ddns_fqdn = $_POST['ddns_fqdn'];
|
||||
$ddns_password = $_POST['ddns_password'];
|
||||
$initial_ip = $_POST['initial_ip'];
|
||||
$ttl = $_POST['ttl'];
|
||||
|
||||
// Validate input
|
||||
if (empty($ddns_fqdn) || empty($ddns_password) || empty($initial_ip) || empty($ttl)) {
|
||||
$error = "DDNS FQDN, password, initial IP, and TTL are required.";
|
||||
} elseif (!filter_var($initial_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$error = "Invalid IPv4 address.";
|
||||
} else {
|
||||
// Check if the DDNS FQDN is a subdomain of the approved FQDN
|
||||
if (strpos($ddns_fqdn, $approved_fqdn) === false || !preg_match('/^[a-zA-Z0-9-]+\.' . preg_quote($approved_fqdn, '/') . '$/', $ddns_fqdn)) {
|
||||
$error = "DDNS FQDN must be a subdomain of $approved_fqdn.";
|
||||
} else {
|
||||
// Check if the DDNS entry already exists
|
||||
$check_sql = "SELECT id FROM ddns_entries WHERE ddns_fqdn = ?";
|
||||
if ($check_stmt = $link->prepare($check_sql)) {
|
||||
$check_stmt->bind_param("s", $ddns_fqdn);
|
||||
$check_stmt->execute();
|
||||
$check_stmt->store_result();
|
||||
|
||||
if ($check_stmt->num_rows > 0) {
|
||||
$error = "DDNS entry with this FQDN already exists.";
|
||||
} else {
|
||||
// Prepare the DNS record
|
||||
$changeBatch = [
|
||||
'Changes' => [
|
||||
[
|
||||
'Action' => 'UPSERT',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $ddns_fqdn . '.',
|
||||
'Type' => 'A',
|
||||
'TTL' => (int)$ttl,
|
||||
'ResourceRecords' => [
|
||||
[
|
||||
'Value' => $initial_ip,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
// Create the DNS record in Route53
|
||||
$result = $route53->changeResourceRecordSets([
|
||||
'HostedZoneId' => $hosted_zone_id,
|
||||
'ChangeBatch' => $changeBatch,
|
||||
]);
|
||||
|
||||
// Insert the new DDNS entry into the database
|
||||
$insert_sql = "INSERT INTO ddns_entries (ddns_fqdn, ddns_password, last_ipv4, ttl) VALUES (?, ?, ?, ?)";
|
||||
if ($insert_stmt = $link->prepare($insert_sql)) {
|
||||
$insert_stmt->bind_param("sssi", $ddns_fqdn, $ddns_password, $initial_ip, $ttl);
|
||||
if ($insert_stmt->execute()) {
|
||||
$ddns_entry_id = $insert_stmt->insert_id;
|
||||
|
||||
// Log the action
|
||||
$action = 'add';
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
$details = "Added DDNS entry with FQDN: $ddns_fqdn, Initial IP: $initial_ip, TTL: $ttl";
|
||||
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||
if ($log_stmt = $link->prepare($log_sql)) {
|
||||
$log_stmt->bind_param("isss", $ddns_entry_id, $action, $ip_address, $details);
|
||||
$log_stmt->execute();
|
||||
$log_stmt->close();
|
||||
}
|
||||
|
||||
$success = "DDNS entry '$ddns_fqdn' added successfully!";
|
||||
} else {
|
||||
$error = "Error adding DDNS entry: " . $insert_stmt->error;
|
||||
}
|
||||
$insert_stmt->close();
|
||||
}
|
||||
} catch (AwsException $e) {
|
||||
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||
}
|
||||
}
|
||||
$check_stmt->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle IP and TTL update for a DDNS entry
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['update_ip'])) {
|
||||
$ddns_id = $_POST['ddns_id'];
|
||||
$new_ip = $_POST['new_ip'];
|
||||
$new_ttl = $_POST['new_ttl'];
|
||||
|
||||
// Validate input
|
||||
if (empty($new_ip) || empty($new_ttl)) {
|
||||
$error = "IP and TTL are required.";
|
||||
} elseif (!filter_var($new_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
$error = "Invalid IPv4 address.";
|
||||
} else {
|
||||
// Fetch the DDNS entry
|
||||
$fetch_sql = "SELECT ddns_fqdn FROM ddns_entries WHERE id = ?";
|
||||
if ($fetch_stmt = $link->prepare($fetch_sql)) {
|
||||
$fetch_stmt->bind_param("i", $ddns_id);
|
||||
$fetch_stmt->execute();
|
||||
$fetch_stmt->store_result();
|
||||
$fetch_stmt->bind_result($ddns_fqdn);
|
||||
$fetch_stmt->fetch();
|
||||
$fetch_stmt->close();
|
||||
|
||||
// Prepare the DNS record update
|
||||
$changeBatch = [
|
||||
'Changes' => [
|
||||
[
|
||||
'Action' => 'UPSERT',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $ddns_fqdn . '.',
|
||||
'Type' => 'A',
|
||||
'TTL' => (int)$new_ttl,
|
||||
'ResourceRecords' => [
|
||||
[
|
||||
'Value' => $new_ip,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
// Update the DNS record in Route53
|
||||
$result = $route53->changeResourceRecordSets([
|
||||
'HostedZoneId' => $hosted_zone_id,
|
||||
'ChangeBatch' => $changeBatch,
|
||||
]);
|
||||
|
||||
// Update the IP and TTL in the database
|
||||
$update_sql = "UPDATE ddns_entries SET last_ipv4 = ?, ttl = ?, last_update = NOW() WHERE id = ?";
|
||||
if ($update_stmt = $link->prepare($update_sql)) {
|
||||
$update_stmt->bind_param("sii", $new_ip, $new_ttl, $ddns_id);
|
||||
if ($update_stmt->execute()) {
|
||||
// Log the action
|
||||
$action = 'update';
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
$details = "Updated IP: $new_ip, TTL: $new_ttl";
|
||||
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||
if ($log_stmt = $link->prepare($log_sql)) {
|
||||
$log_stmt->bind_param("isss", $ddns_id, $action, $ip_address, $details);
|
||||
$log_stmt->execute();
|
||||
$log_stmt->close();
|
||||
}
|
||||
|
||||
$success = "IP and TTL updated successfully for '$ddns_fqdn'!";
|
||||
} else {
|
||||
$error = "Error updating IP and TTL: " . $update_stmt->error;
|
||||
}
|
||||
$update_stmt->close();
|
||||
}
|
||||
} catch (AwsException $e) {
|
||||
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DDNS entry deletion
|
||||
if (isset($_GET['delete'])) {
|
||||
$ddns_id = $_GET['delete'];
|
||||
|
||||
// Fetch the DDNS entry to get the FQDN and last IP
|
||||
$fetch_sql = "SELECT ddns_fqdn, last_ipv4, ttl FROM ddns_entries WHERE id = ?";
|
||||
if ($fetch_stmt = $link->prepare($fetch_sql)) {
|
||||
$fetch_stmt->bind_param("i", $ddns_id);
|
||||
$fetch_stmt->execute();
|
||||
$fetch_stmt->store_result();
|
||||
$fetch_stmt->bind_result($ddns_fqdn, $last_ipv4, $ttl);
|
||||
$fetch_stmt->fetch();
|
||||
$fetch_stmt->close();
|
||||
|
||||
// Prepare the DNS record deletion
|
||||
$changeBatch = [
|
||||
'Changes' => [
|
||||
[
|
||||
'Action' => 'DELETE',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $ddns_fqdn . '.',
|
||||
'Type' => 'A',
|
||||
'TTL' => (int)$ttl,
|
||||
'ResourceRecords' => [
|
||||
[
|
||||
'Value' => $last_ipv4,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
// Delete the DNS record in Route53
|
||||
$result = $route53->changeResourceRecordSets([
|
||||
'HostedZoneId' => $hosted_zone_id,
|
||||
'ChangeBatch' => $changeBatch,
|
||||
]);
|
||||
|
||||
// Delete the DDNS entry from the database
|
||||
$delete_sql = "DELETE FROM ddns_entries WHERE id = ?";
|
||||
if ($delete_stmt = $link->prepare($delete_sql)) {
|
||||
$delete_stmt->bind_param("i", $ddns_id);
|
||||
if ($delete_stmt->execute()) {
|
||||
$success = "DDNS entry deleted successfully and Route53 record removed!";
|
||||
} else {
|
||||
$error = "Error deleting DDNS entry: " . $delete_stmt->error;
|
||||
}
|
||||
$delete_stmt->close();
|
||||
}
|
||||
} catch (AwsException $e) {
|
||||
$error = "Error updating Route53: " . $e->getAwsErrorMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all DDNS entries from the database
|
||||
$sql = "SELECT id, ddns_fqdn, ddns_password, last_ipv4, ttl, last_update FROM ddns_entries";
|
||||
$ddns_entries = [];
|
||||
if ($result = $link->query($sql)) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$ddns_entries[] = $row;
|
||||
}
|
||||
$result->free();
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage DDNS Entries</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="table_sort.js"></script>
|
||||
<style>
|
||||
th.sortable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
th.sortable:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
th.sortable::after {
|
||||
content: '↕';
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
th.sortable.asc::after {
|
||||
content: '↑';
|
||||
opacity: 1;
|
||||
}
|
||||
th.sortable.desc::after {
|
||||
content: '↓';
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Manage DDNS Entries</h1>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($success)): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<h2>Add New DDNS Entry</h2>
|
||||
<form method="post">
|
||||
<label>DDNS FQDN:</label>
|
||||
<input type="text" name="ddns_fqdn" required placeholder="subdomain.<?php echo htmlspecialchars($approved_fqdn); ?>">
|
||||
|
||||
<label>DDNS Password:</label>
|
||||
<input type="password" name="ddns_password" required>
|
||||
|
||||
<label>Initial IP:</label>
|
||||
<input type="text" name="initial_ip" required value="<?php echo $_SERVER['REMOTE_ADDR']; ?>">
|
||||
|
||||
<label>TTL (Time to Live):</label>
|
||||
<input type="number" name="ttl" min="1" required value="300">
|
||||
|
||||
<input type="submit" name="add_ddns" value="Add DDNS Entry">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>DDNS Entries</h2>
|
||||
<div class="table-responsive">
|
||||
<table id="ddnsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-type="string">FQDN</th>
|
||||
<th class="sortable" data-type="string">Password</th>
|
||||
<th class="sortable" data-type="string">Last IPv4</th>
|
||||
<th class="sortable" data-type="number">TTL</th>
|
||||
<th class="sortable" data-type="string">Last Update</th>
|
||||
<th>Update IP/TTL</th>
|
||||
<th>Logs</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($ddns_entries as $entry): ?>
|
||||
<tr>
|
||||
<td><?php echo htmlspecialchars($entry['ddns_fqdn']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['ddns_password']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['last_ipv4']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['ttl']); ?></td>
|
||||
<td><?php echo htmlspecialchars($entry['last_update']); ?></td>
|
||||
<td>
|
||||
<form method="post" style="display:inline; max-width: none;">
|
||||
<input type="hidden" name="ddns_id" value="<?php echo $entry['id']; ?>">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="new_ip" placeholder="New IP" required style="width: 120px;">
|
||||
<input type="number" name="new_ttl" placeholder="TTL" min="1" required style="width: 80px;">
|
||||
<input type="submit" name="update_ip" value="Update" style="padding: 0.5rem;">
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<a href="view_logs.php?ddns_id=<?php echo $entry['id']; ?>" class="btn" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Logs</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="manage_ddns.php?delete=<?php echo $entry['id']; ?>" onclick="return confirm('Are you sure you want to delete this DDNS entry?');" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Delete</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
158
public/manage_users.php
Normal file
158
public/manage_users.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../dbconfig.php';
|
||||
|
||||
// Fetch the logged-in user's ID and username
|
||||
$logged_in_user_id = $_SESSION['id'];
|
||||
$logged_in_username = $_SESSION['username'];
|
||||
|
||||
// Handle form submission to add a new user (admin-only feature)
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['add_user'])) {
|
||||
$username = $_POST['username'];
|
||||
// Password is now optional for SSO users
|
||||
$password = !empty($_POST['password']) ? $_POST['password'] : null;
|
||||
|
||||
// Validate input
|
||||
if (empty($username)) {
|
||||
$error = "Username (Email) is required.";
|
||||
} else {
|
||||
// Check if the username is a valid email address
|
||||
if (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||
$error = "Invalid email address.";
|
||||
} else {
|
||||
// Check if the user already exists
|
||||
$check_sql = "SELECT id FROM users WHERE username = ?";
|
||||
if ($check_stmt = $link->prepare($check_sql)) {
|
||||
$check_stmt->bind_param("s", $username);
|
||||
$check_stmt->execute();
|
||||
$check_stmt->store_result();
|
||||
|
||||
if ($check_stmt->num_rows > 0) {
|
||||
$error = "User with this email already exists.";
|
||||
} else {
|
||||
// Insert the new user
|
||||
$password_hash = $password ? password_hash($password, PASSWORD_DEFAULT) : null;
|
||||
$insert_sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||
if ($insert_stmt = $link->prepare($insert_sql)) {
|
||||
$insert_stmt->bind_param("ss", $username, $password_hash);
|
||||
if ($insert_stmt->execute()) {
|
||||
$success = "User '$username' added successfully!";
|
||||
} else {
|
||||
$error = "Error adding user: " . $insert_stmt->error;
|
||||
}
|
||||
$insert_stmt->close();
|
||||
}
|
||||
}
|
||||
$check_stmt->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle user deletion (admin-only feature)
|
||||
if (isset($_GET['delete'])) {
|
||||
$user_id = $_GET['delete'];
|
||||
|
||||
// Prevent the logged-in user from deleting themselves
|
||||
if ($user_id == $logged_in_user_id) {
|
||||
$error = "You cannot delete your own account.";
|
||||
} else {
|
||||
$sql = "DELETE FROM users WHERE id = ?";
|
||||
if ($stmt = $link->prepare($sql)) {
|
||||
$stmt->bind_param("i", $user_id);
|
||||
if ($stmt->execute()) {
|
||||
$success = "User deleted successfully!";
|
||||
} else {
|
||||
$error = "Error deleting user: " . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all users from the database
|
||||
$sql = "SELECT id, username, password_hash FROM users";
|
||||
$users = [];
|
||||
if ($result = $link->query($sql)) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$users[] = $row;
|
||||
}
|
||||
$result->free();
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manage Users</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Manage Users</h1>
|
||||
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-error"><?php echo htmlspecialchars($error); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (isset($success)): ?>
|
||||
<div class="alert alert-success"><?php echo htmlspecialchars($success); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<h2>Add New User</h2>
|
||||
<p>Add a user to allow them to login via Keycloak SSO. Password is optional and only needed if you plan to support local login (legacy).</p>
|
||||
<form method="post">
|
||||
<label>Email address:</label>
|
||||
<input type="email" name="username" required placeholder="user@example.com">
|
||||
|
||||
<label>Password (Optional):</label>
|
||||
<input type="password" name="password" placeholder="Leave empty for SSO-only users">
|
||||
|
||||
<input type="submit" name="add_user" value="Add User">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>User List</h2>
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email address</th>
|
||||
<th>Auth Type</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<tr>
|
||||
<td><?php echo $user['id']; ?></td>
|
||||
<td><?php echo htmlspecialchars($user['username']); ?></td>
|
||||
<td>
|
||||
<?php echo $user['password_hash'] ? 'Password + SSO' : 'SSO Only'; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($user['id'] != $logged_in_user_id): ?>
|
||||
<a href="manage_users.php?delete=<?php echo $user['id']; ?>" onclick="return confirm('Are you sure you want to delete this user?');" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.875rem;">Delete</a>
|
||||
<?php else: ?>
|
||||
<em>Current User</em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><a href="dashboard.php">Back to Dashboard</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
141
public/setup.php
Normal file
141
public/setup.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
function handleFatalError()
|
||||
{
|
||||
$error = error_get_last();
|
||||
if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
|
||||
die("A fatal error occurred. Please check your configuration and try again.");
|
||||
}
|
||||
}
|
||||
|
||||
register_shutdown_function('handleFatalError');
|
||||
|
||||
if (!file_exists('../dbconfig.php')) {
|
||||
die("The database configuration file (dbconfig.php) is missing. Please create it with the correct database credentials.");
|
||||
}
|
||||
|
||||
include '../dbconfig.php';
|
||||
|
||||
if ($link === null || $link->connect_error) {
|
||||
die("Database connection failed. Please check the dbconfig.php file and ensure the database credentials are correct.");
|
||||
}
|
||||
|
||||
$tables = [];
|
||||
$result = $link->query("SHOW TABLES");
|
||||
if ($result) {
|
||||
while ($row = $result->fetch_row()) {
|
||||
$tables[] = $row[0];
|
||||
}
|
||||
$result->free();
|
||||
}
|
||||
|
||||
if (!empty($tables)) {
|
||||
die("An installation already exists in this database. Please clean up the database or update the dbconfig.php file to use a new database.");
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['create_admin'])) {
|
||||
$username = $_POST['username'];
|
||||
$password = $_POST['password'];
|
||||
|
||||
if (empty($username) || empty($password)) {
|
||||
echo "Username and password are required.";
|
||||
} elseif (!filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||
echo "Username must be a valid email address.";
|
||||
} else {
|
||||
$password_hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$create_tables_sql = [
|
||||
"CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NULL
|
||||
)",
|
||||
"CREATE TABLE aws_credentials (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
region VARCHAR(50) NOT NULL,
|
||||
access_key_id VARCHAR(255) NOT NULL,
|
||||
secret_access_key VARCHAR(255) NOT NULL,
|
||||
hosted_zone_id VARCHAR(255) NOT NULL,
|
||||
approved_fqdn VARCHAR(255) NOT NULL
|
||||
)",
|
||||
"CREATE TABLE ddns_entries (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ddns_fqdn VARCHAR(255) NOT NULL UNIQUE,
|
||||
ddns_password VARCHAR(255) NOT NULL,
|
||||
last_ipv4 VARCHAR(15),
|
||||
ttl INT NOT NULL DEFAULT 300,
|
||||
last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
)",
|
||||
"CREATE TABLE ddns_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ddns_entry_id INT NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
ip_address VARCHAR(15),
|
||||
details TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (ddns_entry_id) REFERENCES ddns_entries(id) ON DELETE CASCADE
|
||||
)",
|
||||
"CREATE TABLE recaptcha_keys (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
site_key VARCHAR(255) NOT NULL,
|
||||
secret_key VARCHAR(255) NOT NULL
|
||||
)",
|
||||
"CREATE PROCEDURE CleanupOldLogs()
|
||||
BEGIN
|
||||
DELETE FROM ddns_logs WHERE timestamp < NOW() - INTERVAL 30 DAY;
|
||||
END"
|
||||
];
|
||||
|
||||
$success = true;
|
||||
foreach ($create_tables_sql as $sql) {
|
||||
if (!$link->query($sql)) {
|
||||
$success = false;
|
||||
echo "Error creating table or procedure: " . $link->error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
$insert_sql = "INSERT INTO users (username, password_hash) VALUES (?, ?)";
|
||||
if ($stmt = $link->prepare($insert_sql)) {
|
||||
$stmt->bind_param("ss", $username, $password_hash);
|
||||
if ($stmt->execute()) {
|
||||
echo "First admin user created successfully! You can now log in.";
|
||||
echo '<p><a href="index.php">Go to Login Page</a></p>';
|
||||
exit;
|
||||
} else {
|
||||
echo "Error creating admin user: " . $stmt->error;
|
||||
}
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Setup</title>
|
||||
<title>Setup</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Setup</h1>
|
||||
<p>Welcome to the setup wizard. This script will help you prepare a new installation.</p>
|
||||
|
||||
<?php if (empty($tables)): ?>
|
||||
<h2>Create First Admin User</h2>
|
||||
<form method="post">
|
||||
<label>Email address:</label>
|
||||
<input type="email" name="username" required><br>
|
||||
<label>Password:</label>
|
||||
<input type="password" name="password" required><br>
|
||||
<input type="submit" name="create_admin" value="Create Admin User">
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
244
public/style.css
Normal file
244
public/style.css
Normal file
@@ -0,0 +1,244 @@
|
||||
/* Modern Dark Theme with Glassmorphism */
|
||||
:root {
|
||||
--bg-color: #0f172a;
|
||||
--text-color: #e2e8f0;
|
||||
--primary-color: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--secondary-color: #10b981;
|
||||
--surface-color: rgba(30, 41, 59, 0.7);
|
||||
--border-color: rgba(148, 163, 184, 0.1);
|
||||
--error-color: #ef4444;
|
||||
--glass-bg: rgba(30, 41, 59, 0.6);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
background-image:
|
||||
radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, rgba(16, 185, 129, 0.15) 0px, transparent 50%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #f8fafc;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Cards / Glassmorphism */
|
||||
.card {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
select,
|
||||
textarea {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button,
|
||||
input[type="submit"],
|
||||
.btn {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input[type="submit"]:hover,
|
||||
.btn:hover {
|
||||
background-color: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button:active,
|
||||
input[type="submit"]:active,
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--error-color);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 1rem;
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(30, 41, 59, 0.8);
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.2);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
/* Login Page Specific */
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.gap-2 {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Stack buttons on very small screens if needed, or keep wrapped */
|
||||
.flex > .btn {
|
||||
flex: 1 1 auto; /* Allow buttons to grow and fill width */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Table Wrapper class to be added in PHP */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Ensure table doesn't force width */
|
||||
table {
|
||||
min-width: 600px; /* Force scroll if content is squished */
|
||||
}
|
||||
}
|
||||
48
public/table_sort.js
Normal file
48
public/table_sort.js
Normal file
@@ -0,0 +1,48 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const table = document.getElementById('ddnsTable');
|
||||
if (!table) return;
|
||||
|
||||
const headers = table.querySelectorAll('th.sortable');
|
||||
const tbody = table.querySelector('tbody');
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
header.addEventListener('click', () => {
|
||||
const type = header.dataset.type || 'string';
|
||||
const isAscending = header.classList.contains('asc');
|
||||
|
||||
// Reset all headers
|
||||
headers.forEach(h => h.classList.remove('asc', 'desc'));
|
||||
|
||||
// Toggle sort order
|
||||
if (!isAscending) {
|
||||
header.classList.add('asc');
|
||||
} else {
|
||||
header.classList.add('desc');
|
||||
}
|
||||
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
const sortedRows = rows.sort((a, b) => {
|
||||
// Find the cell corresponding to the header index
|
||||
// Note: We need to account for the fact that headers might not match columns 1:1 if there are colspans,
|
||||
// but here it seems straightforward. However, we should find the index of the header among all ths in the thead
|
||||
// to match the td index.
|
||||
const allHeaders = Array.from(header.parentElement.children);
|
||||
const colIndex = allHeaders.indexOf(header);
|
||||
|
||||
const aText = a.children[colIndex].textContent.trim();
|
||||
const bText = b.children[colIndex].textContent.trim();
|
||||
|
||||
if (type === 'number') {
|
||||
return isAscending ? bText - aText : aText - bText;
|
||||
} else {
|
||||
return isAscending ? bText.localeCompare(aText) : aText.localeCompare(bText);
|
||||
}
|
||||
});
|
||||
|
||||
// Re-append rows
|
||||
tbody.innerHTML = '';
|
||||
sortedRows.forEach(row => tbody.appendChild(row));
|
||||
});
|
||||
});
|
||||
});
|
||||
120
public/update.php
Normal file
120
public/update.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
include '../dbconfig.php';
|
||||
require '../vendor/aws.phar';
|
||||
|
||||
use Aws\Route53\Route53Client;
|
||||
use Aws\Exception\AwsException;
|
||||
|
||||
// Clean up logs older than 30 days
|
||||
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||
$cleanup_stmt->execute();
|
||||
$cleanup_stmt->close();
|
||||
}
|
||||
|
||||
// Extract the hostname and IP from the request
|
||||
$ddns_fqdn = $_GET['hostname'];
|
||||
$myip = $_GET['myip'];
|
||||
$ddns_password = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
// Validate the request
|
||||
if (empty($ddns_fqdn) || empty($myip) || empty($ddns_password)) {
|
||||
die("badauth");
|
||||
}
|
||||
|
||||
// Fetch the DDNS entry from the database
|
||||
$sql = "SELECT id, ddns_fqdn, ddns_password, last_ipv4, ttl FROM ddns_entries WHERE ddns_fqdn = ? AND ddns_password = ?";
|
||||
if ($stmt = $link->prepare($sql)) {
|
||||
$stmt->bind_param("ss", $ddns_fqdn, $ddns_password);
|
||||
$stmt->execute();
|
||||
$stmt->store_result();
|
||||
|
||||
if ($stmt->num_rows == 1) {
|
||||
$stmt->bind_result($id, $ddns_fqdn, $ddns_password, $last_ipv4, $ttl);
|
||||
$stmt->fetch();
|
||||
|
||||
// Check if the IP has changed
|
||||
if ($last_ipv4 !== $myip) {
|
||||
// Fetch AWS credentials from the database
|
||||
$aws_sql = "SELECT region, access_key_id, secret_access_key, hosted_zone_id FROM aws_credentials LIMIT 1";
|
||||
if ($aws_stmt = $link->prepare($aws_sql)) {
|
||||
$aws_stmt->execute();
|
||||
$aws_stmt->store_result();
|
||||
$aws_stmt->bind_result($region, $access_key_id, $secret_access_key, $hosted_zone_id);
|
||||
$aws_stmt->fetch();
|
||||
$aws_stmt->close();
|
||||
|
||||
// Initialize the Route53 client
|
||||
$route53 = new Route53Client([
|
||||
'version' => 'latest',
|
||||
'region' => $region,
|
||||
'credentials' => [
|
||||
'key' => $access_key_id,
|
||||
'secret' => $secret_access_key,
|
||||
],
|
||||
]);
|
||||
|
||||
// Prepare the DNS record update
|
||||
$changeBatch = [
|
||||
'Changes' => [
|
||||
[
|
||||
'Action' => 'UPSERT',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $ddns_fqdn,
|
||||
'Type' => 'A',
|
||||
'TTL' => $ttl,
|
||||
'ResourceRecords' => [
|
||||
[
|
||||
'Value' => $myip,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
// Update the DNS record in Route53
|
||||
$result = $route53->changeResourceRecordSets([
|
||||
'HostedZoneId' => $hosted_zone_id,
|
||||
'ChangeBatch' => $changeBatch,
|
||||
]);
|
||||
|
||||
// Update the database with the new IP
|
||||
$update_sql = "UPDATE ddns_entries SET last_ipv4 = ?, last_update = NOW() WHERE id = ?";
|
||||
if ($update_stmt = $link->prepare($update_sql)) {
|
||||
$update_stmt->bind_param("si", $myip, $id);
|
||||
$update_stmt->execute();
|
||||
$update_stmt->close();
|
||||
}
|
||||
|
||||
// Log the action
|
||||
$action = 'update';
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
$details = "Updated IP: $myip";
|
||||
$log_sql = "INSERT INTO ddns_logs (ddns_entry_id, action, ip_address, details) VALUES (?, ?, ?, ?)";
|
||||
if ($log_stmt = $link->prepare($log_sql)) {
|
||||
$log_stmt->bind_param("isss", $id, $action, $ip_address, $details);
|
||||
$log_stmt->execute();
|
||||
$log_stmt->close();
|
||||
}
|
||||
|
||||
echo "good"; // Success
|
||||
} catch (AwsException $e) {
|
||||
echo "dnserror"; // DNS update failed
|
||||
}
|
||||
} else {
|
||||
echo "badauth"; // AWS credentials not found
|
||||
}
|
||||
} else {
|
||||
echo "nochg"; // IP hasn't changed
|
||||
}
|
||||
} else {
|
||||
echo "badauth"; // Invalid DDNS credentials
|
||||
}
|
||||
$stmt->close();
|
||||
} else {
|
||||
echo "badauth"; // Database error
|
||||
}
|
||||
$link->close();
|
||||
?>
|
||||
141
public/view_logs.php
Normal file
141
public/view_logs.php
Normal file
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
|
||||
header('location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
include '../dbconfig.php';
|
||||
|
||||
// Clean up logs older than 30 days
|
||||
$cleanup_sql = "CALL CleanupOldLogs()";
|
||||
if ($cleanup_stmt = $link->prepare($cleanup_sql)) {
|
||||
$cleanup_stmt->execute();
|
||||
$cleanup_stmt->close();
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$ddns_id = isset($_GET['ddns_id']) ? (int) $_GET['ddns_id'] : null;
|
||||
$where_clause = "";
|
||||
$params = [];
|
||||
$types = "";
|
||||
|
||||
// Build WHERE clause if ddns_id is specified
|
||||
if ($ddns_id !== null) {
|
||||
$where_clause = " WHERE l.ddns_entry_id = ?";
|
||||
$params[] = $ddns_id;
|
||||
$types = "i";
|
||||
}
|
||||
|
||||
// Pagination setup
|
||||
$per_page = 20;
|
||||
$page = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1;
|
||||
$offset = ($page - 1) * $per_page;
|
||||
|
||||
// Main query with conditional filtering
|
||||
$query = "SELECT l.*, d.ddns_fqdn
|
||||
FROM ddns_logs l
|
||||
LEFT JOIN ddns_entries d ON l.ddns_entry_id = d.id
|
||||
$where_clause
|
||||
ORDER BY l.timestamp DESC
|
||||
LIMIT ?, ?";
|
||||
|
||||
// Count query with same filtering
|
||||
$count_query = "SELECT COUNT(*) as total
|
||||
FROM ddns_logs l
|
||||
$where_clause";
|
||||
|
||||
// Prepare and execute count query
|
||||
$count_stmt = $link->prepare($count_query);
|
||||
if ($ddns_id !== null) {
|
||||
$count_stmt->bind_param($types, ...$params);
|
||||
}
|
||||
$count_stmt->execute();
|
||||
$total = $count_stmt->get_result()->fetch_assoc()['total'];
|
||||
$count_stmt->close();
|
||||
|
||||
// Calculate total pages
|
||||
$pages = ceil($total / $per_page);
|
||||
|
||||
// Prepare main query
|
||||
$stmt = $link->prepare($query);
|
||||
if ($ddns_id !== null) {
|
||||
$params[] = $offset;
|
||||
$params[] = $per_page;
|
||||
$stmt->bind_param($types . "ii", ...$params);
|
||||
} else {
|
||||
$stmt->bind_param("ii", $offset, $per_page);
|
||||
}
|
||||
$stmt->execute();
|
||||
$logs = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt->close();
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= $ddns_id ? "Logs for DDNS #$ddns_id" : "All DDNS Logs" ?></title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1><?= $ddns_id ? "Logs for DDNS Entry #$ddns_id" : "All DDNS Logs" ?></h1>
|
||||
|
||||
<div class="card">
|
||||
<!-- Logs Table -->
|
||||
<div class="table-responsive">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>FQDN</th>
|
||||
<th>Action</th>
|
||||
<th>IP</th>
|
||||
<th>Details</th>
|
||||
<th>Timestamp</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($log['ddns_fqdn'] ?? 'N/A') ?></td>
|
||||
<td><?= htmlspecialchars($log['action']) ?></td>
|
||||
<td><?= htmlspecialchars($log['ip_address']) ?></td>
|
||||
<td><?= htmlspecialchars($log['details']) ?></td>
|
||||
<td><?= htmlspecialchars($log['timestamp']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex gap-2 mt-4" style="justify-content: center;">
|
||||
<?php if ($page > 1): ?>
|
||||
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page - 1])) ?>" class="btn">Previous</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"
|
||||
class="btn <?= $i == $page ? 'btn-primary' : '' ?>"
|
||||
style="<?= $i == $page ? 'background-color: var(--primary-hover);' : '' ?>"><?= $i ?></a>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $pages): ?>
|
||||
<a href="?<?= http_build_query(array_merge($_GET, ['page' => $page + 1])) ?>" class="btn">Next</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4">
|
||||
<a href="<?= $ddns_id ? 'manage_ddns.php' : 'dashboard.php' ?>">
|
||||
Back to <?= $ddns_id ? 'DDNS Management' : 'Dashboard' ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user