generated from CubeCraft-Creations/Tracehound
117 lines
3.4 KiB
Go
117 lines
3.4 KiB
Go
|
|
// Package api provides HTTP handlers for camera operations.
|
||
|
|
package api
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// maxRequestBody is the maximum accepted JSON body size (64KB).
|
||
|
|
const maxRequestBody = 64 * 1024
|
||
|
|
|
||
|
|
// APIError represents a structured API error response.
|
||
|
|
type APIError struct {
|
||
|
|
Error string `json:"error"`
|
||
|
|
Code int `json:"code"`
|
||
|
|
Details string `json:"details,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// validationConstraints defines field-level validation limits.
|
||
|
|
const (
|
||
|
|
maxCameraIDLen = 64
|
||
|
|
maxFriendlyNameLen = 128
|
||
|
|
maxModeLen = 32
|
||
|
|
maxResolutionLen = 32
|
||
|
|
minFPS = 0
|
||
|
|
maxFPS = 240
|
||
|
|
)
|
||
|
|
|
||
|
|
// respondError writes a structured JSON error response.
|
||
|
|
func respondError(w http.ResponseWriter, status int, msg string, details ...string) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
w.WriteHeader(status)
|
||
|
|
e := APIError{
|
||
|
|
Error: msg,
|
||
|
|
Code: status,
|
||
|
|
}
|
||
|
|
if len(details) > 0 {
|
||
|
|
e.Details = details[0]
|
||
|
|
}
|
||
|
|
json.NewEncoder(w).Encode(e)
|
||
|
|
}
|
||
|
|
|
||
|
|
// decodeJSONBody reads, limits, and decodes a JSON request body.
|
||
|
|
// Returns false if validation fails (response already written).
|
||
|
|
func decodeJSONBody(w http.ResponseWriter, r *http.Request, v interface{}) bool {
|
||
|
|
// Validate Content-Type
|
||
|
|
ct := r.Header.Get("Content-Type")
|
||
|
|
if ct != "" && !strings.HasPrefix(ct, "application/json") {
|
||
|
|
respondError(w, http.StatusUnsupportedMediaType, "content-type must be application/json")
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Limit body size
|
||
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBody)
|
||
|
|
|
||
|
|
body, err := io.ReadAll(r.Body)
|
||
|
|
if err != nil {
|
||
|
|
respondError(w, http.StatusBadRequest, "request body too large or unreadable", err.Error())
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := json.Unmarshal(body, v); err != nil {
|
||
|
|
respondError(w, http.StatusBadRequest, "invalid request body", err.Error())
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// validateCameraID checks that cameraID is present and within max length.
|
||
|
|
func validateCameraID(w http.ResponseWriter, cameraID string) bool {
|
||
|
|
if cameraID == "" {
|
||
|
|
respondError(w, http.StatusBadRequest, "camera_id is required")
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if len(cameraID) > maxCameraIDLen {
|
||
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("camera_id must be at most %d characters", maxCameraIDLen))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// validateCameraRegistration validates fields for POST /cameras.
|
||
|
|
func validateCameraRegistration(w http.ResponseWriter, cameraID, friendlyName string) bool {
|
||
|
|
if !validateCameraID(w, cameraID) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if friendlyName == "" {
|
||
|
|
respondError(w, http.StatusBadRequest, "friendly_name is required")
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if len(friendlyName) > maxFriendlyNameLen {
|
||
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("friendly_name must be at most %d characters", maxFriendlyNameLen))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// validateStatusFields validates optional fields on the PushStatus payload.
|
||
|
|
func validateStatusFields(w http.ResponseWriter, mode, resolution string, fps int) bool {
|
||
|
|
if mode != "" && len(mode) > maxModeLen {
|
||
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("mode must be at most %d characters", maxModeLen))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if resolution != "" && len(resolution) > maxResolutionLen {
|
||
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("resolution must be at most %d characters", maxResolutionLen))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if fps < minFPS || fps > maxFPS {
|
||
|
|
respondError(w, http.StatusBadRequest, fmt.Sprintf("fps must be between %d and %d", minFPS, maxFPS))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|