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>
101 lines
3.8 KiB
Go
101 lines
3.8 KiB
Go
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 409(例:unique 約束衝突 — 同 owner+serial 重複註冊)。
|
||
ErrCodeConflict = "CONFLICT"
|
||
// ErrCodeAlreadyRegistered 對齊 HTTP 409:對已註冊(registered_at 非 null)的 device
|
||
// 再次呼叫 register。前端據此顯示「此裝置已註冊」並 refetch(feature-device-mgmt-tdd §3.2)。
|
||
ErrCodeAlreadyRegistered = "ALREADY_REGISTERED"
|
||
// ErrCodeRepresentativeDevice 對齊 HTTP 409:對 representative device(agent 連線佔位、
|
||
// 非真實 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 知道
|
||
// 這台不健康,而非回假資料或 500(500 會誤導為「程式 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)
|
||
}
|