restore github
Some checks failed
Build, Push, Publish / Build & Release (push) Failing after 1s

This commit is contained in:
2025-12-09 15:37:27 -03:00
parent cab5a87bbe
commit 4368e51bf3
22 changed files with 2183 additions and 1 deletions

42
public/dashboard.php Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>