- LabelHandler: POST /api/labels/:deviceID/print with 1/s rate limit (T-04-09) - USBEventsHandler: GET /api/usb/events SSE stream, exits on context cancel (T-04-11) - router.go: two new parameters + routes wired - main.go: USB Manager started with ctx, MockDriver connected, handlers passed to router
125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
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"})
|
|
}
|