MES I/O Gateway / 開發者 / 整合
04開發者 / 整合

Config Server API

📅 最後更新:2026-05-24 | 🛠 對應版本:v2.2.10 | 📌 負責人:KC
🔗 雲端:https://opta.smms.com.twSERVER_ROLE=cloud)|地端:http://<SERVER_IP>:8888SERVER_ROLE=local

本文件涵蓋 MES I/O Gateway Config Server 的全部 REST API。Server 採 單一 codebase + 角色模式;以 SERVER_ROLE 環境變數切換:

  • local:地端備份 / 還原伺服器(檔案系統 + SQLite,無 PG、無 license、無 Cloud Bridge)
  • cloud:雲端授權 + OTA 派送(PostgreSQL 必要,啟用 license 模組與 Cloud Bridge)

標 ☁️ 的端點僅 SERVER_ROLE=cloud 時可用。


#認證機制

middleware/auth.js 採三軌制:

順序 Token 型態 來源 注入 req.user
1 JWT POST /api/auth/login 簽發(12h 有效) { id, email, role, displayName }
2 Legacy DEVICE_TOKEN 環境變數 DEVICE_TOKEN(預設 admin-token { role: 'device', id: 0 }
3 License Token 雲端 license 模組簽發給已啟用 device { role: 'device', uid }

呼叫方式:

Authorization: Bearer <token>

未帶 Bearer header → 401 Unauthorized {"error": "Unauthorized", "message": "Missing Authorization header"}。失敗事件會打 auth.reject log,使用者回報 401/403 時用 grep auth.reject 立刻看到原因。

#角色 (role)

Role 來源 權限
admin DB user role 所有 /api/admin/*、user/license/firmware 管理
local_admin DB user role 等同 admin 但限地端
customer DB user role Tenant-scoped:/api/devices 只看自己擁有的
device Legacy / License Token Device 端 heartbeat、config sync、OTA fetch

adminOnly middleware 額外把關到 role ∈ {admin, local_admin}


#端點總覽

#共用 API(local + cloud)

# Method Path 認證 呼叫方 用途
0 GET /api/health Public All 健康檢查(含 role / version / device 數)
1 POST /api/auth/login Public User 登入取得 JWT
2 GET /api/auth/me JWT User 查目前登入者
3 POST /api/auth/password JWT (非 device) User 自助改密碼
4 POST /api/devices/claim/generate JWT User Dashboard 產生 6 位數 OTP 綁定 device
5 POST /api/devices/claim Public Device Device 用 OTP 完成綁定
6 POST /api/devices/:id/heartbeat Optional Device Device 心跳(含 firmware/license/OTA 回路)
7 GET /api/devices Required Dashboard 列出所有設備(tenant filter)
8 DELETE /api/devices/:id Required Dashboard 刪除設備(記憶體)
9 POST /api/admin/devices/cleanup-offline Admin Dashboard 批次刪除離線設備
10 GET /api/devices/:id/config Required Device 下載設備設定
11 POST /api/devices/:id/config Required Device 上傳設備設定(自動備份 + snapshot)
12 GET /api/devices/:id/backups Required Dashboard Backup 清單(含 diff summary)
13 GET /api/devices/:id/backups/:filename Required Dashboard 取單一 backup 全文
14 GET /api/devices/:id/diff Required Dashboard Backup vs current diff
15 POST /api/devices/:id/restore Required Dashboard 還原至 backup
16 GET /api/devices/:id/snapshots Required Dashboard 自動 snapshot 列表(最近 50 / 30 天)
17 GET /api/devices/:id/snapshots/:filename Required Dashboard 取單一 snapshot
18 POST /api/devices/:id/snapshots/:filename/rollback Required Dashboard Rollback 到 snapshot
19 POST /api/devices/:id/web Required Device 上傳 web bundle(CRC 比對省流量)
20 GET /api/devices/:id/web/crc Required Device 查 web bundle CRC
21 GET /api/firmware/latest Public Device 依 channel 查最新韌體(auto-update)
22 GET /api/firmware/download/:filename Public Device 下載韌體 .bin
23 POST /api/firmware/upload Admin Dashboard 上傳韌體
24 GET /api/firmware Admin Dashboard 列出韌體(含每版本在線 device 數)
25 GET /api/firmware/:filename/devices Admin Dashboard 看哪些 device 在跑這版
26 GET /api/firmware/:filename/channel-history Admin Dashboard Promote/demote 歷史
27 POST /api/admin/firmware/:filename/channel Admin Dashboard 改 channel(dev/beta/stable)— nginx 不擋
28 PATCH /api/admin/firmware/:filename/channel Admin Dashboard 同上
29 DELETE /api/admin/firmware/:filename Admin Dashboard 刪韌體(擋 stable,可 ?force=1
30 POST /api/firmware/deploy/:deviceId Admin Dashboard Queue OTA 給單一 device
31 POST /api/devices/:id/ota (deprecated) Optional - .mesb 上傳 → 一律 410 Gone
32 GET /api/devices/:id/ota/latest (deprecated) Optional - .mesb 查詢 → 一律 410 Gone
33 GET /api/customers Required Dashboard 客戶列表(含 device/online 計數)
34 GET /api/customers/:id Required Dashboard 客戶詳細
35 GET /api/customers/:id/devices Required Dashboard 該客戶旗下設備
36 POST /api/customers Required Dashboard 建立客戶
37 PATCH /api/customers/:id Required Dashboard 修改客戶
38 DELETE /api/customers/:id Required Dashboard 刪除客戶(device 改未指派而非刪除)
39 POST /api/devices/:uid/assign-customer Required Dashboard 指派 / 解除 device 對客戶歸屬
40 GET /api/admin/users Admin Dashboard 使用者列表
41 POST /api/admin/users Admin Dashboard 新增使用者
42 PUT /api/admin/users/:id Admin Dashboard 修改使用者
43 PUT /api/admin/users/:id/password Admin Dashboard Admin 重置別人密碼
44 GET /api/audit-logs Admin Dashboard 稽核日誌(分頁)
45 GET /api/admin/backup-status Admin Dashboard pg_dump 健康度(v2.2.6+)
46 GET /api/cloud/status Public Dashboard Cloud Bridge 狀態(local 模式回 disabled)

#☁️ Cloud-only

僅當 SERVER_ROLE=cloud 時掛載(server.js:302–309):

# Method Path 認證 說明
C1 POST /api/license/activate Public Device 申請啟用(whitelist 直接 active)
C2 GET /api/license/status/:uid Public Device 查狀態
C3 GET /api/admin/license/list Admin 全部 license(含統計)
C4 POST /api/admin/license/import Admin 批次匯入 UID 白名單
C5 POST /api/admin/license/approve/:uid Admin 審核通過(pending → active)
C6 POST /api/admin/license/reject/:uid Admin 拒絕(device 可重新申請回 pending)
C7 POST /api/admin/license/deactivate/:uid Admin 強制停權(active → rejected,無 grace period)
C8 DELETE /api/admin/license/:uid Admin 硬刪除(寫 tombstone,90 天不可重新 activate)
C9 DELETE /api/admin/license-revoked/:uid Admin 清掉 tombstone(讓被刪 UID 可重新 activate)

#0. 健康檢查

GET /api/health

200 OK

json
{
  "status": "ok",
  "role": "cloud",
  "version": "2.2.10",
  "uptime": 3600,
  "timestamp": "2026-05-24T06:00:00.000Z",
  "deviceCount": 12
}
bash
curl https://opta.smms.com.tw/api/health

#1. Auth

#POST /api/auth/login

json
{ "email": "kowei.chen@gmail.com", "password": "..." }

200 OK

json
{
  "ok": true,
  "token": "eyJhbGciOi...",
  "user": { "id": 1, "email": "kowei.chen@gmail.com", "role": "admin", "displayName": "KC" }
}

401{"error": "Unauthorized", "message": "invalid credentials"}

JWT TTL 12 小時,逾期後須重新 login。

#GET /api/auth/me

回目前 JWT 對應的 user 物件。供前端 reload 後重新水合 user state。

#POST /api/auth/password

json
{ "currentPassword": "old", "newPassword": "new12345" }

newPassword 至少 8 字元。Device token 一律 403(不准改 device user)。


#2. Device Claim

OTP-based 雙向綁定(Sprint 21 WI-070):

#POST /api/devices/claim/generate(user 端)

需 JWT。產生 6 位數 OTP,10 分鐘有效。

200 OK

json
{
  "ok": true,
  "claimToken": "493217",
  "expiresAt": "2026-05-24T06:10:00.000Z",
  "instruction": "在設備頁面輸入此 6 位數驗證碼以完成綁定"
}

#POST /api/devices/claim(device 端)

json
{ "deviceId": "opta-AA-BB-CC", "claimToken": "493217" }

200 OK{"ok": true, "message": "Device claimed", "deviceId": "...", "userId": 1}
410:OTP 失效


#3. Heartbeat

#POST /api/devices/:id/heartbeat

最熱門 endpoint。Device 約 30 秒一次回報;server 回 OTA / license verdict / 待派送 web upload。

掛載於 auth middleware 之前 → token 為 optional(若帶會解,缺也不擋)。

Request body

json
{
  "deviceName": "opta-line-3",
  "ip": "192.168.0.100",
  "uptime": 123456,
  "configVersion": 17,
  "uid": "AA:BB:CC:DD:EE:FF",
  "firmwareVersion": "5.9.130",
  "firmwareVariant": "standard",
  "otaProgress": null
}

Response 200 OK

json
{
  "status": "ok",
  "license": { "status": "active", "licenseId": "LIC-2026-..." },
  "ota": {
    "url": "https://opta.smms.com.tw/api/firmware/download/opta_v5.9.131.bin",
    "version": "5.9.131",
    "force": false
  }
}

可能的 status

  • ok — 一切正常
  • ota_available — 有更新可拉
  • download_update — 後端要求立刻拉 config + web(例如 restore / rollback 完成)

Side effects

  1. Persist 至 PG devices 表(uid PK)(cloud 模式;v2.2.10 dff2c2d)
  2. configVersion 變化 → 觸發 auto-snapshot(source=auto-heartbeat
  3. 派送 pending OTA / web upload 給 device
  4. 呼叫 cloud-bridge.onHeartbeat(Arduino Cloud 同步)
  5. License upsert:未見過的 UID 會建 unseen 紀錄

#4. Devices

#GET /api/devices

Auth required。Cloud 模式回傳 PG devices 表 + in-memory map 的 union(uid 去重);Local 模式 fallback 只看 in-memory。

customer role 會被 tenant filter(只看自己擁有的 device IDs)。

json
[
  {
    "id": "opta-AA-BB-CC",
    "uid": "AA:BB:CC:DD:EE:FF",
    "deviceName": "opta-line-3",
    "ip": "192.168.0.100",
    "firmwareVersion": "5.9.130",
    "firmwareVariant": "standard",
    "lastSeen": "2026-05-24T05:59:30.000Z",
    "uptime": 123456,
    "configVersion": 17,
    "otaProgress": null,
    "pendingCommand": null,
    "registered": true,
    "status": "online",
    "customerId": 5,
    "customerName": "Foxconn Tier-1"
  }
]

v2.2.10(commit dff2c2d):原本 /api/devices 用 in-memory deviceId 為 key,導致同樣 deviceName 但不同 UID 的 device 互蓋。改用 PG 的 uid PK 後,每顆 Opta 都有唯一 row;舊資料沒 uid 者標 registered: false

#DELETE /api/devices/:id

刪除 in-memory map 上的 device(不會動 PG row)。

#POST /api/admin/devices/cleanup-offline

json
{ "olderThanSec": 86400 }

?olderThanSec=86400 query。預設 86400(24h)。

200 OK

json
{ "success": true, "count": 3, "devices": [...], "olderThanSec": 86400 }

#5. Device Config

#GET /api/devices/:id/config

回 device 的最新 config JSON(剝掉 crc32 欄位)。

#POST /api/devices/:id/config

json
{ "deviceName": "opta-line-3", "mqtt": { ... }, ... }

Side effects

  1. backups/config.bak.{ts}.json 並 prune 7 天以上舊檔
  2. 觸發 saveSnapshotIfChanged(source=auto-push;v2.2.3+)
  3. 取消任何 pending download_update command

200 OK{"success": true, "message": "Config updated"}


#6. Backups & Restore

#GET /api/devices/:id/backups

json
[
  {
    "filename": "config.bak.1748044800.json",
    "timestamp": 1748044800,
    "date": "2026-05-24T00:00:00.000Z",
    "sizeBytes": 8421,
    "summary": { "added": 1, "removed": 0, "changed": 3 }
  }
]

#GET /api/devices/:id/backups/:filename

filename 需符合 config.bak.\d+.json,否則 400 Bad Request。回完整 backup JSON。

#GET /api/devices/:id/diff?file=config.bak.{ts}.json

json
{
  "added":   { "mqtt.useTls": true },
  "removed": { "mqtt.legacyField": "..." },
  "changed": { "mqtt.broker": { "from": "...", "to": "..." } }
}

#POST /api/devices/:id/restore

json
{ "filename": "config.bak.1748044800.json" }

設置 pendingCommand=download_update + pendingWebUpload=true,下次 heartbeat device 會收到指令並主動拉新 config。


#7. Snapshots(自動觸發)

不同於 manual backup,snapshot 由系統在 configVersion 變化時自動產生。backups/snapshot.{ts}.v{version}.json最多保留 50 個 / 30 天

#GET /api/devices/:id/snapshots?limit=20

json
[
  {
    "filename": "snapshot.1748044800.v17.json",
    "timestamp": 1748044800,
    "date": "2026-05-24T00:00:00.000Z",
    "configVersion": 17,
    "sizeBytes": 8421,
    "source": "auto-heartbeat",
    "deviceName": "opta-line-3",
    "summary": { "added": 1, "removed": 0, "changed": 3 }
  }
]

source 可能值:

  • auto-push — 由 POST /api/devices/:id/config 觸發
  • auto-heartbeat — heartbeat 偵測 configVersion 變化觸發

#GET /api/devices/:id/snapshots/:filename

回完整 snapshot:

json
{
  "_snapshot": { "timestamp": 1748044800, "configVersion": 17, "deviceName": "...", "source": "auto-push" },
  "deviceName": "...",
  "mqtt": { ... }
}

#POST /api/devices/:id/snapshots/:filename/rollback

pendingCommand=download_update 並把 snapshot 內容寫回當前 config。

json
{ "success": true, "message": "Rolled back to snapshot", "newConfigVersion": 18 }

#8. Web Upload

Device firmware 把 inline 的 WebPage.h 算 CRC32 上傳;server 比對 CRC,若無變更則跳過寫檔省流量。

#POST /api/devices/:id/web

Raw binary body(Content-Type: application/octet-stream,最大 2 MB)。

200 OK

json
{ "success": true, "bytes": 23456, "crc": 3735928559 }

未變更:

json
{ "success": true, "unchanged": true, "crc": 3735928559 }

#GET /api/devices/:id/web/crc

json
{ "crc": 3735928559 }

無紀錄回 {"crc": 0}


#9. Firmware Management

#Public — Device 用

#GET /api/firmware/latest?channel=stable

channel 可選 stable / beta / dev / all,預設 stable

json
{
  "filename": "opta_v5.9.131.bin",
  "version": "5.9.131",
  "variant": "standard",
  "releaseChannel": "stable",
  "size": 781234,
  "sha256": "ab12...",
  "changelog": "- Fix MQTTS handshake\n- Add ...",
  "downloadUrl": "/api/firmware/download/opta_v5.9.131.bin"
}

無符合:{"error": "No firmware in channel", "hint": "Try ?channel=all"}

#GET /api/firmware/download/:filename

application/octet-streamfilename 必須是 .bin,否則 400

#Admin — Dashboard 用

#POST /api/firmware/upload

Raw .bin body(最大 4 MB)。Headers:

Header 必要 說明
X-Firmware-Version M.m.p 格式(如 5.9.131
X-Firmware-Changelog-B64X-Firmware-Changelog ≥ 10 字元(純文字者直接放,URL-safe 者放 base64)
X-Firmware-Channel dev / beta / stable(預設 dev
X-Firmware-No-License 1 = no-license variant(變體欄位)

200 OK

json
{
  "ok": true,
  "filename": "opta_v5.9.131.bin",
  "size": 781234,
  "sha256": "ab12...",
  "releaseChannel": "dev",
  "changelogLength": 142
}

#GET /api/firmware?channel=stable&variant=all

回 channel 下韌體列表,附「目前在線跑這版的 device 數」統計:

json
[
  {
    "filename": "opta_v5.9.131.bin",
    "version": "5.9.131",
    "variant": "standard",
    "releaseChannel": "stable",
    "size": 781234,
    "sha256": "ab12...",
    "devices": { "total": 8, "online": 5, "byVersion": { "5.9.131": 3, "5.9.130": 2 } }
  }
]

#GET /api/firmware/:filename/devices

#GET /api/firmware/:filename/channel-history

回 promote/demote 歷史,每筆含 { timestamp, fromChannel, toChannel, changedBy, reason }

#POST /api/admin/firmware/:filename/channel(或 PATCH

json
{ "channel": "stable", "reason": "Passed regression suite 2026-05-24" }

雙寫 POST + PATCH 是因為前端 nginx CDN 預設擋 PATCH。後端兩個 verb 都接到同一 handler。

200 OK

json
{ "ok": true, "filename": "opta_v5.9.131.bin", "releaseChannel": "stable", "previousChannel": "beta" }

#DELETE /api/admin/firmware/:filename?force=1

  • 若該 firmware 在 stable channel → 409 Conflict(必須先 demote)
  • 若有在線 device 跑這版 → 409 Conflict,加 ?force=1 強制
  • 若有 in-flight OTA → 409 Conflict

#POST /api/firmware/deploy/:deviceId

json
{ "filename": "opta_v5.9.131.bin" }

Query ?force=1:device 收到 heartbeat 回應後立刻自動執行(不等使用者按 update)。

json
{ "ok": true, "message": "OTA queued", "targetVersion": "5.9.131", "targetDevice": "opta-line-3", "force": false }

#10. OTA(已廢棄 .mesb 格式)

POST /api/devices/:id/ota
GET  /api/devices/:id/ota/latest

兩個一律回 410 Gone,body 內附遷移指引:

json
{
  "error": "Gone",
  "message": "The .mesb OTA API has been removed. Use /api/firmware/upload and /api/firmware/deploy/:deviceId instead.",
  "migration": "https://docs.smms.com.tw/migration/mesb-to-firmware-v2"
}

#11. Customers(多租戶)

#GET /api/customers?tier=...&active=true

json
[
  {
    "id": 5,
    "name": "Foxconn Tier-1",
    "short_code": "FXN",
    "tier": "enterprise",
    "email": "...",
    "contactPerson": "...",
    "status": "active",
    "createdAt": "2026-04-01T00:00:00.000Z",
    "deviceCount": 12,
    "onlineCount": 8
  }
]

#POST /api/customers

json
{ "name": "...", "short_code": "...", "tier": "smb", "email": "...", "contactPerson": "...", "status": "active" }

201 Created

#PATCH /api/customers/:id

部分更新。

#DELETE /api/customers/:id

不會刪 device,只把該 customer 旗下 device 改為「未指派」。

#POST /api/devices/:uid/assign-customer

指派:

json
{ "customerId": 5 }

解除指派:

json
{ "customerId": null }

Find-or-create(沒有對應 customer 自動建一個):

json
{ "customerName": "新客戶 X" }

#12. Admin Users(Sprint 21)

#GET /api/admin/users

json
[
  { "id": 1, "email": "kc@...", "role": "admin", "displayName": "KC", "is_active": true }
]

#POST /api/admin/users

json
{ "email": "ops@example.com", "password": "min8chars", "role": "customer", "displayName": "Ops" }

#PUT /api/admin/users/:id/password

json
{ "newPassword": "min8chars" }

#PUT /api/admin/users/:id

json
{ "role": "customer", "displayName": "...", "isActive": false }

#13. Audit Logs

#GET /api/audit-logs?limit=50&offset=0

limit 最大 200。

json
{
  "logs": [
    {
      "timestamp": "2026-05-24T06:00:00.000Z",
      "action": "license.approve",
      "userId": 1,
      "objectId": "AA:BB:CC:DD:EE:FF",
      "details": { "previousStatus": "pending", "newStatus": "active" },
      "ip": "1.2.3.4"
    }
  ],
  "total": 4123,
  "limit": 50,
  "offset": 0
}

#14. PG Backup Status(v2.2.6+)

#GET /api/admin/backup-status

/var/backups/pg mount 點,回傳每日 pg_dump 健康度:

json
{
  "mounted": true,
  "dumpDir": "/var/backups/pg",
  "dumpCount": 14,
  "lastDumpFile": "pg_dump_2026-05-23.sql.gz",
  "lastDumpAt": "2026-05-23T02:00:00.000Z",
  "hoursSinceLastDump": 28,
  "oldestDumpAt": "2026-05-10T02:00:00.000Z",
  "totalSize": 1583492000,
  "healthy": false
}

healthy: false 表示最後一份 dump 距今 > 26 小時——需要排查 cronjob / mount。


#15. Cloud Bridge

#GET /api/cloud/status

Local 模式:

json
{ "enabled": false, "reason": "SERVER_ROLE=local" }

Cloud 模式:

json
{
  "enabled": true,
  "started": "2026-05-20T14:00:00.000Z",
  "mqtt": { "connected": true, "host": "...", "port": 1883 },
  "arduinoCloud": { "connected": true, "thingsActive": 12 },
  "devices": 12
}

#☁️ License API(Cloud-only)

License 採七段狀態機,由 device-side activate / heartbeat 與 admin-side 審核共同推進:

State 進入條件 退出條件
unseen Heartbeat 看到新 UID 自動建立 Device 顯式 activatepending
pending Device activate 或 admin 把 unseen 提升 Admin approveactivereject 維持 pending
whitelisted Admin import 批次預核 Device activate 直接到 active
active approve 或 whitelist + activate Admin deactivaterejected
rejected Admin reject / deactivate Device 重新 activatepending
revoked Admin DELETE(90 天 tombstone) Admin DELETE /api/admin/license-revoked 清除

#POST /api/license/activate

json
{ "uid": "AA:BB:CC:DD:EE:FF", "deviceName": "opta-line-3", "firmwareVersion": "5.9.130", "agreed": true }

200 OK(whitelisted):

json
{ "status": "active", "message": "License activated", "licenseId": "LIC-2026-001", "activatedAt": "2026-05-24T06:00:00.000Z" }

200 OK(未預核):

json
{ "status": "pending", "message": "License pending admin approval" }

403(revoked tombstone 仍有效):

json
{ "status": "rejected", "message": "UID revoked. Contact admin." }

#GET /api/license/status/:uid

json
{
  "uid": "AA:BB:CC:DD:EE:FF",
  "status": "active",
  "activatedAt": "2026-05-24T06:00:00.000Z",
  "deviceName": "opta-line-3",
  "expiresAt": null
}

#Admin

#GET /api/admin/license/list?status=pending

json
{
  "stats": { "total": 142, "active": 98, "pending": 12, "whitelisted": 20, "rejected": 8, "unseen": 4 },
  "licenses": [ { "uid": "...", "status": "pending", "deviceName": "...", "firstSeenAt": "..." } ],
  "revoked": [ { "uid": "...", "revokedAt": "..." } ]
}

#POST /api/admin/license/import

json
{ "uids": ["AA:BB:CC:DD:EE:01", "AA:BB:CC:DD:EE:02"] }

{ "success": true, "imported": 1, "skipped": 1, "total": 2 } — pending 的會升級 whitelisted;已 active 的 skip。

#POST /api/admin/license/approve/:uid

Pending → active,發出 licenseId

#POST /api/admin/license/reject/:uid

任意狀態 → rejected。Device 可再次 activate 推回 pending——這跟 deactivate 不同。

#POST /api/admin/license/deactivate/:uid

Active → rejected,無 grace period——下次 heartbeat 立刻通知 device 失權。

#DELETE /api/admin/license/:uid

硬刪 license row + 寫 90 天 tombstone。Device 之後 activate 會直接拒絕。

json
{
  "success": true,
  "deleted": { "uid": "...", "status": "active" },
  "tombstoneTtlDays": 90,
  "hint": "To allow re-activation, call DELETE /api/admin/license-revoked/:uid"
}

#DELETE /api/admin/license-revoked/:uid

清掉 tombstone(讓被刪 UID 可再 activate)。


#HTTP 狀態碼

Code 意義
200 成功
201 Created(POST /api/customers/api/admin/users
400 Bad Request(validation 失敗、filename 格式錯)
401 Unauthorized(缺 Bearer / token 失效)
403 Forbidden(role 不足;rejected/revoked license)
404 Not Found
409 Conflict(韌體在用、stable channel 不可刪)
410 Gone(.mesb API 已廢;claim OTP 過期)
500 Server error

統一錯誤 shape:

json
{ "error": "Unauthorized", "message": "Missing Authorization header" }

#Body Size Limits

Endpoint Limit 設定處
express.json() 預設 1 MB server.js:37
/api/devices/:id/web 2 MB server.js:39
/api/devices/:id/ota 2 MB server.js:41(廢)
/api/firmware/upload 4 MB server.js:43

#CORS

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

OPTIONS 一律回 200


#Legacy / Migration

Method Path 行為
GET /config 400 — Migration: Use /api/devices/:id/config instead.
POST /config 同上
PUT /config 同上

#近期變更(v2.2.x highlights)

Version 變更
v2.2.10 (dff2c2d) /api/devices 改用 PG(uid PK)列設備,修同 deviceId 互蓋 bug。
v2.2.6 新增 /api/admin/backup-status,監控 daily pg_dump
v2.2.4 Self-serve POST /api/auth/password + admin PUT /api/admin/users/:id/password
v2.2.3 POST /api/devices/:id/config 觸發 auto-push snapshot,補上 heartbeat-only 走過的縫。
Sprint 21 WI-065 JWT auth、WI-070 device claim OTP、WI-071 user CRUD。
ADR-008 單一 codebase + SERVER_ROLE 切換 local/cloud;license 模組僅 cloud 載入。
WI-077 MQTT credentials 在 env 改用 base64-encoded default,避免明文密碼進 git。

#實作備註

  1. Auth 順序:JWT → DEVICE_TOKEN → License Token。任一吻合就 next(),全失敗才回 401。失敗事件打 auth.reject log。
  2. Mount 順序很關鍵server.js:103–117 先掛 public router(auth/firmware-public/deviceClaim/heartbeat),再 app.use('/api/devices', authMiddleware) 等於把 /api/devices/* 全段保護起來——但 heartbeat 已先掛載,所以它仍是 optional auth。
  3. Tenant filtercustomer role 在 GET /api/devices handler 內查 user → device 對應關係後過濾結果。Admin role 看全部。
  4. Heartbeat 是 cloud sync 的主軸:device → server 推狀態、server → device 回授權與 OTA 指令;任何 admin 動作(approve / deploy / restore)都是設 pending flag,等下一次 heartbeat 才生效。
  5. Snapshot vs Backup:Backup 是手動觸發 POST /api/devices/:id/config 寫的 timestamped 副本(7 天輪替);Snapshot 是系統在 configVersion 改變時自動寫的(50 個 / 30 天)。Rollback 走 snapshot。
  6. Firmware 刪除安全網:Stable channel 必須先 demote 才能刪;有 in-flight OTA 或在線 device 跑該版時要 ?force=1 才放行。
  7. License revoked tombstone:硬刪後 90 天內 device activate 會被拒(避免使用者隨意拔 PG row 後 device 自動復活);要復活先 DELETE /api/admin/license-revoked/:uid
  8. PATCH 雙寫 POST/api/admin/firmware/:filename/channel 同時掛 POST 跟 PATCH——前端 nginx CDN 預設擋 PATCH,POST 是 escape hatch。