新增雲端版部署設定(Phase 0.6 dev + Phase 0.7 stage 分兩套): dev 環境(docker-compose.dev.yml): - 5 service all-in-one(postgres + member-center + visionA-backend + frontend + Caddy) - Caddy 自動 HTTPS for localhost - .env.dev.example 範本(使用者拷出 .env.dev 後 docker compose up -d) - Makefile dev-with-mc 9 個 target stage 環境(docker-compose.stage.yml + docker/Dockerfile.stage): - multi-stage build(node22 frontend + go1.26 backend × 2 + nginx-alpine runtime) 最終 image 319 MB,含 nginx + nodejs + tini + bash - entrypoint.stage.sh 4 process 共命運(nginx + api-server + remote-proxy + next.js standalone)用 wait -n + SIGTERM trap - nginx.stage.conf:白名單 server_name stage-9527.innovedus.com + 444 default_server + /healthz 例外(127.0.0.0/8 only)+ /api/ 與 /storage/ 強制 no-store + /tunnel/connect WS upgrade + 100M body / 3600s timeout - 對外 mapping 0.0.0.0:9527:80(公司 host nginx 在外層處理 HTTPS termination — Let's Encrypt stage-9527.innovedus.com 自動續簽) - named volume visiona-data(不用 bind mount,因 stage docker daemon 在 host root 無 mkdir 權限) 部署腳本(scripts/deploy-stage.sh): - 仿 edge-ai-platform/scripts/deploy-docker.sh 早期 save/load 模式 - 為什麼不用 internal registry:公司 192.168.0.130:5000 開了 auth、無帳密 - 流程:buildx --load → docker save | gzip → DOCKER_HOST docker load → compose up - 含 --rollback <tag> / --skip-build / --no-push / --skip-deploy 選項 - timestamp + git SHA tag 留 rollback 餘地 文件(docs/): - DEV-SETUP.md:dev 環境一鍵起步驟 - SMOKE-TEST.md:手動煙測 checklist(OIDC flow / pairing / tunnel) - STAGE-DEPLOY.md:stage 完整手冊(架構圖 / 環境前置 / 部署 step / rollback / 7 種故障排除 / 緊急救回 POC) .env.stage.example 對齊 backend A1 改造: - VISIONA_OIDC_CLIENT_SECRET 留空(PKCE-only public client) - VISIONA_OIDC_SERVICE_CLIENT_ID/_SECRET 留空(Phase 1 預留鉤子) - 所有 secret 用 placeholder(CHANGE_ME_OPENSSL_RAND_HEX_32) .dockerignore:避免 node_modules / .next / .git 等進 build context Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
17 KiB
visionA Phase 0.6 OIDC 手動煙測 Checklist
適用對象:Phase 0.6 OIDC + Member Center 接入完成後,使用者首次驗收。
預期時間:~30 分鐘(含 OAuth client 手動設定 ~10 分鐘)
上位文件:
.autoflow/04-architecture/oidc-tdd.md、.autoflow/04-architecture/adr/adr-010-oidc-bff.md、docs/DEV-SETUP.md為什麼是手動而不是自動:MC admin API 目前有兩個 bug(password grant 缺 sub claim → 500、admin API 無法回傳 client_secret),導致 OAuth client 註冊無法用 script 自動化。詳見本文「§故障排除 #1」與「§已知 limitation」。
用法
每個 checkbox 代表一個獨立可驗證的步驟,逐項打勾。 有 ❌ 不通過的步驟 → 看「§故障排除」對應編號 → 修完再 retry → 才往下走。 全部 ✅ 走完 = Phase 0.6 OIDC 接入驗收通過。
前置條件
- Docker Desktop 已開(
docker info不噴錯) - visionA + member_center 已 git clone 平行擺放(
ls -d ../member_center看得到) - 已照
docs/DEV-SETUP.md§4 跑過docker compose -f docker-compose.dev.yml --env-file .env.dev up -d --build - frontend 依賴已裝:
cd visionA-frontend && pnpm install - frontend dev server 已開:
cd visionA-frontend && pnpm dev,能看到 http://localhost:3000 顯示登入頁
階段 1:基礎服務驗證(5 分鐘)
驗證 5 個 docker service + frontend 都健在。
- 1.1
docker compose -f docker-compose.dev.yml ps→ postgres、member-center、member-center-web、visiona-proxy、visiona-api 都顯示Up (healthy);member-center-init 顯示Exited (0) - 1.2
curl -sS http://localhost:5050/.well-known/openid-configuration | head -c 200→ 回 200 + JSON,內含"issuer":"http://localhost:5050/" - 1.3
curl -sS http://localhost:5050/jwks→ 回 200 + JSON,含"keys": [...] - 1.4
curl -sS http://localhost:3721/healthz→ 回 200(visiona-api 健康) - 1.5
curl -sS http://localhost:3800/healthz→ 回 200(visiona-proxy 健康) - 1.6 開 http://localhost:3000 → 看到 visionA login 頁(含「使用您的 Innovedus 帳號登入」按鈕)
- 1.7 開 http://localhost:5060 → 看到 Member Center admin 後台登入頁
任一步驟失敗 → §故障排除 #1。
階段 2:Member Center 設定(10 分鐘)
⚠️ 因 MC admin API 兩個 bug,此階段必須走 Web UI 手動操作。詳見 §故障排除 #1。
2.1 登入 MC admin
- 2.1.1 開 http://localhost:5060
- 2.1.2 用
.env.dev中的MC_ADMIN_EMAIL/MC_ADMIN_PASSWORD登入(預設admin@visiona.local/Admin12345!) - 2.1.3 進到 admin dashboard 不噴錯
2.2 建 Tenant
- 2.2.1 進「Tenants」頁
- 2.2.2 點「Create」
- 2.2.3 填入:
- Name:
visionA - Domains:
visiona.cloud, localhost - Status:
active
- Name:
- 2.2.4 送出 → 列表出現
visionAtenant
2.3 建 OAuth Client
- 2.3.1 進「OAuth Clients」頁
- 2.3.2 點「Create」
- 2.3.3 填入:
- Tenant:
visionA - Name:
visionA Cloud - Client Type:
confidential - Usage:
webhook_outbound(雛形 workaround,見 §已知 limitation #3) - Redirect URIs:
http://localhost:3721/api/auth/callback
- Tenant:
- 2.3.4 送出
- 2.3.5 複製 client_id(GUID 格式,例
a1b2c3d4-...)→ 暫存 - 2.3.6 複製 client_secret(如 UI 沒給,見 §故障排除 #1.b 的 fallback)→ 暫存
2.4 建 Demo User
- 2.4.1 進「Users」頁
- 2.4.2 點「Create」
- 2.4.3 填入:
- Email:
demo@visiona.local - Password:
Demo12345! - Name:
Demo User(或自填) - Tenant:選
visionA
- Email:
- 2.4.4 送出 → users 列表出現 demo user
2.5 寫回 .env.dev + 切到 OIDC mode
- 2.5.1 編輯
.env.dev,填入 2.3.5 / 2.3.6 拿到的值:VISIONA_OIDC_CLIENT_ID=<client_id> VISIONA_OIDC_CLIENT_SECRET=<client_secret> VISIONA_AUTH_TYPE=oidc - 2.5.2 重啟 visiona-api:
docker compose -f docker-compose.dev.yml --env-file .env.dev up -d visiona-api - 2.5.3 看 log:
docker compose -f docker-compose.dev.yml logs visiona-api --tail 20- 看到
OIDC initialized issuer=http://localhost:5050/ client_id=<你的 guid> - 看到
api-server listening addr=0.0.0.0:3721 - 沒有 panic / fatal
- 看到
任一步驟失敗 → §故障排除 #2。
階段 3:完整 Login Flow(5 分鐘)
驗證 OAuth Authorization Code + PKCE 走完整 redirect chain。
- 3.1 開瀏覽器(建議無痕視窗,避免舊 cookie 干擾)→ http://localhost:3000/login
- 3.2 看到「使用您的 Innovedus 帳號登入」按鈕
- 3.3 開 DevTools → Network tab(保持「Preserve log」勾選,這樣才看得到 redirect chain)
- 3.4 點登入按鈕 → 第一個 request 是
GET /api/auth/login,response 302 - 3.5 跟隨 302 → 第二個 request 是
GET http://localhost:5050/oauth/authorize?...(MC 顯示登入頁) - 3.6 確認 authorize URL 內含:
response_type=codeclient_id=<你的 guid>scope=openid email profilestate=<random>code_challenge=<...>+code_challenge_method=S256nonce=<random>redirect_uri=http://localhost:3721/api/auth/callback
- 3.7 DevTools → Application → Cookies → http://localhost:3721 → 應已有
visiona_pending_sid(HttpOnly) - 3.8 MC 登入頁輸入
demo@visiona.local/Demo12345!→ 送出 - 3.9(如有同意授權頁)→ 點「同意」
- 3.10 跟隨 302 →
GET http://localhost:3721/api/auth/callback?code=...&state=... - 3.11 跟隨 302 → 回到 http://localhost:3000(dashboard / workspace 首頁)
- 3.12 DevTools → Cookies → http://localhost:3721 →
visiona_session出現(HttpOnly);visiona_pending_sid已被清除 - 3.13 Header 右上角顯示 demo user 的 email 或 name(visionA-frontend
/account應拿得到 user info)
任一步驟失敗 → §故障排除 #3。
階段 4:API 帶 Cookie 驗證(3 分鐘)
驗證 BFF Pattern:frontend 只用 cookie,所有 API 自動帶 session。
- 4.1 DevTools → Network → 重整任意需登入頁面(如
/devices) - 4.2 找到
GET /api/auth/merequest:- Request Headers 帶
Cookie: visiona_session=... - Response 200,body 含
user_id(OIDC sub,不應是demo-user)、email、name、expires_at
- Request Headers 帶
- 4.3 訪 http://localhost:3000/devices → 頁面正常載入(API 拿到 cookie session)
- 4.4 訪 http://localhost:3000/models → 頁面正常載入
- 4.5 訪 http://localhost:3000/account → 頁面正常載入並顯示 user 資訊
- 4.6 開新 tab 直接用 curl(替換 cookie 值):
→ 200 + JSONcurl -sS -b "visiona_session=<從 DevTools 複製>" http://localhost:3721/api/auth/me - 4.7 curl 不帶 cookie:
→ 401(middleware 擋下)curl -sSi http://localhost:3721/api/auth/me
任一步驟失敗 → §故障排除 #4。
階段 5:Pairing Token 綁 OIDC user(5 分鐘)
驗證關鍵承諾:OIDC sub → UserContext → Pairing Token 的 user binding 正確。 這是 ADR-010 + oidc-tdd §9 的核心驗證點:取代 StaticAuth 後,pairing 不會再綁到
demo-user。
- 5.1 訪 http://localhost:3000/devices/pair
- 5.2 找到「產生 Pairing Token」按鈕,點下去
- 5.3 拿到 token(格式類似
vAc_xxxxx) - 5.4 DevTools → Network → 看
POST /api/pairing/tokenrequest- Request 帶
Cookie: visiona_session=... - Response 200 + JSON
- Request 帶
- 5.5 看 backend log 確認 user binding:
docker compose -f docker-compose.dev.yml logs visiona-api --tail 50 | grep -i pairing- 應看到「PairingStore Create userID=」之類 log(user_id 是 GUID 或 MC 給的 sub,不是
demo-user)
- 應看到「PairingStore Create userID=」之類 log(user_id 是 GUID 或 MC 給的 sub,不是
- 5.6(可選,如有實作)查 storage:訪
/account或對應頁面,確認該 token 列在「我的 pairing tokens」清單下 - 5.7(可選)拿 token 餵給 local-tool agent → agent 連上 → 確認後端 log 看到 agent 連入時 binding 的 user_id 與 5.5 一致
任一步驟失敗 → §故障排除 #5。
階段 6:登出 + 重登(3 分鐘)
- 6.1 Header → User menu → 點「登出」(或訪
/account找登出按鈕) - 6.2 DevTools → Network → 看
POST /api/auth/logout→ 204 No Content - 6.3 DevTools → Cookies →
visiona_session已被清除(或值改成空 + Max-Age=0) - 6.4 自動 redirect 到
/login(或手動訪/login確認沒登入狀態) - 6.5 訪 http://localhost:3000/devices → 應 redirect 到
/login(或顯示「請先登入」),不應看到 devices 列表 - 6.6 curl
GET /api/auth/me不帶 cookie → 401 - 6.7 重新點登入按鈕 → 走完 §3 整套 flow → 又能進到 dashboard
- 6.8 確認重新登入後
/api/auth/me拿到的user_id跟之前一樣(demo user 還是同一個 OIDC sub)
任一步驟失敗 → §故障排除 #6。
階段 7:跨頁 Refresh / 持久化(2 分鐘)
驗證 cookie 7 天 + 24h idle 的 session 生命週期。
- 7.1 登入狀態下重整瀏覽器 → 仍登入(cookie 持久 +
/api/auth/me還能拿 user) - 7.2 關掉瀏覽器 tab(不要清 cookie)→ 開新 tab → 訪 http://localhost:3000 → 仍登入
- 7.3 DevTools → Cookies →
visiona_session的 Max-Age 約 604800(7 天)
故障排除
#1 階段 2 卡住:MC admin 登入失敗、tenant/client 建不起來
a. admin 帳號登不進去
- 確認
.env.dev的MC_ADMIN_EMAIL/MC_ADMIN_PASSWORD與 docker compose 啟動時一致 - 看
docker compose logs member-center-init→ 應有「admin user created」訊息 - 完全重置:
docker compose -f docker-compose.dev.yml down -v→ 重新up -d(會重跑 init)
b. UI 沒顯示 client_secret
- 已知 MC limitation:admin UI 建 confidential client 時可能不回傳 secret
- 暫時 workaround:把 client type 改成
public(VISIONA_OIDC_CLIENT_SECRET留空字串),visionA-backend 應允許 empty secret 通過(雛形可接受) - 永久解:MC 補 admin API 回傳 secret 的功能
c. 為什麼整個流程不能 script 自動化
- MC password grant 在 Identity user 上有 bug:
mandatory subject claim was missing→ 500 - → 拿不到 admin token → 無法呼叫 admin API → 必須走 Web UI
- 等 MC 修這兩個 bug 後,OD2 可實作 seed script,OT2 可改全自動 e2e
#2 visiona-api 看不到 "OIDC initialized" log,或 startup 噴錯
| log 關鍵字 | 原因 | 解法 |
|---|---|---|
discovery fetch failed |
MC 未起 / port 不對 | 確認 docker compose ps member-center healthy;curl localhost:5050/.well-known/openid-configuration 通 |
issuer URL ... did not match |
issuer trailing slash 問題 | .env.dev 的 VISIONA_OIDC_ISSUER_URL 必須結尾 /,例 http://localhost:5050/ |
IssuerURL invalid / ClientID required |
env 沒帶到 | docker compose 命令必須加 --env-file .env.dev |
auth type "oidc" but ClientID is CHANGE_ME |
還沒填 client_id | 完成階段 2.5.1 |
#3 階段 3 卡住:登入 redirect chain 失敗
| 症狀 | 原因 | 解法 |
|---|---|---|
| 點登入按鈕沒反應 | frontend /api/auth/login 沒打到 backend |
確認 VISIONA_FRONTEND_URL=http://localhost:3000、frontend NEXT_PUBLIC_API_BASE_URL=http://localhost:3721(或對應 env) |
| 302 到 MC 後 MC 噴 invalid_client | client_id 不對 / tenant 沒 active | 回 MC admin UI 檢查 client 是否屬於 active tenant |
MC 登入後回 callback 噴 state_mismatch |
visiona_pending_sid cookie 沒帶過去 |
確認 backend Set-Cookie 沒設 Domain 限制;瀏覽器在 callback 時應該帶 cookie(DevTools → Application → Cookies 看是否真有存進去) |
callback 噴 pending_session_not_found |
pending session 過期(10 分鐘)或 session store 重啟丟資料 | 重新點登入按鈕從頭來;雛形 in-memory store 重啟即清空 |
callback 噴 id_token_invalid |
id_token 驗簽失敗(issuer / aud / nonce 對不上) | 看 backend log detail 欄;常見:JWKS cache 過時 → 重啟 visiona-api |
callback 噴 token_exchange_failed |
client_secret 不對 / redirect_uri 不對 | 比對 .env.dev 與 MC admin UI 上的 client 設定 |
#4 階段 4 卡住:API 401 / cookie 沒帶
- DevTools → Application → Cookies → 確認
visiona_session真的存在 + 沒過期 - DevTools → Network → 看 request 是否真的帶
Cookie:header- 如果沒帶 → frontend
fetch沒設credentials: 'include',看 OF2 是否真的補上
- 如果沒帶 → frontend
- backend log
unauthorized帶的 detail:cookie_invalid→ cookie 簽章對不上 → 通常VISIONA_SESSION_SECRET變過導致舊 cookie 失效 → 清 cookie 重登session_not_found→ in-memory store 被重啟清掉 → 重登session_expired→ 真的過期了,重登
#5 階段 5:Pairing token user_id 綁錯(綁到 demo-user 而非 OIDC sub)
這是 OB5 / OF2 必須驗證的關鍵點:
- 看
docker compose logs visiona-api | grep -i 'pairing\|userID\|user_id'- 如果 user_id 是
demo-user→ OB5 沒拔乾淨 /VISIONA_AUTH_TYPE還是static→ 確認階段 2.5.1 真的改成oidc - 如果 user_id 是空 /
unknown→ middleware 沒注入 UserContext → 看 OB3 middleware 程式碼 - 如果 user_id 是 GUID(OIDC sub)→ ✅ 正確
- 如果 user_id 是
- 必要時開 issue:「OB5 + OF2 完成後 pairing 仍綁到 demo-user,需 hotfix」
#6 階段 6:登出後仍能訪問
- DevTools 確認
visiona_sessioncookie 真的被清掉 - 如果 cookie 還在 → backend
Set-Cookie: visiona_session=; Max-Age=0沒設對 - 訪
/api/auth/me不帶 cookie 仍 200 → middleware bypass 了某些路徑 → 看 router 設定 - frontend 沒 redirect 到
/login→ middleware(Next.js)沒檢查 session → 看 OF2/OF3 frontend route guard
已知 Limitation(Phase 0.6 雛形)
| # | Limitation | 影響 | 後續處理(Phase 1) |
|---|---|---|---|
| 1 | MC admin API 不接受 client_secret 輸入 / 不回傳 client_secret | 必須走 MC Web UI 手動建 OAuth client | MC 補 admin API |
| 2 | MC password grant 在 Identity user 缺 sub claim → 500 | 無法用 admin token script 自動化 seed | MC TokenController 補 Subject claim mapping |
| 3 | MC 只支援 usage=webhook_outbound 帶 redirect_uris |
命名語意不對(visionA 借用) | MC 新增 usage=web_app |
| 4 | visionA-backend in-memory session store 重啟即消失 | 雛形階段使用者重登 OK;不適合多 instance | Phase 1 換 Redis |
| 5 | host 上的 visiona-local(local-tool)佔 3721 與 docker compose 衝突 | 兩個只能擇一跑 | 預期;docs 已說明 |
| 6 | 沒有 RP-initiated logout(登出 visionA 不會把 MC session 也登出) | 同 browser 下次點登入會自動跳過 MC 登入頁 | Phase 1 補 |
| 7 | 沒有 refresh token rotation | session 24h idle / 7d absolute 後必須重登 | 等 MC 開 refresh,Phase 1 接 |
自動化進度
| 階段 | 自動化狀態 | 對應測試 |
|---|---|---|
| 階段 1(基礎服務) | ✅ docker-compose healthcheck 自動 | – |
| 階段 2(OAuth client 設定) | ❌ 手動(MC bug 阻擋) | Phase 1 待 MC 修 bug |
| 階段 3-7(Login flow / API / Pairing / Logout) | ✅ 用 fake OIDC 自動測(OT1) | internal/api/handlers/oidc_e2e_test.go(17 packages 全綠 / 6 個 OIDC e2e cases) |
| 階段 3-7(with 真 MC) | ❌ 手動(依賴階段 2 手動產 client) | Phase 1 TODO |
Phase 1 TODO 清單:
- MC team 修 password grant sub claim bug → unblock seed script
- MC team 補 admin API 接受 / 回傳 client_secret → unblock OAuth client 自動化
- MC team 新增
usage=web_app→ 移除雛形 workaround - visionA OD2:寫
make dev-seed把 §2 全部 script 化 - visionA OT2 真 MC e2e:把 OT1 fake OIDC 改 testcontainers 起真 MC,跑 §3-§7 全自動
- visionA OB?:補 RP-initiated logout
- visionA OB?:補 refresh token(等 MC 上 refresh)
- visionA OB?:in-memory session → Redis(Phase 1 上 staging 必做)
通過條件(Phase 0.6 OIDC 接入驗收)
- 階段 1-7 全綠(每個 checkbox 都打勾)
- 階段 5 確認 pairing user_id 是 OIDC sub 而非
demo-user(最關鍵) - 已知 limitation 都有 Phase 1 TODO 對應 issue / 任務
全部達成 → Phase 0.6 OIDC 接入完成 ✅,可進 Phase 1 規劃。