# go-zero 生成風格 GO_ZERO_STYLE := go_zero GO ?= go GOFMT ?= gofmt GOFILES := $(shell find . -name '*.go' -not -path './generate/doc-generate/*') GO_DOC_DIR := generate/doc-generate GO_DOC_BIN := $(GO_DOC_DIR)/bin/go-doc API_ENTRY := ./generate/api/gateway.api DOC_OUT := ./docs/openapi GOCTL ?= goctl GOCTL_PKG := github.com/zeromicro/go-zero/tools/goctl@latest GOLANGCI_PKG := github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 .DEFAULT_GOAL := help help: ## 顯示可用指令 @echo "Gateway Makefile" @echo "" @echo "首次開發:" @echo " make tools 安裝 goctl、goimports、golangci-lint(寫入 \$$GOPATH/bin)" @echo " go mod download" @echo "" @echo "常用:" @grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-14s %s\n", $$1, $$2}' tools: ## 安裝 goctl、goimports、golangci-lint(需 Go,且 GOPATH/bin 在 PATH) @command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install $(GOCTL_PKG)) @command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest) @if ! command -v golangci-lint >/dev/null 2>&1 || ! golangci-lint version 2>/dev/null | grep -q 'version 2\.'; then \ echo ">> installing golangci-lint v2"; \ $(GO) install $(GOLANGCI_PKG); \ fi @echo "tools OK" @echo " goctl: $$(goctl --version 2>/dev/null || echo missing)" @echo " golangci-lint: $$(golangci-lint version 2>/dev/null | head -1 || echo missing)" gen-api: tools ## 由 .api 生成 handler / logic / types(自訂 handler 模板) $(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl gen-mock: ## 依 go:generate 產生 internal/model/*/mock(gomock) $(GO) generate ./internal/model/... build-go-doc: ## 編譯 go-doc(OpenAPI 文件生成器) @echo ">> building $(GO_DOC_BIN)" @mkdir -p $(GO_DOC_DIR)/bin @cd $(GO_DOC_DIR) && $(GO) build -o bin/go-doc ./cmd/go-doc gen-doc: build-go-doc ## 從 .api 生成 OpenAPI 3.0 YAML @mkdir -p $(DOC_OUT) $(GO_DOC_BIN) -a $(API_ENTRY) -d $(DOC_OUT) -f gateway -s openapi3.0 -y @echo "Generated: $(DOC_OUT)/gateway.yaml" test: ## 執行測試 $(GO) test ./... fmt: ## gofmt + goimports(不含 lint) $(GOFMT) -s -w $(GOFILES) @command -v goimports >/dev/null 2>&1 && goimports -w . || (echo "goimports not found; run: make tools" && exit 1) lint: tools ## golangci-lint 靜態檢查 golangci-lint run ./... lint-fix: tools ## 自動修正可修的 lint / formatter 問題(見 .golangci.yml) golangci-lint run --fix ./... fix: fmt lint-fix lint ## 格式化 + 自動修 lint + 再檢查(提交前建議) check: fix test ## 提交 / PR 前完整檢查(fmt、lint、test) # ============================================================ # Docker compose(本機依賴) # ============================================================ DOCKER_COMPOSE ?= docker compose COMPOSE_FILE := deploy/docker-compose.yml COMPOSE := $(DOCKER_COMPOSE) -f $(COMPOSE_FILE) deps-up: ## 起 Mongo + Redis(最小本機依賴) $(COMPOSE) up -d mongo redis deps-up-smtp: ## 起 Mongo + Redis + MailHog $(COMPOSE) --profile smtp up -d mongo redis mailhog deps-down: ## 停服務(保留 volume) $(COMPOSE) --profile smtp --profile k6 --profile ldap down deps-down-v: ## 停服務並刪 volume(會清資料) $(COMPOSE) --profile smtp --profile k6 --profile ldap down -v deps-logs: ## 看 compose log $(COMPOSE) --profile smtp --profile k6 --profile ldap logs -f # OpenLDAP(本機 LDAP 測試,見 deploy/openldap/) LDAP_COMPOSE := $(DOCKER_COMPOSE) -f deploy/openldap/docker-compose.yml ldap-up: ## 只起 OpenLDAP(profile ldap;ZITADEL 用 ldap://openldap:389) $(COMPOSE) --profile ldap up -d openldap @echo "→ run 'make ldap-wait' to seed alice/bob" @echo "→ 測試帳號見 deploy/openldap/README.md" ldap-down: ## 停 OpenLDAP $(COMPOSE) --profile ldap stop openldap ldap-wait: ## 等 OpenLDAP ready 並 seed alice/bob @$(MAKE) -s k6-wait-ldap # ============================================================ # k6 測試(test/k6/) # ============================================================ K6 ?= k6 K6_GATEWAY_CONFIG := etc/gateway.k6.yaml K6_GATEWAY_BIN := bin/gateway-k6 K6_DIR := test/k6 K6_PAT_FILE := deploy/zitadel/machinekey/zitadel-admin-sa.token K6_ENV_FILE := deploy/zitadel/machinekey/k6.env K6_BOOTSTRAP_ENV := deploy/zitadel/machinekey/dev-bootstrap.env ZITADEL_HEALTH_URL := http://localhost:8080/debug/healthz # k6 安裝指引(macOS / Linux) define K6_INSTALL_HINT k6 not found in PATH. Install: macOS (Homebrew): brew install k6 Linux (apt): sudo gpg -k && sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 \ && echo 'deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main' | sudo tee /etc/apt/sources.list.d/k6.list \ && sudo apt update && sudo apt install k6 Other: https://grafana.com/docs/k6/latest/set-up/install-k6/ Or use docker (no install): K6='docker run --rm -i --network host -v $$PWD:/app -w /app grafana/k6:latest' make k6-smoke endef export K6_INSTALL_HINT k6-check: ## 檢查 k6 是否安裝(沒裝會印 install 指引) @command -v $(K6) >/dev/null 2>&1 || (echo "$$K6_INSTALL_HINT"; exit 1) @echo "k6: $$($(K6) version 2>&1 | head -1)" k6-up: ## 起 k6 全棧(mongo + redis + mailhog + postgres + openldap + zitadel) $(COMPOSE) --profile k6 up -d mongo redis mailhog postgres openldap zitadel @echo "OpenLDAP + ZITADEL bootstrapping (ZITADEL 首次約 30–90s)…" @echo "→ run 'make k6-wait' to block until ready" @echo "→ LDAP 測試帳號見 deploy/openldap/README.md(alice / Password1!)" k6-wait-ldap: ## 等 OpenLDAP ready 並 seed alice/bob @echo "waiting for OpenLDAP (gateway-openldap)…" @for i in $$(seq 1 90); do \ if docker exec gateway-openldap ldapsearch -x -H ldap://localhost \ -b "dc=gateway,dc=local" \ -D "cn=admin,dc=gateway,dc=local" -w admin -LLL -s base "(objectClass=*)" dn 2>/dev/null | grep -q 'dc=gateway'; then \ echo "openldap ready ($$i s)"; \ $(MAKE) -s ldap-seed; exit 0; \ fi; \ sleep 1; \ done; \ echo "openldap did not become ready in 90s — check: docker logs gateway-openldap"; exit 1 ldap-seed: ## 將 deploy/openldap/bootstrap 測試帳號寫入目錄(可重複執行) @docker cp deploy/openldap/bootstrap/10-people.ldif gateway-openldap:/tmp/10-people.ldif @docker exec gateway-openldap ldapadd -x -H ldap://localhost \ -D "cn=admin,dc=gateway,dc=local" -w admin -c -f /tmp/10-people.ldif 2>&1 \ | grep -Ev '^(ldap_add: Already exists|adding new entry)' || true @echo "ldap seed done (alice / bob @ ou=people,dc=gateway,dc=local)" ldap-test: ## 列出 LDAP 測試使用者 alice / bob @docker exec gateway-openldap ldapsearch -x -H ldap://localhost \ -b "ou=people,dc=gateway,dc=local" \ -D "cn=admin,dc=gateway,dc=local" -w admin \ "(|(uid=alice)(uid=bob))" uid mail cn k6-bootstrap-zitadel: ## 自動建立 ZITADEL LDAP IdP + OIDC App(dev / k6) @if [ ! -s "$(K6_PAT_FILE)" ]; then echo "PAT missing — run 'make k6-wait' first"; exit 1; fi @python3 deploy/zitadel/bootstrap_dev.py @echo "→ OAuth / LDAP IdP 寫入 $(K6_BOOTSTRAP_ENV)" k6-wait: k6-wait-ldap ## 等 OpenLDAP + ZITADEL ready + bootstrap IdP/OAuth + 寫 k6.env @echo "waiting for ZITADEL at $(ZITADEL_HEALTH_URL)…" @for i in $$(seq 1 120); do \ if curl -fsS $(ZITADEL_HEALTH_URL) >/dev/null 2>&1; then \ echo "zitadel ready ($$i s)"; break; \ fi; \ sleep 1; \ if [ $$i -eq 120 ]; then echo "zitadel did not become ready in 120s"; exit 1; fi; \ done @for i in $$(seq 1 30); do \ if [ -s "$(K6_PAT_FILE)" ]; then break; fi; \ sleep 1; \ done @if [ ! -s "$(K6_PAT_FILE)" ]; then \ echo "PAT file $(K6_PAT_FILE) missing — check 'docker logs gateway-zitadel'"; \ exit 1; \ fi @$(MAKE) -s k6-bootstrap-zitadel @PAT=$$(tr -d '\n' < $(K6_PAT_FILE)); \ printf 'export ZITADEL_SERVICE_TOKEN=%s\nexport BASE_URL=http://localhost:8888\nexport MAILHOG_URL=http://localhost:8025\nexport REDIS_ADDR=localhost:6379\n' "$$PAT" > $(K6_ENV_FILE); \ if [ -s "$(K6_BOOTSTRAP_ENV)" ]; then cat $(K6_BOOTSTRAP_ENV) >> $(K6_ENV_FILE); fi; \ echo "wrote $(K6_ENV_FILE)" @$(MAKE) -s k6-seed-fixtures @echo "tip: 'source $(K6_ENV_FILE)' to load into your shell" @echo "tip: run 'make dev-restart-gateway' if gateway was already running" # k6-seed-fixtures idempotently upserts the k6-tenant + K6INVITE invite code # into gateway_k6 so /auth/register resolves successfully. The invite hash is # sha256("K6INVITE") — see internal/model/auth/domain/const.go HashInviteCode. # Uses mongosh inside the container; no extra Go binary needed. K6_TENANT_ID := k6-tenant K6_INVITE_HASH := e62a291f0dcf88c50c91fdc78e3539c55e1e20ef645c640b1ec778fe4fabb4cb k6-seed-fixtures: ## upsert k6-tenant + K6INVITE 到 mongo @docker exec gateway-mongo mongosh --quiet --eval '\ db = db.getSiblingDB("gateway_k6"); \ var now = NumberLong(Date.now()); \ var t = db.tenants.updateOne( \ {tenant_id: "$(K6_TENANT_ID)"}, \ {$$set: {tenant_id: "$(K6_TENANT_ID)", slug: "$(K6_TENANT_ID)", name: "k6 Tenant", uid_prefix: "K6", status: "active", update_at: now}, $$setOnInsert: {create_at: now}}, \ {upsert: true} \ ); \ var i = db.invite_codes.updateOne( \ {tenant_id: "$(K6_TENANT_ID)", code_hash: "$(K6_INVITE_HASH)"}, \ {$$set: {tenant_id: "$(K6_TENANT_ID)", code_hash: "$(K6_INVITE_HASH)", max_uses: NumberLong(1000000), expires_at: NumberLong(0), new_users_only: false, update_at: now}, $$setOnInsert: {used_count: NumberLong(0), create_at: now}}, \ {upsert: true} \ ); \ print("tenant matched=" + t.matchedCount + " upserted=" + (t.upsertedId?1:0) + ", invite matched=" + i.matchedCount + " upserted=" + (i.upsertedId?1:0));' && \ echo "seeded fixtures (tenant=$(K6_TENANT_ID) invite=K6INVITE) into gateway_k6" # Back-compat alias k6-seed-tenant: k6-seed-fixtures ## (alias for k6-seed-fixtures) k6-build: ## 建 gateway binary 給 k6 使用 @mkdir -p $(dir $(K6_GATEWAY_BIN)) $(GO) build -o $(K6_GATEWAY_BIN) ./gateway.go k6-gateway: k6-build ## 前景啟 gateway(吃 etc/gateway.k6.yaml + ZITADEL env) @if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi @set -a; . $(K6_ENV_FILE); set +a; \ $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) k6-smoke: k6-check ## 跑 smoke 測試(每個端點一發) @if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi @set -a; . $(K6_ENV_FILE); set +a; \ for f in $(K6_DIR)/smoke/*.js; do \ echo "==> $$f"; $(K6) run "$$f" || exit 1; \ done k6-journey: k6-check ## 跑 journey 測試(多步驟流程) @if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi @set -a; . $(K6_ENV_FILE); set +a; \ for f in $(K6_DIR)/journeys/*.js; do \ echo "==> $$f"; $(K6) run "$$f" || exit 1; \ done k6-all: k6-smoke k6-journey ## smoke + journey k6-seed-admin: k6-build ## 註冊 k6-admin 並 seed tenant_admin role(rbac journey 用) @if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi @mkdir -p bin $(GO) build -o bin/k6-seed-admin ./cmd/k6-seed-admin @set -a; . $(K6_ENV_FILE); set +a; \ ./bin/k6-seed-admin > $(K6_ENV_FILE).admin || (echo "k6-seed-admin failed"; exit 1); \ cat $(K6_ENV_FILE).admin >> $(K6_ENV_FILE); \ echo "admin credentials appended to $(K6_ENV_FILE):"; \ cat $(K6_ENV_FILE).admin k6-down: ## 停 k6 stack 並清 volume(會清 Postgres / Mongo / Redis 資料) $(COMPOSE) --profile k6 down -v @rm -f $(K6_PAT_FILE) $(K6_ENV_FILE) $(K6_ENV_FILE).admin $(K6_ENV_FILE).tmp deploy/zitadel/machinekey/zitadel-admin-sa.json @echo "k6 stack stopped, volumes & PAT removed" # ============================================================ # 一鍵本機測試環境(Docker + Gateway + 種子資料) # ============================================================ DEV_DIR := .dev DEV_GATEWAY_PID := $(DEV_DIR)/gateway.pid DEV_GATEWAY_LOG := $(DEV_DIR)/gateway.log .PHONY: dev-up dev-down dev-status dev-restart-gateway dev-up: k6-up k6-wait k6-build dev-restart-gateway ## 一鍵起全套:mongo/redis/mailhog/zitadel + seed + Gateway 背景 @echo "" @echo "==========================================" @echo " 本機測試環境已就緒" @echo "==========================================" @echo " Gateway API http://localhost:8888" @echo " 前端(另開終端) make frontend-dev → http://localhost:5173" @echo " MailHog 收信 http://localhost:8025 (註冊 OTP 在這裡看)" @echo " ZITADEL 主控台 http://localhost:8080/ui/console" @echo "" @echo " 註冊預設:租戶 k6-tenant · 邀請碼 K6INVITE" @echo " LDAP 登入:alice / Password1!(make k6-wait 已自動設定 ZITADEL IdP)" @echo "" @echo " 管理後台:make dev-seed-admin 後用輸出的帳密登入" @echo " 關閉環境:make dev-down" @echo " 查看狀態:make dev-status" @echo " OAuth/LDAP 設定變更後:make dev-restart-gateway" @echo "==========================================" dev-seed-admin: k6-seed-admin ## 建立具 tenant_admin 的管理員(dev-up 之後執行) dev-down: ## 停 Gateway 背景行程 + k6 docker stack @if [ -f $(DEV_GATEWAY_PID) ]; then \ pid=$$(cat $(DEV_GATEWAY_PID)); \ if kill -0 $$pid 2>/dev/null; then kill $$pid && echo "stopped gateway (pid $$pid)"; fi; \ rm -f $(DEV_GATEWAY_PID); \ fi @$(MAKE) -s k6-down @rm -rf $(DEV_DIR) @echo "dev environment stopped" dev-status: ## 顯示 docker / gateway / health 狀態 @echo "=== docker (k6 profile) ===" @$(COMPOSE) --profile k6 ps 2>/dev/null || true @echo "" @echo "=== gateway :8888 ===" @if [ -f $(DEV_GATEWAY_PID) ] && kill -0 $$(cat $(DEV_GATEWAY_PID)) 2>/dev/null; then \ echo "running pid $$(cat $(DEV_GATEWAY_PID))"; \ curl -fsS http://localhost:8888/api/v1/health 2>/dev/null | head -c 200 || echo "(health check failed)"; \ echo ""; \ else \ echo "not running (run: make dev-up)"; \ fi dev-restart-gateway: k6-build ## 重啟 Gateway(載入 k6.env 的 OAuth / LDAP 設定) @if [ ! -s "$(K6_ENV_FILE)" ]; then echo "run 'make k6-wait' first"; exit 1; fi @if [ -f $(DEV_GATEWAY_PID) ]; then \ pid=$$(cat $(DEV_GATEWAY_PID)); \ kill -0 $$pid 2>/dev/null && kill $$pid || true; \ rm -f $(DEV_GATEWAY_PID); \ fi @if command -v lsof >/dev/null 2>&1; then \ orphan=$$(lsof -ti :8888 2>/dev/null || true); \ if [ -n "$$orphan" ]; then kill $$orphan 2>/dev/null || true; sleep 1; fi; \ fi @mkdir -p $(DEV_DIR) @set -a; . $(K6_ENV_FILE); set +a; \ nohup $(K6_GATEWAY_BIN) -f $(K6_GATEWAY_CONFIG) > $(DEV_GATEWAY_LOG) 2>&1 & \ echo $$! > $(DEV_GATEWAY_PID); \ echo "gateway restarted (pid $$(cat $(DEV_GATEWAY_PID)))" @for i in $$(seq 1 30); do \ if curl -fsS http://localhost:8888/api/v1/health >/dev/null 2>&1; then \ echo "gateway ready ($$i s)"; exit 0; \ fi; \ sleep 1; \ done; \ echo "gateway did not become ready — tail $(DEV_GATEWAY_LOG)"; tail -20 $(DEV_GATEWAY_LOG); exit 1 # ============================================================ # Frontend(使用者前台 + 管理後台) # ============================================================ frontend-install: ## 安裝 frontend 依賴 cd frontend && npm install frontend-dev: frontend-install ## 啟動前端 dev server(:5173,proxy /api → :8888) cd frontend && npm run dev frontend-build: frontend-install ## 建置前端靜態檔 → frontend/dist/ cd frontend && npm run build