jim800121chen 47a1d4d0ef feat(backend): 設備註冊 + 模型共享 backend(B 設備管理 + C 模型共享)
B 設備管理(feature-device-mgmt-tdd):
- POST /api/devices/:id/register + /unregister(owner 檢查 + representative 擋
  + 已註冊擋 + SetRegistered 單欄翻轉,不碰 unpair 軟刪)
- error codes ALREADY_REGISTERED / REPRESENTATIVE_DEVICE(409)
- 不需 migration(registered_at 欄/index/讀寫已在 0005)

C 模型共享(feature-model-sharing-tdd,security 深審 APPROVE):
- migration 0006:models.visibility enum DEFAULT 'private'(零行為改變)+ model_shares 表
- canAccessModel single source(owner ∪ share ∪ public ∪ tenant):profile + download 共用
- GET /library(cursor keyset)/ GET /:id/profile(404 防列舉、GetWithOwner join name 不洩 email)
  / PATCH /:id/visibility(owner-only)/ shares CRUD / download 放寬
- tenant 因 OIDC 無 org claim 留 stub(恆空、安全預設;補 org claim 需重送 security 深審)

reviewer 通過(B 三條紅線 / C security APPROVE 無 C/M)。130 dbtest 全綠、gosec 新檔 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-02 16:29:50 +08:00

101 lines
3.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"github.com/gin-gonic/gin"
)
// 錯誤碼常數 — 對齊 api-spec.md §11。
const (
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeForbidden = "FORBIDDEN"
ErrCodeNotFound = "NOT_FOUND"
ErrCodeValidationFailed = "VALIDATION_FAILED"
ErrCodeTunnelDisconnect = "TUNNEL_DISCONNECTED"
ErrCodeTunnelError = "TUNNEL_ERROR"
ErrCodeNotImplemented = "NOT_IMPLEMENTED"
ErrCodeRateLimited = "RATE_LIMITED"
ErrCodeInternalError = "INTERNAL_ERROR"
// ErrCodePayloadTooLarge 對齊 HTTP 413模型上傳超過 MaxUploadSizeMB
ErrCodePayloadTooLarge = "PAYLOAD_TOO_LARGE"
// ErrCodeInvalidSignature 用於 /storage/* 驗簽失敗 / URL 過期。
ErrCodeInvalidSignature = "INVALID_SIGNATURE"
// ErrCodeConflict 對齊 HTTP 409unique 約束衝突 — 同 owner+serial 重複註冊)。
ErrCodeConflict = "CONFLICT"
// ErrCodeAlreadyRegistered 對齊 HTTP 409對已註冊registered_at 非 null的 device
// 再次呼叫 register。前端據此顯示「此裝置已註冊」並 refetchfeature-device-mgmt-tdd §3.2)。
ErrCodeAlreadyRegistered = "ALREADY_REGISTERED"
// ErrCodeRepresentativeDevice 對齊 HTTP 409對 representative deviceagent 連線佔位、
// 非真實 USB呼叫 register/unregister。註冊語意只適用真實 USB device
// feature-device-mgmt-tdd §7.3)。與 ALREADY_REGISTERED 分開,讓 FE/TEST 能區分
// 「已註冊」與「不可註冊的裝置類型」兩種 409。
ErrCodeRepresentativeDevice = "REPRESENTATIVE_DEVICE"
// ErrCodeServiceUnavailable 對齊 HTTP 503。
// DB 接入塊 5.4 fail-fast 策略PG 連線失敗 / context 逾時 → 503讓 load balancer 知道
// 這台不健康,而非回假資料或 500500 會誤導為「程式 bug」503 才是「依賴暫時不可用」)。
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
)
// ErrorBody 是 API 錯誤回應的 envelope 結構。
//
// 對齊 api-spec.md
//
// { "success": false, "error": { "code": "...", "message": "...", "request_id": "..." } }
//
// 為什麼用 envelope 而非裸 error方便前端統一處理 + 與成功回應形狀一致。
type ErrorBody struct {
Success bool `json:"success"`
Error *ErrorDetail `json:"error"`
}
// ErrorDetail 是錯誤的具體資訊。
type ErrorDetail struct {
Code string `json:"code"`
Message string `json:"message"`
Details []FieldError `json:"details,omitempty"` // 例如 validation 細節
RequestID string `json:"request_id,omitempty"`
Extra map[string]any `json:"extra,omitempty"` // 給 specific error 帶結構化資料
}
// FieldError 描述單一欄位的驗證錯誤。
type FieldError struct {
Field string `json:"field"`
Message string `json:"message"`
}
// SuccessBody 是成功回應的 envelope。
//
// 對齊 api-spec.md`{ "success": true, "data": ... }`。
type SuccessBody struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
}
// WriteError 統一寫錯誤回應(會自動帶上 request_id
//
// 注意:呼叫後 caller 仍需自行 c.Abort()(如果是在 middleware 中要終止 chain
// 在 handler 中只需 return 即可。
func WriteError(c *gin.Context, status int, code, message string, details []FieldError) {
c.JSON(status, ErrorBody{
Success: false,
Error: &ErrorDetail{
Code: code,
Message: message,
Details: details,
RequestID: RequestIDFrom(c),
},
})
}
// WriteSuccess 統一寫成功回應。
func WriteSuccess(c *gin.Context, status int, data any) {
c.JSON(status, SuccessBody{
Success: true,
Data: data,
})
}
// WriteNotImplemented 回應 501給 B5 還沒實作的 handler 用。
func WriteNotImplemented(c *gin.Context, hint string) {
WriteError(c, 501, ErrCodeNotImplemented, hint, nil)
}