Compare commits
No commits in common. "fix/bug" and "main" have entirely different histories.
|
|
@ -1,15 +0,0 @@
|
||||||
# --- secrets / env(不要 commit 實值)---
|
|
||||||
infra/.env
|
|
||||||
infra/etc/haixun.env
|
|
||||||
/opt/haixun/
|
|
||||||
|
|
||||||
# --- build 產物 ---
|
|
||||||
backend/bin/
|
|
||||||
frontend/dist/
|
|
||||||
|
|
||||||
# --- 依賴 ---
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# --- 其他 ---
|
|
||||||
*.log
|
|
||||||
.DS_Store
|
|
||||||
287
Makefile
287
Makefile
|
|
@ -1,229 +1,98 @@
|
||||||
# 巡樓 monorepo Makefile
|
GO ?= go
|
||||||
# 兩種模式:
|
GOFMT ?= gofmt
|
||||||
# dev - 本機開發(docker 起 Mongo/Redis,go run / vite dev)
|
GOCTL ?= goctl
|
||||||
# prod - 建置產物(前端 dist + linux Go binary),並可部署成 systemd 服務 + nginx
|
GO_ZERO_STYLE := go_zero
|
||||||
#
|
API_ENTRY := ./generate/api/gateway.api
|
||||||
# 常用:
|
GOFILES := $(shell find . -name '*.go')
|
||||||
# make dev-infra # 起本機 Mongo/Redis
|
|
||||||
# make dev-backend # 跑 gateway (:8890)
|
|
||||||
# make dev-frontend # 跑前端 (:5173,proxy 到 :8890)
|
|
||||||
# make build # 產出 frontend/dist + backend/bin/{gateway,worker}
|
|
||||||
# sudo make install # 部署到目標主機(見 infra/README.md)
|
|
||||||
|
|
||||||
SHELL := /bin/bash
|
|
||||||
|
|
||||||
# --- 路徑 ---
|
|
||||||
BACKEND_DIR := backend
|
|
||||||
FRONTEND_DIR := frontend
|
|
||||||
INFRA_DIR := infra
|
|
||||||
BIN_DIR := $(BACKEND_DIR)/bin
|
|
||||||
|
|
||||||
# --- 部署目標(install 用)---
|
|
||||||
DEPLOY_ROOT := /opt/haixun
|
|
||||||
WEB_ROOT := /var/www/haixun
|
|
||||||
|
|
||||||
# --- 交叉編譯目標(在 mac 上 build 給 linux 主機)---
|
|
||||||
GOOS ?= linux
|
|
||||||
GOARCH ?= amd64
|
|
||||||
|
|
||||||
# --- docker compose ---
|
|
||||||
COMPOSE := docker compose -f $(INFRA_DIR)/docker-compose.yml --env-file $(INFRA_DIR)/.env
|
|
||||||
|
|
||||||
# --- dev 用 worker secret(對應 etc/gateway.yaml 的 InternalWorker.Secret)---
|
|
||||||
DEV_WORKER_SECRET := haixun-dev-worker-secret
|
|
||||||
DEV_BACKEND_URL := http://127.0.0.1:8890
|
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## 顯示可用指令
|
help: ## 顯示可用指令
|
||||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
@echo "Haixun Backend"
|
||||||
| awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# DEV
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
$(INFRA_DIR)/.env:
|
|
||||||
@test -f $(INFRA_DIR)/.env || (cp $(INFRA_DIR)/.env.example $(INFRA_DIR)/.env && echo "已從 .env.example 建立 $(INFRA_DIR)/.env,請視需要修改密碼")
|
|
||||||
|
|
||||||
.PHONY: dev-infra
|
|
||||||
dev-infra: $(INFRA_DIR)/.env ## [dev] 起本機 Mongo + Redis (docker)
|
|
||||||
$(COMPOSE) up -d
|
|
||||||
$(COMPOSE) ps
|
|
||||||
|
|
||||||
.PHONY: dev-infra-down
|
|
||||||
dev-infra-down: ## [dev] 停掉本機 Mongo + Redis
|
|
||||||
$(COMPOSE) down
|
|
||||||
|
|
||||||
.PHONY: dev-backend
|
|
||||||
dev-backend: ## [dev] 跑 gateway API (:8890)
|
|
||||||
cd $(BACKEND_DIR) && go run . -f etc/gateway.yaml
|
|
||||||
|
|
||||||
.PHONY: dev-worker
|
|
||||||
dev-worker: ## [dev] 跑 Go job worker (:8891)
|
|
||||||
cd $(BACKEND_DIR) && go run ./cmd/worker -f etc/gateway.worker.yaml
|
|
||||||
|
|
||||||
.PHONY: dev-node-worker
|
|
||||||
dev-node-worker: ## [dev] 跑 Node playwright worker (style-8d)
|
|
||||||
cd $(BACKEND_DIR)/worker && npm install && \
|
|
||||||
HAIXUN_WORKER_SECRET=$(DEV_WORKER_SECRET) HAIXUN_BACKEND_URL=$(DEV_BACKEND_URL) npm run style-8d
|
|
||||||
|
|
||||||
.PHONY: dev-frontend
|
|
||||||
dev-frontend: ## [dev] 跑前端 dev server (:5173)
|
|
||||||
cd $(FRONTEND_DIR) && npm install && npm run dev
|
|
||||||
|
|
||||||
.PHONY: dev-init
|
|
||||||
dev-init: ## [dev] 初始化 DB(索引/權限)並建立 admin 帳號(可用 INIT_ADMIN_EMAIL / INIT_ADMIN_PASSWORD 覆寫)
|
|
||||||
cd $(BACKEND_DIR) && go run ./cmd/tool init -f etc/gateway.yaml
|
|
||||||
|
|
||||||
.PHONY: dev
|
|
||||||
dev: ## [dev] 顯示本機開發要開的終端
|
|
||||||
@echo "本機開發請分開幾個終端執行:"
|
|
||||||
@echo " 1) make dev-infra # Mongo/Redis"
|
|
||||||
@echo " 2) make dev-backend # gateway :8890"
|
|
||||||
@echo " 3) make dev-worker # go worker(可選)"
|
|
||||||
@echo " 4) make dev-node-worker # node worker(需 style-8d 時)"
|
|
||||||
@echo " 5) make dev-frontend # 前端 :5173"
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# 安裝依賴(新機器 clone 後執行一次)
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: bootstrap-sys
|
|
||||||
bootstrap-sys: ## [需 sudo] 安裝系統依賴(Go, Node.js, Docker, Nginx)— Ubuntu 專用
|
|
||||||
@echo "=== 安裝系統套件: nginx, curl, gnupg ==="
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq nginx curl gnupg ca-certificates
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 安裝 Go 1.22+ ==="
|
|
||||||
@GO_VER=$$(curl -sL https://go.dev/VERSION?m=text 2>/dev/null | head -1 || echo "go1.22"); \
|
|
||||||
echo "下載 $$GO_VER.linux-amd64.tar.gz"; \
|
|
||||||
curl -sL "https://go.dev/dl/$$GO_VER.linux-amd64.tar.gz" -o /tmp/go.tar.gz && \
|
|
||||||
sudo rm -rf /usr/local/go && \
|
|
||||||
sudo tar -C /usr/local -xzf /tmp/go.tar.gz && \
|
|
||||||
rm /tmp/go.tar.gz; \
|
|
||||||
echo 'export PATH=/usr/local/go/bin:$$PATH' | sudo tee /etc/profile.d/go.sh
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 安裝 Node.js LTS ==="
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
|
||||||
sudo apt-get install -y -qq nodejs
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 安裝 Docker Engine + Compose ==="
|
|
||||||
curl -fsSL https://get.docker.com | sudo sh
|
|
||||||
sudo usermod -aG docker $(USER) 2>/dev/null || true
|
|
||||||
@echo ""
|
|
||||||
@echo "=== 安裝 Nginx ==="
|
|
||||||
sudo apt-get install -y -qq nginx
|
|
||||||
@echo ""
|
|
||||||
@echo "✓ 系統依賴安裝完成。"
|
|
||||||
@echo " 登出再登入後,Go/Node/Docker 即可使用(group 變更生效)。"
|
|
||||||
@echo " 接著執行 make bootstrap 安裝專案層依賴。"
|
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@grep -E '^[a-zA-Z0-9_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*## "}; {printf " make %-12s %s\n", $$1, $$2}'
|
||||||
|
|
||||||
.PHONY: bootstrap
|
tools: ## 安裝 goctl / goimports
|
||||||
bootstrap: ## 安裝專案層依賴(Go modules + npm + Playwright),需先執行 make bootstrap-sys
|
@command -v $(GOCTL) >/dev/null 2>&1 || (echo ">> installing goctl" && $(GO) install github.com/zeromicro/go-zero/tools/goctl@latest)
|
||||||
@echo "=== [1/4] Go modules ==="
|
@command -v goimports >/dev/null 2>&1 || (echo ">> installing goimports" && $(GO) install golang.org/x/tools/cmd/goimports@latest)
|
||||||
cd $(BACKEND_DIR) && go mod tidy
|
|
||||||
@echo ""
|
|
||||||
@echo "=== [2/4] 前端 npm ==="
|
|
||||||
cd $(FRONTEND_DIR) && npm install
|
|
||||||
@echo ""
|
|
||||||
@echo "=== [3/4] Node worker npm ==="
|
|
||||||
cd $(BACKEND_DIR)/worker && npm install
|
|
||||||
@echo ""
|
|
||||||
@echo "=== [4/4] Playwright 瀏覽器(chromium)+ 系統依賴 ==="
|
|
||||||
cd $(BACKEND_DIR)/worker && sudo npx playwright install --with-deps chromium
|
|
||||||
@echo ""
|
|
||||||
@echo "✓ 全裝完成。後續步驟:"
|
|
||||||
@echo " 1) make dev-infra # 起 Mongo/Redis (Docker)"
|
|
||||||
@echo " 2) make dev-init # 初始化 DB + 建立 admin"
|
|
||||||
@echo " 3) make dev-backend # 啟動 gateway (:8890)"
|
|
||||||
@echo " 4) sudo make install # 正式部署(含 systemd + nginx)"
|
|
||||||
|
|
||||||
# ============================================================
|
gen-api: tools ## 由 .api 生成 handler / logic / types
|
||||||
# BUILD (prod 產物)
|
$(GOCTL) api go -api $(API_ENTRY) -dir . -style $(GO_ZERO_STYLE) -home generate/goctl
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: build
|
fmt: ## gofmt + goimports
|
||||||
build: build-frontend build-backend ## [prod] 建置前端 dist 與 Go binary
|
$(GOFMT) -s -w $(GOFILES)
|
||||||
|
@command -v goimports >/dev/null 2>&1 && goimports -w . || true
|
||||||
|
|
||||||
.PHONY: build-frontend
|
test: ## 執行測試
|
||||||
build-frontend: ## [prod] 前端靜態建置 (tsc + vite) -> frontend/dist
|
$(GO) test ./...
|
||||||
cd $(FRONTEND_DIR) && npm ci && npm run build
|
|
||||||
|
|
||||||
.PHONY: build-backend
|
run: ## 啟動 API(前景)
|
||||||
build-backend: ## [prod] 交叉編譯 gateway + worker -> backend/bin (linux)
|
$(GO) run ./gateway.go -f etc/gateway.yaml
|
||||||
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
|
||||||
go build -trimpath -ldflags "-s -w" -o bin/gateway .
|
|
||||||
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
|
||||||
go build -trimpath -ldflags "-s -w" -o bin/worker ./cmd/worker
|
|
||||||
cd $(BACKEND_DIR) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) \
|
|
||||||
go build -trimpath -ldflags "-s -w" -o bin/tool ./cmd/tool
|
|
||||||
@echo "binary 已輸出到 $(BIN_DIR)/(gateway / worker / tool)"
|
|
||||||
|
|
||||||
# ============================================================
|
dev-all: ## 一鍵啟動 Mongo/Redis + API + 前端 + 8D worker(背景)
|
||||||
# PROD (部署)
|
bash scripts/start-all.sh
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: prod-infra
|
stop-all: ## 一鍵停止全部開發服務
|
||||||
prod-infra: $(INFRA_DIR)/.env ## [prod] 起 Mongo + Redis (docker,背景)
|
bash scripts/stop-all.sh
|
||||||
$(COMPOSE) up -d
|
|
||||||
|
|
||||||
.PHONY: prod-infra-down
|
restart-all: ## 一鍵重啟全部開發服務
|
||||||
prod-infra-down: ## [prod] 停掉 Mongo + Redis
|
bash scripts/restart-all.sh
|
||||||
$(COMPOSE) down
|
|
||||||
|
|
||||||
.PHONY: prod-init
|
status-all: ## 查看全部開發服務狀態
|
||||||
prod-init: ## [prod] 目標主機初始化 DB + 建立 admin(讀 /opt/haixun/etc/haixun.env;可加 INIT_ADMIN_EMAIL/PASSWORD)
|
bash scripts/status-all.sh
|
||||||
@test -x $(DEPLOY_ROOT)/bin/tool || (echo "缺少 $(DEPLOY_ROOT)/bin/tool,請先 make install" && exit 1)
|
|
||||||
set -a; . $(DEPLOY_ROOT)/etc/haixun.env; set +a; \
|
|
||||||
$(DEPLOY_ROOT)/bin/tool init -f $(DEPLOY_ROOT)/etc/gateway.prod.yaml
|
|
||||||
|
|
||||||
.PHONY: install
|
stop: stop-all ## 同 stop-all
|
||||||
install: ## [prod] 安裝 binary/前端/設定/systemd/nginx(需 root,在目標主機執行)
|
|
||||||
@test -f $(BIN_DIR)/gateway || (echo "缺少 $(BIN_DIR)/gateway,請先在能 build 的機器執行 make build" && exit 1)
|
|
||||||
@test -d $(FRONTEND_DIR)/dist || (echo "缺少 $(FRONTEND_DIR)/dist,請先 make build-frontend" && exit 1)
|
|
||||||
id haixun >/dev/null 2>&1 || useradd --system --home $(DEPLOY_ROOT) --shell /usr/sbin/nologin haixun
|
|
||||||
install -d $(DEPLOY_ROOT)/bin $(DEPLOY_ROOT)/etc $(DEPLOY_ROOT)/node-worker $(WEB_ROOT)
|
|
||||||
install -m 0755 $(BIN_DIR)/gateway $(DEPLOY_ROOT)/bin/gateway
|
|
||||||
install -m 0755 $(BIN_DIR)/worker $(DEPLOY_ROOT)/bin/worker
|
|
||||||
install -m 0755 $(BIN_DIR)/tool $(DEPLOY_ROOT)/bin/tool
|
|
||||||
install -m 0644 $(BACKEND_DIR)/etc/gateway.prod.yaml $(DEPLOY_ROOT)/etc/gateway.prod.yaml
|
|
||||||
install -m 0644 $(BACKEND_DIR)/etc/gateway.worker.prod.yaml $(DEPLOY_ROOT)/etc/gateway.worker.prod.yaml
|
|
||||||
rm -rf $(WEB_ROOT)/* && cp -r $(FRONTEND_DIR)/dist/* $(WEB_ROOT)/
|
|
||||||
cp -r $(BACKEND_DIR)/worker/* $(DEPLOY_ROOT)/node-worker/
|
|
||||||
cd $(DEPLOY_ROOT)/node-worker && npm ci && npx playwright install --with-deps chromium
|
|
||||||
install -m 0644 $(INFRA_DIR)/systemd/haixun-gateway.service /etc/systemd/system/haixun-gateway.service
|
|
||||||
install -m 0644 $(INFRA_DIR)/systemd/haixun-worker.service /etc/systemd/system/haixun-worker.service
|
|
||||||
install -m 0644 $(INFRA_DIR)/systemd/haixun-node-worker.service /etc/systemd/system/haixun-node-worker.service
|
|
||||||
install -m 0644 $(INFRA_DIR)/nginx/haixun.conf /etc/nginx/conf.d/haixun.conf
|
|
||||||
chown -R haixun:haixun $(DEPLOY_ROOT) $(WEB_ROOT)
|
|
||||||
@echo "----"
|
|
||||||
@echo "接著(只做一次)建立 secret 檔:"
|
|
||||||
@echo " cp $(INFRA_DIR)/etc/haixun.env.example $(DEPLOY_ROOT)/etc/haixun.env && chmod 600 $(DEPLOY_ROOT)/etc/haixun.env && sudoedit $(DEPLOY_ROOT)/etc/haixun.env"
|
|
||||||
@echo "再啟用服務:"
|
|
||||||
@echo " systemctl daemon-reload && systemctl enable --now haixun-gateway haixun-worker haixun-node-worker"
|
|
||||||
@echo " nginx -t && systemctl reload nginx"
|
|
||||||
|
|
||||||
# ============================================================
|
restart: restart-all ## 同 restart-all
|
||||||
# 驗證 / 維護
|
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
.PHONY: tidy
|
dev-8d: ## 一鍵啟動 API + Node 8D worker(前景,Ctrl+C 結束)
|
||||||
tidy: ## go mod tidy
|
bash scripts/dev-with-style-8d.sh
|
||||||
cd $(BACKEND_DIR) && go mod tidy
|
|
||||||
|
|
||||||
.PHONY: fmt
|
CONFIG ?= etc/gateway.yaml
|
||||||
fmt: ## gofmt 後端
|
INIT_TENANT ?= default
|
||||||
cd $(BACKEND_DIR) && gofmt -w .
|
INIT_EMAIL ?= admin@30cm.net
|
||||||
|
INIT_PASSWORD ?= Fafafa54088
|
||||||
|
|
||||||
.PHONY: test
|
tool-init: ## 初始化 Mongo indexes、預設權限與 admin 帳號
|
||||||
test: ## 跑後端測試
|
$(GO) run ./cmd/tool init -f $(CONFIG) -tenant $(INIT_TENANT) -email $(INIT_EMAIL) -password '$(INIT_PASSWORD)'
|
||||||
cd $(BACKEND_DIR) && go test ./...
|
|
||||||
|
|
||||||
.PHONY: verify
|
tool: ## 執行 cmd/tool(例:make tool ARGS="init -f etc/gateway.yaml")
|
||||||
verify: ## 後端 build/test + 前端 build + compose 語法
|
$(GO) run ./cmd/tool $(ARGS)
|
||||||
cd $(BACKEND_DIR) && go build ./... && go test ./...
|
|
||||||
cd $(FRONTEND_DIR) && npm ci && npm run build
|
web-install: ## 安裝前端依賴
|
||||||
$(COMPOSE) config >/dev/null && echo "docker compose config OK"
|
cd web && npm install
|
||||||
|
|
||||||
|
web-dev: web-install ## 啟動前端 dev server(proxy 到 :8890)
|
||||||
|
cd web && npm run dev
|
||||||
|
|
||||||
|
extension-pack: ## 打包 Chrome 擴充為 web/public/downloads/*.zip
|
||||||
|
bash scripts/package-extension.sh
|
||||||
|
|
||||||
|
web-build: web-install extension-pack ## 建置前端靜態檔
|
||||||
|
cd web && npm run build
|
||||||
|
|
||||||
|
node-worker-style-8d: ## 啟動 Node 8D 爬蟲 worker
|
||||||
|
cd .. && npm run worker:style-8d
|
||||||
|
|
||||||
|
check: fmt test ## 格式化並測試
|
||||||
|
|
||||||
|
prod: ## 一鍵啟動 production Docker(API + Web + workers,分身數見 deploy/.env)
|
||||||
|
bash scripts/prod-up.sh
|
||||||
|
|
||||||
|
prod-update: ## 只重建/重啟 API+Web+Workers;mongo/redis 不重啟,資料留在 volume
|
||||||
|
bash scripts/prod-update.sh
|
||||||
|
|
||||||
|
prod-deps: ## 只啟動 mongo+redis(named volume 持久化)
|
||||||
|
bash scripts/prod-deps.sh
|
||||||
|
|
||||||
|
prod-down: ## 停止 stack(不刪 volume;Mongo/Redis 資料保留)
|
||||||
|
bash scripts/prod-down.sh
|
||||||
|
|
||||||
|
prod-wipe-data: ## 停止並刪除 mongo/redis volume(危險,需輸入 yes)
|
||||||
|
bash scripts/prod-wipe-data.sh
|
||||||
|
|
||||||
|
prod-logs: ## 追蹤 production logs(可傳 service 名,例:make prod-logs ARGS=api)
|
||||||
|
bash scripts/prod-logs.sh $(ARGS)
|
||||||
|
|
||||||
|
prod-build: web-build ## 建置靜態前端 + production images(不啟動)
|
||||||
|
cd deploy && docker compose -f docker-compose.prod.yml build
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,41 +0,0 @@
|
||||||
Name: haixun-backend
|
|
||||||
Host: 0.0.0.0
|
|
||||||
Port: 8890
|
|
||||||
Timeout: 120000
|
|
||||||
|
|
||||||
# 連線字串與所有 secret 都從環境變數注入(systemd EnvironmentFile=/opt/haixun/etc/haixun.env)。
|
|
||||||
# go-zero 以 conf.UseEnv() + os.ExpandEnv 展開 ${VAR};未設定的變數會展開為空字串並讓服務 fail fast。
|
|
||||||
Mongo:
|
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
|
||||||
Database: ${HAIXUN_MONGO_DB}
|
|
||||||
TimeoutSeconds: 10
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Addr: ${HAIXUN_REDIS_ADDR}
|
|
||||||
Password: ${HAIXUN_REDIS_PASSWORD}
|
|
||||||
DB: 0
|
|
||||||
|
|
||||||
Auth:
|
|
||||||
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
|
||||||
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
|
||||||
AccessExpireSeconds: 900
|
|
||||||
RefreshExpireSeconds: 2592000
|
|
||||||
DevHeaderFallback: false
|
|
||||||
|
|
||||||
Secrets:
|
|
||||||
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
|
||||||
|
|
||||||
JobWorker:
|
|
||||||
Enabled: false
|
|
||||||
WorkerType: go
|
|
||||||
|
|
||||||
JobScheduler:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 60
|
|
||||||
|
|
||||||
JobReaper:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 30
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
Name: haixun-backend
|
|
||||||
Host: 0.0.0.0
|
|
||||||
Port: 8890
|
|
||||||
Timeout: 120000
|
|
||||||
|
|
||||||
# 本機開發設定。預設搭配 `make dev-infra`(infra/docker-compose.yml)跑的 Mongo/Redis,
|
|
||||||
# 帳密對應 infra/.env.example 的預設值。若你改了 .env 密碼,這裡也要同步。
|
|
||||||
Mongo:
|
|
||||||
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
|
||||||
Database: haixun
|
|
||||||
TimeoutSeconds: 10
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Addr: 127.0.0.1:6379
|
|
||||||
Password: change-me-redis-pass
|
|
||||||
DB: 0
|
|
||||||
|
|
||||||
Auth:
|
|
||||||
AccessSecret: haixun-dev-access-secret-change-me
|
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
|
||||||
AccessExpireSeconds: 900
|
|
||||||
RefreshExpireSeconds: 2592000
|
|
||||||
# 僅本機開發開啟:允許用 X-Tenant-ID / X-UID header 模擬登入。正式環境必須為 false。
|
|
||||||
DevHeaderFallback: true
|
|
||||||
|
|
||||||
Secrets:
|
|
||||||
# 留空 = 不加密(本機開發方便)。正式環境用 ${HAIXUN_SECRETS_KEY}。
|
|
||||||
EncryptionKey: ""
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: haixun-dev-worker-secret
|
|
||||||
|
|
||||||
JobWorker:
|
|
||||||
Enabled: true
|
|
||||||
WorkerType: go
|
|
||||||
|
|
||||||
JobScheduler:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 60
|
|
||||||
|
|
||||||
JobReaper:
|
|
||||||
Enabled: true
|
|
||||||
IntervalSeconds: 30
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
// Package crypto provides application-layer encryption for sensitive data at
|
|
||||||
// rest (browser session storage, third-party API keys).
|
|
||||||
//
|
|
||||||
// Ciphertext format: "enc:v1:" + base64url(nonce || ciphertext||tag).
|
|
||||||
// Values without the prefix are treated as legacy plaintext and returned as-is
|
|
||||||
// on Decrypt, so enabling encryption is backward compatible with existing data.
|
|
||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const cipherPrefix = "enc:v1:"
|
|
||||||
|
|
||||||
// Cipher encrypts/decrypts secret strings. When built without a key it is
|
|
||||||
// disabled and acts as a passthrough (for local dev), but Decrypt still
|
|
||||||
// transparently handles previously written ciphertext if any.
|
|
||||||
type Cipher struct {
|
|
||||||
aead cipher.AEAD
|
|
||||||
}
|
|
||||||
|
|
||||||
// New builds a Cipher from a base64-encoded 32-byte (AES-256) key.
|
|
||||||
// An empty key returns a disabled (passthrough) cipher.
|
|
||||||
func New(base64Key string) (*Cipher, error) {
|
|
||||||
base64Key = strings.TrimSpace(base64Key)
|
|
||||||
if base64Key == "" {
|
|
||||||
return &Cipher{}, nil
|
|
||||||
}
|
|
||||||
key, err := base64.StdEncoding.DecodeString(base64Key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("crypto: invalid base64 encryption key: %w", err)
|
|
||||||
}
|
|
||||||
if len(key) != 32 {
|
|
||||||
return nil, fmt.Errorf("crypto: encryption key must be 32 bytes (got %d)", len(key))
|
|
||||||
}
|
|
||||||
block, err := aes.NewCipher(key)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("crypto: new cipher: %w", err)
|
|
||||||
}
|
|
||||||
aead, err := cipher.NewGCM(block)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("crypto: new gcm: %w", err)
|
|
||||||
}
|
|
||||||
return &Cipher{aead: aead}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled reports whether a key is configured.
|
|
||||||
func (c *Cipher) Enabled() bool {
|
|
||||||
return c != nil && c.aead != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt returns ciphertext for non-empty plaintext when enabled; otherwise
|
|
||||||
// it returns the input unchanged.
|
|
||||||
func (c *Cipher) Encrypt(plaintext string) (string, error) {
|
|
||||||
if !c.Enabled() || plaintext == "" {
|
|
||||||
return plaintext, nil
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(plaintext, cipherPrefix) {
|
|
||||||
// Already encrypted; avoid double-encrypting.
|
|
||||||
return plaintext, nil
|
|
||||||
}
|
|
||||||
nonce := make([]byte, c.aead.NonceSize())
|
|
||||||
if _, err := rand.Read(nonce); err != nil {
|
|
||||||
return "", fmt.Errorf("crypto: read nonce: %w", err)
|
|
||||||
}
|
|
||||||
sealed := c.aead.Seal(nonce, nonce, []byte(plaintext), nil)
|
|
||||||
return cipherPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decrypt reverses Encrypt. Values without the cipher prefix are returned
|
|
||||||
// unchanged (legacy plaintext), keeping the change backward compatible.
|
|
||||||
func (c *Cipher) Decrypt(value string) (string, error) {
|
|
||||||
if !strings.HasPrefix(value, cipherPrefix) {
|
|
||||||
return value, nil
|
|
||||||
}
|
|
||||||
if !c.Enabled() {
|
|
||||||
return "", errors.New("crypto: encrypted value found but no encryption key configured")
|
|
||||||
}
|
|
||||||
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(value, cipherPrefix))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("crypto: decode ciphertext: %w", err)
|
|
||||||
}
|
|
||||||
nonceSize := c.aead.NonceSize()
|
|
||||||
if len(raw) < nonceSize {
|
|
||||||
return "", errors.New("crypto: ciphertext too short")
|
|
||||||
}
|
|
||||||
nonce, ct := raw[:nonceSize], raw[nonceSize:]
|
|
||||||
plain, err := c.aead.Open(nil, nonce, ct, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("crypto: decrypt: %w", err)
|
|
||||||
}
|
|
||||||
return string(plain), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package setting
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"haixun-backend/internal/library/authctx"
|
|
||||||
app "haixun-backend/internal/library/errors"
|
|
||||||
"haixun-backend/internal/library/errors/code"
|
|
||||||
"haixun-backend/internal/svc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// authorizeSettingAccess guards the generic settings API against horizontal
|
|
||||||
// privilege escalation (IDOR). The settings collection stores sensitive values
|
|
||||||
// such as AI / Brave / Exa API keys under the "user" and "account" scopes, so
|
|
||||||
// the caller must be the owner of the scope being accessed.
|
|
||||||
//
|
|
||||||
// - "user": scope_id must equal the caller uid (uid is enumerable).
|
|
||||||
// - "account": the threads account must belong to the caller.
|
|
||||||
// - other scopes (brand / persona / placement_topic / copy_mission ...):
|
|
||||||
// keyed by unguessable ObjectIDs and hold non-secret research config;
|
|
||||||
// ownership is enforced by their own feature endpoints. We still require an
|
|
||||||
// authenticated actor here.
|
|
||||||
func authorizeSettingAccess(ctx context.Context, svcCtx *svc.ServiceContext, scope, scopeID string) error {
|
|
||||||
actor, ok := authctx.ActorFromContext(ctx)
|
|
||||||
if !ok || actor.UID == "" {
|
|
||||||
return app.For(code.Auth).AuthUnauthorized("missing actor")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch scope {
|
|
||||||
case "user":
|
|
||||||
if scopeID != actor.UID {
|
|
||||||
return app.For(code.Setting).AuthForbidden("cannot access another user's settings")
|
|
||||||
}
|
|
||||||
case "account":
|
|
||||||
if _, err := svcCtx.ThreadsAccount.Get(ctx, actor.TenantID, actor.UID, scopeID); err != nil {
|
|
||||||
return app.For(code.Setting).AuthForbidden("cannot access settings of an account you do not own")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,593 +0,0 @@
|
||||||
{
|
|
||||||
"name": "haixun-node-worker",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "haixun-node-worker",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright": "^1.49.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"tsx": "^4.19.2",
|
|
||||||
"typescript": "^5.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"aix"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/android-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"freebsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
|
||||||
"cpu": [
|
|
||||||
"loong64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
|
||||||
"cpu": [
|
|
||||||
"mips64el"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
|
||||||
"cpu": [
|
|
||||||
"ppc64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
|
||||||
"cpu": [
|
|
||||||
"s390x"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/linux-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"netbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openbsd"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"openharmony"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"sunos"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/win32-x64": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/esbuild": {
|
|
||||||
"version": "0.28.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
|
||||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"esbuild": "bin/esbuild"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@esbuild/aix-ppc64": "0.28.1",
|
|
||||||
"@esbuild/android-arm": "0.28.1",
|
|
||||||
"@esbuild/android-arm64": "0.28.1",
|
|
||||||
"@esbuild/android-x64": "0.28.1",
|
|
||||||
"@esbuild/darwin-arm64": "0.28.1",
|
|
||||||
"@esbuild/darwin-x64": "0.28.1",
|
|
||||||
"@esbuild/freebsd-arm64": "0.28.1",
|
|
||||||
"@esbuild/freebsd-x64": "0.28.1",
|
|
||||||
"@esbuild/linux-arm": "0.28.1",
|
|
||||||
"@esbuild/linux-arm64": "0.28.1",
|
|
||||||
"@esbuild/linux-ia32": "0.28.1",
|
|
||||||
"@esbuild/linux-loong64": "0.28.1",
|
|
||||||
"@esbuild/linux-mips64el": "0.28.1",
|
|
||||||
"@esbuild/linux-ppc64": "0.28.1",
|
|
||||||
"@esbuild/linux-riscv64": "0.28.1",
|
|
||||||
"@esbuild/linux-s390x": "0.28.1",
|
|
||||||
"@esbuild/linux-x64": "0.28.1",
|
|
||||||
"@esbuild/netbsd-arm64": "0.28.1",
|
|
||||||
"@esbuild/netbsd-x64": "0.28.1",
|
|
||||||
"@esbuild/openbsd-arm64": "0.28.1",
|
|
||||||
"@esbuild/openbsd-x64": "0.28.1",
|
|
||||||
"@esbuild/openharmony-arm64": "0.28.1",
|
|
||||||
"@esbuild/sunos-x64": "0.28.1",
|
|
||||||
"@esbuild/win32-arm64": "0.28.1",
|
|
||||||
"@esbuild/win32-ia32": "0.28.1",
|
|
||||||
"@esbuild/win32-x64": "0.28.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fsevents": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright": {
|
|
||||||
"version": "1.61.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
|
||||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"playwright-core": "1.61.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"playwright": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "2.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/playwright-core": {
|
|
||||||
"version": "1.61.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
|
||||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"playwright-core": "cli.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx": {
|
|
||||||
"version": "4.22.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
|
||||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"esbuild": "~0.28.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"tsx": "dist/cli.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"fsevents": "~2.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tsx/node_modules/fsevents": {
|
|
||||||
"version": "2.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.9.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -36,15 +36,15 @@ func runInit(args []string) error {
|
||||||
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
fs := flag.NewFlagSet("init", flag.ExitOnError)
|
||||||
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
configFile := fs.String("f", "etc/gateway.yaml", "config file")
|
||||||
tenantID := fs.String("tenant", envOr("INIT_TENANT_ID", "default"), "tenant id for admin and role permissions")
|
tenantID := fs.String("tenant", envOr("INIT_TENANT_ID", "default"), "tenant id for admin and role permissions")
|
||||||
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@30cm.net"), "bootstrap admin email")
|
email := fs.String("email", envOr("INIT_ADMIN_EMAIL", "admin@haixun.local"), "bootstrap admin email")
|
||||||
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Fafafa54088"), "bootstrap admin password")
|
password := fs.String("password", envOr("INIT_ADMIN_PASSWORD", "Admin-Pass-1!"), "bootstrap admin password")
|
||||||
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
displayName := fs.String("display-name", envOr("INIT_ADMIN_DISPLAY_NAME", "Admin"), "bootstrap admin display name")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
conf.MustLoad(*configFile, &cfg, conf.UseEnv())
|
conf.MustLoad(*configFile, &cfg)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
@ -20,7 +20,7 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
conf.MustLoad(*configFile, &c)
|
||||||
if !c.JobWorker.Enabled {
|
if !c.JobWorker.Enabled {
|
||||||
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
fmt.Fprintln(os.Stderr, "[worker] JobWorker.Enabled must be true")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
||||||
|
|
||||||
|
# ── 對外埠 ──
|
||||||
|
HAIXUN_WEB_PORT=8080
|
||||||
|
|
||||||
|
# ── 前端打包模式 ──
|
||||||
|
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
||||||
|
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
||||||
|
# HAIXUN_WEB_BUILD_MODE=static
|
||||||
|
|
||||||
|
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
||||||
|
GO_WORKER_REPLICAS=5
|
||||||
|
NODE_STYLE8D_WORKER_REPLICAS=5
|
||||||
|
|
||||||
|
# ── Mongo / Redis(容器內預設,通常不用改)──
|
||||||
|
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
||||||
|
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
||||||
|
# 只改版程式:make prod-update(不碰 mongo/redis)
|
||||||
|
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
||||||
|
HAIXUN_MONGO_DATABASE=haixun
|
||||||
|
HAIXUN_REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
|
# ── 安全金鑰(正式環境務必更換)──
|
||||||
|
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
||||||
|
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
||||||
|
|
||||||
|
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
||||||
|
INIT_TENANT_ID=default
|
||||||
|
INIT_ADMIN_EMAIL=admin@30cm.net
|
||||||
|
INIT_ADMIN_PASSWORD=Fafafa54088
|
||||||
|
|
||||||
|
# ── Node 8D worker 選項 ──
|
||||||
|
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
||||||
|
# HAIXUN_WORKER_POLL_MS=3000
|
||||||
|
|
||||||
|
# ── 略過自動 init ──
|
||||||
|
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
||||||
|
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
||||||
|
# HAIXUN_SKIP_INIT=1
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
# 複製為 deploy/.env 後再啟動:cp deploy/.env.example deploy/.env
|
||||||
|
|
||||||
|
# ── 對外埠 ──
|
||||||
|
HAIXUN_WEB_PORT=8080
|
||||||
|
|
||||||
|
# ── 前端打包模式 ──
|
||||||
|
# static = 本機 make web-build 後 nginx 只 COPY dist(預設,最快)
|
||||||
|
# docker = 在 Docker 內跑 npm build(需改 compose 用 Dockerfile.web)
|
||||||
|
# HAIXUN_WEB_BUILD_MODE=static
|
||||||
|
|
||||||
|
# ── Worker 分身數(make prod 會帶入 docker compose --scale)──
|
||||||
|
GO_WORKER_REPLICAS=1
|
||||||
|
NODE_STYLE8D_WORKER_REPLICAS=1
|
||||||
|
|
||||||
|
# ── Mongo / Redis(容器內預設,通常不用改)──
|
||||||
|
# 資料存在 Docker named volume:haixun-prod_mongo_data、haixun-prod_redis_data
|
||||||
|
# prod-down 不會刪 volume;重啟 container 資料仍在。
|
||||||
|
# 只改版程式:make prod-update(不碰 mongo/redis)
|
||||||
|
HAIXUN_MONGO_URI=mongodb://mongo:27017
|
||||||
|
HAIXUN_MONGO_DATABASE=haixun
|
||||||
|
HAIXUN_REDIS_ADDR=redis:6379
|
||||||
|
|
||||||
|
# ── 安全金鑰(正式環境務必更換)──
|
||||||
|
HAIXUN_AUTH_ACCESS_SECRET=change-me-access-secret
|
||||||
|
HAIXUN_AUTH_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
HAIXUN_WORKER_SECRET=change-me-worker-secret
|
||||||
|
|
||||||
|
# ── 首次初始化管理員(make prod 會自動跑 init;已存在則跳過建立)──
|
||||||
|
INIT_TENANT_ID=default
|
||||||
|
INIT_ADMIN_EMAIL=admin@haixun.local
|
||||||
|
INIT_ADMIN_PASSWORD=Admin-Pass-1!
|
||||||
|
|
||||||
|
# ── Node 8D worker 選項 ──
|
||||||
|
# HAIXUN_NODE_WORKER_ID=custom-node-worker-1
|
||||||
|
# HAIXUN_WORKER_POLL_MS=3000
|
||||||
|
|
||||||
|
# ── 略過自動 init ──
|
||||||
|
# 預設:若 Mongo 已有 members 會自動跳過 init。
|
||||||
|
# 強制重跑 init:PROD_FORCE_INIT=1 make prod
|
||||||
|
# HAIXUN_SKIP_INIT=1
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM golang:1.22-bookworm AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-api .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-worker ./cmd/worker
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/haixun-tool ./cmd/tool
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates gettext-base curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /out/haixun-api /out/haixun-worker /out/haixun-tool /app/
|
||||||
|
COPY deploy/config/gateway.runtime.yaml.tpl deploy/config/gateway.worker.runtime.yaml.tpl /app/deploy/config/
|
||||||
|
COPY deploy/docker/entrypoint-api.sh deploy/docker/entrypoint-worker.sh deploy/docker/entrypoint-init.sh /app/deploy/docker/
|
||||||
|
RUN chmod +x /app/deploy/docker/entrypoint-api.sh /app/deploy/docker/entrypoint-worker.sh /app/deploy/docker/entrypoint-init.sh
|
||||||
|
EXPOSE 8890
|
||||||
|
ENTRYPOINT ["/app/deploy/docker/entrypoint-api.sh"]
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/playwright:v1.49.1-noble AS base
|
||||||
|
WORKDIR /app
|
||||||
|
COPY worker/package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY worker/ ./
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
CMD ["npx", "tsx", "style-8d-worker.ts"]
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# 備用:無本機 Node 時在 Docker 內編譯。預設請用 Dockerfile.web.static + make web-build。
|
||||||
|
|
||||||
|
FROM node:22-bookworm AS web-builder
|
||||||
|
WORKDIR /src/web
|
||||||
|
COPY web/package.json web/package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY web/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY --from=web-builder /src/web/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
# 本機先執行 make web-build,再打包純靜態檔 + nginx(無 Node 編譯,建置最快)
|
||||||
|
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY deploy/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY web/dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# 本機依賴(Docker Compose)
|
||||||
|
|
||||||
|
Gateway 啟用 **Notification** / **Member OTP** 需要:
|
||||||
|
|
||||||
|
| 服務 | 用途 | 預設埠 |
|
||||||
|
|------|------|--------|
|
||||||
|
| **MongoDB** | `notifications`、`notification_dlq` collections | 27017 |
|
||||||
|
| **Redis** | 冪等、配額、異步重試佇列、member OTP challenge | 6379 |
|
||||||
|
| MailHog(選用) | 本機 SMTP 測試 | 1025 / 8025 |
|
||||||
|
| OpenLDAP(`make ldap-up` / `make k6-up`) | ZITADEL LDAP IdP 本機目錄 | 389 |
|
||||||
|
| ZITADEL(`make k6-up`) | OIDC / Social / LDAP 登入 | 8080 |
|
||||||
|
|
||||||
|
Mongo **不需要**事先手動建 collection;應用程式寫入時會自動建立。索引由 init script 或 `make mongo-index` 建立。
|
||||||
|
|
||||||
|
## 快速開始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 啟動 Mongo + Redis
|
||||||
|
make deps-up
|
||||||
|
|
||||||
|
# 2.(選用)含 MailHog
|
||||||
|
make deps-up-smtp
|
||||||
|
|
||||||
|
# 3. 確認索引(首次 docker volume 通常已由 init 建立;可再跑一次保險)
|
||||||
|
make mongo-index
|
||||||
|
|
||||||
|
# 4. 啟動 Gateway(使用 etc/gateway.dev.yaml)
|
||||||
|
make run-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mongo collections
|
||||||
|
|
||||||
|
| Collection | 模組 | 說明 |
|
||||||
|
|------------|------|------|
|
||||||
|
| `notifications` | notification | 發送紀錄、冪等 |
|
||||||
|
| `notification_dlq` | notification | 超過 MaxRetry 的死信 |
|
||||||
|
|
||||||
|
索引定義見 [`deploy/mongo/init/01-gateway-indexes.js`](mongo/init/01-gateway-indexes.js),與 Go 的 `Index20260520001UP` 一致。
|
||||||
|
|
||||||
|
## 常用指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make deps-up # docker compose up -d mongo redis
|
||||||
|
make deps-up-smtp # 再加上 mailhog(profile smtp)
|
||||||
|
make ldap-up # 只起 OpenLDAP(profile ldap)
|
||||||
|
make k6-up # 全棧含 OpenLDAP + ZITADEL(見 deploy/zitadel、deploy/openldap README)
|
||||||
|
make ldap-test # 確認 LDAP 測試帳號 alice/bob
|
||||||
|
make deps-down # 停止並移除容器(保留 volume)
|
||||||
|
make deps-down-v # 停止並刪除 volume(會清掉 Mongo 資料)
|
||||||
|
make deps-logs # 查看 log
|
||||||
|
make mongo-index # 手動建立/補齊索引
|
||||||
|
```
|
||||||
|
|
||||||
|
LDAP 本機測試:[deploy/openldap/README.md](openldap/README.md)
|
||||||
|
|
||||||
|
## 連線設定
|
||||||
|
|
||||||
|
設定說明:[`etc/README.md`](../etc/README.md)
|
||||||
|
|
||||||
|
| 檔案 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| [`etc/gateway.yaml`](../etc/gateway.yaml) | 預設,無需 Docker |
|
||||||
|
| [`etc/gateway.dev.example.yaml`](../etc/gateway.dev.example.yaml) | 範例(可提交) |
|
||||||
|
| `etc/gateway.dev.yaml` | 本機專用(**勿提交**,見 `.gitignore`) |
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
|
Database: ${HAIXUN_MONGO_DATABASE}
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
||||||
|
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: false
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -5,24 +5,20 @@ Timeout: 120000
|
||||||
|
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: ${HAIXUN_MONGO_URI}
|
URI: ${HAIXUN_MONGO_URI}
|
||||||
Database: ${HAIXUN_MONGO_DB}
|
Database: ${HAIXUN_MONGO_DATABASE}
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: ${HAIXUN_REDIS_ADDR}
|
Addr: ${HAIXUN_REDIS_ADDR}
|
||||||
Password: ${HAIXUN_REDIS_PASSWORD}
|
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
AccessSecret: ${HAIXUN_JWT_ACCESS_SECRET}
|
AccessSecret: ${HAIXUN_AUTH_ACCESS_SECRET}
|
||||||
RefreshSecret: ${HAIXUN_JWT_REFRESH_SECRET}
|
RefreshSecret: ${HAIXUN_AUTH_REFRESH_SECRET}
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
DevHeaderFallback: false
|
DevHeaderFallback: false
|
||||||
|
|
||||||
Secrets:
|
|
||||||
EncryptionKey: ${HAIXUN_SECRETS_KEY}
|
|
||||||
|
|
||||||
InternalWorker:
|
InternalWorker:
|
||||||
Secret: ${HAIXUN_WORKER_SECRET}
|
Secret: ${HAIXUN_WORKER_SECRET}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
name: haixun-prod
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: haixun
|
||||||
|
# named volume:重啟/改版不會清資料(只有 prod-wipe-data 或 docker volume rm 才會)
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
# AOF + named volume:重啟後 queue/lock 狀態可從磁碟恢復
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 12
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8890/api/v1/health >/dev/null || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
go-worker:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/app/deploy/docker/entrypoint-worker.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
node-worker-style-8d:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.node-worker
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
HAIXUN_BACKEND_URL: http://api:8890
|
||||||
|
HAIXUN_WORKER_SECRET: ${HAIXUN_WORKER_SECRET}
|
||||||
|
HAIXUN_NODE_WORKER_ID: ${HAIXUN_NODE_WORKER_ID:-}
|
||||||
|
HAIXUN_WORKER_POLL_MS: ${HAIXUN_WORKER_POLL_MS:-3000}
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.web.static
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${HAIXUN_WEB_PORT:-8080}:80"
|
||||||
|
depends_on:
|
||||||
|
api:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
init:
|
||||||
|
profiles: ["init"]
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: deploy/Dockerfile.api
|
||||||
|
entrypoint: ["/app/deploy/docker/entrypoint-init.sh"]
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
mongo:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
redis_data:
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo:7
|
||||||
|
container_name: gateway-mongo
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_DATABASE: gateway
|
||||||
|
volumes:
|
||||||
|
- mongo_data:/data/db
|
||||||
|
- ./mongo/init:/docker-entrypoint-initdb.d:ro
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: gateway-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongo_data:
|
||||||
|
redis_data:
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
||||||
|
exec /app/haixun-api -f /tmp/gateway.runtime.yaml
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
: "${INIT_TENANT_ID:=default}"
|
||||||
|
: "${INIT_ADMIN_EMAIL:=admin@haixun.local}"
|
||||||
|
: "${INIT_ADMIN_PASSWORD:?INIT_ADMIN_PASSWORD is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.runtime.yaml.tpl > /tmp/gateway.runtime.yaml
|
||||||
|
exec /app/haixun-tool init \
|
||||||
|
-f /tmp/gateway.runtime.yaml \
|
||||||
|
-tenant "$INIT_TENANT_ID" \
|
||||||
|
-email "$INIT_ADMIN_EMAIL" \
|
||||||
|
-password "$INIT_ADMIN_PASSWORD"
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
: "${HAIXUN_MONGO_URI:=mongodb://mongo:27017}"
|
||||||
|
: "${HAIXUN_MONGO_DATABASE:=haixun}"
|
||||||
|
: "${HAIXUN_REDIS_ADDR:=redis:6379}"
|
||||||
|
: "${HAIXUN_AUTH_ACCESS_SECRET:?HAIXUN_AUTH_ACCESS_SECRET is required}"
|
||||||
|
: "${HAIXUN_AUTH_REFRESH_SECRET:?HAIXUN_AUTH_REFRESH_SECRET is required}"
|
||||||
|
: "${HAIXUN_WORKER_SECRET:?HAIXUN_WORKER_SECRET is required}"
|
||||||
|
|
||||||
|
export HAIXUN_MONGO_URI HAIXUN_MONGO_DATABASE HAIXUN_REDIS_ADDR
|
||||||
|
export HAIXUN_AUTH_ACCESS_SECRET HAIXUN_AUTH_REFRESH_SECRET HAIXUN_WORKER_SECRET
|
||||||
|
|
||||||
|
envsubst < /app/deploy/config/gateway.worker.runtime.yaml.tpl > /tmp/gateway.worker.runtime.yaml
|
||||||
|
exec /app/haixun-worker -f /tmp/gateway.worker.runtime.yaml
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Gateway MongoDB 初始化(僅在 data volume 首次建立時執行)
|
||||||
|
// 與 internal/model/notification/repository/* Index20260520001UP 對齊
|
||||||
|
// 既有 volume 請執行:make mongo-index
|
||||||
|
|
||||||
|
db = db.getSiblingDB('gateway');
|
||||||
|
|
||||||
|
print('Creating indexes on notifications...');
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ tenant_id: 1, kind: 1, idempotency_key: 1 },
|
||||||
|
{ unique: true, name: 'idx_notifications_tenant_kind_idempotency' }
|
||||||
|
);
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ tenant_id: 1, uid: 1, occurred_at: -1 },
|
||||||
|
{ name: 'idx_notifications_tenant_uid_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
db.notifications.createIndex(
|
||||||
|
{ status: 1, attempts: 1, occurred_at: 1 },
|
||||||
|
{ name: 'idx_notifications_status_attempts_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Creating indexes on notification_dlq...');
|
||||||
|
|
||||||
|
db.notification_dlq.createIndex(
|
||||||
|
{ tenant_id: 1, occurred_at: -1 },
|
||||||
|
{ name: 'idx_notification_dlq_tenant_occurred' }
|
||||||
|
);
|
||||||
|
|
||||||
|
print('Gateway Mongo init done.');
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_comp_level 5;
|
||||||
|
gzip_min_length 256;
|
||||||
|
gzip_types
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
image/svg+xml;
|
||||||
|
|
||||||
|
# Vite 產物:檔名含 hash,可長期快取
|
||||||
|
location /assets/ {
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /downloads/ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /illustrations/ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# SPA 入口與路由:不快取,避免部署後仍載入舊版 shell
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api:8890;
|
||||||
|
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 $scheme;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-backend
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8890
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://mongo:27017
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: redis:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: change-me-in-prod
|
||||||
|
RefreshSecret: change-me-in-prod-too
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: change-me-worker-secret
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: false
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: true
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
Name: haixun-worker
|
||||||
|
Host: 0.0.0.0
|
||||||
|
Port: 8891
|
||||||
|
Timeout: 120000
|
||||||
|
|
||||||
|
Mongo:
|
||||||
|
URI: mongodb://mongo:27017
|
||||||
|
Database: haixun
|
||||||
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
|
Redis:
|
||||||
|
Addr: redis:6379
|
||||||
|
DB: 0
|
||||||
|
|
||||||
|
Auth:
|
||||||
|
AccessSecret: change-me-in-prod
|
||||||
|
RefreshSecret: change-me-in-prod-too
|
||||||
|
AccessExpireSeconds: 900
|
||||||
|
RefreshExpireSeconds: 2592000
|
||||||
|
DevHeaderFallback: false
|
||||||
|
|
||||||
|
InternalWorker:
|
||||||
|
Secret: change-me-worker-secret
|
||||||
|
|
||||||
|
JobWorker:
|
||||||
|
Enabled: true
|
||||||
|
WorkerType: go
|
||||||
|
|
||||||
|
JobScheduler:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 60
|
||||||
|
|
||||||
|
JobReaper:
|
||||||
|
Enabled: false
|
||||||
|
IntervalSeconds: 30
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
Name: haixun-worker
|
Name: haixun-backend
|
||||||
Host: 0.0.0.0
|
Host: 0.0.0.0
|
||||||
Port: 8891
|
Port: 8890
|
||||||
Timeout: 120000
|
Timeout: 120000
|
||||||
|
|
||||||
# 本機開發 worker 設定(go worker)。搭配 `make dev-infra` 的 Mongo/Redis。
|
|
||||||
Mongo:
|
Mongo:
|
||||||
URI: mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
URI: mongodb://127.0.0.1:27017
|
||||||
Database: haixun
|
Database: haixun
|
||||||
TimeoutSeconds: 10
|
TimeoutSeconds: 10
|
||||||
|
|
||||||
Redis:
|
Redis:
|
||||||
Addr: 127.0.0.1:6379
|
Addr: 127.0.0.1:6379
|
||||||
Password: change-me-redis-pass
|
|
||||||
DB: 0
|
DB: 0
|
||||||
|
|
||||||
Auth:
|
Auth:
|
||||||
|
|
@ -19,22 +17,16 @@ Auth:
|
||||||
RefreshSecret: haixun-dev-refresh-secret-change-me
|
RefreshSecret: haixun-dev-refresh-secret-change-me
|
||||||
AccessExpireSeconds: 900
|
AccessExpireSeconds: 900
|
||||||
RefreshExpireSeconds: 2592000
|
RefreshExpireSeconds: 2592000
|
||||||
DevHeaderFallback: false
|
DevHeaderFallback: true
|
||||||
|
|
||||||
Secrets:
|
|
||||||
EncryptionKey: ""
|
|
||||||
|
|
||||||
InternalWorker:
|
|
||||||
Secret: haixun-dev-worker-secret
|
|
||||||
|
|
||||||
JobWorker:
|
JobWorker:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
WorkerType: go
|
WorkerType: go
|
||||||
|
|
||||||
JobScheduler:
|
JobScheduler:
|
||||||
Enabled: false
|
Enabled: true
|
||||||
IntervalSeconds: 60
|
IntervalSeconds: 60
|
||||||
|
|
||||||
JobReaper:
|
JobReaper:
|
||||||
Enabled: false
|
Enabled: true
|
||||||
IntervalSeconds: 30
|
IntervalSeconds: 30
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { Component, type ErrorInfo, type ReactNode } from 'react'
|
|
||||||
|
|
||||||
type ErrorBoundaryProps = {
|
|
||||||
children: ReactNode
|
|
||||||
fallback?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorBoundaryState = {
|
|
||||||
hasError: boolean
|
|
||||||
message: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 攔截子樹的 render 例外,避免單一元件(例如渲染 AI 任意輸出的 IslanderMarkdown)
|
|
||||||
* 出錯就整頁白屏。
|
|
||||||
*/
|
|
||||||
export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
||||||
state: ErrorBoundaryState = { hasError: false, message: '' }
|
|
||||||
|
|
||||||
static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
|
|
||||||
return { hasError: true, message: error instanceof Error ? error.message : '發生未預期錯誤' }
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error: Error, info: ErrorInfo) {
|
|
||||||
console.error('ErrorBoundary caught error:', error, info.componentStack)
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleReset = () => {
|
|
||||||
this.setState({ hasError: false, message: '' })
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
if (!this.state.hasError) return this.props.children
|
|
||||||
if (this.props.fallback) return this.props.fallback
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ac-slot" style={{ padding: '1.5rem', textAlign: 'center' }}>
|
|
||||||
<p className="text-ink" style={{ marginBottom: '0.75rem' }}>
|
|
||||||
這個區塊發生問題,請重試或重新整理頁面。
|
|
||||||
</p>
|
|
||||||
<p className="text-subtle" style={{ marginBottom: '1rem', fontSize: 13 }}>
|
|
||||||
{this.state.message}
|
|
||||||
</p>
|
|
||||||
<button type="button" className="ac-btn-secondary" onClick={this.handleReset}>
|
|
||||||
重試
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -19,7 +19,7 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
var c config.Config
|
var c config.Config
|
||||||
conf.MustLoad(*configFile, &c, conf.UseEnv())
|
conf.MustLoad(*configFile, &c)
|
||||||
|
|
||||||
server := rest.MustNewServer(c.RestConf)
|
server := rest.MustNewServer(c.RestConf)
|
||||||
defer server.Stop()
|
defer server.Stop()
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
# Infra docker compose 環境變數範本
|
|
||||||
# 複製成 infra/.env 後填入實際值;.env 不要 commit。
|
|
||||||
|
|
||||||
# --- Mongo ---
|
|
||||||
MONGO_PORT=27017
|
|
||||||
MONGO_ROOT_USER=haixun
|
|
||||||
MONGO_ROOT_PASSWORD=1qaz@WSX3edc$RFV
|
|
||||||
MONGO_DATABASE=haixun
|
|
||||||
|
|
||||||
# --- Redis ---
|
|
||||||
REDIS_PORT=6379
|
|
||||||
REDIS_PASSWORD=change-me-redis-pass
|
|
||||||
|
|
||||||
INIT_ADMIN_EMAIL=admin@30cm.net
|
|
||||||
PASSWORD=Fafafafa54088
|
|
||||||
|
|
||||||
HAIXUN_JWT_ACCESS_SECRET=1qaz@WSX3edc$RFV
|
|
||||||
HAIXUN_JWT_REFRESH_SECRET=1qaz@WSX3edc$RFV
|
|
||||||
HAIXUN_SECRETS_KEY=1qaz@WSX3edc$RFV
|
|
||||||
HAIXUN_WORKER_SECRET=1qaz@WSX3edc$RFV
|
|
||||||
103
infra/README.md
103
infra/README.md
|
|
@ -1,103 +0,0 @@
|
||||||
# 巡樓部署 (infra)
|
|
||||||
|
|
||||||
部署拓樸:
|
|
||||||
|
|
||||||
```
|
|
||||||
瀏覽器 → nginx(systemd, :80/:443)
|
|
||||||
├─ 靜態前端 /var/www/haixun (frontend/dist)
|
|
||||||
└─ /api 反向代理 → Go gateway (systemd, 127.0.0.1:8890)
|
|
||||||
Go gateway / Go worker (systemd) → Mongo / Redis (docker compose, 綁 127.0.0.1)
|
|
||||||
Node playwright worker (systemd) → 透過 HTTP 打 gateway
|
|
||||||
```
|
|
||||||
|
|
||||||
- 資料服務(Mongo/Redis)用 docker compose,只綁 `127.0.0.1`。
|
|
||||||
- Go gateway / Go worker / Node worker 都是 systemd 原生服務。
|
|
||||||
- secret 一律放 `/opt/haixun/etc/haixun.env`(不進 repo),yaml 用 `${VAR}` 讀取。
|
|
||||||
|
|
||||||
## 目錄
|
|
||||||
|
|
||||||
```
|
|
||||||
infra/
|
|
||||||
docker-compose.yml # mongo + redis
|
|
||||||
.env.example # compose 用環境變數
|
|
||||||
etc/haixun.env.example # systemd EnvironmentFile 範本(secret)
|
|
||||||
nginx/haixun.conf # 靜態前端 + /api 反代 + SSE
|
|
||||||
systemd/
|
|
||||||
haixun-gateway.service
|
|
||||||
haixun-worker.service
|
|
||||||
haixun-node-worker.service
|
|
||||||
```
|
|
||||||
|
|
||||||
## 1. 起資料服務 (docker)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd infra
|
|
||||||
cp .env.example .env # 填入 Mongo/Redis 密碼
|
|
||||||
docker compose --env-file .env up -d
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. 建置產物(本機或 CI)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build # 前端 dist + 兩個 linux Go binary(backend/bin/)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 安裝到目標主機
|
|
||||||
|
|
||||||
於目標主機(需 root):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo make install
|
|
||||||
```
|
|
||||||
|
|
||||||
`make install` 會:
|
|
||||||
|
|
||||||
1. 建立使用者 `haixun` 與目錄 `/opt/haixun/{bin,etc,node-worker}`、`/var/www/haixun`。
|
|
||||||
2. 複製 `backend/bin/{gateway,worker}`、`backend/etc/gateway.prod.yaml`、`backend/etc/gateway.worker.prod.yaml`。
|
|
||||||
3. 複製 `frontend/dist/*` → `/var/www/haixun`。
|
|
||||||
4. 複製 `backend/worker/*`(Node worker)→ `/opt/haixun/node-worker`,並 `npm ci` + `npx playwright install`。
|
|
||||||
5. 安裝 `infra/systemd/*.service` 與 `infra/nginx/haixun.conf`。
|
|
||||||
|
|
||||||
接著手動建立 secret 檔(**只做一次**):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp infra/etc/haixun.env.example /opt/haixun/etc/haixun.env
|
|
||||||
sudo chmod 600 /opt/haixun/etc/haixun.env
|
|
||||||
sudoedit /opt/haixun/etc/haixun.env # 填入實際 secret
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 初始化資料庫與 admin 帳號(只做一次)
|
|
||||||
|
|
||||||
Mongo 起來、secret 填好後,建立索引 / 權限 catalog / role_permissions,並建立第一個 admin:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 可在 haixun.env 內設定 INIT_ADMIN_EMAIL / INIT_ADMIN_PASSWORD,或在這裡用環境變數覆寫
|
|
||||||
sudo make prod-init
|
|
||||||
# 等同:source /opt/haixun/etc/haixun.env 後執行 /opt/haixun/bin/tool init -f /opt/haixun/etc/gateway.prod.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
之後一般使用者可走 `POST /api/v1/auth/register` 自助註冊(前端登入頁)。
|
|
||||||
|
|
||||||
## 5. 啟用服務
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable --now haixun-gateway haixun-worker haixun-node-worker
|
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6. 健康檢查
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8890/api/v1/health
|
|
||||||
sudo systemctl status haixun-gateway haixun-worker haixun-node-worker
|
|
||||||
journalctl -u haixun-gateway -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## 產生 secret
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 48 # JWT access / refresh / worker secret
|
|
||||||
openssl rand -base64 32 # HAIXUN_SECRETS_KEY(機敏資料落地加密)
|
|
||||||
```
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
# 巡樓資料服務:Mongo + Redis
|
|
||||||
# 只綁 127.0.0.1,給同主機上以 systemd 跑的 Go gateway / worker 連線。
|
|
||||||
# 啟動:docker compose -f infra/docker-compose.yml --env-file infra/.env up -d
|
|
||||||
name: haixun-infra
|
|
||||||
|
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo:7
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${MONGO_PORT:-27017}:27017"
|
|
||||||
environment:
|
|
||||||
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER:-haixun}
|
|
||||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:?MONGO_ROOT_PASSWORD is required}
|
|
||||||
MONGO_INITDB_DATABASE: ${MONGO_DATABASE:-haixun}
|
|
||||||
volumes:
|
|
||||||
- mongo_data:/data/db
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 20s
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
restart: unless-stopped
|
|
||||||
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}", "--appendonly", "yes"]
|
|
||||||
ports:
|
|
||||||
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
|
||||||
volumes:
|
|
||||||
- redis_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mongo_data:
|
|
||||||
redis_data:
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
# 部署到目標主機的 /opt/haixun/etc/haixun.env(chmod 600,不要 commit 實值)
|
|
||||||
# gateway.prod.yaml / gateway.worker.yaml 用 ${VAR} 讀取這些值(go-zero conf.UseEnv)。
|
|
||||||
|
|
||||||
# Mongo(含 docker compose 設定的帳密;authSource=admin)
|
|
||||||
HAIXUN_MONGO_URI=mongodb://haixun:change-me-mongo-pass@127.0.0.1:27017/?authSource=admin
|
|
||||||
HAIXUN_MONGO_DB=haixun
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
HAIXUN_REDIS_ADDR=127.0.0.1:6379
|
|
||||||
HAIXUN_REDIS_PASSWORD=change-me-redis-pass
|
|
||||||
|
|
||||||
# JWT secret(請用 openssl rand -base64 48 產生,兩把不同)
|
|
||||||
HAIXUN_JWT_ACCESS_SECRET=replace-with-strong-random
|
|
||||||
HAIXUN_JWT_REFRESH_SECRET=replace-with-another-strong-random
|
|
||||||
|
|
||||||
# 內部 worker secret(gateway 與 node worker 必須一致)
|
|
||||||
HAIXUN_WORKER_SECRET=replace-with-strong-random
|
|
||||||
|
|
||||||
# 機敏資料落地加密金鑰(base64 編碼的 32 bytes;openssl rand -base64 32)
|
|
||||||
HAIXUN_SECRETS_KEY=replace-with-base64-32-bytes
|
|
||||||
|
|
||||||
# Node worker 連線設定
|
|
||||||
HAIXUN_BACKEND_URL=http://127.0.0.1:8890
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
# 巡樓 Console nginx 設定
|
|
||||||
# 安裝:cp infra/nginx/haixun.conf /etc/nginx/conf.d/haixun.conf && nginx -t && systemctl reload nginx
|
|
||||||
# 前端靜態檔部署在 /var/www/haixun(make install 會放 frontend/dist 內容)。
|
|
||||||
# /api 反向代理到本機 systemd 跑的 Go gateway (127.0.0.1:8890),含 SSE 串流設定。
|
|
||||||
|
|
||||||
upstream haixun_gateway {
|
|
||||||
server 127.0.0.1:8890;
|
|
||||||
keepalive 32;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
listen [::]:80;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
root /var/www/haixun;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# 安全標頭
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
|
||||||
add_header X-XSS-Protection "0" always;
|
|
||||||
|
|
||||||
# 靜態資源快取(vite build 帶 hash 檔名)
|
|
||||||
location /assets/ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API 反向代理(一般 JSON 與 SSE 共用)
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://haixun_gateway;
|
|
||||||
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 $scheme;
|
|
||||||
|
|
||||||
# Authorization(provider token)與 X-Member-Authorization(會員 JWT)由 nginx 預設轉發;
|
|
||||||
# 以下確保 SSE 串流不被緩衝、長連線不被提前中斷。
|
|
||||||
proxy_set_header Connection "";
|
|
||||||
proxy_buffering off;
|
|
||||||
proxy_cache off;
|
|
||||||
chunked_transfer_encoding off;
|
|
||||||
proxy_read_timeout 3600s;
|
|
||||||
proxy_send_timeout 3600s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# SPA:所有非檔案路徑都回 index.html,交給 react-router
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Haixun Gateway API (go-zero)
|
|
||||||
After=network-online.target docker.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=haixun
|
|
||||||
Group=haixun
|
|
||||||
WorkingDirectory=/opt/haixun
|
|
||||||
# secrets(JWT / Mongo URI / Redis 密碼 / worker secret / 加密金鑰)放這裡,不進 repo
|
|
||||||
EnvironmentFile=/opt/haixun/etc/haixun.env
|
|
||||||
ExecStart=/opt/haixun/bin/gateway -f /opt/haixun/etc/gateway.prod.yaml
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
LimitNOFILE=65535
|
|
||||||
|
|
||||||
# 加固
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=full
|
|
||||||
ProtectHome=true
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Haixun Node Playwright Worker (style-8d)
|
|
||||||
After=network-online.target haixun-gateway.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=haixun
|
|
||||||
Group=haixun
|
|
||||||
WorkingDirectory=/opt/haixun/node-worker
|
|
||||||
# 至少需要 HAIXUN_BACKEND_URL 與 HAIXUN_WORKER_SECRET(與 gateway 的 InternalWorker.Secret 一致)
|
|
||||||
EnvironmentFile=/opt/haixun/etc/haixun.env
|
|
||||||
ExecStart=/usr/bin/npx tsx style-8d-worker.ts
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
LimitNOFILE=65535
|
|
||||||
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=full
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
[Unit]
|
|
||||||
Description=Haixun Go Job Worker
|
|
||||||
After=network-online.target docker.service haixun-gateway.service
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=haixun
|
|
||||||
Group=haixun
|
|
||||||
WorkingDirectory=/opt/haixun
|
|
||||||
EnvironmentFile=/opt/haixun/etc/haixun.env
|
|
||||||
ExecStart=/opt/haixun/bin/worker -f /opt/haixun/etc/gateway.worker.prod.yaml
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
LimitNOFILE=65535
|
|
||||||
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=full
|
|
||||||
ProtectHome=true
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
@ -5,23 +5,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"haixun-backend/internal/config"
|
"haixun-backend/internal/config"
|
||||||
libcrypto "haixun-backend/internal/library/crypto"
|
|
||||||
libmongo "haixun-backend/internal/library/mongo"
|
libmongo "haixun-backend/internal/library/mongo"
|
||||||
brandrepo "haixun-backend/internal/model/brand/repository"
|
|
||||||
cmatrixrepo "haixun-backend/internal/model/content_matrix/repository"
|
|
||||||
copydraftrepo "haixun-backend/internal/model/copy_draft/repository"
|
|
||||||
copymissionrepo "haixun-backend/internal/model/copy_mission/repository"
|
|
||||||
jobrepo "haixun-backend/internal/model/job/repository"
|
jobrepo "haixun-backend/internal/model/job/repository"
|
||||||
kgrepo "haixun-backend/internal/model/knowledge_graph/repository"
|
|
||||||
memberrepo "haixun-backend/internal/model/member/repository"
|
memberrepo "haixun-backend/internal/model/member/repository"
|
||||||
outreachdraftrepo "haixun-backend/internal/model/outreach_draft/repository"
|
|
||||||
permissionrepo "haixun-backend/internal/model/permission/repository"
|
permissionrepo "haixun-backend/internal/model/permission/repository"
|
||||||
permissionuc "haixun-backend/internal/model/permission/usecase"
|
permissionuc "haixun-backend/internal/model/permission/usecase"
|
||||||
personarepo "haixun-backend/internal/model/persona/repository"
|
|
||||||
placementtopicrepo "haixun-backend/internal/model/placement_topic/repository"
|
|
||||||
scanpostrepo "haixun-backend/internal/model/scan_post/repository"
|
|
||||||
settingrepo "haixun-backend/internal/model/setting/repository"
|
settingrepo "haixun-backend/internal/model/setting/repository"
|
||||||
threadsaccountrepo "haixun-backend/internal/model/threads_account/repository"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type InitOptions struct {
|
type InitOptions struct {
|
||||||
|
|
@ -59,12 +48,6 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
db := mongoClient.Database()
|
db := mongoClient.Database()
|
||||||
report := &InitReport{}
|
report := &InitReport{}
|
||||||
|
|
||||||
// cipher 只用於資料加解密;EnsureIndexes 不會用到,但 secrets repo 建構子需要它。
|
|
||||||
secretsCipher, err := libcrypto.New(cfg.Secrets.EncryptionKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("init secrets cipher: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
settingRepository := settingrepo.NewMongoRepository(db)
|
settingRepository := settingrepo.NewMongoRepository(db)
|
||||||
memberRepository := memberrepo.NewMongoRepository(db)
|
memberRepository := memberrepo.NewMongoRepository(db)
|
||||||
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
permissionRepository := permissionrepo.NewMongoPermissionRepository(db)
|
||||||
|
|
@ -86,17 +69,6 @@ func Init(ctx context.Context, cfg config.Config, opts InitOptions) (*InitReport
|
||||||
{"job_runs", jobRunRepository.EnsureIndexes},
|
{"job_runs", jobRunRepository.EnsureIndexes},
|
||||||
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
{"job_schedules", jobScheduleRepository.EnsureIndexes},
|
||||||
{"job_events", jobEventRepository.EnsureIndexes},
|
{"job_events", jobEventRepository.EnsureIndexes},
|
||||||
{"copy_missions", copymissionrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"copy_drafts", copydraftrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"scan_posts", scanpostrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"outreach_drafts", outreachdraftrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"content_matrix", cmatrixrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"knowledge_graph", kgrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"personas", personarepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"brands", brandrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"placement_topics", placementtopicrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"threads_accounts", threadsaccountrepo.NewMongoRepository(db).EnsureIndexes},
|
|
||||||
{"threads_account_secrets", threadsaccountrepo.NewSecretsMongoRepository(db, secretsCipher).EnsureIndexes},
|
|
||||||
}
|
}
|
||||||
for _, repo := range repos {
|
for _, repo := range repos {
|
||||||
if err := repo.fn(ctx); err != nil {
|
if err := repo.fn(ctx); err != nil {
|
||||||
|
|
@ -9,9 +9,8 @@ type MongoConf struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedisConf struct {
|
type RedisConf struct {
|
||||||
Addr string `json:",optional"`
|
Addr string `json:",optional"`
|
||||||
Password string `json:",optional"`
|
DB int `json:",optional"`
|
||||||
DB int `json:",optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JobWorkerConf struct {
|
type JobWorkerConf struct {
|
||||||
|
|
@ -35,19 +34,13 @@ type AuthConf struct {
|
||||||
RefreshSecret string `json:",optional"`
|
RefreshSecret string `json:",optional"`
|
||||||
AccessExpireSeconds int64 `json:",default=900"`
|
AccessExpireSeconds int64 `json:",default=900"`
|
||||||
RefreshExpireSeconds int64 `json:",default=2592000"`
|
RefreshExpireSeconds int64 `json:",default=2592000"`
|
||||||
DevHeaderFallback bool `json:",default=false"`
|
DevHeaderFallback bool `json:",default=true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InternalWorkerConf struct {
|
type InternalWorkerConf struct {
|
||||||
Secret string `json:",optional"`
|
Secret string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SecretsConf holds the application-layer encryption key (base64 of 32 bytes)
|
|
||||||
// used to encrypt sensitive data at rest (browser session, third-party API keys).
|
|
||||||
type SecretsConf struct {
|
|
||||||
EncryptionKey string `json:",optional"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BraveConf struct {
|
type BraveConf struct {
|
||||||
APIKey string `json:",optional"`
|
APIKey string `json:",optional"`
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +50,6 @@ type Config struct {
|
||||||
Mongo MongoConf `json:",optional"`
|
Mongo MongoConf `json:",optional"`
|
||||||
Redis RedisConf `json:",optional"`
|
Redis RedisConf `json:",optional"`
|
||||||
Auth AuthConf `json:",optional"`
|
Auth AuthConf `json:",optional"`
|
||||||
Secrets SecretsConf `json:",optional"`
|
|
||||||
InternalWorker InternalWorkerConf `json:",optional"`
|
InternalWorker InternalWorkerConf `json:",optional"`
|
||||||
JobWorker JobWorkerConf `json:",optional"`
|
JobWorker JobWorkerConf `json:",optional"`
|
||||||
JobScheduler JobSchedulerConf `json:",optional"`
|
JobScheduler JobSchedulerConf `json:",optional"`
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue