從 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>
214 lines
6.5 KiB
Go
214 lines
6.5 KiB
Go
// Package device 定義 Device domain model 與 Repository 介面。
|
||
//
|
||
// 對齊 database.md §2.2(雙狀態模型 — Minor-3)與 §3(Repository interface)。
|
||
// 雛形以 InMemoryRepository 實作;Phase 1 新增 PostgresRepository 取代。
|
||
package device
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// ==========================================================================
|
||
// Errors
|
||
// ==========================================================================
|
||
|
||
var (
|
||
// ErrNotFound 表示指定 ID 的 Device 不存在。
|
||
ErrNotFound = errors.New("device: not found")
|
||
)
|
||
|
||
// ==========================================================================
|
||
// Remote / USB 狀態常數(對齊 database.md §2.2)
|
||
// ==========================================================================
|
||
|
||
// RemoteStatus 是雲端對 tunnel 連線的觀察值。
|
||
type RemoteStatus = string
|
||
|
||
const (
|
||
// RemoteStatusOnline 表示 tunnel 有效、雲端可達。
|
||
RemoteStatusOnline RemoteStatus = "online"
|
||
// RemoteStatusOffline 表示 tunnel 斷線或從未連上。
|
||
RemoteStatusOffline RemoteStatus = "offline"
|
||
// RemoteStatusReconnecting 表示 tunnel 短暫斷線、local agent 重連中。
|
||
RemoteStatusReconnecting RemoteStatus = "reconnecting"
|
||
// RemoteStatusError 表示 tunnel 發生未預期錯誤(yamux 異常等)。
|
||
RemoteStatusError RemoteStatus = "error"
|
||
)
|
||
|
||
// USBStatus 是 local agent 從 Kneron SDK 讀到的 USB 狀態。
|
||
type USBStatus = string
|
||
|
||
const (
|
||
// USBStatusOnline USB 插著且可用。
|
||
USBStatusOnline USBStatus = "online"
|
||
// USBStatusOffline USB 拔掉了。
|
||
USBStatusOffline USBStatus = "offline"
|
||
// USBStatusUnknown 尚未回報 / 初始狀態。
|
||
USBStatusUnknown USBStatus = "unknown"
|
||
)
|
||
|
||
// ==========================================================================
|
||
// Device struct
|
||
// ==========================================================================
|
||
|
||
// Device 對應 database.md §2.2 的 Device 實體。
|
||
//
|
||
// 雙狀態說明(Minor-3):
|
||
// - Status(USB-level):local agent 觀察到的 USB 連接狀態
|
||
// - RemoteStatus(tunnel-level):雲端觀察到的 tunnel 連線狀態
|
||
//
|
||
// 前端優先顯示 RemoteStatus,次要顯示 Status(見 TDD §10.5.1)。
|
||
type Device struct {
|
||
ID string `json:"id"`
|
||
OwnerUserID string `json:"ownerUserId"`
|
||
Name string `json:"name"`
|
||
DeviceType string `json:"deviceType"`
|
||
SerialNumber string `json:"serialNumber,omitempty"`
|
||
|
||
// tunnel-level 狀態
|
||
RemoteStatus RemoteStatus `json:"remoteStatus"`
|
||
LastSeenAt *time.Time `json:"lastSeenAt,omitempty"`
|
||
LastConnectedAt *time.Time `json:"lastConnectedAt,omitempty"`
|
||
|
||
// USB-level 狀態
|
||
Status USBStatus `json:"status"`
|
||
|
||
CreatedAt time.Time `json:"createdAt"`
|
||
UpdatedAt time.Time `json:"updatedAt"`
|
||
PairedAt *time.Time `json:"pairedAt,omitempty"`
|
||
DeletedAt *time.Time `json:"deletedAt,omitempty"`
|
||
}
|
||
|
||
// ==========================================================================
|
||
// Repository interface
|
||
// ==========================================================================
|
||
|
||
// Repository 是 Device 持久層介面。
|
||
//
|
||
// 所有查詢方法**必須略過 DeletedAt != nil 的紀錄**(soft delete)。
|
||
// Phase 1 的 PostgresRepository 會加上 `WHERE deleted_at IS NULL`。
|
||
type Repository interface {
|
||
// Get 取得單一 device;不存在或已軟刪除回 ErrNotFound。
|
||
Get(ctx context.Context, id string) (*Device, error)
|
||
|
||
// GetBySerial 以 (ownerUserID, serialNumber) 查詢(避免同 user 重複註冊同 serial)。
|
||
GetBySerial(ctx context.Context, ownerUserID, serial string) (*Device, error)
|
||
|
||
// List 列出某 user 的所有(未刪除)device。
|
||
List(ctx context.Context, ownerUserID string) ([]*Device, error)
|
||
|
||
// Save 新增或更新一筆 device(upsert 語意,by ID)。
|
||
// 實作應更新 UpdatedAt;若為新建則同時設定 CreatedAt。
|
||
Save(ctx context.Context, d *Device) error
|
||
|
||
// Delete 標記為軟刪除(設定 DeletedAt)。
|
||
Delete(ctx context.Context, id string) error
|
||
}
|
||
|
||
// ==========================================================================
|
||
// InMemoryRepository
|
||
// ==========================================================================
|
||
|
||
// InMemoryRepository 是 Phase 0 雛形的記憶體實作。
|
||
type InMemoryRepository struct {
|
||
mu sync.RWMutex
|
||
devices map[string]*Device
|
||
}
|
||
|
||
// NewInMemoryRepository 建立一個空的記憶體 Repository。
|
||
func NewInMemoryRepository() *InMemoryRepository {
|
||
return &InMemoryRepository{
|
||
devices: make(map[string]*Device),
|
||
}
|
||
}
|
||
|
||
// Get 取得單一 device。
|
||
func (r *InMemoryRepository) Get(ctx context.Context, id string) (*Device, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
d, ok := r.devices[id]
|
||
if !ok || d.DeletedAt != nil {
|
||
return nil, ErrNotFound
|
||
}
|
||
cp := *d
|
||
return &cp, nil
|
||
}
|
||
|
||
// GetBySerial 以 (owner, serial) 查詢。
|
||
func (r *InMemoryRepository) GetBySerial(ctx context.Context, ownerUserID, serial string) (*Device, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
for _, d := range r.devices {
|
||
if d.DeletedAt != nil {
|
||
continue
|
||
}
|
||
if d.OwnerUserID == ownerUserID && d.SerialNumber == serial {
|
||
cp := *d
|
||
return &cp, nil
|
||
}
|
||
}
|
||
return nil, ErrNotFound
|
||
}
|
||
|
||
// List 列出某 user 的所有未刪除 device。
|
||
func (r *InMemoryRepository) List(ctx context.Context, ownerUserID string) ([]*Device, error) {
|
||
r.mu.RLock()
|
||
defer r.mu.RUnlock()
|
||
|
||
out := make([]*Device, 0)
|
||
for _, d := range r.devices {
|
||
if d.DeletedAt != nil {
|
||
continue
|
||
}
|
||
if d.OwnerUserID == ownerUserID {
|
||
cp := *d
|
||
out = append(out, &cp)
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// Save 新增或更新 device(upsert by ID)。
|
||
func (r *InMemoryRepository) Save(ctx context.Context, d *Device) error {
|
||
if d == nil || d.ID == "" {
|
||
return errors.New("device: Save requires non-nil device with ID")
|
||
}
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
now := time.Now().UTC()
|
||
// Copy 避免外部後續修改影響 store
|
||
cp := *d
|
||
if existing, ok := r.devices[d.ID]; ok && existing.DeletedAt == nil {
|
||
cp.CreatedAt = existing.CreatedAt // 保留原始 CreatedAt
|
||
} else if cp.CreatedAt.IsZero() {
|
||
cp.CreatedAt = now
|
||
}
|
||
cp.UpdatedAt = now
|
||
r.devices[d.ID] = &cp
|
||
return nil
|
||
}
|
||
|
||
// Delete 標記 device 為軟刪除。
|
||
func (r *InMemoryRepository) Delete(ctx context.Context, id string) error {
|
||
r.mu.Lock()
|
||
defer r.mu.Unlock()
|
||
|
||
d, ok := r.devices[id]
|
||
if !ok || d.DeletedAt != nil {
|
||
return ErrNotFound
|
||
}
|
||
now := time.Now().UTC()
|
||
d.DeletedAt = &now
|
||
d.UpdatedAt = now
|
||
return nil
|
||
}
|
||
|
||
// 編譯時檢查:確保 InMemoryRepository 實作 Repository。
|
||
var _ Repository = (*InMemoryRepository)(nil)
|