Commit Graph
34 Commits
Author SHA1 Message Date
Repinoid e429f11e7f chore(iot): убрать мёртвый mqtt_auth.go + constant-time админ-токен (Этап 5)
- Удалён internal/service/api/handler/mqtt_auth.go: методы MQTTAuth/MQTTAcl
  нигде не подключены (мёртвый код; при подключении небезопасен — доверял
  клиентскому clientid и разрешал subscribe на '#').
- admin.go: сравнение ADMIN_STATS_TOKEN через subtle.ConstantTimeCompare.
2026-10-10 19:14:16 +03:00
Repinoid faba28f824 fix(iot): replaceDSNDatabase (смешение тенантов) + guard namespace (Этап 4)
- replaceDSNDatabase: возвращает ОШИБКУ для key=value DSN (раньше молча отдавал
  исходный DSN → телеметрия всех тенантов ушла бы в management-БД). Query
  отделяется первым '?', поэтому sslrootcert=/path больше не ломает поиск БД.
- EnsureTenantDB: guard safeNamespaceRe (defense-in-depth, совпадает с API-валидацией);
  небезопасный namespace → ErrInvalidNamespace → в consumer классифицируется как
  постоянная ошибка (DLQ, не вечный ретрай).
2026-10-10 19:13:35 +03:00
Repinoid c3492cb0f1 fix(iot): JWT — обязательный exp, 401 вместо 403, guard тест-режима, JSON-ошибки (Этап 4)
- validateJWT: exp теперь ОБЯЗАТЕЛЕН (без него токен был вечным).
- middleware: невалидный/просроченный токен → 401 (было 403) — клиент/Terraform
  отличают «протух» от «чужой namespace» (там осталось 403).
- AUTH_TEST_MODE игнорируется при заданном JWT_HMAC_SECRET (иначе обходил подпись).
- Все ошибки middleware — application/json через authErr (были text/plain).
2026-10-10 19:12:48 +03:00
Repinoid 6ff5576e7d fix(iot): /ready отражает факт подписки MQTT (находка #3)
Было: main выставлял ready.SetBridge(true) ПЕРЕД bridge.Run, поэтому «соединён,
но подписка не прошла» давал /ready=200 и молча терял телеметрию до реконнекта.
Теперь bridge сам управляет флагом (колбэк onSubscribed): true только после
успешного SUBSCRIBE, false при потере соединения/сбое подписки.
2026-10-10 19:10:44 +03:00
Repinoid d7701f2845 fix(iot): ограничить повторы ядовитого сообщения (находка #2)
Было: при недоступной DLQ битое сообщение НЕ подтверждалось и крутилось на
основной очереди бесконечно. Теперь читается ApproximateReceiveCount; после
maxReceiveCount=5 битое сообщение подтверждается (дроп с громким логом), а не
зацикливается. При доступной DLQ — уходит туда на первом приёме.
2026-10-10 19:10:12 +03:00
Repinoid 946600238b fix(iot): ретрай GetQueueUrl на старте + выход ≠0 при сбое подсистемы (🔴5)
- Добавлен ResolveQueueURLWithRetry (10 попыток × 2с backoff): временная ошибка
  GetQueueUrl больше не роняет bridge/consumer без ретрая.
- main.go: при ПОСТОЯННОЙ ошибке bridge/consumer процесс завершается с кодом ≠ 0
  (fatal-флаг + cancel), чтобы платформа перезапустила контейнер — раньше процесс
  оставался жив, а /health отдавал 200.
2026-10-10 19:05:08 +03:00
Repinoid 572988a2a1 fix(iot): слить остаток буфера bridge при shutdown + безопасное закрытие канала (🔴3)
- Диспатчер получил собственный ctx (не сигнальный): при shutdown воркеры
  успевают отправить накопленное в SQS (раньше сразу возвращали «context
  canceled» и дропали остаток). Флаш ограничен bridgeFlushTimeout=10с.
- enqueue/closeAndWait синхронизированы мьютексом (closed-флаг): поздний колбэк
  paho больше не паникует на «send on closed channel».
2026-10-10 19:02:30 +03:00
Repinoid 5600646db3 fix(iot): ретраи отправки SQS до shutdown + таймаут на попытку (🔴4)
- sendWithRetry больше не дропает после 3 попыток (~7с) при почти пустом буфере:
  ретраит до ctx.Done() (shutdown) или успеха, backoff капится на 8с.
- Добавлен sendAttemptTimeout=10с на одну попытку — зависшее соединение SQS
  не блокирует воркер навсегда.
2026-10-10 18:59:37 +03:00
Repinoid ab146861d3 fix(iot): не терять битые сообщения при недоступной DLQ (🔴7)
- ResolveQueueURL для DLQ теперь ретраится 3 раза с backoff: временная ошибка
  больше не отключает DLQ навсегда.
- Битый envelope подтверждается только если DLQ реально приняла его; при
  недоступной DLQ/сбое sendToDLQ сообщение НЕ подтверждается (вернётся после
  visibility timeout), а не дропается.
2026-10-10 18:58:26 +03:00
Repinoid 6048777a9e fix(iot): не рвать автореконнект при сбое подписки (🔴6)
OnConnectHandler вызывал c.Disconnect(100) при таймауте/ошибке подписки. В paho
это ЯВНОЕ отключение — автореконнект после него не срабатывает, и bridge
«глохнет» (соединения нет, подписки нет, /ready=true).

Теперь: подписка ретраится 3 раза с паузой 2с; при сбое — ждём следующего
реконнекта (OnConnectHandler снова вызовется). Disconnect не вызывается.
2026-10-10 18:55:34 +03:00
Repinoid 2cca7cb856 fix(iot): семантическая валидация envelope (пустые поля → DLQ)
processTelemetry теперь отбрасывает (в DLQ) envelope с пустым namespace,
device_id или payload ДО записи в БД — иначе пустой namespace создавал бы
tenant-БД «tenant_», а пустой payload давал NULL (23502)/битый JSONB и вечный
ретрай. Дополняет построчный fallback в flushTenant (🔴2).
2026-10-10 18:53:59 +03:00
Repinoid 8335afb617 fix(iot): валидация namespace/name/device_id (запрет _, +, #, /)
- Добавлен identRe ^[a-zA-Z0-9][a-zA-Z0-9-]*$ и validateIdentifier.
- CreateIoTDevice теперь проверяет namespace (≤56), name (≤64), device_id (≤64).
- Причины: _ ломает username «namespace_device_id» (auth EMQX LIMIT 1 без
  ORDER BY), +/# — wildcard в ACL-топике, / — лишний уровень топика.
2026-10-10 18:50:56 +03:00
Repinoid 2aef884d02 fix(iot): telemetry_topic в ответе + нормализация metadata
- iotDeviceResponse: добавлено telemetry_topic = «ns/telemetry/device» (явный
  топик для Terraform; topic_prefix = «ns/» был неполным).
- metadata теперь без omitempty и всегда «{}» при пустом — убран drift
  «{}» против null (ревью 2026-10-10, «Что стабилизировать для Terraform»).
2026-10-10 18:49:40 +03:00
Repinoid e2bb642df2 fix(iot): created_at в RFC3339 + перечитывание после Create
- deviceToResponse: created_at теперь RFC3339 (был «2006-01-02 15:04:05 UTC»)
  — единый формат с last_connected, без drift для Terraform.
- CreateIoTDevice: после INSERT перечитываем устройство из БД (GetByName),
  чтобы в 201 отдавался реальный created_at (in-memory структура его не имеет).
2026-10-10 18:48:52 +03:00
Repinoid 76f0230f5d fix(iot): отдавать MQTT-пароль при создании + писать phase в БД
- handler/devices.go: CreateIoTDevice отдаёт mqtt_password один раз в 201
  (было deviceToResponse(device, "") — пароль терялся, устройство нельзя
  подключить; блокер для Terraform). Заголовок поправлен (GET — без пароля).
- store/devices.go: Create пишет phase явно через phaseOf(enabled) — иначе
  enabled=false лежал в БД как 'Active' (DEFAULT), расходясь с POST/GET.
2026-10-10 18:48:07 +03:00
Repinoid 56fadf6ca3 style(iot-admin): переформатирование HTML (форматтер) 2026-10-10 18:38:12 +03:00
Repinoid a510522660 chore(iot): убрать старое (Kafka/K8s) из живого админ-UI и .gitignore
- iot-admin.html: секции Kafka/Pods заменены на Очередь shared-SQS и Рантайм
  (бэкенд admin.go давно отдаёт sqs/runtime, а фронт рисовал мёртвые секции);
  удалены CSS .pod-*/.badge-*/.dot-*/.lag-bar*, исправлен заголовок.
- .gitignore: удалены сталые пути бинарников старого IoT (/iot-operator,
  /mqtt-bridge, /kafka-consumer, /sqs-consumer).
2026-10-10 18:15:13 +03:00
Repinoid e8873fa24d fix(telemetry): ts = время приёма (ReceivedAt), а не момент INSERT
Раньше ts проставлялся now() в БД при INSERT → при батчинге и параллельных
записях порядок телеметрии (ORDER BY ts DESC) ломался. Теперь в ts идёт
ReceivedAt из envelope (серверное время приёма, ставит bridge); битый/пустой
received_at → zero → стор подставляет now(). Схема не меняется (используется
существующая колонка ts). Обновлены callers: consumer/processor.go и legacy
cmd/sqs-consumer.
2026-10-07 07:47:10 +03:00
Repinoid 4031882cae feat(consumer): битые envelope → DLQ вместо потери
processTelemetry теперь возвращает sentinel errInvalidEnvelope при битом JSON
(раньше — молчаливый nil → mусор подтверждался и терялся). Consumer при этой
ошибке кладёт тело в DLQ (SQS_DLQ_NAME, дефолт iot-telemetry-dlq) и подтверждает
основное сообщение; без DLQ — только логирует, но НЕ подтверждает (оседает).
Добавлены SQSDLQName в config + DefaultSQSDLQName. Реальные ошибки PG (не invalid)
по-прежнему не подтверждаются и уходят в ретрай после visibility timeout.
2026-10-07 07:40:09 +03:00
Repinoid b6c083e80f fix(telemetry): не терять строки при ошибке flush (flushAll + InsertTelemetry)
flushAll и InsertTelemetry (путь заполнения батча) вынимали строки из буфера и
подменяли буфер ДО записи: при ошибке flushTenant строки исчезали. Хуже —
фоновый flusher мог забрать строки, которые consumer ждал в Flush, тогда Flush
видел пустой буфер, возвращал nil, и consumer подтверждал SQS-сообщение с
незаписанными данными (потеря). Теперь при ошибке строки возвращаются в буфер
в обоих путях. ctx проброшен в flushAll/flushTenant. Consumer хранит receipt
handle значением (string), не указателем SDK.
2026-10-07 07:32:00 +03:00
Repinoid 38602b7560 P0-8: идемпотентность телеметрии (dedup по message_id)
Envelope получил MessageID (UUID); таблица iot_telemetry — столбец message_id +
UNIQUE(device_id,message_id); батч-INSERT ON CONFLICT DO NOTHING. Дубли при
at-least-once повторе из SQS больше не вставляются. Пустой message_id → NULL
(без ложного дедупа).
2026-10-07 07:26:50 +03:00
Repinoid 3036572ccc P1-9: readiness-проба /ready отражает состояние bridge/consumer
/health остаётся liveness (процесс жив); /ready возвращает 503, если bridge или
consumer не в строю (горутина упала/не поднялась). Добавлен api.Readiness с
atomic-флагами, NewRouter принимает *Readiness, main поднимает/опускает флаги
вокруг фоновых горутин.
2026-10-07 07:25:09 +03:00
Repinoid 3175605931 P0-7: подтверждение из SQS только после успешного flush телеметрии в БД
InsertTelemetry лишь буферизует и возвращает nil до фактической записи. Consumer
удалял сообщение сразу после processTelemetry == nil — краш/flush-ошибка в этом
окне = безвозвратная потеря. Теперь: копим receipt handles, вызываем store.Flush,
и только при успехе DeleteMessage; при ошибке flush — не удаляем (сообщения
вернутся после visibility timeout). Добавлен iotpg.Flush().
2026-10-07 07:23:25 +03:00
Repinoid a4e786d03b P0-3 (часть 2): не отдавать пароль устройства повторно через GET
GetIoTDevice больше не возвращает mqtt_password (deviceToResponse получает ).
Пароль остаётся доступен только один раз — при создании устройства.
Хэширование паролей в БД + перевод EMQX на алгоритм хэша (часть 1) требует
изменения emqx.conf и миграции — отдельным шагом.
2026-10-06 19:06:02 +03:00
Repinoid c97b63b190 P1-4: убрать неаутентифицированные /internal/mqtt/auth|acl из роутера
EMQX ходит в PostgreSQL напрямую (backend=postgresql), эти HTTP-эндпоинты были
неиспользуемым fallback и висели на публичном порту без аутентификации (перебор
паролей устройств / выдача ACL). Удалены из роутера.
2026-10-06 19:05:12 +03:00
Repinoid 4afee5cdf9 P0-1/P0-2: tenant-граница по sub + обязательная подпись JWT
validateJWT теперь возвращает sub; middleware сверяет sub с {namespace} из URL
(несовпадение → 403) — закрывает горизонтальный доступ к чужим namespace.
Вне AUTH_TEST_MODE подпись HS256 обязательна: без JWT_HMAC_SECRET middleware
отказывает (fail-closed, 500), а config.Load требует секрет на старте.
2026-10-06 19:04:54 +03:00
“Naeel” ce9e20266c feat(landing): vertical large pipeline diagram; v0.1.13 2026-08-16 19:16:58 +04:00
“Naeel” 30e7da8cf8 feat(landing): hand-drawn pipeline SVG diagram; v0.1.12 2026-08-16 19:11:17 +04:00
“Naeel” 0f71e715be docs(landing): rewrite as clear how-to (4 steps with copy-paste commands); v0.1.9 2026-08-16 18:51:59 +04:00
“Naeel” ce01b6f9f8 feat(api): landing page with Nubes design (logo+favicon), /static; v0.1.8 2026-08-16 18:46:17 +04:00
“Naeel” 68becfd6bc fix: review findings (async SQS dispatcher, backoff, batch insert, shutdown order, pagination, HMAC) + loadtest fixes; v0.1.6 2026-08-16 18:24:09 +04:00
“Naeel” 8b3f3ab5df feat(emqx): PG-auth in EMQX v0.2.4 + EnsureBridgeDevice v0.1.2 (tail 3) 2026-08-16 16:18:49 +04:00
“Naeel” 12b43e7999 fix(bridge): resubscribe on every (re)connect; emqx v0.2.1 dashboard off; iot-service v0.1.1 2026-08-16 15:24:26 +04:00
“Naeel” 7647627052 feat: монолит iot-service (Фаза 1) — config/store/api/bridge/consumer без k8s, устройства в PG, новые Dockerfile/Makefile 2026-08-16 10:07:51 +04:00