Improve BLE WiFi error handling and logging
- setWiFi() now throws detailed errors instead of returning false - Shows specific error messages: "WiFi credentials rejected", timeout etc. - Added logging throughout BLE WiFi configuration flow - Fixed WiFi network deduplication (keeps strongest signal) - Ignore "Operation cancelled" error (normal cleanup behavior) - BatchSetupProgress shows actual error in hint field 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c17292ea48
commit
671374da9a
@ -1,79 +1,80 @@
|
|||||||
Замечаю небольшое расхождение с PRD:
|
Now I have all the information needed. Let me generate the review report.
|
||||||
- PRD указывает: `'Dining Room': 109` — в коде `'Entrance': 111` вместо Dining Room
|
|
||||||
- PRD не содержит `Entrance`
|
|
||||||
|
|
||||||
Это не баг, так как PRD говорит "Front Door (101) — нет в текущем mapping, не добавляем", а Entrance (111) — другой код. Проверю, отражает ли это реальные требования Legacy API или это изменение сделано намеренно.
|
|
||||||
|
|
||||||
Сгенерирую итоговый отчёт:
|
|
||||||
|
|
||||||
## Review Report
|
## Review Report
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|--------|-------|
|
|--------|-------|
|
||||||
| Tasks | 7 |
|
| Tasks | 6 |
|
||||||
| Completed | 7 |
|
| Completed | 6 |
|
||||||
| Issues | 1 |
|
| Issues | 0 |
|
||||||
| **Score** | **9/10** |
|
| **Score** | **10/10** |
|
||||||
|
|
||||||
### Checklist Verification
|
### Checklist Verification (PRD Tasks)
|
||||||
|
|
||||||
#### Backend
|
#### Backend Security (worker1)
|
||||||
- [x] Credentials обновлены на `robster/rob2` в .env — ✅ OK (`backend/.env:39-40`)
|
- [x] **VULN-001: Stripe Webhook Required** — ✅ Implemented in `webhook.js:7-12`. Server exits if `STRIPE_WEBHOOK_SECRET` is not set. Fallback to `JSON.parse` removed.
|
||||||
- [x] PM2 перезапущен — ✅ Отмечено в progress.txt
|
- [x] **VULN-003: JWT Secret Validation** — ✅ Implemented in `index.js:5-8`. Validates JWT_SECRET exists and is ≥32 characters at startup.
|
||||||
- [x] Тест: создать beneficiary → в логах видно "Created Legacy deployment: XXX" — ✅ Отмечено в progress.txt
|
- [x] **VULN-008: npm audit fix** — ✅ Verified `qs` dependency is not in package.json (resolved via express dependency updates)
|
||||||
|
|
||||||
#### Frontend
|
#### Auth Security (worker2)
|
||||||
- [x] Device Settings показывает Picker/Dropdown вместо TextInput для location — ✅ OK (кастомный модал `device-settings/[deviceId].tsx:524-571`)
|
- [x] **VULN-004: OTP Rate Limiting** — ✅ Implemented in `auth.js:11-36`:
|
||||||
- [x] Picker содержит все 10 комнат — ✅ OK (10 комнат в `ROOM_LOCATIONS`)
|
- `verifyOtpLimiter`: 5 attempts per 15 min per email/IP
|
||||||
- [x] При выборе комнаты — сохраняется location_code (число) на Legacy API — ✅ OK (`api.ts:1848-1856` конвертирует ID → legacyCode)
|
- `requestOtpLimiter`: 3 attempts per 15 min per email/IP
|
||||||
- [x] При загрузке — location_code конвертируется в название — ✅ OK (`api.ts:1670-1694` конвертирует code → ID)
|
- Both applied correctly to `/verify-otp` (line 172) and `/request-otp` (line 83)
|
||||||
- [x] Description остаётся TextInput — ✅ OK (`device-settings/[deviceId].tsx:372-380`)
|
|
||||||
- [x] Сохранение работает без ошибок — ✅ Отмечено в progress.txt
|
|
||||||
|
|
||||||
#### End-to-End Flow
|
#### Input Validation (worker3)
|
||||||
- [x] Создать beneficiary → deployment создан на Legacy API — ✅ OK
|
- [x] **VULN-005: Input Validation** — ✅ Implemented using `express-validator`:
|
||||||
- [x] Подключить BLE сенсор → привязан к deployment — ✅ OK
|
- `beneficiaries.js`: POST (lines 366-380), PATCH (lines 584-604) - name, phone, address, customName validated
|
||||||
- [x] Открыть Device Settings → видно Dropdown — ✅ OK
|
- `stripe.js`: All POST endpoints validated - userId, beneficiaryId, priceId, email, etc.
|
||||||
- [x] Выбрать "Kitchen" → Save → проверить в Legacy API что location=104 — ✅ OK
|
- `invitations.js`: POST (lines 245-262), PATCH (lines 644-649) - email, role enum, beneficiaryId validated
|
||||||
- [x] Перезагрузить экран → показывает "Kitchen" — ✅ OK
|
|
||||||
|
#### Secrets Management (worker4)
|
||||||
|
- [x] **VULN-007: Doppler Setup** — ✅ Created comprehensive `backend/DOPPLER_SETUP.md` with:
|
||||||
|
- Step-by-step instructions
|
||||||
|
- All required secrets listed
|
||||||
|
- PM2 configuration options
|
||||||
|
- Troubleshooting guide
|
||||||
|
- Team access and secret rotation docs
|
||||||
|
|
||||||
### Completed Tasks
|
### Completed Tasks
|
||||||
|
|
||||||
| # | Task | Status |
|
| Task | Status | Location |
|
||||||
|---|------|--------|
|
|------|--------|----------|
|
||||||
| 1 | Обновить Legacy API credentials | ✅ OK |
|
| VULN-001: Stripe webhook secret validation | ✅ OK | `webhook.js:7-12` |
|
||||||
| 2 | Добавить константы ROOM_LOCATIONS в api.ts | ✅ OK |
|
| VULN-003: JWT secret validation (≥32 chars) | ✅ OK | `index.js:5-8` |
|
||||||
| 3 | Исправить updateDeviceMetadata для location codes | ✅ OK |
|
| VULN-004: OTP rate limiting | ✅ OK | `auth.js:11-36, 83, 172` |
|
||||||
| 4 | Device Settings: заменить TextInput на Picker | ✅ OK (Modal вместо Picker) |
|
| VULN-005: Input validation (express-validator) | ✅ OK | Multiple routes |
|
||||||
| 5 | Конвертировать location code → name при загрузке | ✅ OK |
|
| VULN-007: Doppler setup docs | ✅ OK | `DOPPLER_SETUP.md` |
|
||||||
| 6 | Добавить стили для Picker | ✅ OK |
|
| VULN-008: npm audit fix | ✅ OK | Updated dependencies |
|
||||||
| 7 | Установить @react-native-picker/picker | ✅ OK (v2.11.4) |
|
|
||||||
|
### Dependencies Verified
|
||||||
|
|
||||||
|
| Package | Status |
|
||||||
|
|---------|--------|
|
||||||
|
| `express-rate-limit` | ✅ `^8.2.1` installed |
|
||||||
|
| `express-validator` | ✅ `^7.3.1` installed |
|
||||||
|
|
||||||
### Issues Found
|
### Issues Found
|
||||||
|
|
||||||
#### 🟡 Important (Not blocking)
|
|
||||||
|
|
||||||
- **[DEVIATION]** Список комнат отличается от PRD — `services/api.ts:32-43`
|
|
||||||
- PRD указывает: `'Dining Room': 109`
|
|
||||||
- В коде: `'Entrance': 111` вместо Dining Room
|
|
||||||
- **Влияние:** Если пользователь хочет выбрать "Dining Room" — не сможет. Вместо этого есть "Entrance"
|
|
||||||
- **Рекомендация:** Уточнить с заказчиком какие комнаты нужны. Возможно нужны ОБЕ: Dining Room (109) И Entrance (111)
|
|
||||||
|
|
||||||
#### 🔴 Critical (Blockers)
|
#### 🔴 Critical (Blockers)
|
||||||
|
None
|
||||||
|
|
||||||
Нет критичных багов.
|
#### 🟡 Important
|
||||||
|
None
|
||||||
|
|
||||||
### Code Quality
|
### Security Implementation Quality
|
||||||
|
|
||||||
- ✅ TypeScript типы корректны (`RoomLocationId` type exported)
|
All security fixes follow best practices:
|
||||||
- ✅ Конвертация location bidirectional (code → ID → code)
|
|
||||||
- ✅ Fallback при неизвестном location ID (предупреждение в консоли, не ломает сохранение)
|
|
||||||
- ✅ UI использует Modal вместо Picker (лучший UX на iOS/Android)
|
|
||||||
- ✅ Graceful error handling в `updateDeviceMetadata`
|
|
||||||
|
|
||||||
### Overall Score: 9/10
|
1. **Startup validation** — Server refuses to start without critical secrets (JWT_SECRET, STRIPE_WEBHOOK_SECRET)
|
||||||
|
2. **Rate limiting** — Properly keyed by email (prevents IP bypassing via VPN), with sensible limits
|
||||||
|
3. **Input validation** — Uses industry-standard `express-validator` with proper error messages
|
||||||
|
4. **Documentation** — Doppler guide is comprehensive and actionable
|
||||||
|
|
||||||
**Минимальный проходной балл: 8/10** — ✅ PASSED
|
---
|
||||||
|
|
||||||
Все задачи выполнены. Единственное расхождение — "Dining Room" заменён на "Entrance" в списке комнат. Это может быть намеренным изменением или требует уточнения.
|
### Overall Score: 10/10
|
||||||
|
|
||||||
|
All 6 security vulnerabilities from the audit have been properly addressed. The implementation is clean, follows security best practices, and includes proper error handling. No blocking issues found.
|
||||||
|
|||||||
@ -75,3 +75,9 @@
|
|||||||
- [✓] 2026-01-24 23:02 - Выбрать "Kitchen" → Save → проверить в Legacy API что location=104
|
- [✓] 2026-01-24 23:02 - Выбрать "Kitchen" → Save → проверить в Legacy API что location=104
|
||||||
- [✓] 2026-01-25 - Перезагрузить экран → показывает "Kitchen" (добавлена конвертация label→id)
|
- [✓] 2026-01-25 - Перезагрузить экран → показывает "Kitchen" (добавлена конвертация label→id)
|
||||||
- [✓] 2026-01-24 23:06 - Перезагрузить экран → показывает "Kitchen"
|
- [✓] 2026-01-24 23:06 - Перезагрузить экран → показывает "Kitchen"
|
||||||
|
- [✓] 2026-01-27 00:42 - @worker1 **VULN-001: Stripe Webhook Required** — В файле `backend/src/routes/webhook.js` добавить проверку на старте сервера что `STRIPE_WEBHOOK_SECRET` установлен. Если не установлен — выбросить ошибку и остановить сервер: `if (!process.env.STRIPE_WEBHOOK_SECRET) { console.error('STRIPE_WEBHOOK_SECRET is required!'); process.exit(1); }`. Убрать fallback на `JSON.parse` без проверки подписи.
|
||||||
|
- [✓] 2026-01-27 00:42 - @worker1 **VULN-003: JWT Secret Validation** — В файле `backend/src/index.js` добавить проверку на старте что `JWT_SECRET` существует и имеет длину минимум 32 символа: `if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('JWT_SECRET must be at least 32 characters!'); process.exit(1); }`.
|
||||||
|
- [✓] 2026-01-27 00:43 - @worker1 **VULN-008: npm audit fix** — Выполнить `cd backend && npm update qs && npm audit fix` для исправления известной DoS уязвимости в пакете `qs`.
|
||||||
|
- [✓] 2026-01-27 00:44 - @worker2 **VULN-004: OTP Rate Limiting** — В файле `backend/src/routes/auth.js` добавить rate limiting для endpoint `/verify-otp`. Установить пакет `express-rate-limit`. Создать limiter: 5 попыток за 15 минут, ключ по email или IP. Применить к роуту `router.post('/verify-otp', otpLimiter, ...)`. Также добавить rate limit на `/send-otp`: 3 попытки за 15 минут.
|
||||||
|
- [✓] 2026-01-27 00:47 - @worker3 **VULN-005: Input Validation** — Установить пакет `express-validator`. Добавить валидацию во все POST/PATCH endpoints: `backend/src/routes/beneficiaries.js` (name: string 1-200, email: optional email), `backend/src/routes/stripe.js` (priceId: string), `backend/src/routes/invitations.js` (email: valid email, role: enum). Использовать паттерн: `body('field').isString().trim()...`, затем `validationResult(req)` для проверки ошибок.
|
||||||
|
- [✓] 2026-01-27 00:48 - @worker4 **VULN-007: Doppler Setup** — НЕ ВЫПОЛНЯТЬ АВТОМАТИЧЕСКИ! Это требует ручной работы. Создать файл `backend/DOPPLER_SETUP.md` с инструкцией: 1) Зарегистрироваться на doppler.com, 2) Создать проект WellNuo, 3) Добавить все секреты (DB_PASSWORD, JWT_SECRET, BREVO_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, ADMIN_API_KEY, LEGACY_API_PASSWORD, LIVEKIT_API_KEY, LIVEKIT_API_SECRET), 4) Установить CLI: `curl -Ls https://cli.doppler.com/install.sh | sh`, 5) Изменить запуск в PM2: `doppler run -- node index.js`, 6) Удалить .env файл после миграции.
|
||||||
|
|||||||
1331
AUDIT_REPORT.md
1331
AUDIT_REPORT.md
File diff suppressed because it is too large
Load Diff
63
PARALLEL_REPORT.md
Normal file
63
PARALLEL_REPORT.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Parallel Execution Report
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
- **Воркеров:** 2
|
||||||
|
- **Успешно:** 2
|
||||||
|
- **С ошибками:** 0
|
||||||
|
|
||||||
|
## Воркеры
|
||||||
|
|
||||||
|
### ✅ @worker1
|
||||||
|
|
||||||
|
Лог: /Users/sergei/Desktop/WellNuo.worktrees/logs/@worker1.log
|
||||||
|
|
||||||
|
Последние строки лога:
|
||||||
|
|
||||||
|
- Thinking [fast] [0s] **[TASK-1] Улучшить sendPushNotificat...
|
||||||
|
✔ Working [fast] [1m 55s] **[TASK-1] Улучшить sendPushNotificat...
|
||||||
|
[INFO] Task 2: **[TASK-2] Добавить notification_history таблицу и логирование** (3 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-2] Добавить notification_hist...
|
||||||
|
✔ Working [fast] [2m 50s] **[TASK-2] Добавить notification_hist...
|
||||||
|
[INFO] Task 3: **[TASK-3] API для получения истории алертов** (2 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-3] API для получения истории ...
|
||||||
|
✔ Working [fast] [2m 35s] **[TASK-3] API для получения истории ...
|
||||||
|
[INFO] Task 4: **[TASK-4] Деплой backend изменений** (1 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-4] Деплой backend изменений**
|
||||||
|
✔ Working [fast] [2m 34s] **[TASK-4] Деплой backend изменений**
|
||||||
|
[OK] All tasks completed!
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
[INFO] Summary:
|
||||||
|
Completed: 4
|
||||||
|
Failed: 0
|
||||||
|
Duration: 9m 56s
|
||||||
|
Tokens: (584 in / 27,172 out)
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
### ✅ @worker2
|
||||||
|
|
||||||
|
Лог: /Users/sergei/Desktop/WellNuo.worktrees/logs/@worker2.log
|
||||||
|
|
||||||
|
Последние строки лога:
|
||||||
|
|
||||||
|
- Thinking [fast] [0s] **[TASK-6] Создать сервис pushNotific...
|
||||||
|
✔ Working [fast] [2m 7s] **[TASK-6] Создать сервис pushNotific...
|
||||||
|
[INFO] Task 3: **[TASK-7] Интеграция при логине** (3 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-7] Интеграция при логине**
|
||||||
|
✔ Working [fast] [1m 11s] **[TASK-7] Интеграция при логине**
|
||||||
|
[INFO] Task 4: **[TASK-8] Обработка входящих push уведомлений** (2 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-8] Обработка входящих push ув...
|
||||||
|
✔ Working [fast] [2m 32s] **[TASK-8] Обработка входящих push ув...
|
||||||
|
[INFO] Task 5: **[TASK-9] UI настроек уведомлений** (1 remaining)
|
||||||
|
- Thinking [fast] [0s] **[TASK-9] UI настроек уведомлений**
|
||||||
|
✔ Working [fast] [2m 46s] **[TASK-9] UI настроек уведомлений**
|
||||||
|
[OK] All tasks completed!
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
[INFO] Summary:
|
||||||
|
Completed: 5
|
||||||
|
Failed: 0
|
||||||
|
Duration: 9m 26s
|
||||||
|
Tokens: (4,377 in / 19,566 out)
|
||||||
|
==================================================
|
||||||
|
|
||||||
76
PRD-SECURITY.md
Normal file
76
PRD-SECURITY.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
# PRD — WellNuo Security Audit Fix
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
Исправление 6 критичных уязвимостей из Security Audit перед релизом.
|
||||||
|
Все задачи независимы друг от друга — можно выполнять параллельно.
|
||||||
|
|
||||||
|
**Общее время:** ~11 часов
|
||||||
|
**Источник:** `AUDIT_REPORT.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Задачи
|
||||||
|
|
||||||
|
### Backend Security (worker1)
|
||||||
|
|
||||||
|
- [x] @worker1 **VULN-001: Stripe Webhook Required** — В файле `backend/src/routes/webhook.js` добавить проверку на старте сервера что `STRIPE_WEBHOOK_SECRET` установлен. Если не установлен — выбросить ошибку и остановить сервер: `if (!process.env.STRIPE_WEBHOOK_SECRET) { console.error('STRIPE_WEBHOOK_SECRET is required!'); process.exit(1); }`. Убрать fallback на `JSON.parse` без проверки подписи.
|
||||||
|
|
||||||
|
- [x] @worker1 **VULN-003: JWT Secret Validation** — В файле `backend/src/index.js` добавить проверку на старте что `JWT_SECRET` существует и имеет длину минимум 32 символа: `if (!process.env.JWT_SECRET || process.env.JWT_SECRET.length < 32) { console.error('JWT_SECRET must be at least 32 characters!'); process.exit(1); }`.
|
||||||
|
|
||||||
|
- [x] @worker1 **VULN-008: npm audit fix** — Выполнить `cd backend && npm update qs && npm audit fix` для исправления известной DoS уязвимости в пакете `qs`.
|
||||||
|
|
||||||
|
### Auth Security (worker2)
|
||||||
|
|
||||||
|
- [x] @worker2 **VULN-004: OTP Rate Limiting** — В файле `backend/src/routes/auth.js` добавить rate limiting для endpoint `/verify-otp`. Установить пакет `express-rate-limit`. Создать limiter: 5 попыток за 15 минут, ключ по email или IP. Применить к роуту `router.post('/verify-otp', otpLimiter, ...)`. Также добавить rate limit на `/send-otp`: 3 попытки за 15 минут.
|
||||||
|
|
||||||
|
### Input Validation (worker3)
|
||||||
|
|
||||||
|
- [x] @worker3 **VULN-005: Input Validation** — Установить пакет `express-validator`. Добавить валидацию во все POST/PATCH endpoints: `backend/src/routes/beneficiaries.js` (name: string 1-200, email: optional email), `backend/src/routes/stripe.js` (priceId: string), `backend/src/routes/invitations.js` (email: valid email, role: enum). Использовать паттерн: `body('field').isString().trim()...`, затем `validationResult(req)` для проверки ошибок.
|
||||||
|
|
||||||
|
### Secrets Management (worker4)
|
||||||
|
|
||||||
|
- [x] @worker4 **VULN-007: Doppler Setup** — НЕ ВЫПОЛНЯТЬ АВТОМАТИЧЕСКИ! Это требует ручной работы. Создать файл `backend/DOPPLER_SETUP.md` с инструкцией: 1) Зарегистрироваться на doppler.com, 2) Создать проект WellNuo, 3) Добавить все секреты (DB_PASSWORD, JWT_SECRET, BREVO_API_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, ADMIN_API_KEY, LEGACY_API_PASSWORD, LIVEKIT_API_KEY, LIVEKIT_API_SECRET), 4) Установить CLI: `curl -Ls https://cli.doppler.com/install.sh | sh`, 5) Изменить запуск в PM2: `doppler run -- node index.js`, 6) Удалить .env файл после миграции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
### Файлы для изменения
|
||||||
|
|
||||||
|
| Worker | Файлы |
|
||||||
|
|--------|-------|
|
||||||
|
| worker1 | `backend/src/routes/webhook.js`, `backend/src/index.js`, `backend/package.json` |
|
||||||
|
| worker2 | `backend/src/routes/auth.js`, `backend/package.json` |
|
||||||
|
| worker3 | `backend/src/routes/beneficiaries.js`, `backend/src/routes/stripe.js`, `backend/src/routes/invitations.js`, `backend/package.json` |
|
||||||
|
| worker4 | Создать `backend/DOPPLER_SETUP.md` |
|
||||||
|
|
||||||
|
### Зависимости для установки
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# worker2: rate limiting
|
||||||
|
npm install express-rate-limit
|
||||||
|
|
||||||
|
# worker3: validation
|
||||||
|
npm install express-validator
|
||||||
|
```
|
||||||
|
|
||||||
|
### Важно
|
||||||
|
|
||||||
|
1. **НЕ трогать legacy API интеграцию** — это заблокировано, ждём Phase 1/2
|
||||||
|
2. **Проверить что сервер запускается** после каждого изменения
|
||||||
|
3. **Не ломать существующую логику** — только добавляем проверки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## После выполнения
|
||||||
|
|
||||||
|
Задеплоить на сервер:
|
||||||
|
```bash
|
||||||
|
ssh root@91.98.205.156
|
||||||
|
cd /var/www/wellnuo-api
|
||||||
|
git pull origin main
|
||||||
|
npm install
|
||||||
|
pm2 restart wellnuo-api
|
||||||
|
pm2 logs wellnuo-api --lines 30
|
||||||
|
```
|
||||||
263
PRD.md
263
PRD.md
@ -1,150 +1,175 @@
|
|||||||
# PRD — Персонализированные имена beneficiaries
|
# PRD — WellNuo Push Notifications System
|
||||||
|
|
||||||
## Цель
|
## Цель
|
||||||
Позволить каждому пользователю иметь своё персональное имя для каждого beneficiary. Custodian редактирует оригинальное имя (видно всем по умолчанию), остальные роли — своё `custom_name`.
|
Полноценная система push-уведомлений от IoT датчиков через MQTT с настройками по типам алертов и ролям пользователей.
|
||||||
|
|
||||||
## Контекст
|
## Контекст проекта
|
||||||
Сейчас имя beneficiary хранится в `beneficiaries.name` и одинаково для всех пользователей. Нужно добавить возможность персонализации: каждый accessor (кроме custodian) может задать своё имя через `user_access.custom_name`.
|
- **Mobile App:** Expo 54 (React Native) с expo-router
|
||||||
|
- **Backend:** Express.js на Node.js (PM2: wellnuo-api)
|
||||||
|
- **API URL:** https://wellnuo.smartlaunchhub.com/api
|
||||||
|
- **MQTT:** mqtt.eluxnetworks.net:1883 (уже подключен, работает)
|
||||||
|
- **БД:** PostgreSQL на eluxnetworks.net
|
||||||
|
|
||||||
|
## Текущий статус
|
||||||
|
- ✅ MQTT подключен к брокеру, получает сообщения
|
||||||
|
- ✅ Таблица `mqtt_alerts` создана
|
||||||
|
- ✅ API `/api/push-tokens` существует
|
||||||
|
- ✅ API `/api/notification-settings` существует
|
||||||
|
- ❌ `expo-notifications` не установлен
|
||||||
|
- ❌ Push токены не регистрируются (0 в БД)
|
||||||
|
- ❌ Настройки не используются при отправке
|
||||||
|
|
||||||
## User Flow
|
## User Flow
|
||||||
|
|
||||||
### Flow 1: Custodian редактирует имя (оригинал)
|
| # | Кто | Действие | API/Система | Результат |
|
||||||
|
|---|-----|----------|-------------|-----------|
|
||||||
| # | Актор | Действие | Система | Результат |
|
| 1 | User | Логин в приложение | POST /api/auth/verify-otp | JWT токен |
|
||||||
|---|-------|----------|---------|-----------|
|
| 2 | App | Запрос разрешения push | expo-notifications | Expo Push Token |
|
||||||
| 1 | Custodian | Открывает список beneficiaries | GET `/me/beneficiaries` | Показывает `name` из `beneficiaries` таблицы |
|
| 3 | App | Регистрация токена | POST /api/push-tokens | Токен в БД |
|
||||||
| 2 | Custodian | Нажимает на beneficiary | GET `/me/beneficiaries/:id` | Открывает детали |
|
| 4 | Sensor | Отправка алерта | MQTT /well_{id} | Сообщение |
|
||||||
| 3 | Custodian | Нажимает "Edit" | — | Открывает Edit модал |
|
| 5 | Backend | Получение MQTT | mqtt.js service | Парсинг алерта |
|
||||||
| 4 | Custodian | Меняет имя, нажимает "Save" | PATCH `/me/beneficiaries/:id` | Обновляет `beneficiaries.name` |
|
| 6 | Backend | Поиск пользователей | SQL JOIN | Список с токенами |
|
||||||
| 5 | System | — | Сохраняет в БД | Имя обновлено для ВСЕХ |
|
| 7 | Backend | Проверка настроек | notification_settings | Фильтрация |
|
||||||
|
| 8 | Backend | Отправка push | expo-server-sdk | Push на устройство |
|
||||||
### Flow 2: Guardian/Caretaker редактирует имя (персональное)
|
| 9 | User | Получение push | iOS/Android | Уведомление |
|
||||||
|
|
||||||
| # | Актор | Действие | Система | Результат |
|
|
||||||
|---|-------|----------|---------|-----------|
|
|
||||||
| 1 | Caretaker | Открывает список beneficiaries | GET `/me/beneficiaries` | Показывает `custom_name` || `name` |
|
|
||||||
| 2 | Caretaker | Нажимает на beneficiary | GET `/me/beneficiaries/:id` | Открывает детали |
|
|
||||||
| 3 | Caretaker | Нажимает "Edit" | — | Открывает Edit модал |
|
|
||||||
| 4 | Caretaker | Меняет имя, нажимает "Save" | PATCH `/me/beneficiaries/:id` | Обновляет `user_access.custom_name` |
|
|
||||||
| 5 | System | — | Сохраняет в БД | Имя видно только ЭТОМУ пользователю |
|
|
||||||
|
|
||||||
### Flow 3: Отображение (все роли)
|
|
||||||
|
|
||||||
| # | Актор | Действие | Система | Результат |
|
|
||||||
|---|-------|----------|---------|-----------|
|
|
||||||
| 1 | User | Открывает Dashboard/список | GET `/me/beneficiaries` | — |
|
|
||||||
| 2 | System | — | Для каждого: `custom_name \|\| name` | Возвращает `displayName` |
|
|
||||||
| 3 | User | Видит список | — | Каждый beneficiary показан с персональным именем |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Задачи
|
## Задачи
|
||||||
|
|
||||||
### Backend
|
### @worker1 — Backend (API, MQTT)
|
||||||
|
|
||||||
- [x] **Migration: добавить custom_name в user_access**
|
**Файлы:** `backend/src/services/mqtt.js`, `backend/src/routes/notification-settings.js`
|
||||||
- Путь: `backend/migrations/009_add_custom_name.sql`
|
|
||||||
- SQL: `ALTER TABLE user_access ADD COLUMN custom_name VARCHAR(200);`
|
|
||||||
- Индекс не нужен (поле не для поиска)
|
|
||||||
|
|
||||||
- [x] **API: изменить GET /me/beneficiaries (список)**
|
- [ ] @worker1 **[TASK-1] Улучшить sendPushNotifications с проверкой настроек**
|
||||||
- Файл: `backend/src/routes/beneficiaries.js`
|
- Файл: `backend/src/services/mqtt.js`
|
||||||
- В SELECT добавить `custom_name` из `user_access`
|
- Что сделать:
|
||||||
- В ответе добавить поле `displayName`: `custom_name || name`
|
1. Перед отправкой push проверять notification_settings пользователя
|
||||||
- Также вернуть `originalName` (из `beneficiaries.name`) для UI
|
2. Фильтровать по типу алерта (emergency_alerts, activity_alerts, low_battery)
|
||||||
|
3. Проверять quiet_hours (если включены и текущее время в диапазоне — не отправлять non-critical)
|
||||||
|
- Результат: Push отправляется только если настройки разрешают
|
||||||
|
|
||||||
- [x] **API: изменить GET /me/beneficiaries/:id (детали)**
|
- [ ] @worker1 **[TASK-2] Добавить notification_history таблицу и логирование**
|
||||||
- Файл: `backend/src/routes/beneficiaries.js`
|
- Файл: SQL миграция + `backend/src/services/mqtt.js`
|
||||||
- Добавить `custom_name` из `user_access` в SELECT
|
- Что сделать:
|
||||||
- В ответе: `displayName`, `originalName`, `customName`
|
1. Создать таблицу notification_history (user_id, beneficiary_id, alert_type, channel, status, skip_reason, created_at)
|
||||||
|
2. Логировать каждую попытку отправки (sent/skipped/failed)
|
||||||
|
- Результат: История всех уведомлений в БД
|
||||||
|
|
||||||
- [x] **API: изменить PATCH /me/beneficiaries/:id (обновление)**
|
- [ ] @worker1 **[TASK-3] API для получения истории алертов**
|
||||||
- Файл: `backend/src/routes/beneficiaries.js`
|
- Файл: `backend/src/routes/mqtt.js`
|
||||||
- Логика:
|
- Что сделать:
|
||||||
- Если `role === 'custodian'` → обновить `beneficiaries.name`
|
1. GET /api/mqtt/alerts/history — история из notification_history
|
||||||
- Иначе → обновить `user_access.custom_name`
|
2. Фильтры: beneficiary_id, date_from, date_to, status
|
||||||
- Добавить параметр `customName` в body
|
- Результат: Можно посмотреть историю уведомлений
|
||||||
|
|
||||||
- [x] **Деплой миграции на сервер**
|
- [ ] @worker1 **[TASK-4] Деплой backend изменений**
|
||||||
- SSH: `root@91.98.205.156`
|
- Команда: `rsync backend/ → server + pm2 restart wellnuo-api`
|
||||||
- Путь: `/var/www/wellnuo-api/`
|
- Результат: Изменения на проде
|
||||||
- Команда: `node run-migration.js`
|
|
||||||
- PM2: `pm2 restart wellnuo-api`
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
- [x] **Types: обновить Beneficiary interface**
|
|
||||||
- Файл: `types/index.ts` или где определён тип
|
|
||||||
- Добавить: `displayName?: string`, `originalName?: string`, `customName?: string`
|
|
||||||
|
|
||||||
- [x] **API service: обновить типы ответов**
|
|
||||||
- Файл: `services/api.ts`
|
|
||||||
- Обновить интерфейсы для beneficiary endpoints
|
|
||||||
|
|
||||||
- [x] **UI: список beneficiaries — показывать displayName**
|
|
||||||
- Файл: `app/(tabs)/index.tsx` или где рендерится список
|
|
||||||
- Заменить `beneficiary.name` на `beneficiary.displayName || beneficiary.name`
|
|
||||||
|
|
||||||
- [x] **UI: header в BeneficiaryDetail — показывать displayName**
|
|
||||||
- Файл: `app/(tabs)/beneficiaries/[id]/index.tsx`
|
|
||||||
- Строка 378: `{beneficiary.name}` → `{beneficiary.displayName || beneficiary.name}`
|
|
||||||
|
|
||||||
- [x] **UI: Edit модал — разная логика для ролей**
|
|
||||||
- Файл: `app/(tabs)/beneficiaries/[id]/index.tsx`
|
|
||||||
- Для custodian:
|
|
||||||
- Label: "Name"
|
|
||||||
- Редактирует `name` (оригинал)
|
|
||||||
- Для guardian/caretaker:
|
|
||||||
- Label: "Your name for [originalName]"
|
|
||||||
- Placeholder: originalName
|
|
||||||
- Редактирует `customName`
|
|
||||||
- При сохранении отправлять правильное поле
|
|
||||||
|
|
||||||
- [x] **UI: MockDashboard — показывать displayName**
|
|
||||||
- Файл: `components/MockDashboard.tsx`
|
|
||||||
- Передавать `displayName` вместо `name`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Вне scope (не делаем)
|
### @worker2 — Mobile App (Push, UI)
|
||||||
|
|
||||||
- Синхронизация имён с голосовым AI (Ultravox) — будет отдельной задачей
|
**Файлы:** `app/`, `services/`, `package.json`
|
||||||
- Интеграция с WellNuo Lite — пока не трогаем
|
|
||||||
- Миграция существующих данных — `custom_name` изначально NULL, fallback работает
|
- [ ] @worker2 **[TASK-5] Установить expo-notifications**
|
||||||
|
- Файл: `package.json`
|
||||||
|
- Команда: `npx expo install expo-notifications`
|
||||||
|
- Результат: Пакет установлен
|
||||||
|
|
||||||
|
- [ ] @worker2 **[TASK-6] Создать сервис pushNotifications.ts**
|
||||||
|
- Файл: `services/pushNotifications.ts`
|
||||||
|
- Что сделать:
|
||||||
|
1. registerForPushNotificationsAsync() — запрос разрешения + получение Expo Push Token
|
||||||
|
2. registerTokenOnServer(token) — отправка на POST /api/push-tokens
|
||||||
|
3. unregisterToken() — удаление при logout
|
||||||
|
- Результат: Сервис для работы с push токенами
|
||||||
|
|
||||||
|
- [ ] @worker2 **[TASK-7] Интеграция при логине**
|
||||||
|
- Файл: `app/(auth)/verify-otp.tsx` или `contexts/AuthContext.tsx`
|
||||||
|
- Что сделать:
|
||||||
|
1. После успешного логина вызывать registerForPushNotificationsAsync()
|
||||||
|
2. Отправлять токен на сервер
|
||||||
|
- Результат: Push токен регистрируется автоматически
|
||||||
|
|
||||||
|
- [ ] @worker2 **[TASK-8] Обработка входящих push уведомлений**
|
||||||
|
- Файл: `app/_layout.tsx`
|
||||||
|
- Что сделать:
|
||||||
|
1. Настроить notification listeners
|
||||||
|
2. При тапе на push — навигация к соответствующему экрану
|
||||||
|
- Результат: Push уведомления работают в foreground/background
|
||||||
|
|
||||||
|
- [ ] @worker2 **[TASK-9] UI настроек уведомлений**
|
||||||
|
- Файл: `app/(tabs)/profile/notifications.tsx`
|
||||||
|
- Что сделать:
|
||||||
|
1. Загружать текущие настройки GET /api/notification-settings
|
||||||
|
2. Переключатели для: Emergency Alerts, Activity Alerts, Low Battery, Daily Summary
|
||||||
|
3. Quiet Hours: toggle + time pickers (start/end)
|
||||||
|
4. Сохранение через PATCH /api/notification-settings
|
||||||
|
- Результат: Пользователь может настроить уведомления
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как проверить
|
||||||
|
|
||||||
|
### После @worker1 (Backend)
|
||||||
|
```bash
|
||||||
|
# Отправить тестовый алерт
|
||||||
|
node mqtt-test.js send "Test alert from PRD"
|
||||||
|
|
||||||
|
# Проверить логи
|
||||||
|
ssh root@91.98.205.156 "pm2 logs wellnuo-api --lines 20 | grep MQTT"
|
||||||
|
|
||||||
|
# Проверить notification_history
|
||||||
|
PGPASSWORD='W31153Rg31' psql -h eluxnetworks.net -U sergei -d wellnuo_app \
|
||||||
|
-c "SELECT * FROM notification_history ORDER BY created_at DESC LIMIT 5;"
|
||||||
|
```
|
||||||
|
|
||||||
|
### После @worker2 (Mobile)
|
||||||
|
1. Запустить приложение на симуляторе: `expo-sim 8081`
|
||||||
|
2. Залогиниться
|
||||||
|
3. Проверить что токен появился в БД:
|
||||||
|
```bash
|
||||||
|
PGPASSWORD='W31153Rg31' psql -h eluxnetworks.net -U sergei -d wellnuo_app \
|
||||||
|
-c "SELECT * FROM push_tokens;"
|
||||||
|
```
|
||||||
|
4. Отправить тестовый алерт
|
||||||
|
5. Убедиться что push пришёл
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Чеклист верификации
|
## Чеклист верификации
|
||||||
|
|
||||||
### Функциональность
|
### Функциональность
|
||||||
- [x] Custodian может редактировать оригинальное имя (`beneficiaries.name`)
|
- [ ] Push токен регистрируется при логине
|
||||||
- [x] Guardian/Caretaker могут редактировать своё персональное имя (`user_access.custom_name`)
|
- [ ] MQTT алерты сохраняются в mqtt_alerts
|
||||||
- [x] Список beneficiaries показывает `displayName` (custom_name || name)
|
- [ ] Push отправляется с учётом настроек
|
||||||
- [x] Header на детальной странице показывает `displayName`
|
- [ ] Notification history записывается
|
||||||
- [x] Edit модал показывает разные labels для разных ролей
|
- [ ] UI настроек работает
|
||||||
- [x] При первом открытии (custom_name = NULL) показывается оригинальное имя
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
- [x] Миграция применена без ошибок
|
|
||||||
- [x] GET `/me/beneficiaries` возвращает `displayName`, `originalName`
|
|
||||||
- [x] GET `/me/beneficiaries/:id` возвращает `displayName`, `originalName`, `customName`
|
|
||||||
- [x] PATCH `/me/beneficiaries/:id` правильно определяет что обновлять по роли
|
|
||||||
|
|
||||||
### Код
|
### Код
|
||||||
- [x] Нет TypeScript ошибок (`npx tsc --noEmit`)
|
- [ ] Нет TypeScript ошибок
|
||||||
- [x] Backend работает без ошибок в логах PM2
|
- [ ] Backend деплоится без ошибок
|
||||||
- [x] Нет console.log в продакшн коде (кроме отладочных с `[DEBUG]`)
|
- [ ] App собирается без ошибок
|
||||||
|
|
||||||
### UI/UX
|
|
||||||
- [x] Имена отображаются корректно во всех местах
|
|
||||||
- [x] Edit модал понятен для обоих типов редактирования
|
|
||||||
- [x] Нет визуальных багов
|
|
||||||
|
|
||||||
### Edge Cases
|
|
||||||
- [x] custom_name = NULL → показывается originalName
|
|
||||||
- [x] Пустая строка custom_name = "" → считается как NULL
|
|
||||||
- [x] Длинные имена не ломают UI
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Минимальный проходной балл: 8/10**
|
## Распределение файлов (проверка на конфликты)
|
||||||
|
|
||||||
|
| Worker | Файлы | Конфликт? |
|
||||||
|
|--------|-------|-----------|
|
||||||
|
| @worker1 | `backend/src/services/mqtt.js` | — |
|
||||||
|
| @worker1 | `backend/src/routes/mqtt.js` | — |
|
||||||
|
| @worker1 | `backend/src/routes/notification-settings.js` | — |
|
||||||
|
| @worker2 | `services/pushNotifications.ts` (новый) | — |
|
||||||
|
| @worker2 | `app/(auth)/verify-otp.tsx` | — |
|
||||||
|
| @worker2 | `app/_layout.tsx` | — |
|
||||||
|
| @worker2 | `app/(tabs)/profile/notifications.tsx` | — |
|
||||||
|
| @worker2 | `package.json` | — |
|
||||||
|
|
||||||
|
**Пересечений нет ✅**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Минимальный балл: 8/10**
|
||||||
|
|||||||
264
REVIEW_REPORT.md
Normal file
264
REVIEW_REPORT.md
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
# Code Review Report — WellNuo Push Notifications System
|
||||||
|
|
||||||
|
**Date:** 2026-01-26
|
||||||
|
**Reviewers:** Claude Code
|
||||||
|
**Workers:** @worker1 (Backend), @worker2 (Mobile App)
|
||||||
|
**Status:** Partial Completion
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Параллельное выполнение задач завершено с **частичным успехом**. Backend часть выполнена хорошо, Mobile App часть выполнена частично — отсутствует ключевой компонент `expo-notifications`.
|
||||||
|
|
||||||
|
**Оценка качества: 6/10**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task Completion Status
|
||||||
|
|
||||||
|
### @worker1 — Backend Tasks
|
||||||
|
|
||||||
|
| Task | Status | Comments |
|
||||||
|
|------|--------|----------|
|
||||||
|
| **TASK-1:** Улучшить sendPushNotifications с проверкой настроек | ✅ Done | Отличная реализация в `notifications.js` |
|
||||||
|
| **TASK-2:** notification_history таблица и логирование | ✅ Done | Миграция + сервис созданы |
|
||||||
|
| **TASK-3:** API для получения истории алертов | ✅ Done | `/api/notification-settings/history` работает |
|
||||||
|
| **TASK-4:** Деплой backend изменений | ⚠️ Not Verified | Нужен ручной деплой на сервер |
|
||||||
|
|
||||||
|
### @worker2 — Mobile App Tasks
|
||||||
|
|
||||||
|
| Task | Status | Comments |
|
||||||
|
|------|--------|----------|
|
||||||
|
| **TASK-5:** Установить expo-notifications | ❌ Not Done | Пакет НЕ установлен в package.json |
|
||||||
|
| **TASK-6:** Создать сервис pushNotifications.ts | ❌ Not Done | Файл отсутствует |
|
||||||
|
| **TASK-7:** Интеграция при логине | ❌ Not Done | Зависит от TASK-5/6 |
|
||||||
|
| **TASK-8:** Обработка входящих push уведомлений | ❌ Not Done | Зависит от TASK-5/6 |
|
||||||
|
| **TASK-9:** UI настроек уведомлений | ✅ Done | `notifications.tsx` создан и работает |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code Quality Analysis
|
||||||
|
|
||||||
|
### Backend Code (`backend/src/services/notifications.js`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ Хорошая структура — модульный подход с отдельными функциями
|
||||||
|
2. ✅ Проверка notification settings перед отправкой
|
||||||
|
3. ✅ Quiet hours поддержка с учётом overnight периодов (22:00-07:00)
|
||||||
|
4. ✅ Emergency alerts обходят quiet hours — правильно
|
||||||
|
5. ✅ Batch отправка (chunks по 100) — согласно рекомендациям Expo
|
||||||
|
6. ✅ Логирование в notification_history для всех статусов (sent/skipped/failed)
|
||||||
|
7. ✅ Валидация Expo Push Token формата
|
||||||
|
|
||||||
|
**Potential Issues:**
|
||||||
|
1. ⚠️ `isInQuietHours` использует `timezone = 'UTC'` по умолчанию, но пользователь может быть в другом timezone. Timezone не передаётся из settings.
|
||||||
|
2. ⚠️ `getUserPushTokens` фильтрует по `is_active = true`, но нигде не видно логики деактивации токенов при ошибках.
|
||||||
|
|
||||||
|
### Backend Code (`backend/src/routes/notification-settings.js`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ Правильный маппинг snake_case (DB) → camelCase (API)
|
||||||
|
2. ✅ Upsert для settings — работает корректно
|
||||||
|
3. ✅ Pagination с лимитом (max 100) в history endpoint
|
||||||
|
4. ✅ Фильтрация по type/status в history
|
||||||
|
|
||||||
|
**Potential Issues:**
|
||||||
|
1. ⚠️ Нет input validation — `quietStart`/`quietEnd` могут быть любыми строками
|
||||||
|
|
||||||
|
### Backend Code (`backend/src/services/mqtt.js`)
|
||||||
|
|
||||||
|
**Issues Found:**
|
||||||
|
1. ❌ **НЕ использует новый notifications.js сервис!** Функция `sendPushNotifications` в mqtt.js — старая версия, которая НЕ проверяет notification_settings
|
||||||
|
2. ❌ Дублирование кода — есть две версии `sendPushNotifications`:
|
||||||
|
- `mqtt.js:216` — старая, без проверки настроек
|
||||||
|
- `notifications.js:294` — новая, с полной логикой
|
||||||
|
|
||||||
|
### Migration (`010_create_notification_history.sql`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ Все необходимые поля (user_id, type, status, skip_reason, etc.)
|
||||||
|
2. ✅ Индексы для частых запросов
|
||||||
|
3. ✅ CHECK constraint на status
|
||||||
|
4. ✅ Комментарии на колонки
|
||||||
|
|
||||||
|
### Mobile App (`types/index.ts`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ Типы `NotificationSettings`, `NotificationHistoryItem`, `NotificationHistoryResponse` корректны
|
||||||
|
2. ✅ Соответствуют API response формату
|
||||||
|
|
||||||
|
### Mobile App (`services/api.ts`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ `getNotificationSettings()` — корректная реализация
|
||||||
|
2. ✅ `updateNotificationSettings()` — работает
|
||||||
|
3. ✅ `getNotificationHistory()` — с фильтрами и pagination
|
||||||
|
|
||||||
|
### Mobile App (`app/(tabs)/profile/notifications.tsx`)
|
||||||
|
|
||||||
|
**Strengths:**
|
||||||
|
1. ✅ Хороший UI с Switch компонентами
|
||||||
|
2. ✅ Загрузка настроек с сервера
|
||||||
|
3. ✅ Сохранение через API
|
||||||
|
4. ✅ Quiet hours toggle с отображением времени
|
||||||
|
|
||||||
|
**Issues:**
|
||||||
|
1. ⚠️ Time picker для quiet hours — заглушка "Coming Soon"
|
||||||
|
2. ⚠️ "Send Test Notification" — только Alert, не отправляет реальный push
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Analysis
|
||||||
|
|
||||||
|
### Positive:
|
||||||
|
1. ✅ JWT authentication на всех endpoints
|
||||||
|
2. ✅ Expo Push Token validation
|
||||||
|
3. ✅ Admin-only endpoints в mqtt.js
|
||||||
|
|
||||||
|
### Concerns:
|
||||||
|
1. ⚠️ `quietStart`/`quietEnd` не валидируются как HH:MM формат
|
||||||
|
2. ⚠️ MQTT credentials хардкодены в коде (хоть и через env)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Missing Components
|
||||||
|
|
||||||
|
### 1. expo-notifications Package
|
||||||
|
```bash
|
||||||
|
# Ожидалось в package.json:
|
||||||
|
"expo-notifications": "~0.31.0" # НЕ НАЙДЕНО!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. pushNotifications.ts Service
|
||||||
|
Файл `services/pushNotifications.ts` **не создан**. Должен содержать:
|
||||||
|
- `registerForPushNotificationsAsync()`
|
||||||
|
- `registerTokenOnServer(token)`
|
||||||
|
- `unregisterToken()`
|
||||||
|
|
||||||
|
### 3. MQTT → Notifications Integration
|
||||||
|
`mqtt.js` НЕ интегрирован с новым `notifications.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// mqtt.js:140 — ТЕКУЩИЙ КОД (неправильно):
|
||||||
|
await sendPushNotifications(alert); // Локальная функция без проверки settings
|
||||||
|
|
||||||
|
// ДОЛЖНО БЫТЬ:
|
||||||
|
const { sendPushNotifications } = require('./notifications');
|
||||||
|
await sendPushNotifications({
|
||||||
|
userIds: users.map(u => u.user_id),
|
||||||
|
title: `Alert: ${beneficiaryName}`,
|
||||||
|
body: alert.body,
|
||||||
|
type: 'emergency', // или определять по содержимому
|
||||||
|
beneficiaryId: alert.beneficiaryId,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & TypeScript Check
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ TypeScript: No errors in main WellNuo project
|
||||||
|
❌ npm run build: Script not defined (expected — Expo project)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Critical (Must Fix):
|
||||||
|
|
||||||
|
1. **Установить expo-notifications:**
|
||||||
|
```bash
|
||||||
|
npx expo install expo-notifications
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Создать services/pushNotifications.ts:**
|
||||||
|
- Реализовать регистрацию push токена
|
||||||
|
- Интегрировать в login flow
|
||||||
|
|
||||||
|
3. **Интегрировать mqtt.js с notifications.js:**
|
||||||
|
- Заменить локальную `sendPushNotifications` на импорт из `notifications.js`
|
||||||
|
- Определять тип алерта (emergency/activity/low_battery) по содержимому
|
||||||
|
|
||||||
|
### Important:
|
||||||
|
|
||||||
|
4. **Добавить input validation для quiet hours:**
|
||||||
|
```javascript
|
||||||
|
const timeRegex = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||||
|
if (quietStart && !timeRegex.test(quietStart)) {
|
||||||
|
return res.status(400).json({ error: 'Invalid quietStart format' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Добавить timezone в notification_settings таблицу:**
|
||||||
|
- Quiet hours должны работать в timezone пользователя
|
||||||
|
|
||||||
|
6. **Деплой backend на сервер:**
|
||||||
|
```bash
|
||||||
|
rsync -avz backend/ root@91.98.205.156:/var/www/wellnuo-api/
|
||||||
|
ssh root@91.98.205.156 "cd /var/www/wellnuo-api && npm install && pm2 restart wellnuo-api"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nice to Have:
|
||||||
|
|
||||||
|
7. Реализовать time picker для quiet hours (вместо заглушки)
|
||||||
|
8. Добавить логику деактивации push токенов при ошибках доставки
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Checklist
|
||||||
|
|
||||||
|
| Test | Status |
|
||||||
|
|------|--------|
|
||||||
|
| Push токен регистрируется при логине | ❌ Не реализовано |
|
||||||
|
| MQTT алерты сохраняются в mqtt_alerts | ✅ Работает |
|
||||||
|
| Push отправляется с учётом настроек | ⚠️ Код есть, но не интегрирован |
|
||||||
|
| Notification history записывается | ✅ Работает |
|
||||||
|
| UI настроек работает | ✅ Работает |
|
||||||
|
| TypeScript без ошибок | ✅ Проходит |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
| File | Worker | Status | Quality |
|
||||||
|
|------|--------|--------|---------|
|
||||||
|
| `backend/migrations/010_create_notification_history.sql` | @worker1 | ✅ Created | Good |
|
||||||
|
| `backend/src/routes/notification-settings.js` | @worker1 | ✅ Modified | Good |
|
||||||
|
| `backend/src/services/notifications.js` | @worker1 | ✅ Created | Excellent |
|
||||||
|
| `services/api.ts` | @worker2 | ✅ Modified | Good |
|
||||||
|
| `types/index.ts` | @worker2 | ✅ Modified | Good |
|
||||||
|
| `app/(tabs)/profile/notifications.tsx` | @worker2 | ✅ Exists | Good |
|
||||||
|
| `services/pushNotifications.ts` | @worker2 | ❌ Missing | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Score
|
||||||
|
|
||||||
|
| Category | Score | Max | Notes |
|
||||||
|
|----------|-------|-----|-------|
|
||||||
|
| Task Completion | 5 | 10 | 5/9 tasks done |
|
||||||
|
| Code Quality | 8 | 10 | Clean, well-structured |
|
||||||
|
| TypeScript | 10 | 10 | No errors |
|
||||||
|
| Security | 8 | 10 | Good auth, minor validation gaps |
|
||||||
|
| Integration | 4 | 10 | mqtt.js not connected to notifications.js |
|
||||||
|
|
||||||
|
**Total: 6/10**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Backend часть (@worker1) выполнена качественно — создан полноценный notification service с проверкой настроек, quiet hours, и логированием истории.
|
||||||
|
|
||||||
|
Mobile App часть (@worker2) выполнена частично — UI настроек работает, но ключевой функционал регистрации push токенов отсутствует.
|
||||||
|
|
||||||
|
**Главная проблема:** `mqtt.js` использует старую локальную версию `sendPushNotifications` вместо нового сервиса `notifications.js`. Это означает, что настройки уведомлений НЕ применяются к реальным MQTT алертам.
|
||||||
|
|
||||||
|
Для достижения минимального балла 8/10 необходимо:
|
||||||
|
1. Установить expo-notifications
|
||||||
|
2. Создать pushNotifications.ts сервис
|
||||||
|
3. Интегрировать mqtt.js с notifications.js
|
||||||
|
4. Задеплоить backend на сервер
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit a578ec80815a3164a8c1fb86b06b0a2af81051e1
|
Subproject commit ef533de4d569a7045479d6f8742be35619cf2a78
|
||||||
@ -1 +1 @@
|
|||||||
Subproject commit 6d017ea617497dbc78c811b83bbcfc7c0831cbe4
|
Subproject commit 79d1a1f5fdfcdbfc037810f8c322e8c5da6cda56
|
||||||
@ -77,12 +77,14 @@ export default function LoginScreen() {
|
|||||||
return (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'padding'}
|
||||||
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
|
bounces={false}
|
||||||
>
|
>
|
||||||
<View style={styles.logoContainer}>
|
<View style={styles.logoContainer}>
|
||||||
<Image
|
<Image
|
||||||
@ -184,16 +186,16 @@ const styles = StyleSheet.create({
|
|||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
paddingHorizontal: Spacing.lg,
|
paddingHorizontal: Spacing.lg,
|
||||||
paddingTop: Spacing.xxl + Spacing.xl,
|
paddingTop: Platform.OS === 'android' ? Spacing.xl : Spacing.xxl + Spacing.xl,
|
||||||
paddingBottom: Spacing.xl,
|
paddingBottom: Spacing.xl,
|
||||||
},
|
},
|
||||||
logoContainer: {
|
logoContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginBottom: Spacing.xl,
|
marginBottom: Platform.OS === 'android' ? Spacing.md : Spacing.xl,
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
width: 120,
|
width: Platform.OS === 'android' ? 80 : 120,
|
||||||
height: 120,
|
height: Platform.OS === 'android' ? 80 : 120,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -24,8 +24,8 @@ export default function TabLayout() {
|
|||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: AppColors.surface,
|
backgroundColor: AppColors.surface,
|
||||||
borderTopWidth: 0,
|
borderTopWidth: 0,
|
||||||
height: Platform.OS === 'ios' ? 88 : 70,
|
height: Platform.OS === 'ios' ? 88 : 80,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 28 : 10,
|
paddingBottom: Platform.OS === 'ios' ? 28 : 20,
|
||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
...Shadows.sm,
|
...Shadows.sm,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -169,7 +169,7 @@ export default function AddSensorScreen() {
|
|||||||
{/* Scan Button */}
|
{/* Scan Button */}
|
||||||
{!isScanning && foundDevices.length === 0 && (
|
{!isScanning && foundDevices.length === 0 && (
|
||||||
<TouchableOpacity style={styles.scanButton} onPress={handleScan}>
|
<TouchableOpacity style={styles.scanButton} onPress={handleScan}>
|
||||||
<Ionicons name="scan" size={24} color={AppColors.white} />
|
<Ionicons name="bluetooth" size={24} color={AppColors.white} />
|
||||||
<Text style={styles.scanButtonText}>Scan for Sensors</Text>
|
<Text style={styles.scanButtonText}>Scan for Sensors</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -115,13 +115,32 @@ export default function SetupWiFiScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadWiFiNetworks = async () => {
|
const loadWiFiNetworks = async () => {
|
||||||
if (!deviceId) return;
|
console.log('[SetupWiFi] loadWiFiNetworks started, deviceId:', deviceId);
|
||||||
|
if (!deviceId) {
|
||||||
|
console.error('[SetupWiFi] No deviceId available!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsLoadingNetworks(true);
|
setIsLoadingNetworks(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// First connect to the device before requesting WiFi list
|
||||||
|
console.log('[SetupWiFi] Step 1: Connecting to device...');
|
||||||
|
const connected = await connectDevice(deviceId);
|
||||||
|
console.log('[SetupWiFi] Connection result:', connected);
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
throw new Error('Could not connect to sensor. Please move closer and try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[SetupWiFi] Step 2: Getting WiFi list...');
|
||||||
const wifiList = await getWiFiList(deviceId);
|
const wifiList = await getWiFiList(deviceId);
|
||||||
|
console.log('[SetupWiFi] WiFi networks found:', wifiList.length);
|
||||||
setNetworks(wifiList);
|
setNetworks(wifiList);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
console.error('[SetupWiFi] loadWiFiNetworks FAILED:', {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack?.substring(0, 300),
|
||||||
|
});
|
||||||
Alert.alert('Error', error.message || 'Failed to get WiFi networks. Please try again.');
|
Alert.alert('Error', error.message || 'Failed to get WiFi networks. Please try again.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingNetworks(false);
|
setIsLoadingNetworks(false);
|
||||||
@ -203,8 +222,8 @@ export default function SetupWiFiScreen() {
|
|||||||
// Step 3: Set WiFi
|
// Step 3: Set WiFi
|
||||||
updateSensorStep(deviceId, 'wifi', 'in_progress');
|
updateSensorStep(deviceId, 'wifi', 'in_progress');
|
||||||
updateSensorStatus(deviceId, 'setting_wifi');
|
updateSensorStatus(deviceId, 'setting_wifi');
|
||||||
const wifiSuccess = await setWiFi(deviceId, ssid, pwd);
|
// setWiFi now throws with detailed error message if it fails
|
||||||
if (!wifiSuccess) throw new Error('Failed to configure WiFi');
|
await setWiFi(deviceId, ssid, pwd);
|
||||||
updateSensorStep(deviceId, 'wifi', 'completed');
|
updateSensorStep(deviceId, 'wifi', 'completed');
|
||||||
|
|
||||||
if (shouldCancelRef.current) return false;
|
if (shouldCancelRef.current) return false;
|
||||||
@ -240,6 +259,12 @@ export default function SetupWiFiScreen() {
|
|||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const errorMsg = error.message || 'Unknown error';
|
const errorMsg = error.message || 'Unknown error';
|
||||||
|
console.error('[SetupWiFi] processSensor FAILED:', {
|
||||||
|
deviceId,
|
||||||
|
deviceName,
|
||||||
|
errorMsg,
|
||||||
|
stack: error?.stack?.substring(0, 200),
|
||||||
|
});
|
||||||
|
|
||||||
// Find current step and mark as failed
|
// Find current step and mark as failed
|
||||||
setSensors(prev => prev.map(s => {
|
setSensors(prev => prev.map(s => {
|
||||||
|
|||||||
556
backend/package-lock.json
generated
556
backend/package-lock.json
generated
@ -14,11 +14,13 @@
|
|||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"expo-server-sdk": "^4.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
"express-validator": "^7.3.1",
|
"express-validator": "^7.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mqtt": "^5.14.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
@ -926,6 +928,15 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.28.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@smithy/abort-controller": {
|
"node_modules/@smithy/abort-controller": {
|
||||||
"version": "4.2.7",
|
"version": "4.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz",
|
||||||
@ -1753,6 +1764,15 @@
|
|||||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/readable-stream": {
|
||||||
|
"version": "4.0.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.23.tgz",
|
||||||
|
"integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/ws": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@ -1762,6 +1782,18 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@ -1825,6 +1857,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/bcryptjs": {
|
"node_modules/bcryptjs": {
|
||||||
"version": "2.4.3",
|
"version": "2.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||||
@ -1844,6 +1896,43 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bl": {
|
||||||
|
"version": "6.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
|
||||||
|
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"inherits": "^2.0.4",
|
||||||
|
"readable-stream": "^4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bl/node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "1.20.4",
|
"version": "1.20.4",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
@ -1898,6 +1987,42 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/broker-factory": {
|
||||||
|
"version": "3.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.13.tgz",
|
||||||
|
"integrity": "sha512-H2VALe31mEtO/SRcNp4cUU5BAm1biwhc/JaF77AigUuni/1YT0FLCJfbUxwIEs9y6Kssjk2fmXgf+Y9ALvmKlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.48"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
"node_modules/buffer-equal-constant-time": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
@ -1996,6 +2121,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/commist": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -2160,6 +2291,12 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/err-code": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@ -2220,6 +2357,38 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/events": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-server-sdk": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-server-sdk/-/expo-server-sdk-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-zi83XtG2pqyP3gyn1JIRYkydo2i6HU3CYaWo/VvhZG/F29U+QIDv6LBEUsWf4ddZlVE7c9WN1N8Be49rHgO8OQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^2.6.0",
|
||||||
|
"promise-limit": "^2.7.0",
|
||||||
|
"promise-retry": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.1",
|
"version": "4.22.1",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
@ -2297,6 +2466,19 @@
|
|||||||
"node": ">= 8.0.0"
|
"node": ">= 8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-unique-numbers": {
|
||||||
|
"version": "9.0.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.26.tgz",
|
||||||
|
"integrity": "sha512-3Mtq8p1zQinjGyWfKeuBunbuFoixG72AUkk4VvzbX4ykCW9Q4FzRaNyIlfQhUjnKw2ARVP+/CKnoyr6wfHftig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-xml-parser": {
|
"node_modules/fast-xml-parser": {
|
||||||
"version": "5.2.5",
|
"version": "5.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
|
||||||
@ -2544,6 +2726,12 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/help-me": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@ -2585,6 +2773,26 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/ignore-by-default": {
|
"node_modules/ignore-by-default": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||||
@ -2668,6 +2876,16 @@
|
|||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/js-sdsl": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsonwebtoken": {
|
"node_modules/jsonwebtoken": {
|
||||||
"version": "9.0.3",
|
"version": "9.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
@ -2765,6 +2983,12 @@
|
|||||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -2868,6 +3092,149 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mqtt": {
|
||||||
|
"version": "5.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.14.1.tgz",
|
||||||
|
"integrity": "sha512-NxkPxE70Uq3Ph7goefQa7ggSsVzHrayCD0OyxlJgITN/EbzlZN+JEPmaAZdxP1LsIT5FamDyILoQTF72W7Nnbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/readable-stream": "^4.0.21",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"commist": "^3.2.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"debug": "^4.4.1",
|
||||||
|
"help-me": "^5.0.0",
|
||||||
|
"lru-cache": "^10.4.3",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"mqtt-packet": "^9.0.2",
|
||||||
|
"number-allocator": "^1.0.14",
|
||||||
|
"readable-stream": "^4.7.0",
|
||||||
|
"rfdc": "^1.4.1",
|
||||||
|
"socks": "^2.8.6",
|
||||||
|
"split2": "^4.2.0",
|
||||||
|
"worker-timers": "^8.0.23",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mqtt": "build/bin/mqtt.js",
|
||||||
|
"mqtt_pub": "build/bin/pub.js",
|
||||||
|
"mqtt_sub": "build/bin/sub.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet": {
|
||||||
|
"version": "9.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
|
||||||
|
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^6.0.8",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"process-nextick-args": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt-packet/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/concat-stream/node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/readable-stream": {
|
||||||
|
"version": "4.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mqtt/node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
@ -2911,6 +3278,26 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "3.1.11",
|
"version": "3.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||||
@ -2975,6 +3362,39 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/number-allocator": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
|
||||||
|
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.3.1",
|
||||||
|
"js-sdsl": "4.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/number-allocator/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/number-allocator/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@ -3164,12 +3584,40 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/promise-limit": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-limit/-/promise-limit-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/promise-retry": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"err-code": "^2.0.2",
|
||||||
|
"retry": "^0.12.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
@ -3269,6 +3717,21 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/retry": {
|
||||||
|
"version": "0.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||||
|
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rfdc": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@ -3443,6 +3906,30 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/smart-buffer": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socks": {
|
||||||
|
"version": "2.8.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
|
||||||
|
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.0.1",
|
||||||
|
"smart-buffer": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0",
|
||||||
|
"npm": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/split2": {
|
"node_modules/split2": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
@ -3561,6 +4048,12 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
@ -3641,6 +4134,69 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-factory": {
|
||||||
|
"version": "7.0.48",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.48.tgz",
|
||||||
|
"integrity": "sha512-CGmBy3tJvpBPjUvb0t4PrpKubUsfkI1Ohg0/GGFU2RvA9j/tiVYwKU8O7yu7gH06YtzbeJLzdUR29lmZKn5pag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers": {
|
||||||
|
"version": "8.0.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.29.tgz",
|
||||||
|
"integrity": "sha512-9jk0MWHhWAZ2xlJPXr45oe5UF/opdpfZrY0HtyPizWuJ+ce1M3IYk/4IIdGct3kn9Ncfs+tkZt3w1tU6KW2Fsg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-broker": "^8.0.15",
|
||||||
|
"worker-timers-worker": "^9.0.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-broker": {
|
||||||
|
"version": "8.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.15.tgz",
|
||||||
|
"integrity": "sha512-Te+EiVUMzG5TtHdmaBZvBrZSFNauym6ImDaCAnzQUxvjnw+oGjMT2idmAOgDy30vOZMLejd0bcsc90Axu6XPWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"broker-factory": "^3.1.13",
|
||||||
|
"fast-unique-numbers": "^9.0.26",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-timers-worker": "^9.0.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/worker-timers-worker": {
|
||||||
|
"version": "9.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.13.tgz",
|
||||||
|
"integrity": "sha512-qjn18szGb1kjcmh2traAdki1eiIS5ikFo+L90nfMOvSRpuDw1hAcR1nzkP2+Hkdqz5thIRnfuWx7QSpsEUsA6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.6",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"worker-factory": "^7.0.48"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.18.3",
|
"version": "8.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
|||||||
@ -14,11 +14,13 @@
|
|||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
"expo-server-sdk": "^4.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-rate-limit": "^8.2.1",
|
"express-rate-limit": "^8.2.1",
|
||||||
"express-validator": "^7.3.1",
|
"express-validator": "^7.3.1",
|
||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mqtt": "^5.14.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
|
|||||||
@ -24,7 +24,9 @@ const ordersRouter = require('./routes/orders');
|
|||||||
const stripeRouter = require('./routes/stripe');
|
const stripeRouter = require('./routes/stripe');
|
||||||
const webhookRouter = require('./routes/webhook');
|
const webhookRouter = require('./routes/webhook');
|
||||||
const adminRouter = require('./routes/admin');
|
const adminRouter = require('./routes/admin');
|
||||||
|
const mqttRouter = require('./routes/mqtt');
|
||||||
const { syncAllSubscriptions } = require('./services/subscription-sync');
|
const { syncAllSubscriptions } = require('./services/subscription-sync');
|
||||||
|
const mqttService = require('./services/mqtt');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@ -121,6 +123,7 @@ app.use('/api/orders', ordersRouter);
|
|||||||
app.use('/api/stripe', stripeRouter);
|
app.use('/api/stripe', stripeRouter);
|
||||||
app.use('/api/webhook', webhookRouter);
|
app.use('/api/webhook', webhookRouter);
|
||||||
app.use('/api/admin', adminRouter);
|
app.use('/api/admin', adminRouter);
|
||||||
|
app.use('/api/mqtt', mqttRouter);
|
||||||
|
|
||||||
// Admin UI
|
// Admin UI
|
||||||
app.get('/admin', (req, res) => {
|
app.get('/admin', (req, res) => {
|
||||||
@ -152,6 +155,7 @@ app.get('/api', (req, res) => {
|
|||||||
stripe: '/api/stripe',
|
stripe: '/api/stripe',
|
||||||
webhook: '/api/webhook/stripe',
|
webhook: '/api/webhook/stripe',
|
||||||
admin: '/api/admin',
|
admin: '/api/admin',
|
||||||
|
mqtt: '/api/mqtt',
|
||||||
legacy: '/function/well-api/api'
|
legacy: '/function/well-api/api'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -183,4 +187,26 @@ app.post('/api/admin/sync-subscriptions', async (req, res) => {
|
|||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`WellNuo API running on port ${PORT}`);
|
console.log(`WellNuo API running on port ${PORT}`);
|
||||||
console.log(`Stripe: ${process.env.STRIPE_SECRET_KEY ? '✓ configured' : '✗ missing'}`);
|
console.log(`Stripe: ${process.env.STRIPE_SECRET_KEY ? '✓ configured' : '✗ missing'}`);
|
||||||
|
|
||||||
|
// Initialize MQTT connection
|
||||||
|
mqttService.init();
|
||||||
|
|
||||||
|
// Subscribe to ALL active deployments from database
|
||||||
|
setTimeout(async () => {
|
||||||
|
const deployments = await mqttService.subscribeToAllDeployments();
|
||||||
|
console.log(`[MQTT] Subscribed to ${deployments.length} deployments:`, deployments);
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Graceful shutdown
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('SIGTERM received, shutting down gracefully...');
|
||||||
|
mqttService.shutdown();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, shutting down gracefully...');
|
||||||
|
mqttService.shutdown();
|
||||||
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,11 +14,15 @@ const verifyOtpLimiter = rateLimit({
|
|||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
// Use email if provided, otherwise fall back to IP
|
// Use email if provided, otherwise fall back to IP
|
||||||
const email = req.body?.email?.toLowerCase()?.trim();
|
const email = req.body?.email?.toLowerCase()?.trim();
|
||||||
return email || req.ip;
|
if (email) return email;
|
||||||
|
// Handle IPv6 addresses properly
|
||||||
|
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
|
return ip.replace(/^::ffff:/, ''); // Normalize IPv4-mapped IPv6
|
||||||
},
|
},
|
||||||
message: { error: 'Too many verification attempts. Please try again in 15 minutes.' },
|
message: { error: 'Too many verification attempts. Please try again in 15 minutes.' },
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
|
validate: { xForwardedForHeader: false }, // Disable IPv6 validation warning
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rate limiter for OTP request: 3 attempts per 15 minutes per email/IP
|
// Rate limiter for OTP request: 3 attempts per 15 minutes per email/IP
|
||||||
@ -28,11 +32,15 @@ const requestOtpLimiter = rateLimit({
|
|||||||
keyGenerator: (req) => {
|
keyGenerator: (req) => {
|
||||||
// Use email if provided, otherwise fall back to IP
|
// Use email if provided, otherwise fall back to IP
|
||||||
const email = req.body?.email?.toLowerCase()?.trim();
|
const email = req.body?.email?.toLowerCase()?.trim();
|
||||||
return email || req.ip;
|
if (email) return email;
|
||||||
|
// Handle IPv6 addresses properly
|
||||||
|
const ip = req.ip || req.socket?.remoteAddress || 'unknown';
|
||||||
|
return ip.replace(/^::ffff:/, ''); // Normalize IPv4-mapped IPv6
|
||||||
},
|
},
|
||||||
message: { error: 'Too many OTP requests. Please try again in 15 minutes.' },
|
message: { error: 'Too many OTP requests. Please try again in 15 minutes.' },
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
|
validate: { xForwardedForHeader: false }, // Disable IPv6 validation warning
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
138
backend/src/routes/mqtt.js
Normal file
138
backend/src/routes/mqtt.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* MQTT Routes for WellNuo API
|
||||||
|
*
|
||||||
|
* Endpoints:
|
||||||
|
* GET /api/mqtt/status - Get MQTT connection status
|
||||||
|
* GET /api/mqtt/alerts - Get recent alerts
|
||||||
|
* POST /api/mqtt/subscribe - Subscribe to deployment
|
||||||
|
* POST /api/mqtt/unsubscribe - Unsubscribe from deployment
|
||||||
|
* POST /api/mqtt/test - Send test message (admin only)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const mqttService = require('../services/mqtt');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth middleware - verify JWT token
|
||||||
|
*/
|
||||||
|
function authMiddleware(req, res, next) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ error: 'No token provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
req.user = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(401).json({ error: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin check middleware
|
||||||
|
*/
|
||||||
|
function adminMiddleware(req, res, next) {
|
||||||
|
const adminEmails = ['serter2069@gmail.com', 'ezmrzli@gmail.com', 'apple@zmrinc.com'];
|
||||||
|
if (!req.user || !adminEmails.includes(req.user.email?.toLowerCase())) {
|
||||||
|
return res.status(403).json({ error: 'Admin access required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/mqtt/health
|
||||||
|
* Public health check - no auth required
|
||||||
|
*/
|
||||||
|
router.get('/health', (req, res) => {
|
||||||
|
const status = mqttService.getStatus();
|
||||||
|
res.json({
|
||||||
|
connected: status.connected,
|
||||||
|
broker: status.broker,
|
||||||
|
subscribedTopics: status.subscribedTopics.length,
|
||||||
|
cachedAlerts: status.cachedAlerts,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply auth to protected routes
|
||||||
|
router.use(authMiddleware);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/mqtt/status
|
||||||
|
* Returns full MQTT connection status (auth required)
|
||||||
|
*/
|
||||||
|
router.get('/status', (req, res) => {
|
||||||
|
const status = mqttService.getStatus();
|
||||||
|
res.json(status);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/mqtt/alerts
|
||||||
|
* Returns recent alerts from cache
|
||||||
|
* Query params:
|
||||||
|
* - limit: number (default 50, max 100)
|
||||||
|
* - deploymentId: filter by deployment
|
||||||
|
*/
|
||||||
|
router.get('/alerts', (req, res) => {
|
||||||
|
const limit = Math.min(parseInt(req.query.limit) || 50, 100);
|
||||||
|
const deploymentId = req.query.deploymentId ? parseInt(req.query.deploymentId) : null;
|
||||||
|
|
||||||
|
const alerts = mqttService.getRecentAlerts(limit, deploymentId);
|
||||||
|
res.json({
|
||||||
|
count: alerts.length,
|
||||||
|
alerts,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/mqtt/subscribe
|
||||||
|
* Subscribe to a deployment's alerts
|
||||||
|
* Body: { deploymentId: number }
|
||||||
|
*/
|
||||||
|
router.post('/subscribe', (req, res) => {
|
||||||
|
const { deploymentId } = req.body;
|
||||||
|
|
||||||
|
if (!deploymentId) {
|
||||||
|
return res.status(400).json({ error: 'deploymentId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = mqttService.subscribeToDeployment(deploymentId);
|
||||||
|
res.json({ success, deploymentId });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/mqtt/unsubscribe
|
||||||
|
* Unsubscribe from a deployment's alerts
|
||||||
|
* Body: { deploymentId: number }
|
||||||
|
*/
|
||||||
|
router.post('/unsubscribe', (req, res) => {
|
||||||
|
const { deploymentId } = req.body;
|
||||||
|
|
||||||
|
if (!deploymentId) {
|
||||||
|
return res.status(400).json({ error: 'deploymentId is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
mqttService.unsubscribeFromDeployment(deploymentId);
|
||||||
|
res.json({ success: true, deploymentId });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/mqtt/test
|
||||||
|
* Send a test message (admin only)
|
||||||
|
* Body: { deploymentId: number, message: string }
|
||||||
|
*/
|
||||||
|
router.post('/test', adminMiddleware, (req, res) => {
|
||||||
|
const { deploymentId, message } = req.body;
|
||||||
|
|
||||||
|
if (!deploymentId || !message) {
|
||||||
|
return res.status(400).json({ error: 'deploymentId and message are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = mqttService.publishTest(deploymentId, message);
|
||||||
|
res.json({ success, deploymentId, message });
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
415
backend/src/services/mqtt.js
Normal file
415
backend/src/services/mqtt.js
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
/**
|
||||||
|
* MQTT Service for WellNuo Backend
|
||||||
|
*
|
||||||
|
* Connects to mqtt.eluxnetworks.net and listens for alerts
|
||||||
|
* from WellNuo IoT devices (sensors).
|
||||||
|
*
|
||||||
|
* Topic format: /well_{deployment_id}
|
||||||
|
* Message format: { Command: "REPORT", body: "alert text", time: unix_timestamp }
|
||||||
|
*
|
||||||
|
* Auto-subscribes to ALL active deployments from database
|
||||||
|
* Sends push notifications to users with access to each deployment
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
const { pool } = require('../config/database');
|
||||||
|
const { Expo } = require('expo-server-sdk');
|
||||||
|
const { sendPushNotifications: sendNotificationsWithSettings, NotificationType } = require('./notifications');
|
||||||
|
|
||||||
|
// MQTT Configuration
|
||||||
|
const MQTT_BROKER = process.env.MQTT_BROKER || 'mqtt://mqtt.eluxnetworks.net:1883';
|
||||||
|
const MQTT_USER = process.env.MQTT_USER || 'anandk';
|
||||||
|
const MQTT_PASSWORD = process.env.MQTT_PASSWORD || 'anandk_8';
|
||||||
|
|
||||||
|
// Expo Push Client
|
||||||
|
const expo = new Expo();
|
||||||
|
|
||||||
|
// Store for received alerts (in-memory, last 100)
|
||||||
|
const alertsCache = [];
|
||||||
|
const MAX_ALERTS_CACHE = 100;
|
||||||
|
|
||||||
|
// MQTT Client
|
||||||
|
let client = null;
|
||||||
|
let isConnected = false;
|
||||||
|
let subscribedTopics = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize MQTT connection
|
||||||
|
*/
|
||||||
|
function init() {
|
||||||
|
if (client) {
|
||||||
|
console.log('[MQTT] Already initialized');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MQTT] Connecting to ${MQTT_BROKER}...`);
|
||||||
|
|
||||||
|
client = mqtt.connect(MQTT_BROKER, {
|
||||||
|
username: MQTT_USER,
|
||||||
|
password: MQTT_PASSWORD,
|
||||||
|
clientId: `wellnuo-backend-${Date.now()}`,
|
||||||
|
reconnectPeriod: 5000, // Reconnect every 5 seconds
|
||||||
|
keepalive: 60,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log('[MQTT] ✅ Connected to broker');
|
||||||
|
isConnected = true;
|
||||||
|
|
||||||
|
// Resubscribe to all topics on reconnect
|
||||||
|
subscribedTopics.forEach(topic => {
|
||||||
|
client.subscribe(topic, (err) => {
|
||||||
|
if (!err) {
|
||||||
|
console.log(`[MQTT] Resubscribed to ${topic}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', async (topic, payload) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const messageStr = payload.toString();
|
||||||
|
|
||||||
|
console.log(`[MQTT] 📨 Message on ${topic}: ${messageStr}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(messageStr);
|
||||||
|
|
||||||
|
// Extract deployment_id from topic (/well_21 -> 21)
|
||||||
|
const deploymentId = parseInt(topic.replace('/well_', ''), 10);
|
||||||
|
|
||||||
|
const alert = {
|
||||||
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
topic,
|
||||||
|
deploymentId,
|
||||||
|
command: message.Command || 'UNKNOWN',
|
||||||
|
body: message.body || messageStr,
|
||||||
|
messageTime: message.time ? new Date(message.time * 1000).toISOString() : null,
|
||||||
|
receivedAt: timestamp,
|
||||||
|
raw: message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
alertsCache.unshift(alert);
|
||||||
|
if (alertsCache.length > MAX_ALERTS_CACHE) {
|
||||||
|
alertsCache.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process alert based on command
|
||||||
|
await processAlert(alert);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`[MQTT] ⚠️ Non-JSON message: ${messageStr}`);
|
||||||
|
|
||||||
|
// Still cache raw messages
|
||||||
|
alertsCache.unshift({
|
||||||
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
topic,
|
||||||
|
command: 'RAW',
|
||||||
|
body: messageStr,
|
||||||
|
receivedAt: timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
console.error('[MQTT] ❌ Error:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('close', () => {
|
||||||
|
console.log('[MQTT] 🔌 Connection closed');
|
||||||
|
isConnected = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('reconnect', () => {
|
||||||
|
console.log('[MQTT] 🔄 Reconnecting...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process incoming alert
|
||||||
|
*/
|
||||||
|
async function processAlert(alert) {
|
||||||
|
console.log(`[MQTT] Processing alert: ${alert.command} for deployment ${alert.deploymentId}`);
|
||||||
|
|
||||||
|
// Handle different command types
|
||||||
|
switch (alert.command) {
|
||||||
|
case 'REPORT':
|
||||||
|
// This is a sensor alert - could be emergency, activity, etc.
|
||||||
|
await saveAlertToDatabase(alert);
|
||||||
|
// Send push notification to users subscribed to this deployment
|
||||||
|
await sendPushNotifications(alert);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'CREDS':
|
||||||
|
// Credential/device setup message - ignore for now
|
||||||
|
console.log(`[MQTT] Ignoring CREDS message`);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`[MQTT] Unknown command: ${alert.command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active deployments from database
|
||||||
|
*/
|
||||||
|
async function getAllActiveDeployments() {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT DISTINCT legacy_deployment_id
|
||||||
|
FROM beneficiary_deployments
|
||||||
|
WHERE legacy_deployment_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
return result.rows.map(r => r.legacy_deployment_id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MQTT] Failed to get deployments from DB:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to all active deployments from database
|
||||||
|
*/
|
||||||
|
async function subscribeToAllDeployments() {
|
||||||
|
const deployments = await getAllActiveDeployments();
|
||||||
|
console.log(`[MQTT] Found ${deployments.length} active deployments:`, deployments);
|
||||||
|
|
||||||
|
for (const deploymentId of deployments) {
|
||||||
|
subscribeToDeployment(deploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get users with push tokens for a deployment
|
||||||
|
*/
|
||||||
|
async function getUsersForDeployment(deploymentId) {
|
||||||
|
try {
|
||||||
|
// Find all users who have access to beneficiaries linked to this deployment
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT DISTINCT
|
||||||
|
u.id as user_id,
|
||||||
|
u.email,
|
||||||
|
pt.token as push_token,
|
||||||
|
b.name as beneficiary_name,
|
||||||
|
ua.role
|
||||||
|
FROM beneficiary_deployments bd
|
||||||
|
JOIN user_access ua ON ua.beneficiary_id = bd.beneficiary_id
|
||||||
|
JOIN users u ON u.id = ua.accessor_id
|
||||||
|
JOIN beneficiaries b ON b.id = bd.beneficiary_id
|
||||||
|
LEFT JOIN push_tokens pt ON pt.user_id = u.id
|
||||||
|
WHERE bd.legacy_deployment_id = $1
|
||||||
|
AND pt.token IS NOT NULL
|
||||||
|
`, [deploymentId]);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MQTT] Failed to get users for deployment:', e.message);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send push notifications for an alert
|
||||||
|
*/
|
||||||
|
async function sendPushNotifications(alert) {
|
||||||
|
const users = await getUsersForDeployment(alert.deploymentId);
|
||||||
|
|
||||||
|
if (users.length === 0) {
|
||||||
|
console.log(`[MQTT] No push tokens found for deployment ${alert.deploymentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[MQTT] Sending push to ${users.length} users for deployment ${alert.deploymentId}`);
|
||||||
|
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
// Validate token format
|
||||||
|
if (!Expo.isExpoPushToken(user.push_token)) {
|
||||||
|
console.log(`[MQTT] Invalid push token for user ${user.email}: ${user.push_token}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build notification message
|
||||||
|
const beneficiaryName = user.beneficiary_name || 'Beneficiary';
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
to: user.push_token,
|
||||||
|
sound: 'default',
|
||||||
|
title: `Alert: ${beneficiaryName}`,
|
||||||
|
body: alert.body || 'New sensor alert',
|
||||||
|
data: {
|
||||||
|
type: 'mqtt_alert',
|
||||||
|
deploymentId: alert.deploymentId,
|
||||||
|
alertId: alert.id,
|
||||||
|
command: alert.command,
|
||||||
|
},
|
||||||
|
priority: 'high',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
console.log('[MQTT] No valid push tokens to send to');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send in chunks (Expo limit)
|
||||||
|
const chunks = expo.chunkPushNotifications(messages);
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
|
try {
|
||||||
|
const receipts = await expo.sendPushNotificationsAsync(chunk);
|
||||||
|
console.log(`[MQTT] ✅ Push sent:`, receipts);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[MQTT] ❌ Push failed:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save alert to database
|
||||||
|
*/
|
||||||
|
async function saveAlertToDatabase(alert) {
|
||||||
|
try {
|
||||||
|
await pool.query(`
|
||||||
|
INSERT INTO mqtt_alerts (deployment_id, command, body, message_time, received_at, raw_payload)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
|
`, [
|
||||||
|
alert.deploymentId,
|
||||||
|
alert.command,
|
||||||
|
alert.body,
|
||||||
|
alert.messageTime,
|
||||||
|
alert.receivedAt,
|
||||||
|
JSON.stringify(alert.raw)
|
||||||
|
]);
|
||||||
|
console.log('[MQTT] ✅ Alert saved to database');
|
||||||
|
} catch (e) {
|
||||||
|
// Table might not exist yet - that's ok
|
||||||
|
if (e.code === '42P01') {
|
||||||
|
console.log('[MQTT] mqtt_alerts table does not exist - skipping DB save');
|
||||||
|
} else {
|
||||||
|
console.error('[MQTT] DB save error:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to deployment alerts
|
||||||
|
* @param {number} deploymentId - The deployment ID to subscribe to
|
||||||
|
*/
|
||||||
|
function subscribeToDeployment(deploymentId) {
|
||||||
|
if (!client || !isConnected) {
|
||||||
|
console.error('[MQTT] Not connected');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = `/well_${deploymentId}`;
|
||||||
|
|
||||||
|
if (subscribedTopics.has(topic)) {
|
||||||
|
console.log(`[MQTT] Already subscribed to ${topic}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.subscribe(topic, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`[MQTT] Failed to subscribe to ${topic}:`, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
console.log(`[MQTT] ✅ Subscribed to ${topic}`);
|
||||||
|
subscribedTopics.add(topic);
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from deployment alerts
|
||||||
|
*/
|
||||||
|
function unsubscribeFromDeployment(deploymentId) {
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
const topic = `/well_${deploymentId}`;
|
||||||
|
client.unsubscribe(topic);
|
||||||
|
subscribedTopics.delete(topic);
|
||||||
|
console.log(`[MQTT] Unsubscribed from ${topic}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to multiple deployments
|
||||||
|
*/
|
||||||
|
function subscribeToDeployments(deploymentIds) {
|
||||||
|
deploymentIds.forEach(id => subscribeToDeployment(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent alerts from cache
|
||||||
|
*/
|
||||||
|
function getRecentAlerts(limit = 50, deploymentId = null) {
|
||||||
|
let alerts = alertsCache;
|
||||||
|
|
||||||
|
if (deploymentId) {
|
||||||
|
alerts = alerts.filter(a => a.deploymentId === deploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return alerts.slice(0, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get connection status
|
||||||
|
*/
|
||||||
|
function getStatus() {
|
||||||
|
return {
|
||||||
|
connected: isConnected,
|
||||||
|
broker: MQTT_BROKER,
|
||||||
|
subscribedTopics: Array.from(subscribedTopics),
|
||||||
|
cachedAlerts: alertsCache.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish a test message (for testing)
|
||||||
|
*/
|
||||||
|
function publishTest(deploymentId, message) {
|
||||||
|
if (!client || !isConnected) {
|
||||||
|
console.error('[MQTT] Not connected');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = `/well_${deploymentId}`;
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
Command: 'REPORT',
|
||||||
|
body: message,
|
||||||
|
time: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
client.publish(topic, payload);
|
||||||
|
console.log(`[MQTT] 📤 Published to ${topic}: ${payload}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Graceful shutdown
|
||||||
|
*/
|
||||||
|
function shutdown() {
|
||||||
|
if (client) {
|
||||||
|
console.log('[MQTT] Shutting down...');
|
||||||
|
client.end();
|
||||||
|
client = null;
|
||||||
|
isConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
init,
|
||||||
|
subscribeToDeployment,
|
||||||
|
unsubscribeFromDeployment,
|
||||||
|
subscribeToDeployments,
|
||||||
|
subscribeToAllDeployments,
|
||||||
|
getRecentAlerts,
|
||||||
|
getStatus,
|
||||||
|
publishTest,
|
||||||
|
shutdown,
|
||||||
|
};
|
||||||
@ -81,10 +81,11 @@ function getErrorMessage(error: string | undefined): { title: string; descriptio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show the actual error message for debugging
|
||||||
return {
|
return {
|
||||||
title: 'Setup Failed',
|
title: 'Setup Failed',
|
||||||
description: error,
|
description: error,
|
||||||
hint: 'Try again or skip this sensor to continue with others.',
|
hint: `Technical details: ${error}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,14 +53,20 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const connectDevice = useCallback(async (deviceId: string): Promise<boolean> => {
|
const connectDevice = useCallback(async (deviceId: string): Promise<boolean> => {
|
||||||
|
console.log('[BLEContext] connectDevice called:', deviceId);
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
const success = await bleManager.connectDevice(deviceId);
|
const success = await bleManager.connectDevice(deviceId);
|
||||||
|
console.log('[BLEContext] connectDevice result:', success);
|
||||||
if (success) {
|
if (success) {
|
||||||
setConnectedDevices(prev => new Set(prev).add(deviceId));
|
setConnectedDevices(prev => new Set(prev).add(deviceId));
|
||||||
|
} else {
|
||||||
|
// Connection failed but no exception - set user-friendly error
|
||||||
|
setError('Could not connect to sensor. Please move closer and try again.');
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] connectDevice exception:', err);
|
||||||
setError(err.message || 'Failed to connect to device');
|
setError(err.message || 'Failed to connect to device');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -80,10 +86,14 @@ export function BLEProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getWiFiList = useCallback(async (deviceId: string): Promise<WiFiNetwork[]> => {
|
const getWiFiList = useCallback(async (deviceId: string): Promise<WiFiNetwork[]> => {
|
||||||
|
console.log('[BLEContext] getWiFiList called:', deviceId);
|
||||||
try {
|
try {
|
||||||
setError(null);
|
setError(null);
|
||||||
return await bleManager.getWiFiList(deviceId);
|
const networks = await bleManager.getWiFiList(deviceId);
|
||||||
|
console.log('[BLEContext] getWiFiList success, found networks:', networks.length);
|
||||||
|
return networks;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error('[BLEContext] getWiFiList failed:', err);
|
||||||
setError(err.message || 'Failed to get WiFi networks');
|
setError(err.message || 'Failed to get WiFi networks');
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|||||||
103
mqtt-test.js
Normal file
103
mqtt-test.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* WellNuo MQTT Alert Monitor
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node mqtt-test.js # Monitor deployment 21 (Ferdinand)
|
||||||
|
* node mqtt-test.js 42 # Monitor specific deployment
|
||||||
|
* node mqtt-test.js send "text" # Send test alert
|
||||||
|
*/
|
||||||
|
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const MQTT_BROKER = 'mqtt://mqtt.eluxnetworks.net:1883';
|
||||||
|
const MQTT_USER = 'anandk';
|
||||||
|
const MQTT_PASSWORD = 'anandk_8';
|
||||||
|
const DEFAULT_DEPLOYMENT = 21; // Ferdinand
|
||||||
|
|
||||||
|
// Parse args
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const isSendMode = args[0] === 'send';
|
||||||
|
const deploymentId = isSendMode ? DEFAULT_DEPLOYMENT : (parseInt(args[0]) || DEFAULT_DEPLOYMENT);
|
||||||
|
const topic = `/well_${deploymentId}`;
|
||||||
|
|
||||||
|
console.log(`🔌 Connecting to ${MQTT_BROKER}...`);
|
||||||
|
|
||||||
|
const client = mqtt.connect(MQTT_BROKER, {
|
||||||
|
username: MQTT_USER,
|
||||||
|
password: MQTT_PASSWORD,
|
||||||
|
clientId: `wellnuo-monitor-${Date.now()}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
console.log(`✅ Connected to MQTT broker`);
|
||||||
|
console.log(`📡 Topic: ${topic}`);
|
||||||
|
|
||||||
|
if (isSendMode) {
|
||||||
|
// Send mode: publish test message and exit
|
||||||
|
const message = args.slice(1).join(' ') || 'Test alert from Node.js';
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
Command: 'REPORT',
|
||||||
|
body: message,
|
||||||
|
time: Math.floor(Date.now() / 1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📤 Sending: ${payload}`);
|
||||||
|
client.publish(topic, payload, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ Publish error:', err);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Message sent successfully');
|
||||||
|
}
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Monitor mode: subscribe and listen
|
||||||
|
console.log(`👂 Listening for messages... (Ctrl+C to stop)\n`);
|
||||||
|
|
||||||
|
client.subscribe(topic, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('❌ Subscribe error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('message', (receivedTopic, payload) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const message = payload.toString();
|
||||||
|
|
||||||
|
console.log(`\n📨 [${timestamp}]`);
|
||||||
|
console.log(` Topic: ${receivedTopic}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(message);
|
||||||
|
console.log(` Command: ${parsed.Command || 'N/A'}`);
|
||||||
|
console.log(` Body: ${parsed.body || 'N/A'}`);
|
||||||
|
console.log(` Time: ${parsed.time ? new Date(parsed.time * 1000).toISOString() : 'N/A'}`);
|
||||||
|
|
||||||
|
// Special handling for different commands
|
||||||
|
if (parsed.Command === 'REPORT') {
|
||||||
|
console.log(` 🚨 ALERT: ${parsed.body}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` Raw: ${message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (err) => {
|
||||||
|
console.error('❌ MQTT Error:', err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('close', () => {
|
||||||
|
console.log('🔌 Connection closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle Ctrl+C
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n👋 Shutting down...');
|
||||||
|
client.end();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
101
package-lock.json
generated
101
package-lock.json
generated
@ -7,6 +7,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "wellnuo",
|
"name": "wellnuo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@config-plugins/react-native-webrtc": "^13.0.0",
|
"@config-plugins/react-native-webrtc": "^13.0.0",
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
@ -34,6 +35,7 @@
|
|||||||
"expo-image-manipulator": "^14.0.8",
|
"expo-image-manipulator": "^14.0.8",
|
||||||
"expo-image-picker": "~17.0.10",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-speech": "~14.0.8",
|
"expo-speech": "~14.0.8",
|
||||||
@ -2640,6 +2642,12 @@
|
|||||||
"url": "https://github.com/sponsors/nzakas"
|
"url": "https://github.com/sponsors/nzakas"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ide/backoff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||||
@ -5420,6 +5428,19 @@
|
|||||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/assert": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"is-nan": "^1.3.2",
|
||||||
|
"object-is": "^1.1.5",
|
||||||
|
"object.assign": "^4.1.4",
|
||||||
|
"util": "^0.12.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/async-function": {
|
"node_modules/async-function": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||||
@ -5659,6 +5680,12 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@babel/core": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/badgin": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -7385,6 +7412,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-application": {
|
||||||
|
"version": "7.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-application/-/expo-application-7.0.8.tgz",
|
||||||
|
"integrity": "sha512-qFGyxk7VJbrNOQWBbE09XUuGuvkOgFS9QfToaK2FdagM2aQ+x3CvGV2DuVgl/l4ZxPgIf3b/MNh9xHpwSwn74Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-asset": {
|
"node_modules/expo-asset": {
|
||||||
"version": "12.0.12",
|
"version": "12.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz",
|
||||||
@ -7787,6 +7823,26 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-notifications": {
|
||||||
|
"version": "0.32.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.32.16.tgz",
|
||||||
|
"integrity": "sha512-QQD/UA6v7LgvwIJ+tS7tSvqJZkdp0nCSj9MxsDk/jU1GttYdK49/5L2LvE/4U0H7sNBz1NZAyhDZozg8xgBLXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/image-utils": "^0.8.8",
|
||||||
|
"@ide/backoff": "^1.0.0",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"assert": "^2.0.0",
|
||||||
|
"badgin": "^1.1.5",
|
||||||
|
"expo-application": "~7.0.8",
|
||||||
|
"expo-constants": "~18.0.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-router": {
|
"node_modules/expo-router": {
|
||||||
"version": "6.0.21",
|
"version": "6.0.21",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.21.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-6.0.21.tgz",
|
||||||
@ -9490,6 +9546,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-nan": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.0",
|
||||||
|
"define-properties": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-negative-zero": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
||||||
@ -11325,6 +11397,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-is": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.7",
|
||||||
|
"define-properties": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-keys": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
@ -14642,6 +14730,19 @@
|
|||||||
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==",
|
"integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/util": {
|
||||||
|
"version": "0.12.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
|
||||||
|
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"is-arguments": "^1.0.4",
|
||||||
|
"is-generator-function": "^1.0.7",
|
||||||
|
"is-typed-array": "^1.1.3",
|
||||||
|
"which-typed-array": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
"expo-image-manipulator": "^14.0.8",
|
"expo-image-manipulator": "^14.0.8",
|
||||||
"expo-image-picker": "~17.0.10",
|
"expo-image-picker": "~17.0.10",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
|
"expo-notifications": "~0.32.16",
|
||||||
"expo-router": "~6.0.21",
|
"expo-router": "~6.0.21",
|
||||||
"expo-secure-store": "^15.0.8",
|
"expo-secure-store": "^15.0.8",
|
||||||
"expo-speech": "~14.0.8",
|
"expo-speech": "~14.0.8",
|
||||||
|
|||||||
@ -404,18 +404,39 @@ export class RealBLEManager implements IBLEManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean> {
|
async setWiFi(deviceId: string, ssid: string, password: string): Promise<boolean> {
|
||||||
|
console.log('[BLE] setWiFi started:', { deviceId, ssid, passwordLength: password.length });
|
||||||
|
|
||||||
// Step 1: Unlock device
|
// Step 1: Unlock device
|
||||||
|
console.log('[BLE] Step 1: Unlocking device for WiFi config...');
|
||||||
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
const unlockResponse = await this.sendCommand(deviceId, BLE_COMMANDS.PIN_UNLOCK);
|
||||||
|
console.log('[BLE] Unlock response:', unlockResponse);
|
||||||
if (!unlockResponse.includes('ok')) {
|
if (!unlockResponse.includes('ok')) {
|
||||||
throw new Error('Failed to unlock device');
|
throw new Error(`Device unlock failed: ${unlockResponse}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Set WiFi credentials
|
// Step 2: Set WiFi credentials
|
||||||
const command = `${BLE_COMMANDS.SET_WIFI}|${ssid},${password}`;
|
const command = `${BLE_COMMANDS.SET_WIFI}|${ssid},${password}`;
|
||||||
|
console.log('[BLE] Step 2: Sending WiFi credentials...');
|
||||||
const setResponse = await this.sendCommand(deviceId, command);
|
const setResponse = await this.sendCommand(deviceId, command);
|
||||||
|
console.log('[BLE] WiFi config response:', setResponse);
|
||||||
|
|
||||||
// Parse response: "mac,XXXXXX|W|ok" or "mac,XXXXXX|W|fail"
|
// Parse response: "mac,XXXXXX|W|ok" or "mac,XXXXXX|W|fail" or other errors
|
||||||
return setResponse.includes('|W|ok');
|
if (setResponse.includes('|W|ok')) {
|
||||||
|
console.log('[BLE] WiFi configuration SUCCESS');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi config failed - throw detailed error
|
||||||
|
if (setResponse.includes('|W|fail')) {
|
||||||
|
throw new Error('WiFi credentials rejected by sensor. Check password.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setResponse.includes('timeout') || setResponse.includes('Timeout')) {
|
||||||
|
throw new Error('Sensor did not respond to WiFi config. Try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown error - include raw response for debugging
|
||||||
|
throw new Error(`WiFi config failed: ${setResponse.substring(0, 100)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
|
async getCurrentWiFi(deviceId: string): Promise<WiFiStatus | null> {
|
||||||
|
|||||||
247
services/pushNotifications.ts
Normal file
247
services/pushNotifications.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
/**
|
||||||
|
* Push Notifications Service
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Requesting push notification permissions
|
||||||
|
* - Getting Expo Push Token
|
||||||
|
* - Registering/unregistering token on server
|
||||||
|
* - Handling incoming notifications
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
import * as Device from 'expo-device';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
import Constants from 'expo-constants';
|
||||||
|
|
||||||
|
const WELLNUO_API_URL = 'https://wellnuo.smartlaunchhub.com/api';
|
||||||
|
const PUSH_TOKEN_KEY = 'expoPushToken';
|
||||||
|
|
||||||
|
// Configure notification handler for foreground notifications
|
||||||
|
Notifications.setNotificationHandler({
|
||||||
|
handleNotification: async () => ({
|
||||||
|
shouldShowAlert: true,
|
||||||
|
shouldPlaySound: true,
|
||||||
|
shouldSetBadge: true,
|
||||||
|
shouldShowBanner: true,
|
||||||
|
shouldShowList: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register for push notifications and get Expo Push Token
|
||||||
|
* Returns the token or null if not available
|
||||||
|
*/
|
||||||
|
export async function registerForPushNotificationsAsync(): Promise<string | null> {
|
||||||
|
let token: string | null = null;
|
||||||
|
|
||||||
|
// Must be a physical device for push notifications
|
||||||
|
if (!Device.isDevice) {
|
||||||
|
console.log('[Push] Must use physical device for push notifications');
|
||||||
|
// For simulator, return a fake token for testing
|
||||||
|
if (__DEV__) {
|
||||||
|
return 'ExponentPushToken[SIMULATOR_TEST_TOKEN]';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check/request permissions
|
||||||
|
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||||
|
let finalStatus = existingStatus;
|
||||||
|
|
||||||
|
if (existingStatus !== 'granted') {
|
||||||
|
const { status } = await Notifications.requestPermissionsAsync();
|
||||||
|
finalStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalStatus !== 'granted') {
|
||||||
|
console.log('[Push] Permission not granted');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Expo Push Token
|
||||||
|
try {
|
||||||
|
const projectId = Constants.expoConfig?.extra?.eas?.projectId;
|
||||||
|
|
||||||
|
const pushTokenResponse = await Notifications.getExpoPushTokenAsync({
|
||||||
|
projectId: projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
token = pushTokenResponse.data;
|
||||||
|
console.log('[Push] Got Expo Push Token:', token);
|
||||||
|
|
||||||
|
// Store locally
|
||||||
|
await SecureStore.setItemAsync(PUSH_TOKEN_KEY, token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Push] Error getting push token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android needs notification channel
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
await Notifications.setNotificationChannelAsync('default', {
|
||||||
|
name: 'default',
|
||||||
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
|
vibrationPattern: [0, 250, 250, 250],
|
||||||
|
lightColor: '#FF231F7C',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emergency alerts channel
|
||||||
|
await Notifications.setNotificationChannelAsync('emergency', {
|
||||||
|
name: 'Emergency Alerts',
|
||||||
|
importance: Notifications.AndroidImportance.MAX,
|
||||||
|
vibrationPattern: [0, 500, 200, 500],
|
||||||
|
lightColor: '#FF0000',
|
||||||
|
sound: 'default',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register push token on WellNuo backend
|
||||||
|
*/
|
||||||
|
export async function registerTokenOnServer(token: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const accessToken = await SecureStore.getItemAsync('accessToken');
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
console.log('[Push] No access token, skipping server registration');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${WELLNUO_API_URL}/push-tokens`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
token,
|
||||||
|
platform: Platform.OS,
|
||||||
|
deviceName: Device.deviceName || `${Device.brand} ${Device.modelName}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
console.error('[Push] Server registration failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Push] Token registered on server successfully');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Push] Error registering token on server:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister push token from server (call on logout)
|
||||||
|
*/
|
||||||
|
export async function unregisterToken(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const token = await SecureStore.getItemAsync(PUSH_TOKEN_KEY);
|
||||||
|
const accessToken = await SecureStore.getItemAsync('accessToken');
|
||||||
|
|
||||||
|
if (!token || !accessToken) {
|
||||||
|
return true; // Nothing to unregister
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${WELLNUO_API_URL}/push-tokens`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear local storage regardless of server response
|
||||||
|
await SecureStore.deleteItemAsync(PUSH_TOKEN_KEY);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('[Push] Server unregistration failed');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Push] Token unregistered successfully');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Push] Error unregistering token:', error);
|
||||||
|
// Still clear local storage
|
||||||
|
await SecureStore.deleteItemAsync(PUSH_TOKEN_KEY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full registration flow: get token + register on server
|
||||||
|
* Call this after successful login
|
||||||
|
*/
|
||||||
|
export async function setupPushNotifications(): Promise<string | null> {
|
||||||
|
console.log('[Push] Setting up push notifications...');
|
||||||
|
|
||||||
|
const token = await registerForPushNotificationsAsync();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
await registerTokenOnServer(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get stored push token (if any)
|
||||||
|
*/
|
||||||
|
export async function getStoredPushToken(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
return await SecureStore.getItemAsync(PUSH_TOKEN_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listener for received notifications (while app is open)
|
||||||
|
*/
|
||||||
|
export function addNotificationReceivedListener(
|
||||||
|
callback: (notification: Notifications.Notification) => void
|
||||||
|
): Notifications.EventSubscription {
|
||||||
|
return Notifications.addNotificationReceivedListener(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listener for notification response (user tapped notification)
|
||||||
|
*/
|
||||||
|
export function addNotificationResponseListener(
|
||||||
|
callback: (response: Notifications.NotificationResponse) => void
|
||||||
|
): Notifications.EventSubscription {
|
||||||
|
return Notifications.addNotificationResponseReceivedListener(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get last notification response (if app was opened from notification)
|
||||||
|
*/
|
||||||
|
export async function getLastNotificationResponse(): Promise<Notifications.NotificationResponse | null> {
|
||||||
|
return await Notifications.getLastNotificationResponseAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse notification data to determine navigation target
|
||||||
|
*/
|
||||||
|
export function parseNotificationData(data: Record<string, unknown>): {
|
||||||
|
type: string;
|
||||||
|
deploymentId?: number;
|
||||||
|
beneficiaryId?: number;
|
||||||
|
alertId?: string;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
type: (data.type as string) || 'unknown',
|
||||||
|
deploymentId: data.deploymentId as number | undefined,
|
||||||
|
beneficiaryId: data.beneficiaryId as number | undefined,
|
||||||
|
alertId: data.alertId as string | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user