# B8 預設模型(簡單版:寫死常數 + 打包 + 不簽 token) 模型庫「預設模型」原本實際是空的(舊 seed 是 demo 假資料、production 不啟用)。 補 7 個公用預設模型(kl520×4 + kl720×3,來源 local-tool models.json): - metadata 寫死成 Go 常數(presets.go),不進 DB → 公用、不可刪、無 seed 重複 - 7 個 .nef 打包進 image(assets/preset-models/,~61MB;Dockerfile COPY) - download 走 visionA 自己(簡單版不簽 token,preset 公用本不需授權): download handler 三分支 preset→visionA URL / converted→FAA / uploaded→501 - 新 GET /preset-models/*filepath 靜態 serve(無 auth、Content-Disposition attachment 用 mime.FormatMediaType、path-traversal 防禦) - list/get 含 preset(對所有人可見) # B5 詳細頁下載按鈕 + preset 可下載 - model-detail-client.tsx 補下載按鈕(行為對齊列表卡片) - isModelDownloadable 加 preset(source==="preset" → true) - normalizeModelSummary 對 target_chip toLowerCase(修 preset 大寫 KL520 在小寫晶片篩選下被隱藏的 bug;收斂所有來源大小寫) # 測試 backend:presets/preset 下載三分支/靜態 serve/path-traversal/條件對稱 全綠 frontend:詳細頁下載鈕 + preset 可下載 + target_chip 大小寫篩選回歸 38 PASS Reviewer 兩輪通過(Major-1 + Minor-1/2 修畢、複審 0 問題) # backlog(未做的優化) preset 下載改 HMAC presigned / preset .nef 改放 FAA 減 image / metadata 後台可管理 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
494 lines
23 KiB
Go
494 lines
23 KiB
Go
// Package config 定義 visionA-backend 的組態結構,對齊 TDD §2.10。
|
||
//
|
||
// 雛形遵循 12-Factor App:所有可變設定皆透過環境變數注入,不寫死在程式碼裡。
|
||
// `api-server` 與 `remote-proxy` 共享同一份 Config;各自只消費自己需要的欄位。
|
||
package config
|
||
|
||
import "time"
|
||
|
||
// Config 是整個 visionA-backend 的環境設定。
|
||
//
|
||
// 所有欄位皆由 Load() 從環境變數讀取並套用預設值。
|
||
// 欄位命名對齊 TDD §2.10;新增欄位時請同步更新 `.env.example`(待 B6)。
|
||
type Config struct {
|
||
Server ServerConfig
|
||
Session SessionConfig
|
||
Auth AuthConfig
|
||
OIDC OIDCConfig
|
||
UserSession UserSessionConfig
|
||
Storage StorageConfig
|
||
Model ModelConfig
|
||
Tunnel TunnelConfig
|
||
Logger LoggerConfig
|
||
CORS CORSConfig
|
||
// Conversion 控制 Phase 0.8 轉檔功能整合(converter / FAA / MC service token)。
|
||
// 對齊 .autoflow/04-architecture/conversion.md §5.3。
|
||
Conversion ConversionConfig
|
||
// FileAccess 控制 Phase 0.9 模型庫 model 直連 FAA 下載鏈(ADR-017 (a))。
|
||
FileAccess FileAccessConfig
|
||
// Database 控制 PostgreSQL 連線(DB 接入塊 0;持久業務資料 model / device / token)。
|
||
// 對齊 docs/autoflow/04-architecture/database.md §5.5.1。
|
||
Database DatabaseConfig
|
||
// Redis 控制 Redis 連線(DB 接入塊 4;僅 userSession browser cookie session)。
|
||
// 對齊 docs/autoflow/04-architecture/database.md §5.5.2。塊 0 不 wire,只先把 config 鉤子留好。
|
||
Redis RedisConfig
|
||
}
|
||
|
||
// ServerConfig 控制 HTTP listener 的位址與埠號。
|
||
//
|
||
// api-server 端使用 Port 提供 REST / WebSocket;
|
||
// remote-proxy 端使用 TunnelPort(面向 local agent)與 InternalPort(面向 api-server)。
|
||
//
|
||
// Port 預設為 3721 — 對齊 local-tool 的 base URL,這樣 local-tool 前端切到雲端版時
|
||
// base URL 可以維持一致,降低前端的 dev 流程切換成本(B4 決定)。
|
||
type ServerConfig struct {
|
||
Host string // VISIONA_HOST,預設 "0.0.0.0"
|
||
Port int // VISIONA_API_PORT,預設 3721(對齊 local-tool)
|
||
TunnelPort int // VISIONA_TUNNEL_PORT,預設 3800
|
||
InternalPort int // VISIONA_PROXY_INTERNAL_PORT,預設 3801
|
||
|
||
// RelayPublicURL 是 agent 連 tunnel 用的對外可達 URL(通常是 wss://.../tunnel/connect
|
||
// 的 origin 部分,例:wss://relay.visionA.cloud)。
|
||
// AB11 新增:/api/pairing/exchange 會把這個值回傳給 agent。
|
||
// 雛形預設為空 — handler 會 fallback 到 placeholder `wss://relay.visionA.cloud`。
|
||
RelayPublicURL string
|
||
|
||
// SeedDemoData 控制 api-server 啟動時是否塞入示範用 device + model + pairing token。
|
||
// 預設 false;本機開發或 demo 時可設 VISIONA_SEED_DEMO_DATA=true 開啟,
|
||
// 方便前端不必跑完整 pairing 流程就能看到資料。
|
||
SeedDemoData bool
|
||
}
|
||
|
||
// SessionConfig 控制 SessionStore 的實作選擇與連線資訊。
|
||
//
|
||
// Backend:
|
||
// - "inmemory" — remote-proxy 端持有 yamux session 的唯一來源
|
||
// - "proxy-client" — api-server 端透過 internal HTTP 查詢 remote-proxy
|
||
type SessionConfig struct {
|
||
Backend string // VISIONA_SESSION_BACKEND,預設 "inmemory"
|
||
ProxyInternalURL string // VISIONA_PROXY_INTERNAL_URL,預設 "http://localhost:3801"
|
||
}
|
||
|
||
// AuthConfig 控制雛形專用的 user fallback 與 pairing token。
|
||
//
|
||
// OB5(2026-04-26)起認證走 OIDC(OIDCConfig);
|
||
// Phase 0.7(2026-05-01)security audit 移除了 api.Deps.StaticUserID handler fallback
|
||
// (見 .autoflow/05-implementation/review/phase-0.7-security-audit.md C1)。
|
||
// 此處的 StaticUserID 欄位**僅供 dev seed(VISIONA_SEED_DEMO_DATA=true)與 unit test
|
||
// fixture 讀取使用**,不再注入 api.Deps、不影響 stage/prod 認證行為。
|
||
type AuthConfig struct {
|
||
// StaticUserID — Deprecated for routing/auth use. 僅供 dev seed / unit test。
|
||
// 見 internal/api/api.go 的 Deps 註解;stage/prod 留空無影響。
|
||
StaticUserID string // VISIONA_STATIC_USER_ID,預設 "demo-user"(dev seed only)
|
||
PairingToken string // VISIONA_PAIRING_TOKEN,格式必須為 vAc_ + 32 hex
|
||
SigningSecret string // VISIONA_STORAGE_SIGNING_SECRET,presigned URL HMAC secret
|
||
}
|
||
|
||
// OIDCConfig 控制 OpenID Connect 登入流程(BFF 模式)。
|
||
//
|
||
// 對齊 oidc-tdd.md §13.1 + ADR-010 + ADR-011 + ADR-013。
|
||
// OB5 起 OIDC 是唯一認證路徑;A1 起支援 public PKCE-only client。
|
||
type OIDCConfig struct {
|
||
// IssuerURL 是 OIDC IdP 的 issuer(不帶結尾斜線),例如
|
||
// dev: http://localhost:5050
|
||
// prod: https://members.innovedus.com
|
||
// 對齊 VISIONA_OIDC_ISSUER_URL。
|
||
IssuerURL string
|
||
|
||
// ClientID 是 visionA 在 IdP 註冊的 OAuth client id(confidential 或 public 皆可)。
|
||
// 對齊 VISIONA_OIDC_CLIENT_ID。
|
||
ClientID string
|
||
|
||
// ClientSecret 為**選填**(A1, 2026-05-01):
|
||
// - 有值 → confidential client mode(client_secret + PKCE 雙保險)
|
||
// - 留空 → PKCE-only public client mode(純依靠 PKCE 防 code interception)
|
||
// 兩種 mode 由 IdP 決定,visionA-backend 都支援(見 ADR-013)。
|
||
// **禁止 commit 進 repo**;對齊 VISIONA_OIDC_CLIENT_SECRET。
|
||
ClientSecret string
|
||
|
||
// RedirectURL 是 visionA-backend 的 callback URL,必須與 IdP 註冊值完全一致。
|
||
// dev: http://localhost:3721/api/auth/callback
|
||
// prod: https://api.visiona.cloud/api/auth/callback
|
||
// 對齊 VISIONA_OIDC_REDIRECT_URL。
|
||
RedirectURL string
|
||
|
||
// PostLoginURL 是 callback 完成後 302 回 frontend 的 base URL。
|
||
// dev: http://localhost:3000
|
||
// prod: https://app.visiona.cloud
|
||
// 對齊 VISIONA_FRONTEND_URL(沿用 oidc-tdd.md §13.1 命名)。
|
||
PostLoginURL string
|
||
|
||
// LogoutURL 是「讓使用者連帶登出 IdP(Member Center)session」的入口 URL。
|
||
//
|
||
// 背景(MC 限制,2026-06):MC 不支援標準 OIDC RP-initiated logout —
|
||
// discovery 雖列 end_session_endpoint=/auth/logout,但那是 POST+JSON 的 API
|
||
// (AuthController.Logout),非標準 GET + id_token_hint + post_logout_redirect_uri;
|
||
// 且 client 未註冊 post_logout_redirect_uri。因此後端無法乾淨地 302 導向 MC end_session。
|
||
//
|
||
// MC 端唯一「瀏覽器可觸發」的登出是 MemberCenter.**Web**(:7880)的 AccountController.Logout
|
||
// (路由 /account/logout,_signInManager.SignOutAsync() 清 MC session)。實測 GET 即回 302,
|
||
// 不必 form POST。**注意 host 必須是 MC Web :7880,不是 MC Api :7850**——:7850 沒 logout 頁、會 404。
|
||
// MC Web/Api 共享 DataProtection(SetApplicationName("MemberCenter")),清 :7880 session 後
|
||
// :7850 authorize 也視為未登入、下次登入會問帳密,可換帳號。
|
||
//
|
||
// 因此本欄位填 MC Web logout 入口(例:
|
||
// stage: https://stage-9527.innovedus.com:7880/account/logout)。
|
||
// 後端 logout handler 會把這個 URL 連同 method=GET 回給前端,由前端 navigate 觸發 MC 登出
|
||
// (見 oidc_auth.go oidcLogoutHandler)。
|
||
//
|
||
// ⚠️ stage MC 跑 master 舊版(2026-04-30 image)、logout 不支援 returnUrl,清完會停在 MC 首頁、
|
||
// 不自動回 visionA,前端需自行導回 visionA 登入。
|
||
//
|
||
// **設計成可關**:留空時 logout response 不回 idp_logout 欄位、維持「只清本地 session」
|
||
// 的舊行為(向下相容)。對齊 VISIONA_OIDC_LOGOUT_URL。
|
||
//
|
||
// TODO(MC 補標準 RP-initiated logout 後):改回後端 302 導向標準 end_session_endpoint,
|
||
// 移除這個權宜欄位。
|
||
LogoutURL string
|
||
|
||
// PromptLogin 控制 authorize request 是否帶 OIDC `prompt=login`:
|
||
// - true → 每次登入都讓 IdP 強制重新認證(忽略既有 SSO session)。
|
||
// - false → 沿用 IdP 既有 session(標準 SSO),為預設值。
|
||
// stage 想「每次都重新輸入帳密」時設 true;prod 視 UX 決定。
|
||
// 對齊 VISIONA_OIDC_PROMPT_LOGIN(預設 false)。
|
||
PromptLogin bool
|
||
|
||
// ServiceClientID 是「visionA-backend 以服務身份呼叫 MC API」用的 client id,
|
||
// 預留給未來 client_credentials grant flow(例如查詢使用者組織、推送通知等)。
|
||
//
|
||
// **A1 階段不啟用**:Validate() 不檢查、main.go 不 wire;只先把 config 鉤子留好,
|
||
// 之後接時不必再改 OIDCConfig schema。對齊 VISIONA_OIDC_SERVICE_CLIENT_ID。
|
||
ServiceClientID string
|
||
|
||
// ServiceClientSecret 是 service client(client_credentials grant)的 secret。
|
||
// 與 ServiceClientID 配對使用;同樣 A1 階段不啟用、Validate() 不檢查。
|
||
// **禁止 commit 進 repo**;對齊 VISIONA_OIDC_SERVICE_CLIENT_SECRET。
|
||
ServiceClientSecret string
|
||
}
|
||
|
||
// UserSessionConfig 控制 OIDC 登入後在 browser 端建立的 cookie session。
|
||
//
|
||
// 注意:與既有 SessionConfig(tunnel session 用)刻意分開,避免命名混淆。
|
||
// 對齊 oidc-tdd.md §5、§13.1。
|
||
type UserSessionConfig struct {
|
||
// Secret 是 cookie HMAC-SHA256 簽章金鑰;應為至少 32 byte 隨機字串。
|
||
// 對齊 VISIONA_SESSION_SECRET。
|
||
Secret string
|
||
|
||
// CookieName 預設 "visiona_session"。
|
||
// 對齊 VISIONA_SESSION_COOKIE_NAME。
|
||
CookieName string
|
||
|
||
// CookieDomain:dev 留空(host-only cookie),prod 設 ".visiona.cloud"。
|
||
// 對齊 VISIONA_SESSION_COOKIE_DOMAIN。
|
||
CookieDomain string
|
||
|
||
// CookieSecure 控制 Secure flag。dev=false(http),prod=true(https)。
|
||
// 對齊 VISIONA_SESSION_COOKIE_SECURE。
|
||
CookieSecure bool
|
||
|
||
// AbsoluteTTL 是 session 的最長存活時間(從 Create 起算)。預設 168h(7 天)。
|
||
// 對齊 VISIONA_SESSION_ABSOLUTE_TTL。
|
||
AbsoluteTTL time.Duration
|
||
|
||
// IdleTTL 是 session 的閒置存活時間(從 LastSeenAt 起算)。預設 24h。
|
||
// 對齊 VISIONA_SESSION_IDLE_TTL。
|
||
IdleTTL time.Duration
|
||
}
|
||
|
||
// StorageConfig 控制儲存層實作(LocalFS / S3)與路徑。
|
||
type StorageConfig struct {
|
||
Backend string // VISIONA_STORAGE_BACKEND,預設 "localfs"
|
||
RootDir string // VISIONA_STORAGE_LOCALFS_ROOT,預設 "./data/storage"
|
||
BaseURL string // VISIONA_STORAGE_LOCALFS_BASE_URL,預設 "http://localhost:3721/storage"(對齊 api-server port)
|
||
|
||
// PresetModelsDir 是 7 個系統預設模型 .nef 檔(B8)打包進 image 的目錄。
|
||
// VISIONA_PRESET_MODELS_DIR,預設 "./assets/preset-models"(image 內為 /app/assets/preset-models)。
|
||
// 由 GET /preset-models/* handler 直接讀檔串流(不簽 token,preset 公用)。
|
||
PresetModelsDir string
|
||
|
||
// PresetBaseURL 是 preset download_url 前綴(api-server 對外 origin,不含 /storage)。
|
||
// VISIONA_PRESET_BASE_URL,預設 "http://localhost:3721"。
|
||
// download handler 組 `{PresetBaseURL}/preset-models/{id}.nef` 回給前端導航下載。
|
||
PresetBaseURL string
|
||
}
|
||
|
||
// ModelConfig 針對模型資源的驗證限制(大小等)。
|
||
type ModelConfig struct {
|
||
// MaxSizeMB 是允許上傳的單一模型檔案大小上限(MB)。
|
||
// PRD §8.4 規範 Phase 0 為 100 MB;可由 VISIONA_MODEL_MAX_SIZE_MB 覆寫。
|
||
MaxSizeMB int
|
||
}
|
||
|
||
// TunnelConfig 控制 tunnel 心跳與掉線判定閾值,對齊 tunnel.md §4.2。
|
||
type TunnelConfig struct {
|
||
// HeartbeatInterval 為 yamux KeepAliveInterval 值。預設 10s。
|
||
HeartbeatInterval time.Duration
|
||
// IdleTimeout 為判定對端失聯的時間。預設 30s(= 3 次心跳未回)。
|
||
IdleTimeout time.Duration
|
||
}
|
||
|
||
// LoggerConfig 控制結構化 logger 的輸出等級。
|
||
type LoggerConfig struct {
|
||
Level string // VISIONA_LOG_LEVEL:debug / info / warn / error,預設 "info"
|
||
}
|
||
|
||
// ConversionConfig 控制 Phase 0.8 / 0.8b 轉檔功能整合。
|
||
//
|
||
// 對齊 docs/autoflow/04-architecture/conversion.md §3、
|
||
// `adr/adr-015-server-to-server-api-key.md`、`adr/adr-016-download-via-converter.md`。
|
||
//
|
||
// 啟用判定(由 Enabled() 給 main.go 用):Phase 0.8b v0.6 起,2 個必要欄位
|
||
// (ConverterBaseURL / ConverterAPIKey)**全部非空**才視為啟用;任一缺即視為未啟用,
|
||
// 5 個 /api/conversion/* endpoint 不會 wire(main.go 在 wire 階段跳過、log warn)。
|
||
//
|
||
// **Phase 0.8b 變更**:服務間認證從 OAuth client_credentials 改為 pre-shared API key(ADR-015)。
|
||
//
|
||
// **Phase 0.8b T5 完成**(見 conversion.md §3.2 / ADR-015 §5 §7):原暫留欄位
|
||
// TenantID / DelegatedTTLSeconds 已移除 — MC 認證鏈與 delegated download token 機制
|
||
// 都不存在了,兩個欄位連同對應 env(VISIONA_OIDC_TENANT_ID /
|
||
// VISIONA_FAA_DELEGATED_TTL_SECONDS)一併清除。
|
||
//
|
||
// **Phase 0.8b v0.6 T4 完成**(見 ADR-016 §2 / conversion.md v0.6.1 §3.1):原 FAA 相關
|
||
// 欄位 FAABaseURL / FAAAPIKey 已移除 — visionA 端不再直接呼叫 FAA,download / promote
|
||
// 流程改走 converter.GetResult(ADR-016 撤回 v0.5 設計缺口),兩個欄位連同對應 env
|
||
// (VISIONA_FAA_BASE_URL / VISIONA_FAA_API_KEY)一併清除;`Enabled()` 也簡化為只判
|
||
// converter 兩欄位。
|
||
type ConversionConfig struct {
|
||
// ConverterBaseURL 是 kneron_model_converter task-scheduler 服務的 base URL。
|
||
// 例:http://192.168.0.130:9501(dev / stage) / https://converter.visiona.cloud(prod)
|
||
// 對齊 VISIONA_CONVERTER_BASE_URL;留空 = 不啟用 Phase 0.8 轉檔功能。
|
||
ConverterBaseURL string
|
||
|
||
// ConverterAPIKey 是 visionA → converter 服務間認證的 pre-shared API key(Phase 0.8b 新增)。
|
||
// 對齊 VISIONA_CONVERTER_API_KEY;以 `Authorization: Bearer <key>` 形式帶上。
|
||
// 雙方獨立產生(`openssl rand -hex 32`),visionA 端的值必須與 converter 端的
|
||
// `CONVERTER_API_KEY` env 對齊;不對齊 → 下游 401(visionA 端不重試,回 502 converter_auth_failed)。
|
||
// 對應 ADR-015 §3。
|
||
//
|
||
// 安全:log 永遠不印此值全文(可印 `api_key_set=true/false` 或前 8 字元 prefix);
|
||
// 部署用 AWS Secrets Manager / Vault;嚴格分環境(dev / stage / prod 各自獨立 key)。
|
||
ConverterAPIKey string
|
||
|
||
// MaxModelSizeMB 是 visionA-backend 端對上傳模型檔的大小上限(MB)。
|
||
// 與 converter 端 limit 對齊(converter 預設 500 MB)。
|
||
// 對齊 VISIONA_CONVERTER_MAX_MODEL_SIZE_MB;預設 500。
|
||
MaxModelSizeMB int
|
||
}
|
||
|
||
// FileAccessConfig 控制「模型庫 model 直連 FAA 下載」鏈路(ADR-017 (a))。
|
||
//
|
||
// 對齊 adr/adr-017-model-library-access.md §10(stage e2e 實測藍本):
|
||
//
|
||
// visionA 用 service client 打 MC `/oauth/token`(scope files:download.delegate)
|
||
// → 打 MC `POST /file-access/download-tokens`(Issue)簽 opaque `fdt_` token
|
||
// → 回給 Client「FAA 下載 URL + fdt token」,Client 帶 `Authorization: Bearer fdt_...`
|
||
// 直接 GET `{FAA}/files/{object_key}`。
|
||
//
|
||
// 啟用判定(由 Enabled() 給 main.go 用):4 個必要欄位
|
||
// (MCBaseURL / ServiceClientID / ServiceClientSecret / TenantID)全部非空才視為啟用;
|
||
// 任一缺即不 wire download token issuer,model download endpoint 回 501。
|
||
// FAABaseURL 是「組對外 download_url」用,留空時 endpoint 也回 501(無從組 URL)。
|
||
//
|
||
// ⚠️ 技術債(ADR-017 §7 R1 / Q10):第一階段 PoC 短期**共用 FAA 的 service client**
|
||
// (`4242ba63...`,stage 實測可拿 files:download.delegate token)。MC 規範明訂「OAuth client
|
||
// 禁止混用 usage、secret 不共用」,故正式上線前須請 MC 配發 visionA 專屬 usage=file_api
|
||
// client 換掉此共用 client,把 secret 邊界收回 visionA。詳見 .env.example 對應註解。
|
||
type FileAccessConfig struct {
|
||
// MCBaseURL 是 Member Center API base URL(不帶結尾斜線)。
|
||
// stage:https://stage-9527.innovedus.com:7850
|
||
// 對齊 VISIONA_FILE_ACCESS_MC_BASE_URL。
|
||
MCBaseURL string
|
||
|
||
// ServiceClientID 是打 MC `/oauth/token`(client_credentials)的 client id。
|
||
// ⚠️ 技術債:第一階段 PoC 共用 FAA service client(`4242ba63...`)。
|
||
// 對齊 VISIONA_FILE_ACCESS_SERVICE_CLIENT_ID。
|
||
ServiceClientID string
|
||
|
||
// ServiceClientSecret 是 service client 的 secret。
|
||
// **禁止 commit 進 repo**;對齊 VISIONA_FILE_ACCESS_SERVICE_CLIENT_SECRET。
|
||
// 安全:log 永遠不印此值全文。
|
||
ServiceClientSecret string
|
||
|
||
// TenantID 是簽 download token 時帶給 MC 的 tenant_id(須與 FAA validate 的 tenant 一致)。
|
||
// stage:732270c0-449c-489c-bfad-321e9bf89b3d
|
||
// 對齊 VISIONA_FILE_ACCESS_TENANT_ID。
|
||
TenantID string
|
||
|
||
// FAABaseURL 是 File Access Agent 對外 base URL(不帶結尾斜線),用來組回給 Client 的
|
||
// download_url(`{FAABaseURL}/files/{object_key}`)。
|
||
// stage:https://stage-9527.innovedus.com:5081
|
||
// 對齊 VISIONA_FILE_ACCESS_FAA_BASE_URL。
|
||
FAABaseURL string
|
||
|
||
// DownloadTokenTTLSeconds 是簽 download token 時帶給 MC 的 expires_in_seconds。
|
||
// ADR-017 Q2 區間 60–300s,預設 120s。對齊 VISIONA_FILE_ACCESS_DOWNLOAD_TOKEN_TTL_SECONDS。
|
||
DownloadTokenTTLSeconds int
|
||
}
|
||
|
||
// Enabled 回傳「模型庫 FAA 直連下載」是否啟用。
|
||
//
|
||
// 4 個必要欄位(MCBaseURL / ServiceClientID / ServiceClientSecret / TenantID)+ FAABaseURL
|
||
// 全部非空才視為啟用;任一缺 → main.go 不 wire download token issuer,
|
||
// GET /api/models/:id/download 回 501。
|
||
func (c FileAccessConfig) Enabled() bool {
|
||
return c.MCBaseURL != "" &&
|
||
c.ServiceClientID != "" &&
|
||
c.ServiceClientSecret != "" &&
|
||
c.TenantID != "" &&
|
||
c.FAABaseURL != ""
|
||
}
|
||
|
||
// Enabled 回傳 Phase 0.8 / 0.8b conversion 是否啟用。
|
||
//
|
||
// **Phase 0.8b v0.6 T4 簡化**(ADR-016 §2 / conversion.md v0.6.1 §3.1):visionA 端撤回
|
||
// 對 FAA 的直接呼叫,download / promote 改走 converter.GetResult;只剩 ConverterBaseURL
|
||
// 與 ConverterAPIKey 兩個欄位需非空。任一缺 → 視為未啟用,main.go 不會 wire
|
||
// conversion.Service(5 個 endpoint 回 501 / 不註冊)。
|
||
func (c ConversionConfig) Enabled() bool {
|
||
return c.ConverterBaseURL != "" && c.ConverterAPIKey != ""
|
||
}
|
||
|
||
// DatabaseConfig 控制 PostgreSQL 連線(持久業務資料:model / device / token)。
|
||
//
|
||
// 對齊 docs/autoflow/04-architecture/database.md §5.5.1。
|
||
//
|
||
// 啟用判定(Enabled()):Host / User / DBName 三者全非空才視為啟用;
|
||
// 任一缺 → main.go 不建連線池、6 個 repository 仍用 in-memory(local dev fallback)。
|
||
//
|
||
// 安全:Password / DSN 永遠不印 log 全文(可印 host:port/dbname);
|
||
// 部署走既有 secrets 機制(AWS Secrets Manager / Vault),禁止 commit 進 repo。
|
||
type DatabaseConfig struct {
|
||
Host string // VISIONA_DB_HOST
|
||
Port int // VISIONA_DB_PORT,預設 5432
|
||
User string // VISIONA_DB_USER
|
||
Password string // VISIONA_DB_PASSWORD(禁止 commit)
|
||
DBName string // VISIONA_DB_NAME
|
||
SSLMode string // VISIONA_DB_SSLMODE,預設 "require"(stage/prod);本機 testcontainers 用 "disable"
|
||
|
||
// 連線池(pgxpool)
|
||
MaxConns int // VISIONA_DB_MAX_CONNS,預設 10
|
||
MinConns int // VISIONA_DB_MIN_CONNS,預設 2
|
||
MaxConnLifetime time.Duration // VISIONA_DB_MAX_CONN_LIFETIME,預設 1h
|
||
ConnTimeout time.Duration // VISIONA_DB_CONN_TIMEOUT,預設 5s(建池/ping 逾時)
|
||
|
||
// AutoMigrate 控制連線池建立後是否自動跑 migrate up。
|
||
// 預設 true;可由 VISIONA_DB_AUTO_MIGRATE=false 關閉(例如改用獨立 cmd/migrate)。
|
||
AutoMigrate bool // VISIONA_DB_AUTO_MIGRATE,預設 true
|
||
}
|
||
|
||
// Enabled 回傳 PostgreSQL 是否啟用。
|
||
//
|
||
// Host / User / DBName 三者全非空才視為啟用;任一缺 →
|
||
// main.go 不建連線池,model / device / token repository 維持 in-memory(local dev fallback)。
|
||
func (c DatabaseConfig) Enabled() bool {
|
||
return c.Host != "" && c.User != "" && c.DBName != ""
|
||
}
|
||
|
||
// RedisConfig 控制 Redis 連線(僅 userSession:browser cookie session)。
|
||
//
|
||
// 對齊 docs/autoflow/04-architecture/database.md §5.5.2。
|
||
//
|
||
// ⚠️ visionA 專用 Redis 實例:由使用者自行在 stage host(130) 另起、設密碼。
|
||
// visionA 端不 provision、只接上。
|
||
//
|
||
// 啟用判定(Enabled()):Host 非空才視為啟用;
|
||
// 未啟用 → userSession 仍用 in-memory(雛形行為,process 重啟掉 session)。
|
||
//
|
||
// 安全:Password 永遠不印 log 全文。禁止 commit 進 repo。
|
||
//
|
||
// 塊 0 範圍:只先把 config 鉤子留好,main.go 尚未 wire(等塊 4 接 RedisUserSessionStore)。
|
||
type RedisConfig struct {
|
||
Host string // VISIONA_REDIS_HOST
|
||
Port int // VISIONA_REDIS_PORT,預設 6379
|
||
Password string // VISIONA_REDIS_PASSWORD(visionA 專用實例必設密碼;禁止 commit)
|
||
DB int // VISIONA_REDIS_DB,預設 0(db index)
|
||
|
||
ConnTimeout time.Duration // VISIONA_REDIS_CONN_TIMEOUT,預設 5s
|
||
}
|
||
|
||
// Enabled 回傳 Redis 是否啟用(Host 非空即啟用)。
|
||
func (c RedisConfig) Enabled() bool {
|
||
return c.Host != ""
|
||
}
|
||
|
||
// CORSConfig 控制 api-server 對瀏覽器的 CORS 白名單。
|
||
//
|
||
// AllowedOrigins 為逗號分隔字串解析後的 slice;
|
||
// 空時 api.Deps.validate() 會 fallback 到 http://localhost:3000(前端 dev server)。
|
||
type CORSConfig struct {
|
||
AllowedOrigins []string // VISIONA_CORS_ALLOWED_ORIGINS(逗號分隔)
|
||
}
|
||
|
||
// Validate 在 Load() 之後檢查交叉依賴與必填欄位。
|
||
//
|
||
// OB5 起 OIDC 是唯一認證路徑,所有 OIDC 必填欄位永遠都要非空:
|
||
// - IssuerURL / ClientID / RedirectURL / PostLoginURL
|
||
// - UserSession.Secret(cookie HMAC 簽章)
|
||
//
|
||
// ClientSecret 為**選填**(A1, 2026-05-01):
|
||
// - 有值 → confidential client mode(標準 OAuth + PKCE 雙保險)
|
||
// - 留空 → PKCE-only public client mode(依靠 PKCE 防 code interception)
|
||
//
|
||
// 兩種 mode 由 IdP 決定,visionA 都支援(見 ADR-013、oidc-tdd.md §13.1)。
|
||
//
|
||
// ServiceClientID / ServiceClientSecret 為 client_credentials grant 預留欄位,
|
||
// A1 階段不啟用、不檢查;之後若接服務間 API 呼叫再補 Validate。
|
||
//
|
||
// 缺任何**必填**項 → 回 *MissingEnvError,main.go 啟動時 fatal log 退出。
|
||
// 維持單一 error 而非列表 — caller 只是 fail-fast 紀錄,不需要結構化處理。
|
||
func (c *Config) Validate() error {
|
||
missing := make([]string, 0, 5)
|
||
if c.OIDC.IssuerURL == "" {
|
||
missing = append(missing, "VISIONA_OIDC_ISSUER_URL")
|
||
}
|
||
if c.OIDC.ClientID == "" {
|
||
missing = append(missing, "VISIONA_OIDC_CLIENT_ID")
|
||
}
|
||
// ClientSecret 為選填(public PKCE-only client 留空)— 不檢查。
|
||
if c.OIDC.RedirectURL == "" {
|
||
missing = append(missing, "VISIONA_OIDC_REDIRECT_URL")
|
||
}
|
||
if c.OIDC.PostLoginURL == "" {
|
||
missing = append(missing, "VISIONA_FRONTEND_URL")
|
||
}
|
||
if c.UserSession.Secret == "" {
|
||
missing = append(missing, "VISIONA_SESSION_SECRET")
|
||
}
|
||
|
||
if len(missing) > 0 {
|
||
return &MissingEnvError{Vars: missing}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// MissingEnvError 表示 OIDC 必填環境變數缺少(OB5 起永遠檢查)。
|
||
type MissingEnvError struct {
|
||
Vars []string
|
||
}
|
||
|
||
func (e *MissingEnvError) Error() string {
|
||
return "config: OIDC enabled but required env vars are missing: " + joinStrings(e.Vars, ", ")
|
||
}
|
||
|
||
// joinStrings 是 strings.Join 的本地版本,避免單純為了 join 引入 strings package。
|
||
func joinStrings(parts []string, sep string) string {
|
||
switch len(parts) {
|
||
case 0:
|
||
return ""
|
||
case 1:
|
||
return parts[0]
|
||
}
|
||
n := len(sep) * (len(parts) - 1)
|
||
for _, p := range parts {
|
||
n += len(p)
|
||
}
|
||
out := make([]byte, 0, n)
|
||
out = append(out, parts[0]...)
|
||
for _, p := range parts[1:] {
|
||
out = append(out, sep...)
|
||
out = append(out, p...)
|
||
}
|
||
return string(out)
|
||
}
|