新增雲端版部署設定(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>
119 lines
5.4 KiB
Bash
Executable File
119 lines
5.4 KiB
Bash
Executable File
#!/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: $1(brew install $1)"; exit 1; }; }
|
||
require curl
|
||
require jq
|
||
|
||
# ── 1. 拿 admin token(MC 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 bug:Identity 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}"
|