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>
311 lines
14 KiB
Plaintext
311 lines
14 KiB
Plaintext
# visionA — stage 環境 nginx 設定
|
||
#
|
||
# 角色定位:
|
||
# 公司 host nginx ── HTTPS :9527(LE 證書) ──→ 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 HTTPS(stage-9527.innovedus.com:9527),
|
||
# 但 reverse proxy 把原始 Host header 透傳進來。為避免 backend 拿到偽造 Host
|
||
# 來組 absolute URL(return_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 healthcheck(Host: localhost、來源 127.0.0.1)
|
||
# 的 /healthz 也被打成 444 → container 長期 unhealthy(false 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;
|
||
|
||
# 安全 headers(nginx 已有公司 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;
|
||
# 注意:不在這層加 HSTS(HTTPS 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):健康探針不該 hang;backend 自身 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 已 termination;container 收到的是 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-lived(24h 心跳級別);拉到 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;
|
||
}
|
||
}
|