package handler import ( "context" "database/sql" "errors" "log/slog" "net/http" "strconv" cf "goflare/internal/cloudflare" "goflare/internal/database/queries" "github.com/labstack/echo/v4" ) type RecordHandler struct { q *queries.Queries } func NewRecordHandler(q *queries.Queries) *RecordHandler { return &RecordHandler{q: q} } type recordRequest struct { ZoneID int64 `json:"zone_id"` CfRecordID string `json:"cf_record_id"` Name string `json:"name"` Type string `json:"type"` Content string `json:"content"` Proxied bool `json:"proxied"` IsStatic bool `json:"is_static"` } func boolToInt64(b bool) int64 { if b { return 1 } return 0 } // pushRecordToCloudflare updates the DNS record on Cloudflare to match the local record. func (h *RecordHandler) pushRecordToCloudflare(ctx context.Context, record queries.Record) error { zone, err := h.q.GetZone(ctx, record.ZoneID) if err != nil { return err } return cf.UpdateDNSRecord(ctx, zone.ApiKey, zone.ZoneID, record.CfRecordID, cf.DNSRecord{ Type: record.Type, Name: record.Name, Content: record.Content, Proxied: record.Proxied == 1, TTL: 1, // auto }) } func (h *RecordHandler) List(c echo.Context) error { zoneIDStr := c.QueryParam("zone_id") if zoneIDStr != "" { zoneID, err := strconv.ParseInt(zoneIDStr, 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid zone_id"}) } records, err := h.q.ListRecordsByZone(c.Request().Context(), zoneID) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list records"}) } if records == nil { records = []queries.ListRecordsByZoneRow{} } return c.JSON(http.StatusOK, records) } records, err := h.q.ListRecords(c.Request().Context()) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list records"}) } if records == nil { records = []queries.ListRecordsRow{} } return c.JSON(http.StatusOK, records) } func (h *RecordHandler) Get(c echo.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) } record, err := h.q.GetRecord(c.Request().Context(), id) if errors.Is(err, sql.ErrNoRows) { return c.JSON(http.StatusNotFound, map[string]string{"error": "record not found"}) } if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get record"}) } return c.JSON(http.StatusOK, record) } func (h *RecordHandler) Create(c echo.Context) error { var req recordRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } if req.ZoneID == 0 || req.CfRecordID == "" || req.Name == "" || req.Content == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "zone_id, cf_record_id, name, and content required"}) } if req.Type == "" { req.Type = "A" } record, err := h.q.CreateRecord(c.Request().Context(), queries.CreateRecordParams{ ZoneID: req.ZoneID, CfRecordID: req.CfRecordID, Name: req.Name, Type: req.Type, Content: req.Content, Proxied: boolToInt64(req.Proxied), IsStatic: boolToInt64(req.IsStatic), }) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create record"}) } return c.JSON(http.StatusCreated, record) } func (h *RecordHandler) Update(c echo.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) } var req recordRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } if req.Name == "" || req.Content == "" { return c.JSON(http.StatusBadRequest, map[string]string{"error": "name and content required"}) } if req.Type == "" { req.Type = "A" } ctx := c.Request().Context() record, err := h.q.UpdateRecord(ctx, queries.UpdateRecordParams{ ID: id, Name: req.Name, Type: req.Type, Content: req.Content, Proxied: boolToInt64(req.Proxied), IsStatic: boolToInt64(req.IsStatic), }) if errors.Is(err, sql.ErrNoRows) { return c.JSON(http.StatusNotFound, map[string]string{"error": "record not found"}) } if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update record"}) } if err := h.pushRecordToCloudflare(ctx, record); err != nil { slog.Error("failed to update record on Cloudflare", "record_id", record.ID, "error", err) return c.JSON(http.StatusBadGateway, map[string]string{"error": "record updated locally but failed to update on Cloudflare: " + err.Error()}) } return c.JSON(http.StatusOK, record) } func (h *RecordHandler) Delete(c echo.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) } if err := h.q.DeleteRecord(c.Request().Context(), id); err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to delete record"}) } return c.JSON(http.StatusOK, map[string]string{"message": "record deleted"}) } type partialRecordRequest struct { Name *string `json:"name"` Type *string `json:"type"` Content *string `json:"content"` Proxied *bool `json:"proxied"` IsStatic *bool `json:"is_static"` } func (h *RecordHandler) PartialUpdate(c echo.Context) error { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"}) } var req partialRecordRequest if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request body"}) } ctx := c.Request().Context() existing, err := h.q.GetRecord(ctx, id) if errors.Is(err, sql.ErrNoRows) { return c.JSON(http.StatusNotFound, map[string]string{"error": "record not found"}) } if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to get record"}) } params := queries.UpdateRecordParams{ ID: id, Name: existing.Name, Type: existing.Type, Content: existing.Content, Proxied: existing.Proxied, IsStatic: existing.IsStatic, } if req.Name != nil { params.Name = *req.Name } if req.Type != nil { params.Type = *req.Type } if req.Content != nil { params.Content = *req.Content } if req.Proxied != nil { params.Proxied = boolToInt64(*req.Proxied) } if req.IsStatic != nil { params.IsStatic = boolToInt64(*req.IsStatic) } record, err := h.q.UpdateRecord(ctx, params) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to update record"}) } if err := h.pushRecordToCloudflare(ctx, record); err != nil { slog.Error("failed to update record on Cloudflare", "record_id", record.ID, "error", err) return c.JSON(http.StatusBadGateway, map[string]string{"error": "record updated locally but failed to update on Cloudflare: " + err.Error()}) } return c.JSON(http.StatusOK, record) } func (h *RecordHandler) Sync(c echo.Context) error { ctx := c.Request().Context() zones, err := h.q.ListZones(ctx) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to list zones"}) } var synced int for _, zone := range zones { cfRecords, err := cf.ListDNSRecords(ctx, zone.ApiKey, zone.ZoneID) if err != nil { slog.Error("failed to fetch CF records", "zone", zone.Name, "error", err) continue } cfIDs := make(map[string]struct{}, len(cfRecords)) for _, rec := range cfRecords { cfIDs[rec.ID] = struct{}{} var proxied int64 if rec.Proxied { proxied = 1 } _, err := h.q.UpsertRecord(ctx, queries.UpsertRecordParams{ ZoneID: zone.ID, CfRecordID: rec.ID, Name: rec.Name, Type: rec.Type, Content: rec.Content, Proxied: proxied, IsStatic: 1, }) if err != nil { slog.Error("failed to upsert record", "name", rec.Name, "error", err) continue } synced++ } localIDs, err := h.q.ListRecordCfIDsByZone(ctx, zone.ID) if err != nil { slog.Error("failed to list local record IDs", "zone", zone.Name, "error", err) continue } for _, localID := range localIDs { if _, exists := cfIDs[localID]; !exists { _ = h.q.DeleteRecordByCfID(ctx, queries.DeleteRecordByCfIDParams{ ZoneID: zone.ID, CfRecordID: localID, }) } } } return c.JSON(http.StatusOK, map[string]int{"synced": synced}) }