batman
This commit is contained in:
commit
682f25edcd
19 changed files with 1907 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
./mzdns.db*
|
27
Dockerfile
Normal file
27
Dockerfile
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
############################
|
||||||
|
# STEP 1: Build executable
|
||||||
|
############################
|
||||||
|
FROM golang:1.24.1-alpine AS builder
|
||||||
|
RUN apk add --no-cache git build-base sqlite-dev
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN go install github.com/a-h/templ/cmd/templ@latest
|
||||||
|
RUN templ generate
|
||||||
|
RUN CGO_ENABLED=1 go build -o ddns-manager .
|
||||||
|
|
||||||
|
############################
|
||||||
|
# STEP 2: Build final image
|
||||||
|
############################
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
RUN apk add --no-cache sqlite-libs ca-certificates tzdata
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/ddns-manager /app/
|
||||||
|
COPY --from=builder /app/assets /app/assets
|
||||||
|
VOLUME /data
|
||||||
|
EXPOSE 3000
|
||||||
|
ENV DB_PATH=/data/ddns.db
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/ddns-manager"]
|
512
assets/js/app.js
Normal file
512
assets/js/app.js
Normal file
|
@ -0,0 +1,512 @@
|
||||||
|
// Global variables
|
||||||
|
let isConfigured = false;
|
||||||
|
let currentIP = "";
|
||||||
|
let domainName = "mz.uy";
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
function showToast(message, type = "success") {
|
||||||
|
const toastContainer = document.querySelector(".toast-container");
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
toast.className = `toast align-items-center text-white bg-${type}`;
|
||||||
|
toast.setAttribute("role", "alert");
|
||||||
|
toast.setAttribute("aria-live", "assertive");
|
||||||
|
toast.setAttribute("aria-atomic", "true");
|
||||||
|
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
${message}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toastContainer.appendChild(toast);
|
||||||
|
const bsToast = new bootstrap.Toast(toast);
|
||||||
|
bsToast.show();
|
||||||
|
|
||||||
|
// Remove the toast after it's hidden
|
||||||
|
toast.addEventListener("hidden.bs.toast", () => {
|
||||||
|
toast.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the application
|
||||||
|
async function initApp() {
|
||||||
|
// Show loading indicator
|
||||||
|
document.getElementById("loading").style.display = "block";
|
||||||
|
document.getElementById("config-warning").style.display = "none";
|
||||||
|
document.getElementById("config-status").style.display = "none";
|
||||||
|
document.getElementById("dns-records-section").style.display = "none";
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
await loadConfig();
|
||||||
|
|
||||||
|
// Load current IP
|
||||||
|
await loadCurrentIP();
|
||||||
|
|
||||||
|
// Load update frequencies for the dropdown
|
||||||
|
await loadUpdateFrequencies();
|
||||||
|
|
||||||
|
// If configured, load DNS records
|
||||||
|
if (isConfigured) {
|
||||||
|
await loadDNSRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide loading indicator
|
||||||
|
document.getElementById("loading").style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/config");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
isConfigured = data.is_configured;
|
||||||
|
domainName = data.domain;
|
||||||
|
|
||||||
|
if (isConfigured) {
|
||||||
|
document.getElementById("config-warning").style.display = "none";
|
||||||
|
document.getElementById("config-status").style.display = "block";
|
||||||
|
document.getElementById("dns-records-section").style.display = "block";
|
||||||
|
|
||||||
|
document.getElementById("zone-id").textContent = data.zone_id;
|
||||||
|
document.getElementById("domain-name").textContent = data.domain;
|
||||||
|
document.getElementById("domain-suffix").textContent = "." + data.domain;
|
||||||
|
document.getElementById("domain-input").value = data.domain;
|
||||||
|
document.getElementById("zone-id-input").value = data.zone_id;
|
||||||
|
|
||||||
|
// Set the update schedule display
|
||||||
|
let scheduleDisplay = "Manual updates only";
|
||||||
|
if (data.update_period) {
|
||||||
|
switch (data.update_period) {
|
||||||
|
case "*/5 * * * *":
|
||||||
|
scheduleDisplay = "Every 5 minutes";
|
||||||
|
break;
|
||||||
|
case "*/30 * * * *":
|
||||||
|
scheduleDisplay = "Every 30 minutes";
|
||||||
|
break;
|
||||||
|
case "0 * * * *":
|
||||||
|
scheduleDisplay = "Hourly";
|
||||||
|
break;
|
||||||
|
case "0 */6 * * *":
|
||||||
|
scheduleDisplay = "Every 6 hours";
|
||||||
|
break;
|
||||||
|
case "0 0 * * *":
|
||||||
|
scheduleDisplay = "Daily";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
scheduleDisplay = data.update_period;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById("update-schedule").textContent = scheduleDisplay;
|
||||||
|
} else {
|
||||||
|
document.getElementById("config-warning").style.display = "block";
|
||||||
|
document.getElementById("config-status").style.display = "none";
|
||||||
|
document.getElementById("dns-records-section").style.display = "none";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load configuration:", error);
|
||||||
|
showToast("Failed to load configuration: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current IP
|
||||||
|
async function loadCurrentIP() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/current-ip");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
currentIP = data.ip;
|
||||||
|
document.getElementById("current-ip").textContent = currentIP;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load current IP:", error);
|
||||||
|
document.getElementById("current-ip").textContent = "Failed to load";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load update frequencies
|
||||||
|
async function loadUpdateFrequencies() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/update-frequencies");
|
||||||
|
const frequencies = await response.json();
|
||||||
|
|
||||||
|
const select = document.getElementById("update-period");
|
||||||
|
select.innerHTML = "";
|
||||||
|
|
||||||
|
frequencies.forEach((freq) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = freq.value;
|
||||||
|
option.textContent = freq.label;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load update frequencies:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load DNS records
|
||||||
|
async function loadDNSRecords() {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/records");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch DNS records");
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await response.json();
|
||||||
|
const tbody = document.getElementById("dns-records");
|
||||||
|
tbody.innerHTML = "";
|
||||||
|
|
||||||
|
if (records.length === 0) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td colspan="6" class="text-center">No DNS records found</td>';
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
records.forEach((record) => {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
|
||||||
|
// Highlight records that match the current IP
|
||||||
|
const isCurrentIP = record.type === "A" && record.content === currentIP;
|
||||||
|
const ipBadge = isCurrentIP
|
||||||
|
? '<span class="badge bg-success update-badge">Current IP</span>'
|
||||||
|
: record.type === "A"
|
||||||
|
? '<span class="badge bg-warning update-badge">Outdated IP</span>'
|
||||||
|
: "";
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${record.type}</td>
|
||||||
|
<td>${record.name}</td>
|
||||||
|
<td>${record.content} ${ipBadge}</td>
|
||||||
|
<td>${record.ttl === 1 ? "Auto" : record.ttl + "s"}</td>
|
||||||
|
<td>${record.proxied ? '<i class="bi bi-check-lg text-success"></i>' : ""}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary me-1 edit-record" data-id="${record.id}">
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger delete-record" data-id="${record.id}" data-name="${record.name}">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add event listeners for edit and delete buttons
|
||||||
|
document.querySelectorAll(".edit-record").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => editRecord(button.dataset.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll(".delete-record").forEach((button) => {
|
||||||
|
button.addEventListener("click", () =>
|
||||||
|
deleteRecord(button.dataset.id, button.dataset.name),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load DNS records:", error);
|
||||||
|
showToast("Failed to load DNS records: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit a DNS record
|
||||||
|
async function editRecord(id) {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/records");
|
||||||
|
const records = await response.json();
|
||||||
|
|
||||||
|
const record = records.find((r) => r.id === id);
|
||||||
|
if (!record) {
|
||||||
|
showToast("Record not found", "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the modal
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById("recordModal"));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Update modal title
|
||||||
|
document.getElementById("recordModalLabel").textContent = "Edit DNS Record";
|
||||||
|
|
||||||
|
// Fill the form
|
||||||
|
document.getElementById("record-id").value = record.id;
|
||||||
|
|
||||||
|
// Set the subdomain name without the domain suffix
|
||||||
|
let name = record.name;
|
||||||
|
if (name === domainName) {
|
||||||
|
name = "@";
|
||||||
|
} else if (name.endsWith("." + domainName)) {
|
||||||
|
name = name.substring(0, name.length - domainName.length - 1);
|
||||||
|
}
|
||||||
|
document.getElementById("record-name").value = name;
|
||||||
|
|
||||||
|
document.getElementById("record-type").value = record.type;
|
||||||
|
document.getElementById("record-content").value = record.content;
|
||||||
|
document.getElementById("record-ttl").value = record.ttl;
|
||||||
|
document.getElementById("record-proxied").checked = record.proxied;
|
||||||
|
document.getElementById("use-my-ip").checked = false;
|
||||||
|
|
||||||
|
// Show/hide the "Use my current IP" option based on record type
|
||||||
|
toggleMyIPOption();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load record:", error);
|
||||||
|
showToast("Failed to load record: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a DNS record
|
||||||
|
async function deleteRecord(id, name) {
|
||||||
|
if (!confirm(`Are you sure you want to delete the record for "${name}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/records/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Failed to delete record");
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(`Record for "${name}" deleted successfully`);
|
||||||
|
await loadDNSRecords();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete record:", error);
|
||||||
|
showToast("Failed to delete record: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the "Use my current IP" option based on record type
|
||||||
|
function toggleMyIPOption() {
|
||||||
|
const recordType = document.getElementById("record-type").value;
|
||||||
|
const useMyIPGroup = document.getElementById("use-my-ip-group");
|
||||||
|
const contentGroup = document.getElementById("content-group");
|
||||||
|
|
||||||
|
if (recordType === "A") {
|
||||||
|
useMyIPGroup.style.display = "block";
|
||||||
|
|
||||||
|
// If the checkbox is checked, hide the content field
|
||||||
|
const useMyIP = document.getElementById("use-my-ip").checked;
|
||||||
|
contentGroup.style.display = useMyIP ? "none" : "block";
|
||||||
|
} else {
|
||||||
|
useMyIPGroup.style.display = "none";
|
||||||
|
contentGroup.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
// Initialize the application
|
||||||
|
initApp();
|
||||||
|
|
||||||
|
// Refresh IP button
|
||||||
|
document
|
||||||
|
.getElementById("refresh-ip")
|
||||||
|
.addEventListener("click", async function () {
|
||||||
|
await loadCurrentIP();
|
||||||
|
showToast("Current IP refreshed");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save configuration button
|
||||||
|
document
|
||||||
|
.getElementById("save-config")
|
||||||
|
.addEventListener("click", async function () {
|
||||||
|
const apiToken = document.getElementById("api-token").value;
|
||||||
|
const zoneId = document.getElementById("zone-id-input").value;
|
||||||
|
const domain = document.getElementById("domain-input").value;
|
||||||
|
const updatePeriod = document.getElementById("update-period").value;
|
||||||
|
|
||||||
|
if (!apiToken || !zoneId || !domain) {
|
||||||
|
showToast("Please fill all required fields", "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/config", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_token: apiToken,
|
||||||
|
zone_id: zoneId,
|
||||||
|
domain: domain,
|
||||||
|
update_period: updatePeriod,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Failed to save configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(
|
||||||
|
document.getElementById("configModal"),
|
||||||
|
);
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
showToast("Configuration saved successfully");
|
||||||
|
|
||||||
|
// Reload the app
|
||||||
|
initApp();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save configuration:", error);
|
||||||
|
showToast("Failed to save configuration: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Record type change event
|
||||||
|
document
|
||||||
|
.getElementById("record-type")
|
||||||
|
.addEventListener("change", toggleMyIPOption);
|
||||||
|
|
||||||
|
// Use my IP checkbox change event
|
||||||
|
document.getElementById("use-my-ip").addEventListener("change", function () {
|
||||||
|
const contentGroup = document.getElementById("content-group");
|
||||||
|
contentGroup.style.display = this.checked ? "none" : "block";
|
||||||
|
|
||||||
|
if (this.checked) {
|
||||||
|
document.getElementById("record-content").value = currentIP;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save record button
|
||||||
|
document
|
||||||
|
.getElementById("save-record")
|
||||||
|
.addEventListener("click", async function () {
|
||||||
|
const id = document.getElementById("record-id").value;
|
||||||
|
let name = document.getElementById("record-name").value;
|
||||||
|
const type = document.getElementById("record-type").value;
|
||||||
|
const content = document.getElementById("record-content").value;
|
||||||
|
const ttl = parseInt(document.getElementById("record-ttl").value);
|
||||||
|
const proxied = document.getElementById("record-proxied").checked;
|
||||||
|
const useMyIP = document.getElementById("use-my-ip").checked;
|
||||||
|
|
||||||
|
// Validate the form
|
||||||
|
if (!name) {
|
||||||
|
showToast("Name is required", "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useMyIP && !content) {
|
||||||
|
showToast("Content is required", "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the record data
|
||||||
|
const recordData = {
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
content: useMyIP ? "" : content,
|
||||||
|
ttl: ttl,
|
||||||
|
proxied: proxied,
|
||||||
|
use_my_ip: useMyIP,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
// Update existing record
|
||||||
|
response = await fetch(`/api/records/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(recordData),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new record
|
||||||
|
response = await fetch("/api/records", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(recordData),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Failed to save record");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the modal
|
||||||
|
const modal = bootstrap.Modal.getInstance(
|
||||||
|
document.getElementById("recordModal"),
|
||||||
|
);
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
id ? "Record updated successfully" : "Record created successfully",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset the form
|
||||||
|
document.getElementById("record-form").reset();
|
||||||
|
document.getElementById("record-id").value = "";
|
||||||
|
|
||||||
|
// Reload DNS records
|
||||||
|
await loadDNSRecords();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to save record:", error);
|
||||||
|
showToast("Failed to save record: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update all records button
|
||||||
|
document
|
||||||
|
.getElementById("update-all-records")
|
||||||
|
.addEventListener("click", async function () {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Are you sure you want to update all A records to your current IP?",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/update-all", {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || "Failed to update records");
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("All A records updated to current IP");
|
||||||
|
|
||||||
|
// Reload DNS records
|
||||||
|
await loadDNSRecords();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update records:", error);
|
||||||
|
showToast("Failed to update records: " + error.message, "danger");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset record form when opening the modal
|
||||||
|
document
|
||||||
|
.getElementById("recordModal")
|
||||||
|
.addEventListener("show.bs.modal", function (event) {
|
||||||
|
const button = event.relatedTarget;
|
||||||
|
|
||||||
|
// If opening from the "Add Record" button, reset the form
|
||||||
|
if (button && button.textContent.trim().includes("Add Record")) {
|
||||||
|
document.getElementById("recordModalLabel").textContent =
|
||||||
|
"Add DNS Record";
|
||||||
|
document.getElementById("record-form").reset();
|
||||||
|
document.getElementById("record-id").value = "";
|
||||||
|
document.getElementById("record-type").value = "A";
|
||||||
|
|
||||||
|
// Show/hide the "Use my current IP" option based on record type
|
||||||
|
toggleMyIPOption();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
31
db/db.go
Normal file
31
db/db.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.29.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DBTX interface {
|
||||||
|
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(context.Context, string) (*sql.Stmt, error)
|
||||||
|
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db DBTX) *Queries {
|
||||||
|
return &Queries{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Queries struct {
|
||||||
|
db DBTX
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
||||||
|
return &Queries{
|
||||||
|
db: tx,
|
||||||
|
}
|
||||||
|
}
|
12
db/models.go
Normal file
12
db/models.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.29.0
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ApiToken string `json:"api_token"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
UpdatePeriod string `json:"update_period"`
|
||||||
|
}
|
32
db/queries.sql
Normal file
32
db/queries.sql
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
-- name: GetConfig :one
|
||||||
|
SELECT * FROM config LIMIT 1;
|
||||||
|
|
||||||
|
-- name: UpsertConfig :exec
|
||||||
|
INSERT INTO config (api_token, zone_id, domain, update_period)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT DO UPDATE SET
|
||||||
|
api_token = excluded.api_token,
|
||||||
|
zone_id = excluded.zone_id,
|
||||||
|
domain = excluded.domain,
|
||||||
|
update_period = excluded.update_period;
|
||||||
|
|
||||||
|
-- name: InitSchema :exec
|
||||||
|
-- This query is used to ensure the schema is set up properly
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
api_token TEXT,
|
||||||
|
zone_id TEXT,
|
||||||
|
domain TEXT NOT NULL DEFAULT 'mz.uy',
|
||||||
|
update_period TEXT NOT NULL DEFAULT '0 */6 * * *'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER IF NOT EXISTS enforce_single_config
|
||||||
|
BEFORE INSERT ON config
|
||||||
|
WHEN (SELECT COUNT(*) FROM config) > 0
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(FAIL, 'Only one config record allowed');
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Insert default config if none exists
|
||||||
|
INSERT OR IGNORE INTO config (domain, update_period)
|
||||||
|
SELECT 'mz.uy', '0 */6 * * *'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM config);
|
68
db/queries.sql.go
Normal file
68
db/queries.sql.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.29.0
|
||||||
|
// source: queries.sql
|
||||||
|
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const getConfig = `-- name: GetConfig :one
|
||||||
|
SELECT api_token, zone_id, domain, update_period FROM config LIMIT 1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetConfig(ctx context.Context) (Config, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getConfig)
|
||||||
|
var i Config
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ApiToken,
|
||||||
|
&i.ZoneID,
|
||||||
|
&i.Domain,
|
||||||
|
&i.UpdatePeriod,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSchema = `-- name: InitSchema :exec
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
api_token TEXT,
|
||||||
|
zone_id TEXT,
|
||||||
|
domain TEXT NOT NULL DEFAULT 'mz.uy',
|
||||||
|
update_period TEXT NOT NULL DEFAULT '0 */6 * * *'
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
// This query is used to ensure the schema is set up properly
|
||||||
|
func (q *Queries) InitSchema(ctx context.Context) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, initSchema)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const upsertConfig = `-- name: UpsertConfig :exec
|
||||||
|
INSERT INTO config (api_token, zone_id, domain, update_period)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
ON CONFLICT DO UPDATE SET
|
||||||
|
api_token = excluded.api_token,
|
||||||
|
zone_id = excluded.zone_id,
|
||||||
|
domain = excluded.domain,
|
||||||
|
update_period = excluded.update_period
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpsertConfigParams struct {
|
||||||
|
ApiToken string `json:"api_token"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
UpdatePeriod string `json:"update_period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpsertConfig(ctx context.Context, arg UpsertConfigParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, upsertConfig,
|
||||||
|
arg.ApiToken,
|
||||||
|
arg.ZoneID,
|
||||||
|
arg.Domain,
|
||||||
|
arg.UpdatePeriod,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
6
db/schema.sql
Normal file
6
db/schema.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
api_token TEXT NOT NULL DEFAULT '',
|
||||||
|
zone_id TEXT NOT NULL DEFAULT '',
|
||||||
|
domain TEXT NOT NULL DEFAULT 'mz.uy',
|
||||||
|
update_period TEXT NOT NULL DEFAULT '0 */6 * * *'
|
||||||
|
);
|
15
db/util.go
Normal file
15
db/util.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitSchema initializes the database schema
|
||||||
|
func InitSchema(db *sql.DB) error {
|
||||||
|
// Create a new Queries instance
|
||||||
|
q := New(db)
|
||||||
|
|
||||||
|
// Execute the initialization query
|
||||||
|
return q.InitSchema(context.Background())
|
||||||
|
}
|
26
go.mod
Normal file
26
go.mod
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
module ddns-manager
|
||||||
|
|
||||||
|
go 1.24.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/a-h/templ v0.3.865
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0
|
||||||
|
github.com/labstack/echo/v4 v4.13.3
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
|
golang.org/x/net v0.39.0 // indirect
|
||||||
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
|
golang.org/x/text v0.24.0 // indirect
|
||||||
|
golang.org/x/time v0.9.0 // indirect
|
||||||
|
)
|
49
go.sum
Normal file
49
go.sum
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
github.com/a-h/templ v0.3.865 h1:nYn5EWm9EiXaDgWcMQaKiKvrydqgxDUtT1+4zU2C43A=
|
||||||
|
github.com/a-h/templ v0.3.865/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
|
||||||
|
github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
499
main.go
Normal file
499
main.go
Normal file
|
@ -0,0 +1,499 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"ddns-manager/db"
|
||||||
|
"ddns-manager/templates"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cloudflare-go"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSRecord struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Proxied bool `json:"proxied"`
|
||||||
|
CreatedOn string `json:"created_on"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateFrequency struct {
|
||||||
|
Label string `json:"label"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
api *cloudflare.API
|
||||||
|
scheduler *cron.Cron
|
||||||
|
lastIP string
|
||||||
|
jobID cron.EntryID
|
||||||
|
queries *db.Queries
|
||||||
|
)
|
||||||
|
|
||||||
|
func initDatabase() (*sql.DB, error) {
|
||||||
|
dbPath := os.Getenv("DB_PATH")
|
||||||
|
if dbPath == "" {
|
||||||
|
dbPath = "./ddns.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Using database path: %s", dbPath)
|
||||||
|
|
||||||
|
sqlDB, err := sql.Open("sqlite3", dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to open database: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.InitSchema(sqlDB); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to initialize schema: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
queries = db.New(sqlDB)
|
||||||
|
|
||||||
|
return sqlDB, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCloudflare(apiToken, zoneID string) error {
|
||||||
|
if apiToken == "" || zoneID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
api, err = cloudflare.NewWithAPIToken(apiToken)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize Cloudflare API: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentIP() (string, error) {
|
||||||
|
resp, err := http.Get("https://api.ipify.org")
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get current IP: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
ip, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read IP response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(ip), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSRecords(zoneID string) ([]DNSRecord, error) {
|
||||||
|
if api == nil {
|
||||||
|
return nil, fmt.Errorf("cloudflare API not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := cloudflare.ZoneIdentifier(zoneID)
|
||||||
|
|
||||||
|
recs, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get DNS records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []DNSRecord
|
||||||
|
for _, rec := range recs {
|
||||||
|
records = append(records, DNSRecord{
|
||||||
|
ID: rec.ID,
|
||||||
|
Type: rec.Type,
|
||||||
|
Name: rec.Name,
|
||||||
|
Content: rec.Content,
|
||||||
|
TTL: rec.TTL,
|
||||||
|
Proxied: *rec.Proxied,
|
||||||
|
CreatedOn: rec.CreatedOn.Format(time.RFC3339),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDNSRecord(zoneID, domain, name, recordType, content string, ttl int, proxied bool) error {
|
||||||
|
if api == nil {
|
||||||
|
return fmt.Errorf("cloudflare API not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(name, domain) && name != "@" {
|
||||||
|
name = name + "." + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "@" {
|
||||||
|
name = domain
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := cloudflare.ZoneIdentifier(zoneID)
|
||||||
|
|
||||||
|
_, err := api.CreateDNSRecord(ctx, rc, cloudflare.CreateDNSRecordParams{
|
||||||
|
Type: recordType,
|
||||||
|
Name: name,
|
||||||
|
Content: content,
|
||||||
|
TTL: ttl,
|
||||||
|
Proxied: &proxied,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create DNS record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateDNSRecord(zoneID, id, name, recordType, content string, ttl int, proxied bool) error {
|
||||||
|
if api == nil {
|
||||||
|
return fmt.Errorf("cloudflare API not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := cloudflare.ZoneIdentifier(zoneID)
|
||||||
|
|
||||||
|
_, err := api.UpdateDNSRecord(ctx, rc, cloudflare.UpdateDNSRecordParams{
|
||||||
|
ID: id,
|
||||||
|
Type: recordType,
|
||||||
|
Name: name,
|
||||||
|
Content: content,
|
||||||
|
TTL: ttl,
|
||||||
|
Proxied: &proxied,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update DNS record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteDNSRecord(zoneID, id string) error {
|
||||||
|
if api == nil {
|
||||||
|
return fmt.Errorf("cloudflare API not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := cloudflare.ZoneIdentifier(zoneID)
|
||||||
|
|
||||||
|
err := api.DeleteDNSRecord(ctx, rc, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete DNS record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAllRecordsWithCurrentIP(zoneID string) error {
|
||||||
|
if api == nil {
|
||||||
|
return fmt.Errorf("cloudflare API not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIP, err := getCurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentIP == lastIP {
|
||||||
|
log.Println("IP hasn't changed, no updates needed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastIP = currentIP
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
rc := cloudflare.ZoneIdentifier(zoneID)
|
||||||
|
|
||||||
|
records, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{
|
||||||
|
Type: "A",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get DNS records: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
if rec.Content != currentIP {
|
||||||
|
proxied := rec.Proxied
|
||||||
|
_, err := api.UpdateDNSRecord(ctx, rc, cloudflare.UpdateDNSRecordParams{
|
||||||
|
ID: rec.ID,
|
||||||
|
Type: rec.Type,
|
||||||
|
Name: rec.Name,
|
||||||
|
Content: currentIP,
|
||||||
|
TTL: rec.TTL,
|
||||||
|
Proxied: proxied,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to update record %s: %v", rec.Name, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Updated record %s to %s", rec.Name, currentIP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scheduleUpdates(zoneID, updatePeriod string) error {
|
||||||
|
if jobID != 0 {
|
||||||
|
scheduler.Remove(jobID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updatePeriod == "" {
|
||||||
|
log.Println("Automatic updates disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
jobID, err = scheduler.AddFunc(updatePeriod, func() {
|
||||||
|
log.Println("Running scheduled IP update")
|
||||||
|
if err := updateAllRecordsWithCurrentIP(zoneID); err != nil {
|
||||||
|
log.Printf("Scheduled update failed: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to schedule updates: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Scheduled IP updates with cron: %s", updatePeriod)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sqlDB, err := initDatabase()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
defer sqlDB.Close()
|
||||||
|
|
||||||
|
config, err := queries.GetConfig(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to load configuration: %v", err)
|
||||||
|
config = db.Config{
|
||||||
|
Domain: "mz.uy",
|
||||||
|
UpdatePeriod: "0 */6 * * *",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil {
|
||||||
|
log.Printf("Warning: Cloudflare initialization failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler = cron.New()
|
||||||
|
scheduler.Start()
|
||||||
|
defer scheduler.Stop()
|
||||||
|
|
||||||
|
if config.ApiToken != "" && config.ZoneID != "" && config.UpdatePeriod != "" {
|
||||||
|
if err := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil {
|
||||||
|
log.Printf("Warning: Failed to schedule updates: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e := echo.New()
|
||||||
|
e.Use(middleware.Logger())
|
||||||
|
e.Use(middleware.Recover())
|
||||||
|
e.Static("/assets", "assets")
|
||||||
|
|
||||||
|
apiGroup := e.Group("/api")
|
||||||
|
{
|
||||||
|
apiGroup.GET("/config", func(c echo.Context) error {
|
||||||
|
configStatus := struct {
|
||||||
|
IsConfigured bool `json:"is_configured"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
UpdatePeriod string `json:"update_period"`
|
||||||
|
}{
|
||||||
|
IsConfigured: config.ApiToken != "" && config.ZoneID != "",
|
||||||
|
ZoneID: config.ZoneID,
|
||||||
|
Domain: config.Domain,
|
||||||
|
UpdatePeriod: config.UpdatePeriod,
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, configStatus)
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.POST("/config", func(c echo.Context) error {
|
||||||
|
var newConfig struct {
|
||||||
|
APIToken string `json:"api_token"`
|
||||||
|
ZoneID string `json:"zone_id"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
UpdatePeriod string `json:"update_period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Bind(&newConfig); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid request"})
|
||||||
|
}
|
||||||
|
|
||||||
|
err := queries.UpsertConfig(context.Background(), db.UpsertConfigParams{
|
||||||
|
ApiToken: newConfig.APIToken,
|
||||||
|
ZoneID: newConfig.ZoneID,
|
||||||
|
Domain: newConfig.Domain,
|
||||||
|
UpdatePeriod: newConfig.UpdatePeriod,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to save configuration: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
config.ApiToken = newConfig.APIToken
|
||||||
|
config.ZoneID = newConfig.ZoneID
|
||||||
|
config.Domain = newConfig.Domain
|
||||||
|
config.UpdatePeriod = newConfig.UpdatePeriod
|
||||||
|
|
||||||
|
if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to initialize Cloudflare client: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to schedule updates: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "Configuration updated successfully"})
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.GET("/update-frequencies", func(c echo.Context) error {
|
||||||
|
frequencies := []UpdateFrequency{
|
||||||
|
{Label: "Every 1 minutes", Value: "*/1 * * * *"},
|
||||||
|
{Label: "Every 5 minutes", Value: "*/5 * * * *"},
|
||||||
|
{Label: "Every 30 minutes", Value: "*/30 * * * *"},
|
||||||
|
{Label: "Hourly", Value: "0 * * * *"},
|
||||||
|
{Label: "Every 6 hours", Value: "0 */6 * * *"},
|
||||||
|
{Label: "Daily", Value: "0 0 * * *"},
|
||||||
|
{Label: "Never (manual only)", Value: ""},
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, frequencies)
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.GET("/current-ip", func(c echo.Context) error {
|
||||||
|
ip, err := getCurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to get current IP: %v", err)})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"ip": ip})
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.GET("/records", func(c echo.Context) error {
|
||||||
|
if config.ApiToken == "" || config.ZoneID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := getDNSRecords(config.ZoneID)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to get DNS records: %v", err)})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, records)
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.POST("/records", func(c echo.Context) error {
|
||||||
|
if config.ApiToken == "" || config.ZoneID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
var record struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Proxied bool `json:"proxied"`
|
||||||
|
UseMyIP bool `json:"use_my_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Bind(&record); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid request"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.UseMyIP {
|
||||||
|
ip, err := getCurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to get current IP: %v", err)})
|
||||||
|
}
|
||||||
|
record.Content = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := createDNSRecord(config.ZoneID, config.Domain, record.Name, record.Type, record.Content, record.TTL, record.Proxied); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to create DNS record: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "DNS record created successfully"})
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.PUT("/records/:id", func(c echo.Context) error {
|
||||||
|
if config.ApiToken == "" || config.ZoneID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
var record struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Proxied bool `json:"proxied"`
|
||||||
|
UseMyIP bool `json:"use_my_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.Bind(&record); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid request"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.UseMyIP {
|
||||||
|
ip, err := getCurrentIP()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to get current IP: %v", err)})
|
||||||
|
}
|
||||||
|
record.Content = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateDNSRecord(config.ZoneID, id, record.Name, record.Type, record.Content, record.TTL, record.Proxied); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to update DNS record: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "DNS record updated successfully"})
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.DELETE("/records/:id", func(c echo.Context) error {
|
||||||
|
if config.ApiToken == "" || config.ZoneID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
id := c.Param("id")
|
||||||
|
if err := deleteDNSRecord(config.ZoneID, id); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to delete DNS record: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "DNS record deleted successfully"})
|
||||||
|
})
|
||||||
|
|
||||||
|
apiGroup.POST("/update-all", func(c echo.Context) error {
|
||||||
|
if config.ApiToken == "" || config.ZoneID == "" {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateAllRecordsWithCurrentIP(config.ZoneID); err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to update records: %v", err)})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{"message": "All A records updated with current IP"})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
component := templates.Index(templates.IndexProps{
|
||||||
|
Title: "mz.uy DNS Manager",
|
||||||
|
})
|
||||||
|
return templates.Render(c.Response(), component)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println("Starting server on http://localhost:3000")
|
||||||
|
log.Fatal(e.Start(":3000"))
|
||||||
|
}
|
13
sqlc.yaml
Normal file
13
sqlc.yaml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
version: "2"
|
||||||
|
sql:
|
||||||
|
- engine: "sqlite"
|
||||||
|
queries: "db/queries.sql"
|
||||||
|
schema: "db/schema.sql"
|
||||||
|
gen:
|
||||||
|
go:
|
||||||
|
package: "db"
|
||||||
|
out: "db"
|
||||||
|
emit_json_tags: true
|
||||||
|
emit_prepared_queries: false
|
||||||
|
emit_interface: false
|
||||||
|
emit_exact_table_names: false
|
136
templates/index.templ
Normal file
136
templates/index.templ
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
// IndexProps contains the properties for the Index component
|
||||||
|
type IndexProps struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index is the main page component
|
||||||
|
templ Index(props IndexProps) {
|
||||||
|
@Layout(props.Title) {
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
|
<h1>{ props.Title }</h1>
|
||||||
|
<div class="current-ip d-flex align-items-center">
|
||||||
|
<span class="me-2">Current IP:</span>
|
||||||
|
<span id="current-ip" class="fw-bold"></span>
|
||||||
|
<button
|
||||||
|
id="refresh-ip"
|
||||||
|
class="btn btn-sm btn-outline-secondary ms-2"
|
||||||
|
title="Refresh current IP"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-clockwise"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Configuration Warning -->
|
||||||
|
<div
|
||||||
|
id="config-warning"
|
||||||
|
class="alert alert-warning config-warning"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<h4>Configuration Required</h4>
|
||||||
|
<p>
|
||||||
|
Please configure your Cloudflare API credentials to manage your
|
||||||
|
DNS records.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#configModal"
|
||||||
|
>
|
||||||
|
Configure Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Configuration Status -->
|
||||||
|
<div id="config-status" class="card mb-4" style="display: none">
|
||||||
|
<div
|
||||||
|
class="card-header d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h5 class="mb-0">Configuration</h5>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-outline-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#configModal"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Domain:</strong> <span id="domain-name">mz.uy</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>Zone ID:</strong> <span id="zone-id"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<strong>IP Update Schedule:</strong>
|
||||||
|
<span id="update-schedule"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- DNS Records Section -->
|
||||||
|
<div id="dns-records-section" style="display: none">
|
||||||
|
<div class="card">
|
||||||
|
<div
|
||||||
|
class="card-header d-flex justify-content-between align-items-center"
|
||||||
|
>
|
||||||
|
<h5 class="mb-0">DNS Records</h5>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
id="update-all-records"
|
||||||
|
class="btn btn-sm btn-success me-2"
|
||||||
|
>
|
||||||
|
<i class="bi bi-arrow-repeat"></i> Update All to Current IP
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-primary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#recordModal"
|
||||||
|
>
|
||||||
|
<i class="bi bi-plus-lg"></i> Add Record
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Content</th>
|
||||||
|
<th>TTL</th>
|
||||||
|
<th>Proxied</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="dns-records">
|
||||||
|
<!-- DNS records will be inserted here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Loading Indicator -->
|
||||||
|
<div id="loading" class="text-center my-5">
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2">Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@ConfigModal()
|
||||||
|
@RecordModal()
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div class="toast-container"></div>
|
||||||
|
<script src="/assets/js/app.js"></script>
|
||||||
|
}
|
||||||
|
}
|
93
templates/index_templ.go
Normal file
93
templates/index_templ.go
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.857
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
// IndexProps contains the properties for the Index component
|
||||||
|
type IndexProps struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index is the main page component
|
||||||
|
func Index(props IndexProps) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"container\"><div class=\"row justify-content-center\"><div class=\"col-md-10\"><div class=\"d-flex justify-content-between align-items-center mb-4\"><h1>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(props.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 15, Col: 23}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</h1><div class=\"current-ip d-flex align-items-center\"><span class=\"me-2\">Current IP:</span> <span id=\"current-ip\" class=\"fw-bold\"></span> <button id=\"refresh-ip\" class=\"btn btn-sm btn-outline-secondary ms-2\" title=\"Refresh current IP\"><i class=\"bi bi-arrow-clockwise\"></i></button></div></div><!-- Configuration Warning --><div id=\"config-warning\" class=\"alert alert-warning config-warning\" style=\"display: none\"><h4>Configuration Required</h4><p>Please configure your Cloudflare API credentials to manage your DNS records.</p><button class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#configModal\">Configure Now</button></div><!-- Configuration Status --><div id=\"config-status\" class=\"card mb-4\" style=\"display: none\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\">Configuration</h5><button class=\"btn btn-sm btn-outline-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#configModal\">Edit</button></div><div class=\"card-body\"><div class=\"row\"><div class=\"col-md-4\"><strong>Domain:</strong> <span id=\"domain-name\">mz.uy</span></div><div class=\"col-md-4\"><strong>Zone ID:</strong> <span id=\"zone-id\"></span></div><div class=\"col-md-4\"><strong>IP Update Schedule:</strong> <span id=\"update-schedule\"></span></div></div></div></div><!-- DNS Records Section --><div id=\"dns-records-section\" style=\"display: none\"><div class=\"card\"><div class=\"card-header d-flex justify-content-between align-items-center\"><h5 class=\"mb-0\">DNS Records</h5><div><button id=\"update-all-records\" class=\"btn btn-sm btn-success me-2\"><i class=\"bi bi-arrow-repeat\"></i> Update All to Current IP</button> <button class=\"btn btn-sm btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#recordModal\"><i class=\"bi bi-plus-lg\"></i> Add Record</button></div></div><div class=\"card-body p-0\"><div class=\"table-responsive\"><table class=\"table table-striped table-hover mb-0\"><thead><tr><th>Type</th><th>Name</th><th>Content</th><th>TTL</th><th>Proxied</th><th>Actions</th></tr></thead> <tbody id=\"dns-records\"><!-- DNS records will be inserted here --></tbody></table></div></div></div></div><!-- Loading Indicator --><div id=\"loading\" class=\"text-center my-5\"><div class=\"spinner-border\" role=\"status\"><span class=\"visually-hidden\">Loading...</span></div><p class=\"mt-2\">Loading...</p></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = ConfigModal().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = RecordModal().Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " <!-- Toast container for notifications --> <div class=\"toast-container\"></div><script src=\"/assets/js/app.js\"></script>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
templ_7745c5c3_Err = Layout(props.Title).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
56
templates/layout.templ
Normal file
56
templates/layout.templ
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Render renders a component to an io.Writer
|
||||||
|
func Render(w io.Writer, component templ.Component) error {
|
||||||
|
return component.Render(context.Background(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout is the base layout for all pages
|
||||||
|
templ Layout(title string) {
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>{ title }</title>
|
||||||
|
<link
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
padding-top: 20px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||||
|
}
|
||||||
|
.config-warning {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
|
.update-badge {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{ children... }
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
}
|
70
templates/layout_templ.go
Normal file
70
templates/layout_templ.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.857
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Render renders a component to an io.Writer
|
||||||
|
func Render(w io.Writer, component templ.Component) error {
|
||||||
|
return component.Render(context.Background(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout is the base layout for all pages
|
||||||
|
func Layout(title string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 18, Col: 17}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css\"><style>\n\t\t\t\tbody {\n\t\t\t\t\tpadding-top: 20px;\n\t\t\t\t\tbackground-color: #f8f9fa;\n\t\t\t\t}\n\t\t\t\t.card {\n\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t\tbox-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);\n\t\t\t\t}\n\t\t\t\t.config-warning {\n\t\t\t\t\tmargin-bottom: 20px;\n\t\t\t\t}\n\t\t\t\t.toast-container {\n\t\t\t\t\tposition: fixed;\n\t\t\t\t\ttop: 20px;\n\t\t\t\t\tright: 20px;\n\t\t\t\t\tz-index: 1050;\n\t\t\t\t}\n\t\t\t\t.update-badge {\n\t\t\t\t\tfont-size: 0.8em;\n\t\t\t\t\tmargin-left: 10px;\n\t\t\t\t}\n\t\t\t</style></head><body>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js\"></script></body></html>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
190
templates/modal.templ
Normal file
190
templates/modal.templ
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
package templates
|
||||||
|
|
||||||
|
// ConfigModal is the configuration dialog component
|
||||||
|
templ ConfigModal() {
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="configModal"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="configModalLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="configModalLabel">Configuration</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="config-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="api-token" class="form-label">Cloudflare API Token</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="api-token"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
Create a token with <code>Zone.DNS:Edit</code> permissions in
|
||||||
|
the Cloudflare dashboard.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="zone-id-input" class="form-label">Zone ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="zone-id-input"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="form-text">
|
||||||
|
Found in the Cloudflare dashboard under your domain's overview
|
||||||
|
page.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="domain-input" class="form-label">Domain</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="domain-input"
|
||||||
|
value="mz.uy"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="update-period" class="form-label">Update Frequency</label>
|
||||||
|
<select class="form-select" id="update-period">
|
||||||
|
<!-- Options will be loaded from API -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="save-config">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordModal is the DNS record dialog component
|
||||||
|
templ RecordModal() {
|
||||||
|
<div
|
||||||
|
class="modal fade"
|
||||||
|
id="recordModal"
|
||||||
|
tabindex="-1"
|
||||||
|
aria-labelledby="recordModalLabel"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="recordModalLabel">Add DNS Record</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-close"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="record-form">
|
||||||
|
<input type="hidden" id="record-id"/>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="record-name" class="form-label">Name</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="record-name"
|
||||||
|
placeholder="subdomain"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="input-group-text" id="domain-suffix">.mz.uy</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Use @ for the root domain</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="record-type" class="form-label">Type</label>
|
||||||
|
<select class="form-select" id="record-type">
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="AAAA">AAAA</option>
|
||||||
|
<option value="CNAME">CNAME</option>
|
||||||
|
<option value="TXT">TXT</option>
|
||||||
|
<option value="MX">MX</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="content-group">
|
||||||
|
<label for="record-content" class="form-label">Content</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
id="record-content"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check" id="use-my-ip-group">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="use-my-ip"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="use-my-ip">Use my current IP address</label>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="record-ttl" class="form-label">TTL</label>
|
||||||
|
<select class="form-select" id="record-ttl">
|
||||||
|
<option value="1">Auto</option>
|
||||||
|
<option value="120">2 minutes</option>
|
||||||
|
<option value="300">5 minutes</option>
|
||||||
|
<option value="600">10 minutes</option>
|
||||||
|
<option value="1800">30 minutes</option>
|
||||||
|
<option value="3600">1 hour</option>
|
||||||
|
<option value="7200">2 hours</option>
|
||||||
|
<option value="18000">5 hours</option>
|
||||||
|
<option value="43200">12 hours</option>
|
||||||
|
<option value="86400">1 day</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="record-proxied"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="record-proxied">Proxied through Cloudflare</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" id="save-record">
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
71
templates/modal_templ.go
Normal file
71
templates/modal_templ.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.857
|
||||||
|
package templates
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
// ConfigModal is the configuration dialog component
|
||||||
|
func ConfigModal() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"modal fade\" id=\"configModal\" tabindex=\"-1\" aria-labelledby=\"configModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"configModalLabel\">Configuration</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"config-form\"><div class=\"mb-3\"><label for=\"api-token\" class=\"form-label\">Cloudflare API Token</label> <input type=\"password\" class=\"form-control\" id=\"api-token\" required><div class=\"form-text\">Create a token with <code>Zone.DNS:Edit</code> permissions in the Cloudflare dashboard.</div></div><div class=\"mb-3\"><label for=\"zone-id-input\" class=\"form-label\">Zone ID</label> <input type=\"text\" class=\"form-control\" id=\"zone-id-input\" required><div class=\"form-text\">Found in the Cloudflare dashboard under your domain's overview page.</div></div><div class=\"mb-3\"><label for=\"domain-input\" class=\"form-label\">Domain</label> <input type=\"text\" class=\"form-control\" id=\"domain-input\" value=\"mz.uy\" required></div><div class=\"mb-3\"><label for=\"update-period\" class=\"form-label\">Update Frequency</label> <select class=\"form-select\" id=\"update-period\"><!-- Options will be loaded from API --></select></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" id=\"save-config\">Save</button></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordModal is the DNS record dialog component
|
||||||
|
func RecordModal() templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var2 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var2 == nil {
|
||||||
|
templ_7745c5c3_Var2 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"modal fade\" id=\"recordModal\" tabindex=\"-1\" aria-labelledby=\"recordModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"recordModalLabel\">Add DNS Record</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"record-form\"><input type=\"hidden\" id=\"record-id\"><div class=\"mb-3\"><label for=\"record-name\" class=\"form-label\">Name</label><div class=\"input-group\"><input type=\"text\" class=\"form-control\" id=\"record-name\" placeholder=\"subdomain\" required> <span class=\"input-group-text\" id=\"domain-suffix\">.mz.uy</span></div><div class=\"form-text\">Use @ for the root domain</div></div><div class=\"mb-3\"><label for=\"record-type\" class=\"form-label\">Type</label> <select class=\"form-select\" id=\"record-type\"><option value=\"A\">A</option> <option value=\"AAAA\">AAAA</option> <option value=\"CNAME\">CNAME</option> <option value=\"TXT\">TXT</option> <option value=\"MX\">MX</option></select></div><div class=\"mb-3\" id=\"content-group\"><label for=\"record-content\" class=\"form-label\">Content</label> <input type=\"text\" class=\"form-control\" id=\"record-content\" required></div><div class=\"mb-3 form-check\" id=\"use-my-ip-group\"><input type=\"checkbox\" class=\"form-check-input\" id=\"use-my-ip\"> <label class=\"form-check-label\" for=\"use-my-ip\">Use my current IP address</label></div><div class=\"mb-3\"><label for=\"record-ttl\" class=\"form-label\">TTL</label> <select class=\"form-select\" id=\"record-ttl\"><option value=\"1\">Auto</option> <option value=\"120\">2 minutes</option> <option value=\"300\">5 minutes</option> <option value=\"600\">10 minutes</option> <option value=\"1800\">30 minutes</option> <option value=\"3600\">1 hour</option> <option value=\"7200\">2 hours</option> <option value=\"18000\">5 hours</option> <option value=\"43200\">12 hours</option> <option value=\"86400\">1 day</option></select></div><div class=\"mb-3 form-check\"><input type=\"checkbox\" class=\"form-check-input\" id=\"record-proxied\"> <label class=\"form-check-label\" for=\"record-proxied\">Proxied through Cloudflare</label></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" id=\"save-record\">Save</button></div></div></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
Loading…
Add table
Add a link
Reference in a new issue