dev: automated commit - 2025-06-16 09:37:04
This commit is contained in:
parent
682f25edcd
commit
d71a7e37c5
7 changed files with 1513 additions and 430 deletions
427
main.go
427
main.go
|
@ -8,6 +8,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -21,25 +22,6 @@ import (
|
|||
"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
|
||||
|
@ -66,7 +48,6 @@ func initDatabase() (*sql.DB, error) {
|
|||
}
|
||||
|
||||
queries = db.New(sqlDB)
|
||||
|
||||
return sqlDB, nil
|
||||
}
|
||||
|
||||
|
@ -99,7 +80,7 @@ func getCurrentIP() (string, error) {
|
|||
return string(ip), nil
|
||||
}
|
||||
|
||||
func getDNSRecords(zoneID string) ([]DNSRecord, error) {
|
||||
func getDNSRecords(zoneID string) ([]templates.DNSRecord, error) {
|
||||
if api == nil {
|
||||
return nil, fmt.Errorf("cloudflare API not initialized")
|
||||
}
|
||||
|
@ -112,9 +93,9 @@ func getDNSRecords(zoneID string) ([]DNSRecord, error) {
|
|||
return nil, fmt.Errorf("failed to get DNS records: %w", err)
|
||||
}
|
||||
|
||||
var records []DNSRecord
|
||||
var records []templates.DNSRecord
|
||||
for _, rec := range recs {
|
||||
records = append(records, DNSRecord{
|
||||
records = append(records, templates.DNSRecord{
|
||||
ID: rec.ID,
|
||||
Type: rec.Type,
|
||||
Name: rec.Name,
|
||||
|
@ -271,6 +252,18 @@ func scheduleUpdates(zoneID, updatePeriod string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func getUpdateFrequencies() []templates.UpdateFrequency {
|
||||
return []templates.UpdateFrequency{
|
||||
{Label: "Every 1 minute", 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: ""},
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
sqlDB, err := initDatabase()
|
||||
if err != nil {
|
||||
|
@ -306,191 +299,259 @@ func main() {
|
|||
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 != "",
|
||||
// Main page
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
currentIP, _ := getCurrentIP()
|
||||
|
||||
var records []templates.DNSRecord
|
||||
isConfigured := config.ApiToken != "" && config.ZoneID != ""
|
||||
|
||||
if isConfigured {
|
||||
records, _ = getDNSRecords(config.ZoneID)
|
||||
}
|
||||
|
||||
component := templates.Index(templates.IndexProps{
|
||||
Title: "mz.uy DNS Manager",
|
||||
IsConfigured: isConfigured,
|
||||
CurrentIP: currentIP,
|
||||
Config: templates.ConfigData{
|
||||
ZoneID: config.ZoneID,
|
||||
Domain: config.Domain,
|
||||
UpdatePeriod: config.UpdatePeriod,
|
||||
}
|
||||
return c.JSON(http.StatusOK, configStatus)
|
||||
ApiToken: config.ApiToken,
|
||||
},
|
||||
Records: records,
|
||||
UpdateFreqs: getUpdateFrequencies(),
|
||||
})
|
||||
return templates.Render(c.Response(), component)
|
||||
})
|
||||
|
||||
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"`
|
||||
}
|
||||
// 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 c.String(http.StatusOK, ip)
|
||||
})
|
||||
|
||||
if err := c.Bind(&newConfig); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid request"})
|
||||
}
|
||||
// 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")
|
||||
|
||||
err := queries.UpsertConfig(context.Background(), db.UpsertConfigParams{
|
||||
ApiToken: newConfig.APIToken,
|
||||
ZoneID: newConfig.ZoneID,
|
||||
Domain: newConfig.Domain,
|
||||
UpdatePeriod: newConfig.UpdatePeriod,
|
||||
})
|
||||
if apiToken == "" || zoneID == "" || domain == "" {
|
||||
c.Response().Header().Set("HX-Error-Message", "Please fill all required fields")
|
||||
return c.String(http.StatusBadRequest, "Invalid input")
|
||||
}
|
||||
|
||||
err := queries.UpsertConfig(context.Background(), db.UpsertConfigParams{
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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, "/")
|
||||
})
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
name := c.FormValue("name")
|
||||
recordType := c.FormValue("type")
|
||||
content := c.FormValue("content")
|
||||
ttlStr := 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 {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to save configuration: %v", err)})
|
||||
c.Response().Header().Set("HX-Error-Message", "Failed to get current IP")
|
||||
return c.String(http.StatusInternalServerError, "IP error")
|
||||
}
|
||||
content = currentIP
|
||||
}
|
||||
|
||||
config.ApiToken = newConfig.APIToken
|
||||
config.ZoneID = newConfig.ZoneID
|
||||
config.Domain = newConfig.Domain
|
||||
config.UpdatePeriod = newConfig.UpdatePeriod
|
||||
if content == "" {
|
||||
c.Response().Header().Set("HX-Error-Message", "Content is required")
|
||||
return c.String(http.StatusBadRequest, "Invalid input")
|
||||
}
|
||||
|
||||
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)})
|
||||
}
|
||||
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 := scheduleUpdates(config.ZoneID, config.UpdatePeriod); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to schedule updates: %v", err)})
|
||||
}
|
||||
c.Response().Header().Set("HX-Success-Message", "DNS record created successfully")
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "Configuration updated successfully"})
|
||||
})
|
||||
// Return updated records table
|
||||
records, _ := getDNSRecords(config.ZoneID)
|
||||
currentIP, _ := getCurrentIP()
|
||||
component := templates.DNSRecordsTable(records, currentIP)
|
||||
return templates.Render(c.Response(), component)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
// 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")
|
||||
}
|
||||
|
||||
apiGroup.GET("/current-ip", func(c echo.Context) error {
|
||||
ip, err := getCurrentIP()
|
||||
id := c.Param("id")
|
||||
name := c.FormValue("name")
|
||||
recordType := c.FormValue("type")
|
||||
content := c.FormValue("content")
|
||||
ttlStr := 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 {
|
||||
return c.JSON(http.StatusInternalServerError, ErrorResponse{Error: fmt.Sprintf("Failed to get current IP: %v", err)})
|
||||
c.Response().Header().Set("HX-Error-Message", "Failed to get current IP")
|
||||
return c.String(http.StatusInternalServerError, "IP error")
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]string{"ip": ip})
|
||||
})
|
||||
content = currentIP
|
||||
}
|
||||
|
||||
apiGroup.GET("/records", func(c echo.Context) error {
|
||||
if config.ApiToken == "" || config.ZoneID == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||
// Convert name to full domain name if needed
|
||||
fullName := name
|
||||
if name != "@" && !strings.HasSuffix(name, config.Domain) {
|
||||
fullName = name + "." + config.Domain
|
||||
}
|
||||
if name == "@" {
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
c.Response().Header().Set("HX-Success-Message", "DNS record deleted successfully")
|
||||
return c.String(http.StatusOK, "")
|
||||
})
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
var record templates.DNSRecord
|
||||
for _, r := range records {
|
||||
if r.ID == id {
|
||||
record = r
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
if record.ID == "" {
|
||||
c.Response().Header().Set("HX-Error-Message", "Record not found")
|
||||
return c.String(http.StatusNotFound, "Not found")
|
||||
}
|
||||
|
||||
apiGroup.POST("/records", func(c echo.Context) error {
|
||||
if config.ApiToken == "" || config.ZoneID == "" {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "API not configured"})
|
||||
}
|
||||
component := templates.RecordForm("Edit DNS Record", id, config.Domain, record)
|
||||
return templates.Render(c.Response(), component)
|
||||
})
|
||||
|
||||
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"`
|
||||
}
|
||||
// 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 := c.Bind(&record); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, ErrorResponse{Error: "Invalid request"})
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
c.Response().Header().Set("HX-Success-Message", "All A records updated with current 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 updated records table
|
||||
records, _ := getDNSRecords(config.ZoneID)
|
||||
currentIP, _ := getCurrentIP()
|
||||
component := templates.DNSRecordsTable(records, currentIP)
|
||||
return templates.Render(c.Response(), component)
|
||||
})
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue