從 edge-ai-platform POC 轉為正式產品的雲端後端,含以下整合階段:
- Phase 0:雛形骨架 — `cmd/api-server` (REST :3721) + `cmd/remote-proxy`
(tunnel :3800 / internal :3801) 雙 binary 共用 internal/,沿用 POC 的
WebSocket+yamux tunnel 協定但解耦 relay 與 API
- Phase 0.6:OIDC BFF 接 Innovedus Member Center
- internal/oidc package(coreos/go-oidc + PKCE S256 + state + nonce)
- internal/usersession package(HMAC-SHA256 cookie + RotateSessionID
防 session fixation, OWASP ASVS V3.2.1)
- 4 個 OIDC handler(/api/auth/login|callback|me|logout)+ AuthMiddleware
- 完全拔除 StaticAuthProvider,OIDC 是唯一認證路徑
- 9 個 ADR(含 ADR-010 BFF / ADR-011 取代 static auth /
ADR-012 pending session shared cookie / ADR-013 PKCE-only public client)
- Phase 0.7:A1 改造 + security audit 修復
- OIDC ClientSecret 變選填,支援 stage MC 的 public PKCE-only client
(AuthStyleInParams 強制 token endpoint 不送 client_secret)
- 預留 ServiceClient* 欄位給未來 client_credentials grant
- 移除 13+ 處 resolveUserID(uc, StaticUserID) fallback 改 strict mode
(Audit C1:multi-tenant 隔離破口)
- Pairing exchange MarkUsed 失敗 abort + revoke session token(Audit M3)
- 新增 all_endpoints_require_auth_test 整合測試(51 endpoint × 401)
驗證:go test -race -count=3 ./... 17 packages 全綠 / go vet 0 warning
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
4.4 KiB
Go
86 lines
4.4 KiB
Go
package api
|
||
|
||
import (
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// registerStubRoutes 註冊 B5 尚未實作 / Phase 1 才處理的 endpoint,一律回 501 NOT_IMPLEMENTED。
|
||
//
|
||
// **為什麼只留這些**:Auth / Pairing 補齊 / Devices / Models / GET /clusters /
|
||
// system/deps / /storage 都在 B5 補實作(見 auth.go / devices.go / models.go /
|
||
// clusters.go / storage.go 各檔)。這裡只剩:
|
||
// - Cloud 裝置記錄(非 tunnel 的 CRUD,Phase 1)
|
||
// - Clusters 寫入類(Phase 1)
|
||
// - Camera / Media(走 tunnel proxy;B5 先不實作以避免過度擴張,B7 補)
|
||
// - Converter(Phase 1)
|
||
// - WebSocket endpoints(B7 TODO — 需要 Hijack + WS relay)
|
||
//
|
||
// 讓前端對錯誤路徑能拿到 501 而非 404,減少除錯成本。
|
||
func registerStubRoutes(g *gin.RouterGroup, _ Deps) {
|
||
stub := func(hint string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
WriteNotImplemented(c, hint)
|
||
}
|
||
}
|
||
|
||
// --- Cloud-side device records(非 tunnel) ---
|
||
g.GET("/cloud/devices", stub("cloud.devices.list — pending Phase 1"))
|
||
g.POST("/cloud/devices/:id/rename", stub("cloud.devices.rename — pending Phase 1"))
|
||
|
||
// --- Models 其餘 ---
|
||
// load-to-device 已在 models.go 註冊為 501 stub(為了讓 /api/models/:id/load-to-device 路徑註冊完整)。
|
||
|
||
// --- Clusters 寫入類 ---
|
||
g.POST("/clusters", stub("clusters.create — pending Phase 1"))
|
||
g.GET("/clusters/:id", stub("clusters.get — pending Phase 1"))
|
||
g.DELETE("/clusters/:id", stub("clusters.delete — pending Phase 1"))
|
||
g.POST("/clusters/:id/devices", stub("clusters.add-device — pending Phase 1"))
|
||
g.DELETE("/clusters/:id/devices/:deviceId", stub("clusters.remove-device — pending Phase 1"))
|
||
g.PUT("/clusters/:id/devices/:deviceId/weight", stub("clusters.set-weight — pending Phase 1"))
|
||
g.POST("/clusters/:id/flash", stub("clusters.flash — pending Phase 1"))
|
||
g.POST("/clusters/:id/inference/start", stub("clusters.inference.start — pending Phase 1"))
|
||
g.POST("/clusters/:id/inference/stop", stub("clusters.inference.stop — pending Phase 1"))
|
||
|
||
// --- Camera / Media(B7 補;走 tunnel proxy) ---
|
||
g.GET("/camera/list", stub("camera.list via tunnel — pending B7"))
|
||
g.POST("/camera/start", stub("camera.start via tunnel — pending B7"))
|
||
g.POST("/camera/stop", stub("camera.stop via tunnel — pending B7"))
|
||
g.GET("/camera/stream", stub("camera.stream MJPEG via tunnel — pending B7"))
|
||
g.POST("/media/upload/image", stub("media.upload.image — pending B7"))
|
||
g.POST("/media/upload/video", stub("media.upload.video — pending B7"))
|
||
g.POST("/media/upload/batch-images", stub("media.upload.batch — pending B7"))
|
||
g.GET("/media/batch-images/:index", stub("media.batch.get — pending B7"))
|
||
g.POST("/media/seek", stub("media.seek — pending B7"))
|
||
|
||
// --- Converter(Phase 1) ---
|
||
g.POST("/converter/jobs", stub("converter.submit — pending Phase 1"))
|
||
g.GET("/converter/jobs", stub("converter.list — pending Phase 1"))
|
||
g.GET("/converter/jobs/:id", stub("converter.get — pending Phase 1"))
|
||
g.GET("/converter/jobs/:id/download", stub("converter.download — pending Phase 1"))
|
||
}
|
||
|
||
// registerWebSocketStubs 註冊 /ws/* 的 stub。WebSocket proxy 在 B5 雛形不實作,
|
||
// 留 501 讓前端能收到明確錯誤,由 B7 補齊。
|
||
//
|
||
// 為什麼不做 WS proxy:實作 WS relay 需要在 api-server 端做 Hijack、雙向 io.Copy,
|
||
// 而且 Forwarder.ForwardWebSocket 尚未實作(見 forwarder.go §ForwardWebSocket)。
|
||
// 加這條路徑會顯著擴張 B5 範圍;按 prompt 指示先留 TODO。
|
||
//
|
||
// 注意:ws endpoint 在 /ws 而非 /api/ws,所以由 NewRouter 直接註冊而非 apiGroup。
|
||
func registerWebSocketStubs(r *gin.Engine) {
|
||
stub := func(hint string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
WriteNotImplemented(c, hint)
|
||
}
|
||
}
|
||
// 用 GET(WebSocket upgrade 的初始 HTTP request)
|
||
r.GET("/ws/devices/events", stub("ws.devices.events — pending B7"))
|
||
r.GET("/ws/devices/:id/flash-progress", stub("ws.flash-progress — pending B7"))
|
||
r.GET("/ws/devices/:id/inference", stub("ws.inference — pending B7"))
|
||
r.GET("/ws/server-logs", stub("ws.server-logs — pending B7"))
|
||
r.GET("/ws/system", stub("ws.system — pending B7"))
|
||
r.GET("/ws/clusters/:id/inference", stub("ws.clusters.inference — pending B7"))
|
||
r.GET("/ws/clusters/:id/flash-progress", stub("ws.clusters.flash — pending B7"))
|
||
r.GET("/ws/pairing/status", stub("ws.pairing.status — pending B7"))
|
||
}
|