# API Spec — 模型共享(Model Sharing) > 對齊 `feature-model-sharing-tdd.md`。base URL / envelope / 認證同 `api-spec.md`(§0)。 > 通用回應:`{ "success": true, "data": {...} }` / `{ "success": false, "error": { "code", "message" } }`。 > 認證:cookie session(BFF,同 `api-spec.md`),handler 從 `UserContext` 取 `userID` / `roles` / `orgId`。 > > **狀態**:Draft(待三方互審)。標 `[需 PM 確認]` / `[需 Design 確認]` / `[需 security 審]` 的點見各節。 --- ## 0. 名詞與前提 | 名詞 | 定義 | |------|------| | **visibility** | 模型的「公開對象」維度(廣播式):`private`(僅擁有者)/ `tenant`(同租戶可見)/ `public`(全平台可見)。存在 `models.visibility` 欄。 | | **model_shares** | 「點對點分享給特定人」維度(ADR-017 決策 3 已定義,本功能沿用、不重造)。與 visibility 正交。 | | **共享模型庫** | 「依身份權限的可見模型列表」= 我的 ∪ 公開(public) ∪ 同租戶(tenant 且同 org) ∪ 分享給我(model_shares) ∪ preset。 | | **tenant / org** | 沿用既有 `users.org_id`(migration 0001 已有欄位、domain 尚未使用,見 TDD §2)。`tenant` visibility = 同 `org_id` 可見。`[需 PM 確認]` org 邊界語意。 | > **與既有 `/api/models` 的分工**:既有 `GET /api/models`(`api-spec.md §4`)維持「**我的模型 + preset**」語意不變(owner dashboard 用),**不動**。共享模型庫是**新端點** `GET /api/models/library`(跨擁有者、依權限可見)。兩者刻意分開,避免破壞既有 owner dashboard 行為。`[需 Design 確認]` UI 是否分兩個入口。 --- ## 1. GET `/api/models/library` — 共享模型庫列表(依身份權限可見) 依當前 user 身份,回「可見模型」的分頁列表。可見範圍 = 我的 ∪ public ∪ (tenant 且同 org) ∪ 分享給我 ∪ preset。 ### Query 參數 | 參數 | 型別 | 預設 | 說明 | |------|------|------|------| | `cursor` | string | — | 分頁游標(不透明 base64)。首頁不帶。見 §1.3 分頁契約。 | | `limit` | int | 20 | 每頁筆數,1–100,超出 clamp。 | | `sort` | enum | `created_at` | 排序欄位:`created_at` / `name` / `file_size`。 | | `order` | enum | `desc` | `asc` / `desc`。 | | `q` | string | — | 搜尋關鍵字(比對 `name` + `description`,ILIKE 前綴/包含,見 TDD §5 效能)。 | | `target_chip` | enum | — | filter:`kl520`/`kl720`/`kl630`/`kl730`。 | | `source` | enum | — | filter:`uploaded`/`converted`/`preset`。 | | `visibility` | enum | — | filter:`public`/`tenant`(僅過濾廣播類;`private` 不在共享庫語意內、傳了視為忽略)。 | | `owned` | bool | — | `true`=只看我的、`false`=只看別人分享/公開給我的;不帶=全部可見。 | ### Response 200(envelope `data`) ```json { "items": [ { "id": "uuid", "name": "yolov5s-kl520", "description": "...", "target_chip": "kl520", "file_size": 10485760, "source": "converted", "status": "ready", "visibility": "public", "owner": { "id": "uuid", "name": "Alice", "is_me": false }, "shared_with_me": false, "my_access": "viewer", "created_at": "2026-07-01T00:00:00Z", "updated_at": "2026-07-02T00:00:00Z" } ], "next_cursor": "eyJ...", "has_more": true } ``` **欄位說明(權限相關,重要)**: - `owner`:只揭露 `id` / `name` / `is_me`(**不揭露 owner email**——非擁有者不該看到他人 email,見 §4 安全)。`[需 security 審]`。 - `my_access`:當前 user 對此模型的有效權限(`owner`/`editor`/`viewer`)。由「owner_user_id 命中 / model_shares.role / visibility 落 viewer」計算,取最高。 - `shared_with_me`:是否因 model_shares 命中(供 UI 標「已分享給我」)。`[需 Design 確認]` 標示方式。 - `status`:沿用既有 DTO(`pending`/`ready` 由 `uploaded_at` 決定,見 `models.go toModelResponse`)。**共享庫只列 `ready`**(未 finalize 的不進共享庫,見 TDD §5)。 ### 錯誤碼 | HTTP | code | 情境 | |------|------|------| | 400 | `VALIDATION_FAILED` | limit / cursor / sort 非法。 | | 401 | (middleware) | 未登入。 | | 500/503 | `INTERNAL_ERROR` | DB 錯誤(經 `WriteDBError` 映射,不洩漏 raw)。 | --- ## 2. GET `/api/models/:id/profile` — 模型 profile 頁(公開版詳情) 非擁有者也可看的模型詳情頁。**權限檢查為核心**:只有「對此 model 有可見性」的 user 能看,且回傳欄位依身份裁剪。 ### 可見性判斷(handler 第一步) user 能看此 profile ⟺ 命中任一: 1. `owner_user_id == userID`(我的) 2. `visibility == public` 3. `visibility == tenant` 且 model.owner.org_id == user.org_id 4. `model_shares` 命中(grantee == userID) 5. preset 模型(公用) 不命中 → **404**(不是 403):不揭露「這個 id 存在但你沒權限」,避免 enumeration 洩漏存在性。`[需 security 審]` 404 vs 403 策略。 ### Response 200(envelope `data`) ```json { "id": "uuid", "name": "...", "description": "...", "target_chip": "kl520", "file_size": 10485760, "source": "converted", "status": "ready", "visibility": "public", "input_shape": [1, 3, 224, 224], "classes": ["cat", "dog"], "framework": "onnx", "owner": { "id": "uuid", "name": "Alice", "is_me": false }, "my_access": "viewer", "can_download": true, "created_at": "...", "updated_at": "...", "uploaded_at": "..." } ``` **依身份裁剪的欄位(重要,`[需 security 審]`)**: - **絕不揭露**(無論身份):`storage_key`、`faa_object_key`(FAA 內部 key,ADR-017 決策 2 防曝露)、owner email、`file_checksum`(`[需 PM 確認]` checksum 是否對非 owner 公開)。 - `can_download`:`my_access != none` 時 true。前端據此決定是否顯示下載鈕。實際下載仍走既有 `GET /api/models/:id/download`(該端點需同步加共享權限檢查,見 §3)。 ### 錯誤碼 | HTTP | code | 情境 | |------|------|------| | 404 | `NOT_FOUND` | model 不存在 **或** 無可見性(合併,防 enumeration)。 | --- ## 3. PATCH `/api/models/:id/visibility` — 設定公開對象 擁有者設定模型的公開對象。**只有 owner(或 editor?`[需 PM 確認]`)能改**。 ### Request ```json { "visibility": "public" } ``` - `visibility`(required):`private` / `tenant` / `public`。 - `tenant` 但 user 無 `org_id` → 400(無租戶歸屬不能設 tenant 可見)。`[需 PM 確認]` demo/無 org user 的行為。 ### Response 200(envelope `data`) ```json { "id": "uuid", "visibility": "public", "updated_at": "..." } ``` ### 錯誤碼 | HTTP | code | 情境 | |------|------|------| | 400 | `VALIDATION_FAILED` | visibility 非法 / 設 tenant 但無 org。 | | 403 | `FORBIDDEN` | 非 owner。 | | 404 | `NOT_FOUND` | model 不存在。 | | 409 | `CONFLICT` | model 未 `ready`(未 finalize 不允許公開,`[需 PM 確認]`)。 | > **既有 `GET /api/models/:id/download` 的連帶變更(見 TDD §5)**:目前是 owner-only(`m.OwnerUserID != userID → 403`)。共享後需改為「有可見性且 `can_download` → 放行」。此變更**涉及 auth 繞過風險**,`[需 security 審]`。 --- ## 4. 安全考量摘要(詳見 TDD §6) | 風險 | 對策 | |------|------| | IDOR / enumeration(猜 id 看他人 model) | profile / download 的可見性檢查在 handler 第一步;不命中回 404(不回 403 揭露存在性)。`[需 security 審]` | | 越權改 visibility | PATCH visibility 強制 owner 檢查。 | | 資訊洩漏(email / storage_key / faa_object_key) | DTO 白名單序列化,內部 key 用 `json:"-"`(沿用 `model.Model.FAAObjectKey` 既有做法)。 | | tenant 邊界誤放 | tenant 可見性需 `model.owner.org_id == user.org_id` 且兩者皆非空;空 org 不落 tenant 可見。 | | 下載端點權限繞過 | `GET /api/models/:id/download` 的 owner-only 改共享權限檢查,須與 profile 可見性邏輯**共用同一函式**(single source of truth,避免兩處判斷不一致)。`[需 security 審]` | --- ## 5. 待三方確認清單(本檔) | # | 項目 | 待誰 | |---|------|------| | S1 | org/tenant 邊界語意(org_id 是否等於租戶;跨 org 是否可能) | PM | | S2 | editor 是否能改 visibility / 未 ready 能否公開 / checksum 是否對非 owner 公開 | PM | | S3 | 共享庫 UI 入口(與既有「我的模型」分頁 or 合併 tab)、`shared_with_me` 標示、profile 頁欄位 | Design | | S4 | 404 vs 403 防 enumeration、download 端點權限改動、owner email 不揭露 | security |