Files
IoT/internal/service/api/handler/handler.go
T

93 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package handler — HTTP-обработчики IoT REST API (новый монолит).
//
// Отличие от старого кода: устройства — в PostgreSQL (таблица iot_devices),
// без k8s-клиента и CRD. Телеметрия — через iotpg, как раньше.
package handler
import (
"encoding/json"
"log/slog"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/gorilla/mux"
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/config"
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/store"
"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg"
)
// Handler — зависимости всех HTTP-обработчиков.
type Handler struct {
Devices *store.DeviceStore
IoTPG *iotpg.IoTPostgresStore
SQS *sqs.Client
Cfg *config.Config
Log *slog.Logger
// Version — версия бинарника (ldflags), StartedAt — для админ-статистики.
Version string
StartedAt time.Time
}
// writeJSON отправляет JSON-ответ с указанным статусом.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// maxRequestBodyBytes — максимум размера тела запроса (устройства/метаданные
// небольшие; 64 КиБ с запасом).
const maxRequestBodyBytes = 64 * 1024
// decodeJSON читает JSON-тело с лимитом размера и запретом неизвестных полей
// (ревью 2026-10-10). MaxBytesReader защищает от раздутых тел;
// DisallowUnknownFields ловит опечатки в полях — критично для Terraform (лучше
// 400 с понятной ошибкой, чем молча проигнорированное поле).
func decodeJSON(w http.ResponseWriter, r *http.Request, v any) error {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodyBytes)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
return dec.Decode(v)
}
// errResp возвращает структуру ошибки для JSON.
func errResp(msg string) map[string]string {
return map[string]string{"error": msg}
}
// pathVar читает переменную пути из gorilla/mux.
func pathVar(r *http.Request, key string) string {
return mux.Vars(r)[key]
}
// defaultNamespace — namespace, когда в пути он не задан.
const defaultNamespace = "default"
// namespaceFromPath возвращает {namespace} из URL (пусто → "default").
func namespaceFromPath(r *http.Request) string {
ns := pathVar(r, "namespace")
if ns == "" {
ns = defaultNamespace
}
return ns
}
// requireNamespace — namespace из URL + его валидация. При невалидном значении
// отвечает 400 и возвращает ok=false (вызывающий обязан выйти).
//
// Зачем единая точка: раньше проверка была ТОЛЬКО в CreateIoTDevice, а
// GET/DELETE/PATCH/telemetry пропускали любой namespace дальше (тест 2026-10-11:
// `GET /v1/namespaces/bad_ns/iot/devices` → 200, а чтение телеметрии с таким ns
// падало уже в хранилище с 500). Валидируем на входе у всех — единообразный 400.
func requireNamespace(w http.ResponseWriter, r *http.Request) (string, bool) {
ns := namespaceFromPath(r)
if msg := validateIdentifier(ns, "namespace", 56); msg != "" {
writeJSON(w, http.StatusBadRequest, errResp(msg))
return "", false
}
return ns, true
}