visionA/docs/SMOKE-TEST.md
jim800121chen eb66a7287a feat(deploy): visionA Cloud dev / stage docker compose + Caddy/nginx + 部署腳本
新增雲端版部署設定(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>
2026-05-01 11:22:44 +08:00

330 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 目前有兩個 bugpassword 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` → 回 200visiona-api 健康)
- [ ] **1.5** `curl -sS http://localhost:3800/healthz` → 回 200visiona-proxy 健康)
- [ ] **1.6** 開 http://localhost:3000 → 看到 visionA login 頁(含「使用您的 Innovedus 帳號登入」按鈕)
- [ ] **1.7** 開 http://localhost:5060 → 看到 Member Center admin 後台登入頁
任一步驟失敗 → §故障排除 #1
---
## 階段 2Member 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`
- [ ] **2.2.4** 送出 → 列表出現 `visionA` tenant
### 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`
- [ ] **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`
- [ ] **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 Flow5 分鐘)
驗證 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=code`
- `client_id=<你的 guid>`
- `scope=openid email profile`
- `state=<random>`
- `code_challenge=<...>` + `code_challenge_method=S256`
- `nonce=<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:3000dashboard / workspace 首頁)
- [ ] **3.12** DevTools → Cookies → http://localhost:3721 → `visiona_session` 出現HttpOnly`visiona_pending_sid` 已被清除
- [ ] **3.13** Header 右上角顯示 demo user 的 email 或 namevisionA-frontend `/account` 應拿得到 user info
任一步驟失敗 → §故障排除 #3。
---
## 階段 4API 帶 Cookie 驗證3 分鐘)
驗證 BFF Patternfrontend 只用 cookie所有 API 自動帶 session。
- [ ] **4.1** DevTools → Network → 重整任意需登入頁面(如 `/devices`
- [ ] **4.2** 找到 `GET /api/auth/me` request
- Request Headers 帶 `Cookie: visiona_session=...`
- Response 200body 含 `user_id`OIDC sub**不應是 `demo-user`**)、`email`、`name`、`expires_at`
- [ ] **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 值):
```
curl -sS -b "visiona_session=<從 DevTools 複製>" http://localhost:3721/api/auth/me
```
→ 200 + JSON
- [ ] **4.7** curl 不帶 cookie
```
curl -sSi http://localhost:3721/api/auth/me
```
→ 401middleware 擋下)
任一步驟失敗 → §故障排除 #4。
---
## 階段 5Pairing Token 綁 OIDC user5 分鐘)
> 驗證關鍵承諾:**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/token` request
- Request 帶 `Cookie: visiona_session=...`
- Response 200 + JSON
- [ ] **5.5** 看 backend log 確認 user binding
```
docker compose -f docker-compose.dev.yml logs visiona-api --tail 50 | grep -i pairing
```
- 應看到「PairingStore Create userID=<OIDC sub>」之類 loguser_id 是 GUID 或 MC 給的 sub**不是** `demo-user`
- [ ] **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 約 6048007 天)
---
## 故障排除
### #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 limitationadmin 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 scriptOT2 可改全自動 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 時應該帶 cookieDevTools → 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 是否真的補上
- backend log `unauthorized` 帶的 detail
- `cookie_invalid` → cookie 簽章對不上 → 通常 `VISIONA_SESSION_SECRET` 變過導致舊 cookie 失效 → 清 cookie 重登
- `session_not_found` → in-memory store 被重啟清掉 → 重登
- `session_expired` → 真的過期了,重登
### #5 階段 5Pairing 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 是 GUIDOIDC sub→ ✅ 正確
- 必要時開 issue「OB5 + OF2 完成後 pairing 仍綁到 demo-user需 hotfix」
### #6 階段 6登出後仍能訪問
- DevTools 確認 `visiona_session` cookie 真的被清掉
- 如果 cookie 還在 → backend `Set-Cookie: visiona_session=; Max-Age=0` 沒設對
- 訪 `/api/auth/me` 不帶 cookie 仍 200 → middleware bypass 了某些路徑 → 看 router 設定
- frontend 沒 redirect 到 `/login` → middlewareNext.js沒檢查 session → 看 OF2/OF3 frontend route guard
---
## 已知 LimitationPhase 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-locallocal-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 開 refreshPhase 1 接 |
---
## 自動化進度
| 階段 | 自動化狀態 | 對應測試 |
|------|----------|---------|
| 階段 1基礎服務| ✅ docker-compose healthcheck 自動 | |
| 階段 2OAuth client 設定)| ❌ **手動**MC bug 阻擋)| Phase 1 待 MC 修 bug |
| 階段 3-7Login flow / API / Pairing / Logout| ✅ 用 fake OIDC 自動測OT1| `internal/api/handlers/oidc_e2e_test.go`17 packages 全綠 / 6 個 OIDC e2e cases|
| 階段 3-7with 真 MC| ❌ **手動**(依賴階段 2 手動產 client| Phase 1 TODO |
**Phase 1 TODO 清單**
1. MC team 修 password grant sub claim bug → unblock seed script
2. MC team 補 admin API 接受 / 回傳 client_secret → unblock OAuth client 自動化
3. MC team 新增 `usage=web_app` → 移除雛形 workaround
4. visionA OD2寫 `make dev-seed` 把 §2 全部 script 化
5. visionA OT2 真 MC e2e把 OT1 fake OIDC 改 testcontainers 起真 MC跑 §3-§7 全自動
6. visionA OB?:補 RP-initiated logout
7. visionA OB?:補 refresh token等 MC 上 refresh
8. visionA OB?in-memory session → RedisPhase 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 規劃。