diff --git a/assets/js/app.js b/assets/js/app.js deleted file mode 100644 index f45a2cd..0000000 --- a/assets/js/app.js +++ /dev/null @@ -1,512 +0,0 @@ -// 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 = ` -
-
- ${message} -
- -
- `; - - 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 = - 'No DNS records found'; - 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 - ? 'Current IP' - : record.type === "A" - ? 'Outdated IP' - : ""; - - tr.innerHTML = ` - ${record.type} - ${record.name} - ${record.content} ${ipBadge} - ${record.ttl === 1 ? "Auto" : record.ttl + "s"} - ${record.proxied ? '' : ""} - - - - - `; - - 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(); - } - }); -}); diff --git a/db/queries.sql b/db/queries.sql index ba18ddc..ae40bc0 100644 --- a/db/queries.sql +++ b/db/queries.sql @@ -10,23 +10,22 @@ ON CONFLICT DO UPDATE SET domain = excluded.domain, update_period = excluded.update_period; +-- name: DeleteAllConfig :exec +DELETE FROM config; + +-- name: InsertConfig :exec +INSERT INTO config (api_token, zone_id, domain, update_period) +VALUES (?, ?, ?, ?); + -- 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, + 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 * * *' ); -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 * * *' +INSERT OR IGNORE INTO config (api_token, zone_id, domain, update_period) +SELECT '', '', 'mz.uy', '0 */6 * * *' WHERE NOT EXISTS (SELECT 1 FROM config); diff --git a/db/queries.sql.go b/db/queries.sql.go index 1ab708e..8ba426c 100644 --- a/db/queries.sql.go +++ b/db/queries.sql.go @@ -9,6 +9,15 @@ import ( "context" ) +const deleteAllConfig = `-- name: DeleteAllConfig :exec +DELETE FROM config +` + +func (q *Queries) DeleteAllConfig(ctx context.Context) error { + _, err := q.db.ExecContext(ctx, deleteAllConfig) + return err +} + const getConfig = `-- name: GetConfig :one SELECT api_token, zone_id, domain, update_period FROM config LIMIT 1 ` @@ -27,19 +36,40 @@ func (q *Queries) GetConfig(ctx context.Context) (Config, error) { const initSchema = `-- name: InitSchema :exec CREATE TABLE IF NOT EXISTS config ( - api_token TEXT, - zone_id TEXT, + 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 * * *' ) ` -// 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 insertConfig = `-- name: InsertConfig :exec +INSERT INTO config (api_token, zone_id, domain, update_period) +VALUES (?, ?, ?, ?) +` + +type InsertConfigParams struct { + ApiToken string `json:"api_token"` + ZoneID string `json:"zone_id"` + Domain string `json:"domain"` + UpdatePeriod string `json:"update_period"` +} + +func (q *Queries) InsertConfig(ctx context.Context, arg InsertConfigParams) error { + _, err := q.db.ExecContext(ctx, insertConfig, + arg.ApiToken, + arg.ZoneID, + arg.Domain, + arg.UpdatePeriod, + ) + return err +} + const upsertConfig = `-- name: UpsertConfig :exec INSERT INTO config (api_token, zone_id, domain, update_period) VALUES (?, ?, ?, ?) diff --git a/go.mod b/go.mod index dee76e5..53c6bc6 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,72 @@ module ddns-manager go 1.24.1 require ( - github.com/a-h/templ v0.3.865 + github.com/a-h/templ v0.3.898 github.com/cloudflare/cloudflare-go v0.115.0 + github.com/davecgh/go-spew v1.1.1 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 ( + cel.dev/expr v0.19.1 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/go-sql-driver/mysql v1.9.2 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/google/cel-go v0.24.1 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.4 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.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/ncruces/go-strftime v0.1.9 // indirect + github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect + github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect + github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect + github.com/pingcap/log v1.1.0 // indirect + github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/riza-io/grpc-go v0.2.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/sqlc-dev/sqlc v1.29.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.13.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 + google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.71.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.62.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.9.1 // indirect + modernc.org/sqlite v1.37.0 // indirect ) + +tool github.com/sqlc-dev/sqlc/cmd/sqlc diff --git a/go.sum b/go.sum index dcf0502..ef1d1fb 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,56 @@ -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= +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s= +github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 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/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/cel-go v0.24.1 h1:jsBCtxG8mM5wiUJDSGUqU0K7Mtr3w7Eyv00rw4DiZxI= +github.com/google/cel-go v0.24.1/go.mod h1:Hdf9TqOaTNSFQA1ybQaRqATVoK7m/zcf7IMhGXP5zI8= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/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-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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= @@ -22,28 +62,121 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= +github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= +github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= +github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= +github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/sqlc-dev/sqlc v1.29.0 h1:HQctoD7y/i29Bao53qXO7CZ/BV9NcvpGpsJWvz9nKWs= +github.com/sqlc-dev/sqlc v1.29.0/go.mod h1:BavmYw11px5AdPOjAVHmb9fctP5A8GTziC38wBF9tp0= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= 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= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= +google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= +google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.62.1 h1:s0+fv5E3FymN8eJVmnk0llBe6rOxCu/DEU+XygRbS8s= +modernc.org/libc v1.62.1/go.mod h1:iXhATfJQLjG3NWy56a6WVU73lWOcdYVxsvwCgoPljuo= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= +modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= diff --git a/main.go b/main.go index 6203c55..afed9bf 100644 --- a/main.go +++ b/main.go @@ -6,8 +6,10 @@ import ( "fmt" "io" "log" + "net" "net/http" "os" + "regexp" "strconv" "strings" "time" @@ -18,7 +20,7 @@ import ( "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/mattn/go-sqlite3" "github.com/robfig/cron/v3" ) @@ -30,54 +32,189 @@ var ( queries *db.Queries ) +// Simple validation +func validateDNSRecord(name, recordType, content string) error { + if strings.TrimSpace(name) == "" { + return fmt.Errorf("name is required") + } + + if strings.TrimSpace(content) == "" { + return fmt.Errorf("content is required") + } + + // Validate by type + switch recordType { + case "A": + if net.ParseIP(content) == nil { + return fmt.Errorf("invalid IP address") + } + case "CNAME": + if !regexp.MustCompile(`^[a-zA-Z0-9\-\.]+$`).MatchString(content) { + return fmt.Errorf("invalid domain name") + } + } + + return nil +} + +// Clean input sanitization +func sanitizeInput(input string) string { + return strings.TrimSpace(input) +} + +// Enhanced error responses +func errorResponse(c echo.Context, message string) error { + c.Response().WriteHeader(http.StatusBadRequest) + return templates.Render(c.Response(), templates.ErrorNotification(message)) +} + +func successResponse(c echo.Context, message string) error { + return templates.Render(c.Response(), templates.SuccessNotification(message)) +} + +// Improved createDNSRecord +func createDNSRecord(zoneID, domain, name, recordType, content string, ttl int, proxied bool) error { + if api == nil { + return fmt.Errorf("cloudflare API not initialized") + } + + // Validate input + if err := validateDNSRecord(name, recordType, content); err != nil { + return err + } + + // Prepare full name + fullName := name + if name != "@" && !strings.HasSuffix(name, domain) { + fullName = name + "." + domain + } + if name == "@" { + fullName = domain + } + + // Create record + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + rc := cloudflare.ZoneIdentifier(zoneID) + _, err := api.CreateDNSRecord(ctx, rc, cloudflare.CreateDNSRecordParams{ + Type: recordType, + Name: fullName, + Content: content, + TTL: ttl, + Proxied: &proxied, + }) + if err != nil { + // Simple error handling + if cfErr, ok := err.(*cloudflare.Error); ok { + switch cfErr.ErrorCodes[0] { + case 10000: + return fmt.Errorf("invalid API credentials") + case 81044: + return fmt.Errorf("record already exists") + default: + return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0]) + } + } + return fmt.Errorf("failed to create 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") + } + + if err := validateDNSRecord(name, recordType, content); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + 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 { + if cfErr, ok := err.(*cloudflare.Error); ok { + return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0]) + } + return fmt.Errorf("failed to update record: %w", err) + } + + return nil +} + +func deleteDNSRecord(zoneID, id string) error { + if api == nil { + return fmt.Errorf("cloudflare API not initialized") + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + rc := cloudflare.ZoneIdentifier(zoneID) + err := api.DeleteDNSRecord(ctx, rc, id) + if err != nil { + if cfErr, ok := err.(*cloudflare.Error); ok { + return fmt.Errorf("cloudflare error: %s", cfErr.ErrorMessages[0]) + } + return fmt.Errorf("failed to delete record: %w", err) + } + + return nil +} + 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) + return nil, err } if err := db.InitSchema(sqlDB); err != nil { - return nil, fmt.Errorf("failed to initialize schema: %w", err) + return nil, err } queries = db.New(sqlDB) return sqlDB, nil } -func initCloudflare(apiToken, zoneID string) error { - if apiToken == "" || zoneID == "" { +func initCloudflare(apiToken string) error { + if apiToken == "" { 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 + return err } 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) + return "", 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 "", err } - return string(ip), nil + return strings.TrimSpace(string(ip)), nil } func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) { @@ -85,12 +222,13 @@ func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) { return nil, fmt.Errorf("cloudflare API not initialized") } - ctx := context.Background() - rc := cloudflare.ZoneIdentifier(zoneID) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + 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) + return nil, err } var records []templates.DNSRecord @@ -109,117 +247,39 @@ func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) { 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) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() - records, _, err := api.ListDNSRecords(ctx, rc, cloudflare.ListDNSRecordsParams{ - Type: "A", - }) + 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) + return 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, + Proxied: rec.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) } } } @@ -230,26 +290,22 @@ func updateAllRecordsWithCurrentIP(zoneID string) error { func scheduleUpdates(zoneID, updatePeriod string) error { if jobID != 0 { scheduler.Remove(jobID) + log.Println("Scheduled update removed") } 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) } + log.Println("Scheduled update completed") }) - if err != nil { - return fmt.Errorf("failed to schedule updates: %w", err) - } - log.Printf("Scheduled IP updates with cron: %s", updatePeriod) - return nil + return err } func getUpdateFrequencies() []templates.UpdateFrequency { @@ -265,44 +321,42 @@ func getUpdateFrequencies() []templates.UpdateFrequency { } func main() { + // Initialize database sqlDB, err := initDatabase() if err != nil { - log.Fatalf("Failed to initialize database: %v", err) + log.Fatalf("Database init failed: %v", err) } defer sqlDB.Close() + // Load config 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 * * *", - } + config = db.Config{Domain: "example.com", UpdatePeriod: "0 */6 * * *"} } - if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil { - log.Printf("Warning: Cloudflare initialization failed: %v", err) + // Initialize Cloudflare + if err := initCloudflare(config.ApiToken); err != nil { + log.Printf("Cloudflare init failed: %v", err) } + // Initialize scheduler 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) - } + scheduleUpdates(config.ZoneID, config.UpdatePeriod) } + // Setup Echo e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) - e.Static("/assets", "assets") + e.Use(middleware.CORS()) - // Main page + // Routes e.GET("/", func(c echo.Context) error { currentIP, _ := getCurrentIP() - var records []templates.DNSRecord isConfigured := config.ApiToken != "" && config.ZoneID != "" @@ -310,8 +364,8 @@ func main() { records, _ = getDNSRecords(config.ZoneID) } - component := templates.Index(templates.IndexProps{ - Title: "mz.uy DNS Manager", + return templates.Render(c.Response(), templates.Index(templates.IndexProps{ + Title: "DNS Manager", IsConfigured: isConfigured, CurrentIP: currentIP, Config: templates.ConfigData{ @@ -322,145 +376,126 @@ func main() { }, Records: records, UpdateFreqs: getUpdateFrequencies(), - }) - return templates.Render(c.Response(), component) + })) }) - // Refresh current IP e.GET("/refresh-ip", func(c echo.Context) error { ip, err := getCurrentIP() if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to get current IP") - return c.String(http.StatusInternalServerError, "Error") + return errorResponse(c, "Failed to get current IP") } - return c.String(http.StatusOK, ip) + return c.HTML(http.StatusOK, fmt.Sprintf(`%s`, ip)) }) - // Configuration e.POST("/config", func(c echo.Context) error { - apiToken := c.FormValue("api_token") - zoneID := c.FormValue("zone_id") - domain := c.FormValue("domain") - updatePeriod := c.FormValue("update_period") + apiToken := sanitizeInput(c.FormValue("api_token")) + zoneID := sanitizeInput(c.FormValue("zone_id")) + domain := sanitizeInput(c.FormValue("domain")) + updatePeriod := sanitizeInput(c.FormValue("update_period")) if apiToken == "" || zoneID == "" || domain == "" { - c.Response().Header().Set("HX-Error-Message", "Please fill all required fields") - return c.String(http.StatusBadRequest, "Invalid input") + return errorResponse(c, "Please fill all required fields") } - err := queries.UpsertConfig(context.Background(), db.UpsertConfigParams{ + // Save config + queries.DeleteAllConfig(context.Background()) + err := queries.InsertConfig(context.Background(), db.InsertConfigParams{ ApiToken: apiToken, ZoneID: zoneID, Domain: domain, UpdatePeriod: updatePeriod, }) if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to save configuration") - return c.String(http.StatusInternalServerError, "Database error") + return errorResponse(c, "Failed to save configuration") } + // Update global config config.ApiToken = apiToken config.ZoneID = zoneID config.Domain = domain config.UpdatePeriod = updatePeriod - if err := initCloudflare(config.ApiToken, config.ZoneID); err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to initialize Cloudflare client") - return c.String(http.StatusInternalServerError, "API error") - } + // Reinitialize Cloudflare + initCloudflare(apiToken) + scheduleUpdates(zoneID, updatePeriod) - if err := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to schedule updates") - return c.String(http.StatusInternalServerError, "Scheduler error") - } - - c.Response().Header().Set("HX-Success-Message", "Configuration saved successfully") - return c.Redirect(http.StatusSeeOther, "/") + return templates.Render(c.Response(), templates.ConfigStatus(templates.ConfigData{ + ZoneID: zoneID, + Domain: domain, + UpdatePeriod: updatePeriod, + ApiToken: apiToken, + })) }) - // Create DNS record - e.POST("/records", func(c echo.Context) error { - if config.ApiToken == "" || config.ZoneID == "" { - c.Response().Header().Set("HX-Error-Message", "API not configured") - return c.String(http.StatusBadRequest, "Not configured") - } + e.GET("/config", func(c echo.Context) error { + return templates.Render(c.Response(), templates.ConfigModal(templates.ConfigData{ + ZoneID: config.ZoneID, + Domain: config.Domain, + UpdatePeriod: config.UpdatePeriod, + ApiToken: config.ApiToken, + }, getUpdateFrequencies())) + }) - name := c.FormValue("name") - recordType := c.FormValue("type") - content := c.FormValue("content") - ttlStr := c.FormValue("ttl") + e.GET("/records/new", func(c echo.Context) error { + return templates.Render(c.Response(), templates.RecordForm("Add DNS Record", "", config.Domain, templates.DNSRecord{Type: "A", TTL: 1})) + }) + + e.POST("/records", func(c echo.Context) error { + name := sanitizeInput(c.FormValue("name")) + recordType := sanitizeInput(c.FormValue("type")) + content := sanitizeInput(c.FormValue("content")) + ttlStr := sanitizeInput(c.FormValue("ttl")) proxied := c.FormValue("proxied") == "on" useMyIP := c.FormValue("use_my_ip") == "on" - if name == "" { - c.Response().Header().Set("HX-Error-Message", "Name is required") - return c.String(http.StatusBadRequest, "Invalid input") - } - - ttl, err := strconv.Atoi(ttlStr) - if err != nil { - ttl = 1 - } - if useMyIP { currentIP, err := getCurrentIP() if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to get current IP") - return c.String(http.StatusInternalServerError, "IP error") + return errorResponse(c, "Failed to get current IP") } content = currentIP } - if content == "" { - c.Response().Header().Set("HX-Error-Message", "Content is required") - return c.String(http.StatusBadRequest, "Invalid input") + ttl, _ := strconv.Atoi(ttlStr) + if ttl == 0 { + ttl = 1 } - err = createDNSRecord(config.ZoneID, config.Domain, name, recordType, content, ttl, proxied) - if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to create DNS record") - return c.String(http.StatusInternalServerError, "DNS error") + if err := createDNSRecord(config.ZoneID, config.Domain, name, recordType, content, ttl, proxied); err != nil { + return errorResponse(c, err.Error()) } - c.Response().Header().Set("HX-Success-Message", "DNS record created successfully") - - // Return updated records table + // Return updated table records, _ := getDNSRecords(config.ZoneID) currentIP, _ := getCurrentIP() - component := templates.DNSRecordsTable(records, currentIP) - return templates.Render(c.Response(), component) + notification := templates.SuccessNotification("DNS record created") + table := templates.DNSRecordsTable(records, currentIP) + return templates.RenderMultiple(c.Response().Writer, notification, table) }) - // Update DNS record e.PUT("/records/:id", func(c echo.Context) error { - if config.ApiToken == "" || config.ZoneID == "" { - c.Response().Header().Set("HX-Error-Message", "API not configured") - return c.String(http.StatusBadRequest, "Not configured") - } - id := c.Param("id") - name := c.FormValue("name") - recordType := c.FormValue("type") - content := c.FormValue("content") - ttlStr := c.FormValue("ttl") + name := sanitizeInput(c.FormValue("name")) + recordType := sanitizeInput(c.FormValue("type")) + content := sanitizeInput(c.FormValue("content")) + ttlStr := sanitizeInput(c.FormValue("ttl")) proxied := c.FormValue("proxied") == "on" useMyIP := c.FormValue("use_my_ip") == "on" - ttl, err := strconv.Atoi(ttlStr) - if err != nil { - ttl = 1 - } - if useMyIP { currentIP, err := getCurrentIP() if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to get current IP") - return c.String(http.StatusInternalServerError, "IP error") + return errorResponse(c, "Failed to get current IP") } content = currentIP } - // Convert name to full domain name if needed + ttl, _ := strconv.Atoi(ttlStr) + if ttl == 0 { + ttl = 1 + } + + // Convert name to full domain fullName := name if name != "@" && !strings.HasSuffix(name, config.Domain) { fullName = name + "." + config.Domain @@ -469,51 +504,36 @@ func main() { fullName = config.Domain } - err = updateDNSRecord(config.ZoneID, id, fullName, recordType, content, ttl, proxied) - if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to update DNS record") - return c.String(http.StatusInternalServerError, "DNS error") + if err := updateDNSRecord(config.ZoneID, id, fullName, recordType, content, ttl, proxied); err != nil { + return errorResponse(c, err.Error()) } - c.Response().Header().Set("HX-Success-Message", "DNS record updated successfully") - - // Return updated records table records, _ := getDNSRecords(config.ZoneID) currentIP, _ := getCurrentIP() - component := templates.DNSRecordsTable(records, currentIP) - return templates.Render(c.Response(), component) + notification := templates.SuccessNotification("DNS record updated") + table := templates.DNSRecordsTable(records, currentIP) + return templates.RenderMultiple(c.Response().Writer, notification, table) }) - // Delete DNS record e.DELETE("/records/:id", func(c echo.Context) error { - if config.ApiToken == "" || config.ZoneID == "" { - c.Response().Header().Set("HX-Error-Message", "API not configured") - return c.String(http.StatusBadRequest, "Not configured") - } - id := c.Param("id") - err := deleteDNSRecord(config.ZoneID, id) - if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to delete DNS record") - return c.String(http.StatusInternalServerError, "DNS error") + + if err := deleteDNSRecord(config.ZoneID, id); err != nil { + return errorResponse(c, "Failed to delete record") } - c.Response().Header().Set("HX-Success-Message", "DNS record deleted successfully") - return c.String(http.StatusOK, "") + records, _ := getDNSRecords(config.ZoneID) + currentIP, _ := getCurrentIP() + notification := templates.SuccessNotification("DNS record deleted") + table := templates.DNSRecordsTable(records, currentIP) + return templates.RenderMultiple(c.Response().Writer, notification, table) }) - // Edit record form e.GET("/edit-record/:id", func(c echo.Context) error { - if config.ApiToken == "" || config.ZoneID == "" { - c.Response().Header().Set("HX-Error-Message", "API not configured") - return c.String(http.StatusBadRequest, "Not configured") - } - id := c.Param("id") records, err := getDNSRecords(config.ZoneID) if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to load DNS records") - return c.String(http.StatusInternalServerError, "DNS error") + return errorResponse(c, "Failed to load records") } var record templates.DNSRecord @@ -525,36 +545,24 @@ func main() { } if record.ID == "" { - c.Response().Header().Set("HX-Error-Message", "Record not found") - return c.String(http.StatusNotFound, "Not found") + return errorResponse(c, "Record not found") } - component := templates.RecordForm("Edit DNS Record", id, config.Domain, record) - return templates.Render(c.Response(), component) + return templates.Render(c.Response(), templates.RecordForm("Edit DNS Record", id, config.Domain, record)) }) - // Update all records with current IP e.POST("/update-all-records", func(c echo.Context) error { - if config.ApiToken == "" || config.ZoneID == "" { - c.Response().Header().Set("HX-Error-Message", "API not configured") - return c.String(http.StatusBadRequest, "Not configured") + if err := updateAllRecordsWithCurrentIP(config.ZoneID); err != nil { + return errorResponse(c, "Failed to update records") } - err := updateAllRecordsWithCurrentIP(config.ZoneID) - if err != nil { - c.Response().Header().Set("HX-Error-Message", "Failed to update records") - return c.String(http.StatusInternalServerError, "Update error") - } - - c.Response().Header().Set("HX-Success-Message", "All A records updated with current IP") - - // Return updated records table records, _ := getDNSRecords(config.ZoneID) currentIP, _ := getCurrentIP() - component := templates.DNSRecordsTable(records, currentIP) - return templates.Render(c.Response(), component) + notification := templates.SuccessNotification("All A records updated") + table := templates.DNSRecordsTable(records, currentIP) + return templates.RenderMultiple(c.Response().Writer, notification, table) }) - log.Println("Starting server on http://localhost:3000") + log.Println("Starting server on :3000") log.Fatal(e.Start(":3000")) } diff --git a/templates/alert.templ b/templates/alert.templ new file mode 100644 index 0000000..2f80f44 --- /dev/null +++ b/templates/alert.templ @@ -0,0 +1,34 @@ + +package templates + +templ Alert(id, alertType, message string, dismissible bool) { + +} + +// Helper components for common alert types +templ ErrorAlert(id, message string) { + @Alert(id, "danger", message, false) +} + +templ SuccessAlert(id, message string) { + @Alert(id, "success", message, true) +} + +templ WarningAlert(id, message string) { + @Alert(id, "warning", message, true) +} + +templ InfoAlert(id, message string) { + @Alert(id, "info", message, true) +} diff --git a/templates/alert_templ.go b/templates/alert_templ.go new file mode 100644 index 0000000..83efe2d --- /dev/null +++ b/templates/alert_templ.go @@ -0,0 +1,214 @@ +// Code generated by templ - DO NOT EDIT. + +// templ: version: v0.3.898 + +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" + +func Alert(id, alertType, message string, dismissible bool) 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) + var templ_7745c5c3_Var2 = []any{"alert", "alert-" + alertType, templ.KV("alert-dismissible", dismissible), templ.KV("d-none", message == "")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if message != "" { + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(message) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/alert.templ`, Line: 11, Col: 12} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + if dismissible { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +// Helper components for common alert types +func ErrorAlert(id, message 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_Var6 := templ.GetChildren(ctx) + if templ_7745c5c3_Var6 == nil { + templ_7745c5c3_Var6 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Alert(id, "danger", message, false).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func SuccessAlert(id, message 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_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Alert(id, "success", message, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func WarningAlert(id, message 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_Var8 := templ.GetChildren(ctx) + if templ_7745c5c3_Var8 == nil { + templ_7745c5c3_Var8 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Alert(id, "warning", message, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func InfoAlert(id, message 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_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Alert(id, "info", message, true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +var _ = templruntime.GeneratedTemplate diff --git a/templates/index.templ b/templates/index.templ index b5f84d7..86b942b 100644 --- a/templates/index.templ +++ b/templates/index.templ @@ -43,17 +43,16 @@ templ Index(props IndexProps) {

{ props.Title }

Current IP: - { props.CurrentIP } - +
if !props.IsConfigured { @@ -62,12 +61,9 @@ templ Index(props IndexProps) { @ConfigStatus(props.Config) @DNSRecordsSection(props.Records, props.CurrentIP) } - @ConfigModal(props.Config, props.UpdateFreqs) - @RecordModal(props.Config.Domain) -
} } @@ -86,16 +82,34 @@ templ ConfigWarning() { } templ ConfigStatus(config ConfigData) { -
-
+
+
Configuration
- + + +
@@ -118,25 +132,57 @@ templ DNSRecordsSection(records []DNSRecord, currentIP string) {
DNS Records
-
- - + + - Add Record - + + +
@@ -174,7 +220,7 @@ templ DNSRecordsTable(records []DNSRecord, currentIP string) { } templ DNSRecordRow(record DNSRecord, currentIP string) { - + { record.Type } { record.Name } @@ -200,23 +246,46 @@ templ DNSRecordRow(record DNSRecord, currentIP string) { } - - + + } diff --git a/templates/index_templ.go b/templates/index_templ.go index 683c94b..fc95a30 100644 --- a/templates/index_templ.go +++ b/templates/index_templ.go @@ -88,20 +88,20 @@ func Index(props IndexProps) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Current IP: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Current IP: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(props.CurrentIP) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 46, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 46, Col: 69} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -124,15 +124,7 @@ func Index(props IndexProps) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = ConfigModal(props.Config, props.UpdateFreqs).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = RecordModal(props.Config.Domain).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -196,14 +188,14 @@ func ConfigStatus(config ConfigData) templ.Component { templ_7745c5c3_Var6 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Configuration
Domain: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
Configuration
Domain: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(config.Domain) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 103, Col: 51} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 117, Col: 51} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -216,7 +208,7 @@ func ConfigStatus(config ConfigData) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(config.ZoneID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 106, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 120, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -229,7 +221,7 @@ func ConfigStatus(config ConfigData) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(formatUpdateSchedule(config.UpdatePeriod)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 110, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 124, Col: 54} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -264,7 +256,7 @@ func DNSRecordsSection(records []DNSRecord, currentIP string) templ.Component { templ_7745c5c3_Var10 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
DNS Records
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
DNS Records
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -347,14 +339,14 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component { templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(record.Type) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 178, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 224, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -367,7 +359,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(record.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 179, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 225, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -380,7 +372,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(record.Content) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 181, Col: 19} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 227, Col: 19} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -416,7 +408,7 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%ds", record.TTL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 194, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 240, Col: 36} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -433,46 +425,46 @@ func DNSRecordRow(record DNSRecord, currentIP string) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" @ajax:after=\"deleteLoading = false\" @ajax:error=\"deleteLoading = false\" @ajax:success=\"$el.closest('tr').remove()\" style=\"display: inline;\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/layout.templ b/templates/layout.templ index b4918e4..4d4f62d 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -8,6 +8,16 @@ func Render(w io.Writer, component templ.Component) error { return component.Render(context.Background(), w) } +// RenderMultiple renders multiple components to an io.Writer in sequence +func RenderMultiple(w io.Writer, components ...templ.Component) error { + for _, component := range components { + if err := component.Render(context.Background(), w); err != nil { + return err + } + } + return nil +} + // Layout is the base layout for all pages templ Layout(title string) { @@ -16,6 +26,8 @@ templ Layout(title string) { { title } + + - + - + +
+
    +
    { children... } - - + +
    +
    } diff --git a/templates/layout_templ.go b/templates/layout_templ.go index 7edbbae..7782eb7 100644 --- a/templates/layout_templ.go +++ b/templates/layout_templ.go @@ -16,6 +16,16 @@ func Render(w io.Writer, component templ.Component) error { return component.Render(context.Background(), w) } +// RenderMultiple renders multiple components to an io.Writer in sequence +func RenderMultiple(w io.Writer, components ...templ.Component) error { + for _, component := range components { + if err := component.Render(context.Background(), w); err != nil { + return err + } + } + return nil +} + // 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) { @@ -45,13 +55,13 @@ func Layout(title string) templ.Component { 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} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 28, 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, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -59,7 +69,7 @@ func Layout(title string) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
      ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/modal.templ b/templates/modal.templ index e04f715..da916c2 100644 --- a/templates/modal.templ +++ b/templates/modal.templ @@ -3,253 +3,314 @@ package templates import "fmt" templ ConfigModal(config ConfigData, frequencies []UpdateFrequency) { -