- feature-device-mgmt-tdd.md + api-device-mgmt.md(B 設備管理 TDD) - feature-model-sharing-tdd.md + api-model-sharing.md + PRD feature + 設計規格(C 模型共享三方規劃) - adr-020-ffmpeg-camera-indev.md(camera 三平台 indev) - PRD.md / TDD.md 索引增補 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
132 lines
5.4 KiB
Markdown
132 lines
5.4 KiB
Markdown
# API 規格 — 設備註冊 / 取消註冊(個人設備管理 P0)
|
||
|
||
- **上位**:[`../feature-device-mgmt-tdd.md`](../feature-device-mgmt-tdd.md)、[`api-spec.md`](api-spec.md) §3 Devices
|
||
- **狀態**:Draft(契約,contract-first single source of truth)
|
||
- **最後更新**:2026-08-02
|
||
|
||
> 本檔為 register / unregister 兩個新端點的權威契約。前端對此形狀寫 normalize、後端對此形狀保證回應。既有 `GET /api/devices`、`GET /api/devices/:id`、`POST /api/devices/:id/unpair` 見 `api-spec.md` §3、**本 P0 不改**。
|
||
|
||
---
|
||
|
||
## 通用
|
||
|
||
- **認證**:JWT/OIDC bearer(同既有 `/api/devices/*` route group)。
|
||
- **識別值**:`:id` = device UUID(雲端 DB 主鍵)。register/unregister 是純雲端 DB 操作、不路由到 local agent,故用 UUID(對齊 ADR-018 FE-A:DB 操作用 UUID、路由操作才用 serial)。
|
||
- **回應信封**:沿用既有 `{ "success": true, "data": ... }`(前端 api client 已 unwrap `data`)。錯誤為 `{ "success": false, "error": { "code", "message" } }`。
|
||
|
||
### DeviceListItem(回應 data 形狀,沿用既有,snake_case)
|
||
|
||
register/unregister 成功皆回**更新後的單筆 DeviceListItem**(與 `GET /api/devices/:id` 同形狀):
|
||
|
||
```json
|
||
{
|
||
"id": "uuid",
|
||
"name": "KL520 #A1B2",
|
||
"device_type": "kl520",
|
||
"serial_number": "0x1234ABCD",
|
||
"agent_id": "uuid",
|
||
"registered_at": "2026-08-02T10:00:00Z",
|
||
"remote_status": "online",
|
||
"last_seen_at": "2026-08-02T10:00:00Z",
|
||
"last_connected_at": null,
|
||
"status": "connected",
|
||
"tunnel_online": true,
|
||
"created_at": "2026-08-01T00:00:00Z",
|
||
"updated_at": "2026-08-02T10:00:00Z"
|
||
}
|
||
```
|
||
|
||
- `registered_at`:register 後為 ISO 8601 時間;unregister 後為 `null`(omitempty → 欄位可能不出現,前端 normalize 皆視為 null=未註冊)。
|
||
|
||
---
|
||
|
||
## 1. `POST /api/devices/:id/register` — 註冊設備
|
||
|
||
把裝置從「未註冊」翻成「已註冊」(`registered_at` NULL → now())。
|
||
|
||
- **Request body**:無。
|
||
- **Success**:`200 OK` + 更新後 DeviceListItem(`registered_at` 非 null)。
|
||
|
||
### 行為順序(後端)
|
||
|
||
| 步驟 | 條件 | 回應 |
|
||
|------|------|------|
|
||
| 1 | 缺 UserContext | 500 `INTERNAL_ERROR` |
|
||
| 2 | `:id` 空 | 400 `VALIDATION_FAILED` |
|
||
| 3 | device 不存在 / 已軟刪 | 404 `NOT_FOUND` |
|
||
| 4 | `owner_user_id != caller` | 403 `FORBIDDEN`(IDOR 防護) |
|
||
| 5 | `is_representative == true` | 409 `CONFLICT`(代表 device 不可註冊) |
|
||
| 6 | `registered_at != null`(已註冊) | 409 `ALREADY_REGISTERED` |
|
||
| 7 | 正常 | 200 + DeviceListItem |
|
||
|
||
### 範例
|
||
|
||
```
|
||
POST /api/devices/7f3a.../register
|
||
→ 200 { "success": true, "data": { ...,"registered_at":"2026-08-02T10:00:00Z" } }
|
||
|
||
(已註冊再打)
|
||
→ 409 { "success": false, "error": { "code":"ALREADY_REGISTERED","message":"device already registered" } }
|
||
```
|
||
|
||
---
|
||
|
||
## 2. `POST /api/devices/:id/unregister` — 取消註冊(退回未註冊)
|
||
|
||
把裝置退回「未註冊」(`registered_at` → NULL),**保留裝置列**、不軟刪、不撤 token。
|
||
|
||
> ⚠️ **與 `unpair` 完全不同**:unpair 是軟刪整台 + cascade 撤 token(device 從清單消失);unregister 只清 `registered_at`(device 仍在清單、顯示為未註冊)。兩端點各走各的,不可合併。詳見 TDD §1。
|
||
|
||
- **Request body**:無。
|
||
- **Success**:`200 OK` + 更新後 DeviceListItem(`registered_at` = null)。
|
||
|
||
### 行為順序(後端)
|
||
|
||
| 步驟 | 條件 | 回應 |
|
||
|------|------|------|
|
||
| 1 | 缺 UserContext | 500 `INTERNAL_ERROR` |
|
||
| 2 | `:id` 空 | 400 `VALIDATION_FAILED` |
|
||
| 3 | device 不存在 / 已軟刪 | 404 `NOT_FOUND` |
|
||
| 4 | `owner_user_id != caller` | 403 `FORBIDDEN` |
|
||
| 5 | `is_representative == true` | 409 `CONFLICT` |
|
||
| 6 | 已是未註冊(`registered_at == null`) | **200**(冪等 no-op,非錯誤) |
|
||
| 7 | 正常 | 200 + DeviceListItem(registered_at=null) |
|
||
|
||
### 範例
|
||
|
||
```
|
||
POST /api/devices/7f3a.../unregister
|
||
→ 200 { "success": true, "data": { ...,"registered_at": null } }
|
||
(device 仍存在於 GET /api/devices)
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 錯誤碼
|
||
|
||
| code | HTTP | 情境 | 新增? |
|
||
|------|------|------|-------|
|
||
| `VALIDATION_FAILED` | 400 | id 空 | 既有 |
|
||
| `FORBIDDEN` | 403 | 非 owner(IDOR) | 既有 |
|
||
| `NOT_FOUND` | 404 | device 不存在/軟刪 | 既有 |
|
||
| `CONFLICT` | 409 | representative device 不可註冊/取消 | 既有(或沿用既有 CONFLICT 碼) |
|
||
| `ALREADY_REGISTERED` | 409 | register 時已註冊 | **新增** |
|
||
| `INTERNAL_ERROR` | 500 | 缺 UserContext / DB 錯 | 既有(DB down 經 errors.go 映射 503) |
|
||
|
||
- `ALREADY_REGISTERED` 為本功能新增錯誤碼,需在後端 errors 常數 + 前端 i18n(`devices.register.error.alreadyRegistered`)同步登記。
|
||
- representative 衝突若不想新增碼,可沿用既有 `CONFLICT` + message 區分;由 backend agent 依既有錯誤碼慣例定,前端據 message/context 提示。
|
||
|
||
---
|
||
|
||
## 4. 前端呼叫(對齊 device-store.ts 範式)
|
||
|
||
```
|
||
// 皆用 UUID(DB 操作);不帶 serial。
|
||
registerDevice(id): POST /api/devices/{id}/register → 更新 store 該筆 registeredAt
|
||
unregisterDevice(id): POST /api/devices/{id}/unregister → 清 store 該筆 registeredAt(保留列)
|
||
```
|
||
|
||
- 成功後就地更新 store 對應 device 的 `registeredAt`(避免 refetch 延遲,比照既有 unpairDevice 就地移除的範式),或 refetch 該筆。
|
||
- 409 `ALREADY_REGISTERED` → toast「已註冊」+ refetch。
|
||
- **不像 unpair 從 list 移除**——unregister 只改欄、device 留在 list。
|