package handlers import ( "context" "net/http" "strconv" "sync" "time" "github.com/go-chi/chi/v5" "git.georgsen.dk/hwlab/internal/labels" "git.georgsen.dk/hwlab/internal/netbox" "git.georgsen.dk/hwlab/internal/printer" ) // LabelNetBoxClient is the narrow interface LabelHandler needs from NetBox. // *netbox.Client satisfies this interface. type LabelNetBoxClient interface { GetDevice(ctx context.Context, id int) (*netbox.Device, error) } // LabelPrinter is the narrow interface over printer.PrinterDriver that the // label handler requires. Allows mock injection in tests. type LabelPrinter interface { Print(bitmap []byte, width, height int) error } // LabelHandler handles POST /api/labels/:deviceID/print. // T-04-09 mitigation: print calls are rate-limited to 1/second per instance // to prevent runaway label waste. type LabelHandler struct { nb LabelNetBoxClient printer LabelPrinter // rate limiting (T-04-09 — DoS / label waste mitigation) mu sync.Mutex lastPrintTime time.Time printCooldown time.Duration } // NewLabelHandler constructs a LabelHandler with a 1-second print cooldown. func NewLabelHandler(nb LabelNetBoxClient, p LabelPrinter) *LabelHandler { return &LabelHandler{ nb: nb, printer: p, printCooldown: time.Second, } } // PrintLabel handles POST /api/labels/:deviceID/print. // // Flow: // 1. Parse deviceID path param // 2. Rate-limit check (1/second — T-04-09) // 3. Fetch device from NetBox // 4. Render label via labels.RenderCable or labels.RenderStandard // 5. Convert to 1-bit bitmap via printer.ImageToRawBitmap // 6. Send to printer // 7. Return 200 {"status":"printed"} func (h *LabelHandler) PrintLabel(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "deviceID") id, err := strconv.Atoi(idStr) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid device ID"}) return } // T-04-09: rate limit — reject if last print was less than 1 second ago. h.mu.Lock() now := time.Now() if !h.lastPrintTime.IsZero() && now.Sub(h.lastPrintTime) < h.printCooldown { h.mu.Unlock() writeJSON(w, http.StatusTooManyRequests, map[string]string{"error": "print rate limit: wait 1 second between prints"}) return } h.lastPrintTime = now h.mu.Unlock() device, err := h.nb.GetDevice(r.Context(), id) if err != nil { writeJSON(w, http.StatusNotFound, map[string]string{"error": "device not found"}) return } // Choose renderer based on device type. var bitmap []byte var bWidth, bHeight int if labels.IsCableDevice(*device) { d := labels.CableLabelData{ HWID: device.CustomFields.HWID, Name: device.Name, TestDate: device.CustomFields.TestDate, // USBVersion/MaxSpeedGbps/MaxWatts: parsed from TestData in Phase 5. // Default values allow label to render without error before that. USBVersion: "Unknown", } labelImg, err := labels.RenderCable(d) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "render failed: " + err.Error()}) return } bitmap, bWidth, bHeight = printer.ImageToRawBitmap(labelImg) } else { d := labels.LabelData{ HWID: device.CustomFields.HWID, Name: device.Name, SpecLine: device.CustomFields.AINotes, } labelImg, err := labels.RenderStandard(d) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "render failed: " + err.Error()}) return } bitmap, bWidth, bHeight = printer.ImageToRawBitmap(labelImg) } if err := h.printer.Print(bitmap, bWidth, bHeight); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "print failed: " + err.Error()}) return } writeJSON(w, http.StatusOK, map[string]string{"status": "printed"}) }