visionA/docker/register-oauth-client.sh
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

119 lines
5.4 KiB
Bash
Executable File
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.

#!/usr/bin/env bash
# register-oauth-client.sh
#
# ⚠️ 目前狀態MC 的 password grant 在 Identity user 上拿不到 sub claim 而 500已知 MC bug
# 所以這支 script 暫時跑不通。註冊 OAuth client 請走 MC Web UI見 docs/DEV-SETUP.md
#
# 保留此檔案是為了:
# 1. 等 MC 修好 password flow 後可立刻啟用
# 2. 留下「應該長什麼樣」的程式碼參考
#
# 用法MC 修好後):
# bash docker/register-oauth-client.sh
#
# 需要環境變數(會從 .env.dev / .env 讀,否則用預設):
# MC_BASE_URL 預設 http://localhost:5050
# MC_ADMIN_EMAIL 預設 admin@visiona.local
# MC_ADMIN_PASSWORD 預設 Admin12345!
# VISIONA_REDIRECT_URI 預設 http://localhost:3721/api/auth/callback
# OUTPUT_FILE 預設 .env.dev.generated在 repo 根)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ -f "${REPO_ROOT}/.env.dev" ]]; then
# shellcheck disable=SC1091
set -a; source "${REPO_ROOT}/.env.dev"; set +a
fi
MC_BASE_URL="${MC_BASE_URL:-http://localhost:5050}"
MC_ADMIN_EMAIL="${MC_ADMIN_EMAIL:-admin@visiona.local}"
MC_ADMIN_PASSWORD="${MC_ADMIN_PASSWORD:-Admin12345!}"
VISIONA_REDIRECT_URI="${VISIONA_OIDC_REDIRECT_URL:-http://localhost:3721/api/auth/callback}"
OUTPUT_FILE="${OUTPUT_FILE:-${REPO_ROOT}/.env.dev.generated}"
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
log() { echo -e "${CYAN}[register]${NC} $*"; }
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
err() { echo -e "${RED}[error]${NC} $*" >&2; }
require() { command -v "$1" >/dev/null 2>&1 || { err "missing tool: $1brew install $1"; exit 1; }; }
require curl
require jq
# ── 1. 拿 admin tokenMC password flow目前有 bug────────
log "fetching admin token from ${MC_BASE_URL}/oauth/token..."
TOKEN_RES=$(curl -sS -X POST "${MC_BASE_URL}/oauth/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password&username=${MC_ADMIN_EMAIL}&password=${MC_ADMIN_PASSWORD}&scope=openid email profile") || {
err "MC unreachable at ${MC_BASE_URL}. Is docker compose up?"; exit 1;
}
if ! echo "${TOKEN_RES}" | head -c 1 | grep -q '{'; then
err "MC password grant failed (回應非 JSON可能 500 error)。"
err "已知 MC bugIdentity user principal 缺 sub claim → OpenIddict reject。"
err "請改走 MC Web admin UI 註冊 OAuth client詳見 docs/DEV-SETUP.md。"
exit 1
fi
ACCESS_TOKEN=$(echo "${TOKEN_RES}" | jq -r '.access_token // empty')
if [[ -z "${ACCESS_TOKEN}" || "${ACCESS_TOKEN}" == "null" ]]; then
err "failed to get admin token. response:"; echo "${TOKEN_RES}" | jq . >&2 || echo "${TOKEN_RES}" >&2
exit 1
fi
ok "got admin token (length=${#ACCESS_TOKEN})"
# ── 2. 建 tenant ───────────────────────────────────────────
log "ensuring visionA tenant..."
EXISTING_TENANT_ID=$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${MC_BASE_URL}/admin/tenants" | jq -r '.[] | select(.name == "visionA") | .id // empty' | head -1)
if [[ -n "${EXISTING_TENANT_ID}" ]]; then
TENANT_ID="${EXISTING_TENANT_ID}"
ok "tenant 'visionA' already exists: ${TENANT_ID}"
else
TENANT_RES=$(curl -sS -X POST "${MC_BASE_URL}/admin/tenants" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"name":"visionA","domains":["visiona.cloud","localhost"],"status":"active"}')
TENANT_ID=$(echo "${TENANT_RES}" | jq -r '.id // empty')
[[ -z "${TENANT_ID}" ]] && { err "tenant create failed:"; echo "${TENANT_RES}" >&2; exit 1; }
ok "created tenant 'visionA': ${TENANT_ID}"
fi
# ── 3. 建 OAuth client ─────────────────────────────────────
log "ensuring OAuth client..."
EXISTING_CLIENT_ID=$(curl -sS -H "Authorization: Bearer ${ACCESS_TOKEN}" \
"${MC_BASE_URL}/admin/oauth-clients" | jq -r '.[] | select(.name == "visionA Cloud") | .client_id // empty' | head -1)
if [[ -n "${EXISTING_CLIENT_ID}" ]]; then
CLIENT_ID="${EXISTING_CLIENT_ID}"
ok "OAuth client 'visionA Cloud' already exists: ${CLIENT_ID}"
else
CLIENT_RES=$(curl -sS -X POST "${MC_BASE_URL}/admin/oauth-clients" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tenant_id\":\"${TENANT_ID}\",\"name\":\"visionA Cloud\",\"client_type\":\"public\",\"usage\":\"webhook_outbound\",\"redirect_uris\":[\"${VISIONA_REDIRECT_URI}\"]}")
CLIENT_ID=$(echo "${CLIENT_RES}" | jq -r '.clientId // .ClientId // empty')
[[ -z "${CLIENT_ID}" ]] && { err "OAuth client create failed:"; echo "${CLIENT_RES}" >&2; exit 1; }
ok "created OAuth client: ${CLIENT_ID}"
fi
# ── 4. 寫出 env ────────────────────────────────────────────
cat > "${OUTPUT_FILE}" <<ENV
# Generated by docker/register-oauth-client.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
VISIONA_OIDC_CLIENT_ID=${CLIENT_ID}
VISIONA_OIDC_CLIENT_SECRET=
VISIONA_OIDC_ISSUER_URL=http://localhost:5050/
VISIONA_OIDC_REDIRECT_URL=${VISIONA_REDIRECT_URI}
ENV
ok "wrote ${OUTPUT_FILE}"
echo
echo "下一步:把 ${OUTPUT_FILE} 內容合併進 .env.dev → docker compose up -d visiona-api"
echo "Demo 帳號:${MC_ADMIN_EMAIL} / ${MC_ADMIN_PASSWORD}"