visionA/docker/nginx.stage.conf
jim800121chen cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。

OIDC / pairing FK 修復(接 DB 上線必要):
- 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback
  驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID)
- pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token;
  自建 device 空 serial 寫 NULL(避免撞 partial unique)
- device.SaveTx / session.CreateTx 新增 tx-aware 版本

B4 model metadata:
- 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model:
  converter_client → flow → adapter → model.Model → PG → ModelResponse DTO
- input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性
- 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md

nginx healthz(部署層):
- 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB
- 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因)

storage error 統一映射(不洩漏 storage 後端細節)。

測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、
internal/db(redis)、relay/session 弱處,含 testcontainers integration。
DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:36:35 +08:00

311 lines
14 KiB
Plaintext
Raw 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 — stage 環境 nginx 設定
#
# 角色定位:
# 公司 host nginx ── HTTPS :9527LE 證書) ──→ container :80本檔
# ↓
# ┌───────────────────────────────────────────────────────────────┐
# │ container 內 nginx :80 │
# │ /healthz → 直接回 200給 docker healthcheck
# │ /api/* → 127.0.0.1:3721 (api-server) │
# │ /storage/* → 127.0.0.1:3721 (api-server presigned URL) │
# │ /tunnel/connect→ 127.0.0.1:3800 (remote-proxy WS upgrade) │
# │ /_next/static/ → 127.0.0.1:3000 (next standalone, 1y cache) │
# │ / → 127.0.0.1:3000 (next standalone server.js) │
# └───────────────────────────────────────────────────────────────┘
#
# 上游 process 由 entrypoint.stage.sh 啟動,全在同一 container loopback。
# ────────── upstream 定義 ──────────
# 在 server block 外層 alias讓 proxy_pass 可重用。
upstream visiona_api {
server 127.0.0.1:3721;
keepalive 32;
}
upstream visiona_tunnel {
server 127.0.0.1:3800;
keepalive 16;
}
upstream visiona_frontend {
server 127.0.0.1:3000;
keepalive 32;
}
# WebSocket upgrade map給 /tunnel/connect 用)
# 在 http context 用server block 內也可繼承。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# ────────── Host header 白名單M2 — trust boundary ──────────
#
# 公司 host nginx 已 termination HTTPSstage-9527.innovedus.com:9527
# 但 reverse proxy 把原始 Host header 透傳進來。為避免 backend 拿到偽造 Host
# 來組 absolute URLreturn_to / email link / cache key這層強制白名單
#
# - 任何不符合 server_name 的 host含直接打 IP、攻擊者偽造 Host→ 444 close
# - 命中 stage-9527.innovedus.com 的請求才進真正的 server block
#
# 444 = nginx 專屬 status直接關連線、不回 response不給攻擊者反饋。
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# /healthz 例外Docker healthcheck 從 container 內打 localhost/healthz
# Host: localhost 不命中 stage-9527 白名單,但內部源頭可信任)
# 限制 source = 127.0.0.0/8 防止外部偽造 Host 跳過白名單
#
# ⚠️ 為什麼這層不能用 server-level `return 444`(修正前的 bug
# server context 的 `return` 在 nginx rewrite phase 執行會「先於」location
# 匹配短路掉所有請求 —— 包含這個 `location = /healthz`。修正前 default_server
# 結尾寫 `return 444;`,導致 docker healthcheckHost: localhost、來源 127.0.0.1
# 的 /healthz 也被打成 444 → container 長期 unhealthyfalse 444
# 正解:把 catch-all 444 收進 `location /`,讓 exact-match `location = /healthz`
# 依 nginx location 優先序勝出。
location = /healthz {
allow 127.0.0.0/8;
allow ::1/128;
deny all;
access_log off;
# 直接內部回 200不轉到 api-server淺層只證明 nginx 活著)
return 200 "ok\n";
add_header Content-Type "text/plain" always;
}
# 其他任何 host header 不符合白名單 → 444 close不回 response 給攻擊者反饋)
# 用 location / 包起來(而非 server-level return才不會 shadow 掉上面的 /healthz。
location / {
return 444;
}
}
server {
listen 80;
listen [::]:80;
server_name stage-9527.innovedus.com;
# ============================================================
# 全域行為
# ============================================================
# 模型上傳上限PRD §8.4 — Phase 0 為 100 MB
# /api/models/* 走 multipart/form-data這個值是上限。
client_max_body_size 100M;
# 大量 long-lived 連線tunnel WS、SSE、模型轉檔輪詢— 拉長 read timeout
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
# gzip — 對 JSON / JS / CSS 有效,避免重複壓縮二進位資源
gzip on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
image/svg+xml;
# 安全 headersnginx 已有公司 host 那層 HTTPS這層補通用安全
# X-Frame-Options 預設 SAMEORIGIN 給 iframe 防護agent / pair view 不嵌 iframe
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 注意:不在這層加 HSTSHTTPS termination 在公司 host nginx由那層加
# ============================================================
# 健康檢查(淺層)— 不打到 backend
# 用途「nginx 程序活著」的最廉價證明。docker healthcheck走 default_server
# 那條)與「只想確認反代層在線」的外部探針用這條。
# ⚠️ 注意:這條「不」反映 DB / Redis 健康 —— load balancer 若要在 DB 掛掉時
# 把本實例踢出輪替,必須打下面的 /healthz/deep不能打這條。
# ============================================================
location = /healthz {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
# ============================================================
# 健康檢查(深層)— proxy 到 backend /healthz會 ping Postgres + Redis
# 用途load balancer / 監控的 readiness 探針。
# - PG + Redis 都健康 → 200 {"status":"ok","checks":{"postgres":"ok","redis":"ok"}}
# - 任一依賴 ping 失敗 → 503 {"status":"unavailable","checks":{...:"down"}}
# 讓上游 LB 在 DB 掛掉時把本實例拉出輪替,而非繼續送流量進來碰 503 / 假資料。
#
# 設計取捨:
# - proxy_pass 改寫 path → 後端命中的是 /healthz後端只實作這一個健康端點
# - timeout 全部壓短2s健康探針不該 hangbackend 自身 ping 逾時也是 2s
# 這層再加一道 nginx 短逾時,避免單一探針卡住 worker。
# - access_log off高頻探針不洗版 access log與淺層一致
# - 不繼承 server-level 的 proxy_read_timeout 3600s那是給長連線用的
# ============================================================
location = /healthz/deep {
access_log off;
proxy_pass http://visiona_api/healthz;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
# 健康探針要快、不該 hang —— 壓短所有 timeout覆蓋 server-level 3600s
proxy_connect_timeout 2s;
proxy_read_timeout 2s;
proxy_send_timeout 2s;
# 探針結果不可被任何中間層 cache
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "no-store" always;
}
# ============================================================
# /api/* → api-server :3721
# 包含 /api/auth/*OIDC callback、/api/devices、/api/models、/api/pairing 等
# ============================================================
location /api/ {
proxy_pass http://visiona_api;
proxy_http_version 1.1;
proxy_buffering off; # 模型上傳串流 / SSE friendly
# ── Proxy headers ──
# X-Forwarded-Proto = https讓 backend 產的 redirect / cookie Secure 判斷正確
# (公司 host nginx 已 terminationcontainer 收到的是 HTTP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Connection "";
# ── Cache 防護M3 ──
# /api/auth/me 等含 PII/api/auth/callback 帶 code/state
# 一律禁止任何中間 proxy / browser cache含 BFCache
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "no-store, no-cache, must-revalidate, private" always;
add_header Pragma "no-cache" always;
# nginx add_header 在 location level 會完全覆蓋 server level不 merge
# 因此 server level 的安全 header 必須在這裡 re-add
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ============================================================
# /storage/* → api-server :3721
# presigned URL 走這條HMAC 簽章 query string、不帶 cookie
# 雛形 LocalFS backend 的下載端點
# ============================================================
location /storage/ {
proxy_pass http://visiona_api;
proxy_http_version 1.1;
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";
# ── Cache 防護M3 — 保守作法) ──
# presigned URL 含短期 HMAC、理論上不可重複使用但內容是用戶上傳模型敏感
# 一律禁止中間 proxy 共用 cache避免 query string 漏進 cache key 時被旁人取得。
# 若未來改 S3 backend + presigned 直連,這條 location 會被拆掉,屆時改 backend 自行決定 cache 策略。
proxy_no_cache 1;
proxy_cache_bypass 1;
add_header Cache-Control "private, no-store" always;
# nginx location level 的 add_header 會完全覆蓋 server level — re-add 安全 header
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
}
# ============================================================
# /tunnel/connect — WebSocket upgrade由 visionA Agent 連入
# 要 long-lived24h 心跳級別);拉到 86400s。
# 注意path 必須是 /tunnel/connect不是 /tunnel/)— remote-proxy 只開這個 endpoint
# ============================================================
location /tunnel/connect {
proxy_pass http://visiona_tunnel;
proxy_http_version 1.1;
# WebSocket upgrade headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# tunnel 連線可長達數小時,且心跳由 yamux 處理nginx 不要中途斷
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
# 不 buffer避免延遲 WS 訊框
proxy_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
# ============================================================
# Next.js hashed static assets — 永久 cache
# /_next/static/{hash}.js 等
# ============================================================
location /_next/static/ {
proxy_pass http://visiona_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
# Hash 帶在路徑裡,內容變了路徑就變 → 可以 immutable 1 年
expires 1y;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}
# ============================================================
# 其他靜態資源public/ 下的圖片、字型等)— 1 day cache
# ============================================================
location ~* ^/(favicon\.ico|.*\.(?:png|jpg|jpeg|gif|svg|webp|ico|woff|woff2|ttf|eot))$ {
proxy_pass http://visiona_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
expires 1d;
add_header Cache-Control "public, max-age=86400" always;
}
# ============================================================
# 全部其他請求 → Next.js standalone server (:3000)
# 包含:
# - / (首頁)
# - /login, /register, /account, /clusters, /devices, /devices/[id],
# /devices/pair, /models, /models/[id], /workspace/[deviceId], /settings
# - /_next/data/* (RSC payload)
# - /_next/image (Next image optimizer雖然 standalone 預設啟用 sharp)
# 不在這裡做 SPA fallback — Next.js server 自己會處理 404 與動態 route
# ============================================================
location / {
proxy_pass http://visiona_frontend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Connection "";
# SSE / streaming 友善
proxy_buffering off;
proxy_cache off;
}
}