feat: move account service to repo root

# Conflicts:
#	account/Makefile
#	account/go.mod
#	docs/account-admin-settings.md
#	docs/account-svc-plus.md
This commit is contained in:
Haitao Pan 2026-01-16 16:15:23 +08:00
parent 232da26361
commit 89bd31458f
87 changed files with 1294 additions and 1093 deletions

29
Dockerfile.accounts-api Normal file
View File

@ -0,0 +1,29 @@
# ------------------------------
# Stage 1 — Build
# ------------------------------
FROM golang:1.25 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o accounts-api ./cmd/accountsapi
# ------------------------------
# Stage 2 — Runtime
# ------------------------------
FROM ubuntu:24.04
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=builder /src/accounts-api /usr/local/bin/accounts-api
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/accounts-api"]

539
Makefile
View File

@ -1,333 +1,294 @@
OS := $(shell uname -s)
SHELL := /bin/bash
O_BIN ?= /usr/local/go/bin
PG_MAJOR ?= 16
NODE_MAJOR ?= 22
BASE_IMAGE_DIR ?= deploy/base-images
OPENRESTY_IMAGE ?= xcontrol/openresty-geoip:latest
POSTGRES_EXT_IMAGE ?= xcontrol/postgres-extensions:16
NODE_BUILDER_IMAGE ?= xcontrol/node-builder:22
NODE_RUNTIME_IMAGE ?= xcontrol/node-runtime:22
GO_BUILDER_IMAGE ?= xcontrol/go-builder:1.23
GO_RUNTIME_IMAGE ?= xcontrol/go-runtime:1.23
ARCH := $(shell dpkg --print-architecture)
PG_DSN ?= postgres://shenlan:password@127.0.0.1:5432/xserver?sslmode=disable
# =========================================
# 📦 XControl Account Service Makefile
# =========================================
ifeq ($(shell id -u),0)
SUDO :=
else
SUDO ?= sudo
endif
APP_NAME := xcontrol-account
MAIN_FILE := ./cmd/accountsvc/main.go
PORT ?= 8080
OS := $(shell uname -s)
HOSTS_FILE ?= /etc/hosts
HOSTS_IP ?= 127.0.0.1
HOSTS_DOMAINS ?= dev-accounts.svc.plus dev-api.svc.plus
DB_NAME := account
DB_USER := shenlan
DB_PASS := password
DB_HOST := 127.0.0.1
DB_PORT := 5432
DB_URL := postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
ifeq ($(OS),Darwin)
NGINX_PREFIX ?= /opt/homebrew/openresty/nginx
NGINX_MAIN_TEMPLATE ?= example/macos/openresty/nginx.conf
else
NGINX_PREFIX ?= /usr/local/openresty/nginx
endif
REPLICATION_MODE ?= pgsync
NGINX_CONF_ROOT ?= $(NGINX_PREFIX)/conf
NGINX_CONF_DIR ?= $(NGINX_CONF_ROOT)/conf.d
NGINX_MAIN_CONF ?= $(NGINX_CONF_ROOT)/nginx.conf
DB_ADMIN_USER ?= $(DB_USER)
DB_ADMIN_PASS ?= $(DB_PASS)
NGINX_SIT_CONFIGS := example/sit/nginx/nginx.conf
NGINX_SIT_CONFIGS += example/sit/nginx/dev.svc.plus.conf
NGINX_SIT_CONFIGS += example/sit/nginx/dev-api.svc.plus.conf
NGINX_SIT_CONFIGS := example/sit/nginx/dev-accounts.svc.plus.conf
SCHEMA_FILE := ./sql/schema.sql
PGLOGICAL_INIT_FILE := ./sql/schema_pglogical_init.sql
PGLOGICAL_PATCH_FILE := ./sql/schema_pglogical_patch.sql
PGLOGICAL_REGION_FILE := ./sql/schema_pglogical_region.sql
NGINX_PROD_CONFIGS := example/prod/nginx/nginx.conf
NGINX_PROD_CONFIGS := example/prod/nginx/dev.svc.plus.conf
NGINX_PROD_CONFIGS := example/prod/nginx/api.svc.plus.conf
NGINX_PROD_CONFIGS := example/prod/nginx/accounts.svc.plus.conf
ACCOUNT_EXPORT_FILE ?= account-export.yaml
ACCOUNT_IMPORT_FILE ?= account-export.yaml
ACCOUNT_EMAIL_KEYWORD ?=
ACCOUNT_SYNC_CONFIG ?= config/sync.yaml
SUPERADMIN_USERNAME ?= Admin
SUPERADMIN_PASSWORD ?= ChangeMe
SUPERADMIN_EMAIL ?= admin@svc.plus
NGINX_ALL_CONFIGS := $(NGINX_SIT_CONFIGS) $(NGINX_PROD_CONFIGS)
export PATH := /usr/local/go/bin:$(PATH)
export PATH := $(GO_BIN):$(PATH)
# =========================================
# 🧩 基础命令
# =========================================
# -----------------------------------------------------------------------------
# Environment bootstrap (hosts & services)
# -----------------------------------------------------------------------------
.PHONY: all init build clean start stop restart dev test help \
init-db-core init-db-replication init-db-pglogical \
reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset
init: configure-hosts init-nginx init-account init-rag-server
all: build
install-services: configure-hosts install-nginx install-account install-rag-server
help:
@echo "🧭 XControl Account Service Makefile"
@echo "make init 初始化 Go 环境与数据库"
@echo "make init-db 执行数据库 schema支持 REPLICATION_MODE=pgsync|pglogical"
@echo "make create-db-user 创建数据库用户并授权"
@echo "make db-reset 重置整个 PostgreSQL 集群 (危险操作!)"
@echo "make migrate-db 执行数据库迁移"
@echo "make dump-schema 导出数据库 schema"
@echo "make account-export 导出账号数据为 YAML"
@echo "make account-import 从 YAML 导入账号数据"
@echo "make create-super-admin 创建超级管理员"
@echo "make reinit-db 重置业务 schema (不涉及 pglogical)"
@echo "make reinit-pglogical 重新初始化 pglogical schema"
@echo "make dev 热重载开发模式"
@echo "make clean 清理构建产物"
upgrade-services: configure-hosts upgrade-nginx upgrade-account upgrade-rag-server
# =========================================
# 🧰 初始化
# =========================================
configure-hosts:
@set -e; \
if [ ! -f "$(HOSTS_FILE)" ]; then \
echo "⚠️ Hosts file $(HOSTS_FILE) not found; skipping host configuration."; \
init: init-go init-db
init-go:
@if [ ! -f go.mod ]; then \
echo ">>> go.mod not found, initializing module"; \
go mod init account; \
fi
go mod tidy
@echo ">>> 检查 Go 环境"
@if ! command -v go >/dev/null; then \
echo "未安装 Go自动安装中..."; \
([ "$(OS)" = "Darwin" ] && brew install go@1.24 && brew link --overwrite --force go@1.24) || \
(sudo apt-get update && sudo apt-get install -y golang); \
fi
@echo ">>> 配置 Go Proxy"
@(curl -fsSL --max-time 5 https://goproxy.cn >/dev/null && go env -w GOPROXY=https://goproxy.cn,direct) || \
(go env -w GOPROXY=https://proxy.golang.org,direct)
@go mod tidy
init-db:
@echo ">>> 初始化数据库 schema"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@$(MAKE) init-db-core
@$(MAKE) init-db-replication
init-db-core:
@echo ">>> 初始化业务 schema ($(SCHEMA_FILE))"
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(SCHEMA_FILE)
init-db-replication:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
$(MAKE) init-db-pglogical; \
else \
for domain in $(HOSTS_DOMAINS); do \
if grep -qE "^[[:space:]]*$(HOSTS_IP)[[:space:]]+.*\b$$domain\b" "$(HOSTS_FILE)"; then \
echo "✅ Hosts entry exists for $$domain"; \
else \
echo " Adding $(HOSTS_IP) $$domain to $(HOSTS_FILE)"; \
echo "$(HOSTS_IP) $$domain" | $(SUDO) tee -a "$(HOSTS_FILE)" >/dev/null; \
fi; \
done; \
echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=$(REPLICATION_MODE))"; \
fi
init-nginx:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
if [ -f "$(NGINX_MAIN_CONF)" ]; then \
if cmp -s "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; then \
echo "$(NGINX_MAIN_CONF) already up to date"; \
else \
echo "⬆️ Updating $(NGINX_MAIN_CONF) from template"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi; \
else \
echo " Installing $(NGINX_MAIN_CONF)"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi; \
fi
@for file in $(NGINX_ALL_CONFIGS); do \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
if [ -f "$$dest" ]; then \
echo "$$dest already exists; skipping"; \
else \
echo " Installing $$dest"; \
$(SUDO) install -m 0644 "$$file" "$$dest"; \
fi; \
done
init-db-pglogical:
@if [ -f $(PGLOGICAL_INIT_FILE) ]; then \
echo ">>> 初始化 pglogical schema (REPLICATION_MODE=pglogical)"; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" 2>/dev/null | grep -qx 't'; then \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
elif psql "$(DB_URL)" -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" | grep -qx 't'; then \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
else \
echo "⚠️ 当前用户非超级用户,跳过 pglogical 初始化"; \
fi; \
fi; \
if [ -f $(PGLOGICAL_PATCH_FILE) ]; then \
echo ">>> 应用 pglogical 默认值补丁"; \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_PATCH_FILE); \
fi
install-nginx: init-nginx reload-openresty
# =========================================
# 🧠 PGLogical 双节点初始化
# =========================================
upgrade-nginx:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
echo "⬆️ Updating $(NGINX_MAIN_CONF)"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi
@for file in $(NGINX_ALL_CONFIGS); do \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
echo "⬆️ Updating $$dest"; \
$(SUDO) install -m 0644 "$$file" "$$dest"; \
done
@$(MAKE) reload-openresty
init-pglogical-region:
@[ -n "$(REGION_DB_URL)" ] || (echo "❌ 缺少 REGION_DB_URL"; exit 1)
@[ -n "$(NODE_NAME)" ] || (echo "❌ 缺少 NODE_NAME"; exit 1)
@[ -n "$(NODE_DSN)" ] || (echo "❌ 缺少 NODE_DSN"; exit 1)
@[ -n "$(SUBSCRIPTION_NAME)" ] || (echo "❌ 缺少 SUBSCRIPTION_NAME"; exit 1)
@[ -n "$(PROVIDER_DSN)" ] || (echo "❌ 缺少 PROVIDER_DSN"; exit 1)
@psql "$(REGION_DB_URL)" -v ON_ERROR_STOP=1 \
-v NODE_NAME="$(NODE_NAME)" \
-v NODE_DSN="$(NODE_DSN)" \
-v SUBSCRIPTION_NAME="$(SUBSCRIPTION_NAME)" \
-v PROVIDER_DSN="$(PROVIDER_DSN)" \
-f $(PGLOGICAL_REGION_FILE)
reload-openresty:
@echo "🔄 Reloading OpenResty/Nginx if available..."
@command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^openresty.service' && { \
$(SUDO) systemctl reload openresty 2>/dev/null || $(SUDO) systemctl restart openresty 2>/dev/null || true; \
echo "✅ openresty.service reloaded"; \
} || echo " openresty.service not managed by systemd or systemctl missing; please reload manually."
init-pglogical-region-cn:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_cn" \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_global" \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
init-account:
@$(MAKE) -C account init
init-pglogical-region-global:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_global" \
NODE_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_cn" \
PROVIDER_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
install-account:
@$(MAKE) -C account build
# =========================================
# 📦 数据库迁移与管理
# =========================================
upgrade-account:
@$(MAKE) -C account upgrade
create-db-user:
@echo ">>> 创建数据库用户 $(DB_USER)"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@echo "正在以 postgres 超级用户身份创建用户..."
@sudo -u postgres psql -c "CREATE USER $(DB_USER) WITH PASSWORD '$(DB_PASS)';" || echo "⚠️ 用户可能已存在"
@sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $(DB_NAME) TO $(DB_USER);"
@echo "✓ 数据库用户创建完成"
init-rag-server:
@$(MAKE) -C rag-server init
migrate-db:
@echo ">>> 执行数据库迁移"
@go run ./cmd/migratectl/main.go migrate --dsn "$(DB_URL)" --dir sql/migrations
install-rag-server:
@$(MAKE) -C rag-server build
dump-schema:
@echo ">>> 导出 schema 到 $(SCHEMA_FILE)"
@pg_dump -s -O -x "$(DB_URL)" > $(SCHEMA_FILE)
upgrade-rag-server:
@$(MAKE) -C rag-server build
@$(MAKE) -C rag-server restart
db-reset:
@echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..."
@read -p "确定要重置数据库集群? 这将删除所有数据! [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 停止 PostgreSQL 服务 ..."; \
sudo systemctl stop postgresql; \
echo ">>> 删除数据库集群 16 main ..."; \
sudo pg_dropcluster --stop 16 main; \
echo ">>> 清理数据目录 ..."; \
sudo rm -rf /var/lib/postgresql/16/main; \
echo ">>> 清理配置目录 ..."; \
sudo rm -rf /etc/postgresql/16/main; \
echo ">>> 创建新的数据库集群 ..."; \
sudo pg_createcluster 16 main --start; \
echo "✓ PostgreSQL 集群重置完成"; \
else \
echo "取消重置"; \
fi
.PHONY: install install-openresty install-redis install-postgresql init-db \
build update-dashboard-manifests build-server build-dashboard \
start start-openresty start-server start-dashboard \
stop stop-server stop-dashboard stop-openresty restart lint-cms \
init init-nginx install-nginx upgrade-nginx reload-openresty \
init-account install-account upgrade-account \
init-rag-server install-rag-server upgrade-rag-server \
configure-hosts install-services upgrade-services \
build-base-images docker-openresty-geoip docker-postgres-extensions \
docker-node-builder docker-node-runtime docker-go-builder docker-go-runtime
drop-db:
@echo "⚠️ 即将删除数据库 $(DB_NAME) ..."
@read -p "确定要删除数据库 $(DB_NAME)? [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 强制断开现有连接 ..."; \
if ! PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$(DB_NAME)' AND pid <> pg_backend_pid();"; then \
echo "⚠️ 无法断开所有连接(需要超级用户权限)"; \
fi; \
echo ">>> 清理 pglogical schema ..."; \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-c "DROP SCHEMA IF EXISTS pglogical CASCADE;" >/dev/null 2>&1 || \
echo "⚠️ 无法删除 pglogical schema数据库可能不存在或缺少权限"; \
echo ">>> 删除数据库 $(DB_NAME) ..."; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "DROP DATABASE IF EXISTS $(DB_NAME);"; then \
echo ">>> 数据库已删除"; \
else \
echo ">>> 删除失败"; \
fi; \
else \
echo "取消删除"; \
fi
# -----------------------------------------------------------------------------
# Dependency installation
# -----------------------------------------------------------------------------
reset-public-schema:
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -v db_user="$(DB_USER)" -f sql/reset_public_schema.sql
install: install-nodejs install-go install-openresty install-redis install-postgresql
reinit-db:
@echo ">>> 重置业务 schema (sql/schema.sql)"
@$(MAKE) reset-public-schema
@$(MAKE) init-db-core
# --- Node.js ---------------------------------------------------------------
install-nodejs:
ifeq ($(OS),Darwin)
( brew install node@22 && brew link --overwrite --force node@22 ) || brew install node
corepack enable || true
corepack prepare yarn@stable --activate || true
@echo "✅ Node: $$(node -v)"; echo "✅ Yarn: $$(yarn -v 2>/dev/null || echo n/a)"
else
@echo "🟦 Installing Node.js $(NODE_MAJOR) via setup_ubuntu_2204.sh..."
NODE_MAJOR=$(NODE_MAJOR) bash scripts/setup_ubuntu_2204.sh install-nodejs
endif
reinit-pglogical:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
echo ">>> 重新初始化 pglogical schema"; \
$(MAKE) init-db-pglogical; \
else \
echo ">>> 当前 REPLICATION_MODE=$(REPLICATION_MODE),无需 pglogical 处理"; \
fi
# --- Go --------------------------------------------------------------------
install-go:
ifeq ($(OS),Darwin)
brew install go
else
GO_VERSION=$(GO_VERSION) bash scripts/setup_ubuntu_2204.sh install-go
endif
# =========================================
# 💾 账号导入导出
# =========================================
# --- OpenResty -------------------------------------------------------------
install-openresty:
@echo "🚀 Installing OpenResty using external script..."
@bash scripts/install-openresty.sh; \
account-export:
@go run ./cmd/migratectl/main.go export --dsn "$(DB_URL)" --output "$(ACCOUNT_EXPORT_FILE)" $(if $(ACCOUNT_EMAIL_KEYWORD),--email "$(ACCOUNT_EMAIL_KEYWORD)")
# --- Redis -----------------------------------------------------------------
install-redis:
ifeq ($(OS),Darwin)
brew install redis && brew services start redis
else
@echo "🟥 Installing Redis via setup_ubuntu_2204.sh..."
bash scripts/setup_ubuntu_2204.sh install-redis
endif
account-import:
@[ -f "$(ACCOUNT_IMPORT_FILE)" ] || (echo "❌ 未找到文件 $(ACCOUNT_IMPORT_FILE)"; exit 1)
@go run ./cmd/migratectl/main.go import --dsn "$(DB_URL)" --file "$(ACCOUNT_IMPORT_FILE)" \
$(if $(ACCOUNT_IMPORT_MERGE),--merge) \
$(if $(ACCOUNT_IMPORT_MERGE_STRATEGY),--merge-strategy "$(ACCOUNT_IMPORT_MERGE_STRATEGY)") \
$(if $(ACCOUNT_IMPORT_DRY_RUN),--dry-run) \
$(foreach UUID,$(ACCOUNT_IMPORT_MERGE_ALLOWLIST),--merge-allowlist $(UUID)) \
$(ACCOUNT_IMPORT_EXTRA_FLAGS)
# --- PostgreSQL ------------------------------------------------------------
install-postgresql:
ifeq ($(OS),Darwin)
@set -e; \
echo "🍎 Installing PostgreSQL 16 via Homebrew..."; \
brew install postgresql@16 || true; \
brew services start postgresql@16; \
echo "📦 Installing pgvector extension..."; \
brew install pgvector || true; \
echo "📦 Installing pg_jieba (替代 zhparser + scws)..."; \
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
cd pg_jieba && mkdir build && cd build && \
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=$$(brew --prefix postgresql@16)/include/postgresql/server .. && \
make -j$$(sysctl -n hw.ncpu) && sudo make install && \
cd / && rm -rf $$tmp_dir; \
echo "✅ PostgreSQL extensions installed successfully!"
else
@set -e; \
echo "🟨 Installing PostgreSQL 16..."; \
bash scripts/setup_ubuntu_2204.sh install-postgresql; \
echo "🟨 Installing pgvector extension..."; \
bash scripts/setup_ubuntu_2204.sh install-pgvector; \
echo "🟨 Installing pg_jieba extension (替代 zhparser + scws)..."; \
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
sudo apt-get install -y cmake g++ git postgresql-server-dev-${PG_MAJOR}; \
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
cd pg_jieba && mkdir build && cd build && \
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql/${PG_MAJOR}/server .. && \
make -j$$(nproc) && sudo make install && \
cd / && rm -rf $$tmp_dir; \
echo "✅ PostgreSQL extensions installed successfully!"
endif
account-sync-push:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go push --config "$(ACCOUNT_SYNC_CONFIG)"
# -----------------------------------------------------------------------------
# Base container images
# -----------------------------------------------------------------------------
account-sync-pull:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go pull --config "$(ACCOUNT_SYNC_CONFIG)"
build-base-images:
@OPENRESTY_IMAGE=$(OPENRESTY_IMAGE) POSTGRES_EXT_IMAGE=$(POSTGRES_EXT_IMAGE) \
NODE_BUILDER_IMAGE=$(NODE_BUILDER_IMAGE) NODE_RUNTIME_IMAGE=$(NODE_RUNTIME_IMAGE) \
GO_BUILDER_IMAGE=$(GO_BUILDER_IMAGE) GO_RUNTIME_IMAGE=$(GO_RUNTIME_IMAGE) \
bash scripts/build-base-images.sh
account-sync-mirror:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go mirror --config "$(ACCOUNT_SYNC_CONFIG)"
docker-openresty-geoip:
docker build -f $(BASE_IMAGE_DIR)/openresty-geoip.Dockerfile -t $(OPENRESTY_IMAGE) $(BASE_IMAGE_DIR)
create-super-admin:
@[ -n "$(SUPERADMIN_USERNAME)" ] && [ -n "$(SUPERADMIN_PASSWORD)" ] || (echo "❌ 请指定用户名与密码"; exit 1)
@go run ./cmd/createadmin/main.go \
--driver postgres \
--dsn "$(DB_URL)" \
--username "$(SUPERADMIN_USERNAME)" \
--password "$(SUPERADMIN_PASSWORD)" \
--email "$(SUPERADMIN_EMAIL)"
docker-postgres-extensions:
docker build -f $(BASE_IMAGE_DIR)/postgres-extensions.Dockerfile -t $(POSTGRES_EXT_IMAGE) $(BASE_IMAGE_DIR)
# =========================================
# ⚙️ 编译与运行
# =========================================
docker-node-builder:
docker build -f $(BASE_IMAGE_DIR)/node-builder.Dockerfile -t $(NODE_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
build: init-go
@go build -o $(APP_NAME) $(MAIN_FILE)
docker-node-runtime:
docker build -f $(BASE_IMAGE_DIR)/node-runtime.Dockerfile -t $(NODE_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
upgrade: build
systemctl stop xcontrol-account
cp xcontrol-account /usr/bin/xcontrol-account
systemctl start xcontrol-account
docker-go-builder:
docker build -f $(BASE_IMAGE_DIR)/go-builder.Dockerfile -t $(GO_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
start: build
@./$(APP_NAME) --config config/account.yaml
docker-go-runtime:
docker build -f $(BASE_IMAGE_DIR)/go-runtime.Dockerfile -t $(GO_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
# -----------------------------------------------------------------------------
# Database initialization
# -----------------------------------------------------------------------------
init-db:
@psql $(PG_DSN) -f rag-server/sql/schema.sql
# -----------------------------------------------------------------------------
# Build targets
# -----------------------------------------------------------------------------
build: update-dashboard-manifests build-cli build-server build-dashboard
build-cli:
$(MAKE) -C rag-server/cmd/rag-server-cli build
build-server:
$(MAKE) -C rag-server build
build-dashboard:
$(MAKE) -C dashboard build SKIP_SYNC=1
update-dashboard-manifests:
$(MAKE) -C dashboard sync-dl-index
# -----------------------------------------------------------------------------
# Run targets
# -----------------------------------------------------------------------------
start: start-openresty start-server start-dashboard
start-server:
$(MAKE) -C rag-server start
start-dashboard:
$(MAKE) -C dashboard start
stop: stop-server stop-dashboard stop-openresty
stop-server:
$(MAKE) -C rag-server stop
stop-dashboard:
$(MAKE) -C dashboard stop
start-openresty:
ifeq ($(OS),Darwin)
@brew services start openresty >/dev/null 2>&1 || \
( echo "Creating LaunchAgent for OpenResty..." && \
mkdir -p ~/Library/LaunchAgents && \
printf '%s\n' '<?xml version="1.0" encoding="UTF-8?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0"><dict>' \
' <key>Label</key><string>homebrew.mxcl.openresty</string>' \
' <key>ProgramArguments</key>' \
' <array>' \
' <string>/opt/homebrew/openresty/nginx/sbin/nginx</string>' \
' <string>-g</string>' \
' <string>daemon off;</string>' \
' </array>' \
' <key>RunAtLoad</key><true/>' \
'</dict></plist>' \
> ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist && \
brew services start ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist )
else
sudo systemctl enable --now openresty || echo "⚠️ openresty.service missing or inactive"
endif
stop-openresty:
ifeq ($(OS),Darwin)
-brew services stop openresty >/dev/null 2>&1
else
-sudo systemctl stop openresty >/dev/null 2>&1
endif
stop:
@pkill -f "$(APP_NAME)" || echo "⚠️ 未找到运行进程"
restart: stop start
# -----------------------------------------------------------------------------
# CMS configuration validation
# -----------------------------------------------------------------------------
lint-cms:
python3 scripts/validate_cms_config.py
test:
go test ./...
clean:
rm -f $(APP_NAME) *.pid *.log

333
Makefile.account Normal file
View File

@ -0,0 +1,333 @@
OS := $(shell uname -s)
SHELL := /bin/bash
O_BIN ?= /usr/local/go/bin
PG_MAJOR ?= 16
NODE_MAJOR ?= 22
BASE_IMAGE_DIR ?= deploy/base-images
OPENRESTY_IMAGE ?= xcontrol/openresty-geoip:latest
POSTGRES_EXT_IMAGE ?= xcontrol/postgres-extensions:16
NODE_BUILDER_IMAGE ?= xcontrol/node-builder:22
NODE_RUNTIME_IMAGE ?= xcontrol/node-runtime:22
GO_BUILDER_IMAGE ?= xcontrol/go-builder:1.23
GO_RUNTIME_IMAGE ?= xcontrol/go-runtime:1.23
ARCH := $(shell dpkg --print-architecture)
PG_DSN ?= postgres://shenlan:password@127.0.0.1:5432/xserver?sslmode=disable
ifeq ($(shell id -u),0)
SUDO :=
else
SUDO ?= sudo
endif
HOSTS_FILE ?= /etc/hosts
HOSTS_IP ?= 127.0.0.1
HOSTS_DOMAINS ?= dev-accounts.svc.plus dev-api.svc.plus
ifeq ($(OS),Darwin)
NGINX_PREFIX ?= /opt/homebrew/openresty/nginx
NGINX_MAIN_TEMPLATE ?= example/macos/openresty/nginx.conf
else
NGINX_PREFIX ?= /usr/local/openresty/nginx
endif
NGINX_CONF_ROOT ?= $(NGINX_PREFIX)/conf
NGINX_CONF_DIR ?= $(NGINX_CONF_ROOT)/conf.d
NGINX_MAIN_CONF ?= $(NGINX_CONF_ROOT)/nginx.conf
NGINX_SIT_CONFIGS := example/sit/nginx/nginx.conf
NGINX_SIT_CONFIGS += example/sit/nginx/dev.svc.plus.conf
NGINX_SIT_CONFIGS += example/sit/nginx/dev-api.svc.plus.conf
NGINX_SIT_CONFIGS := example/sit/nginx/dev-accounts.svc.plus.conf
NGINX_PROD_CONFIGS := example/prod/nginx/nginx.conf
NGINX_PROD_CONFIGS := example/prod/nginx/dev.svc.plus.conf
NGINX_PROD_CONFIGS := example/prod/nginx/api.svc.plus.conf
NGINX_PROD_CONFIGS := example/prod/nginx/accounts.svc.plus.conf
NGINX_ALL_CONFIGS := $(NGINX_SIT_CONFIGS) $(NGINX_PROD_CONFIGS)
export PATH := $(GO_BIN):$(PATH)
# -----------------------------------------------------------------------------
# Environment bootstrap (hosts & services)
# -----------------------------------------------------------------------------
init: configure-hosts init-nginx init-account init-rag-server
install-services: configure-hosts install-nginx install-account install-rag-server
upgrade-services: configure-hosts upgrade-nginx upgrade-account upgrade-rag-server
configure-hosts:
@set -e; \
if [ ! -f "$(HOSTS_FILE)" ]; then \
echo "⚠️ Hosts file $(HOSTS_FILE) not found; skipping host configuration."; \
else \
for domain in $(HOSTS_DOMAINS); do \
if grep -qE "^[[:space:]]*$(HOSTS_IP)[[:space:]]+.*\b$$domain\b" "$(HOSTS_FILE)"; then \
echo "✅ Hosts entry exists for $$domain"; \
else \
echo " Adding $(HOSTS_IP) $$domain to $(HOSTS_FILE)"; \
echo "$(HOSTS_IP) $$domain" | $(SUDO) tee -a "$(HOSTS_FILE)" >/dev/null; \
fi; \
done; \
fi
init-nginx:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
if [ -f "$(NGINX_MAIN_CONF)" ]; then \
if cmp -s "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; then \
echo "$(NGINX_MAIN_CONF) already up to date"; \
else \
echo "⬆️ Updating $(NGINX_MAIN_CONF) from template"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi; \
else \
echo " Installing $(NGINX_MAIN_CONF)"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi; \
fi
@for file in $(NGINX_ALL_CONFIGS); do \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
if [ -f "$$dest" ]; then \
echo "$$dest already exists; skipping"; \
else \
echo " Installing $$dest"; \
$(SUDO) install -m 0644 "$$file" "$$dest"; \
fi; \
done
install-nginx: init-nginx reload-openresty
upgrade-nginx:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)"
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \
echo "⬆️ Updating $(NGINX_MAIN_CONF)"; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \
fi
@for file in $(NGINX_ALL_CONFIGS); do \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \
echo "⬆️ Updating $$dest"; \
$(SUDO) install -m 0644 "$$file" "$$dest"; \
done
@$(MAKE) reload-openresty
reload-openresty:
@echo "🔄 Reloading OpenResty/Nginx if available..."
@command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^openresty.service' && { \
$(SUDO) systemctl reload openresty 2>/dev/null || $(SUDO) systemctl restart openresty 2>/dev/null || true; \
echo "✅ openresty.service reloaded"; \
} || echo " openresty.service not managed by systemd or systemctl missing; please reload manually."
init-account:
@$(MAKE) -C account init
install-account:
@$(MAKE) -C account build
upgrade-account:
@$(MAKE) -C account upgrade
init-rag-server:
@$(MAKE) -C rag-server init
install-rag-server:
@$(MAKE) -C rag-server build
upgrade-rag-server:
@$(MAKE) -C rag-server build
@$(MAKE) -C rag-server restart
.PHONY: install install-openresty install-redis install-postgresql init-db \
build update-dashboard-manifests build-server build-dashboard \
start start-openresty start-server start-dashboard \
stop stop-server stop-dashboard stop-openresty restart lint-cms \
init init-nginx install-nginx upgrade-nginx reload-openresty \
init-account install-account upgrade-account \
init-rag-server install-rag-server upgrade-rag-server \
configure-hosts install-services upgrade-services \
build-base-images docker-openresty-geoip docker-postgres-extensions \
docker-node-builder docker-node-runtime docker-go-builder docker-go-runtime
# -----------------------------------------------------------------------------
# Dependency installation
# -----------------------------------------------------------------------------
install: install-nodejs install-go install-openresty install-redis install-postgresql
# --- Node.js ---------------------------------------------------------------
install-nodejs:
ifeq ($(OS),Darwin)
( brew install node@22 && brew link --overwrite --force node@22 ) || brew install node
corepack enable || true
corepack prepare yarn@stable --activate || true
@echo "✅ Node: $$(node -v)"; echo "✅ Yarn: $$(yarn -v 2>/dev/null || echo n/a)"
else
@echo "🟦 Installing Node.js $(NODE_MAJOR) via setup_ubuntu_2204.sh..."
NODE_MAJOR=$(NODE_MAJOR) bash scripts/setup_ubuntu_2204.sh install-nodejs
endif
# --- Go --------------------------------------------------------------------
install-go:
ifeq ($(OS),Darwin)
brew install go
else
GO_VERSION=$(GO_VERSION) bash scripts/setup_ubuntu_2204.sh install-go
endif
# --- OpenResty -------------------------------------------------------------
install-openresty:
@echo "🚀 Installing OpenResty using external script..."
@bash scripts/install-openresty.sh; \
# --- Redis -----------------------------------------------------------------
install-redis:
ifeq ($(OS),Darwin)
brew install redis && brew services start redis
else
@echo "🟥 Installing Redis via setup_ubuntu_2204.sh..."
bash scripts/setup_ubuntu_2204.sh install-redis
endif
# --- PostgreSQL ------------------------------------------------------------
install-postgresql:
ifeq ($(OS),Darwin)
@set -e; \
echo "🍎 Installing PostgreSQL 16 via Homebrew..."; \
brew install postgresql@16 || true; \
brew services start postgresql@16; \
echo "📦 Installing pgvector extension..."; \
brew install pgvector || true; \
echo "📦 Installing pg_jieba (替代 zhparser + scws)..."; \
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
cd pg_jieba && mkdir build && cd build && \
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=$$(brew --prefix postgresql@16)/include/postgresql/server .. && \
make -j$$(sysctl -n hw.ncpu) && sudo make install && \
cd / && rm -rf $$tmp_dir; \
echo "✅ PostgreSQL extensions installed successfully!"
else
@set -e; \
echo "🟨 Installing PostgreSQL 16..."; \
bash scripts/setup_ubuntu_2204.sh install-postgresql; \
echo "🟨 Installing pgvector extension..."; \
bash scripts/setup_ubuntu_2204.sh install-pgvector; \
echo "🟨 Installing pg_jieba extension (替代 zhparser + scws)..."; \
tmp_dir=$$(mktemp -d) && cd $$tmp_dir && \
sudo apt-get install -y cmake g++ git postgresql-server-dev-${PG_MAJOR}; \
git clone --recursive https://github.com/jaiminpan/pg_jieba.git && \
cd pg_jieba && mkdir build && cd build && \
cmake -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql/${PG_MAJOR}/server .. && \
make -j$$(nproc) && sudo make install && \
cd / && rm -rf $$tmp_dir; \
echo "✅ PostgreSQL extensions installed successfully!"
endif
# -----------------------------------------------------------------------------
# Base container images
# -----------------------------------------------------------------------------
build-base-images:
@OPENRESTY_IMAGE=$(OPENRESTY_IMAGE) POSTGRES_EXT_IMAGE=$(POSTGRES_EXT_IMAGE) \
NODE_BUILDER_IMAGE=$(NODE_BUILDER_IMAGE) NODE_RUNTIME_IMAGE=$(NODE_RUNTIME_IMAGE) \
GO_BUILDER_IMAGE=$(GO_BUILDER_IMAGE) GO_RUNTIME_IMAGE=$(GO_RUNTIME_IMAGE) \
bash scripts/build-base-images.sh
docker-openresty-geoip:
docker build -f $(BASE_IMAGE_DIR)/openresty-geoip.Dockerfile -t $(OPENRESTY_IMAGE) $(BASE_IMAGE_DIR)
docker-postgres-extensions:
docker build -f $(BASE_IMAGE_DIR)/postgres-extensions.Dockerfile -t $(POSTGRES_EXT_IMAGE) $(BASE_IMAGE_DIR)
docker-node-builder:
docker build -f $(BASE_IMAGE_DIR)/node-builder.Dockerfile -t $(NODE_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
docker-node-runtime:
docker build -f $(BASE_IMAGE_DIR)/node-runtime.Dockerfile -t $(NODE_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
docker-go-builder:
docker build -f $(BASE_IMAGE_DIR)/go-builder.Dockerfile -t $(GO_BUILDER_IMAGE) $(BASE_IMAGE_DIR)
docker-go-runtime:
docker build -f $(BASE_IMAGE_DIR)/go-runtime.Dockerfile -t $(GO_RUNTIME_IMAGE) $(BASE_IMAGE_DIR)
# -----------------------------------------------------------------------------
# Database initialization
# -----------------------------------------------------------------------------
init-db:
@psql $(PG_DSN) -f rag-server/sql/schema.sql
# -----------------------------------------------------------------------------
# Build targets
# -----------------------------------------------------------------------------
build: update-dashboard-manifests build-cli build-server build-dashboard
build-cli:
$(MAKE) -C rag-server/cmd/rag-server-cli build
build-server:
$(MAKE) -C rag-server build
build-dashboard:
$(MAKE) -C dashboard build SKIP_SYNC=1
update-dashboard-manifests:
$(MAKE) -C dashboard sync-dl-index
# -----------------------------------------------------------------------------
# Run targets
# -----------------------------------------------------------------------------
start: start-openresty start-server start-dashboard
start-server:
$(MAKE) -C rag-server start
start-dashboard:
$(MAKE) -C dashboard start
stop: stop-server stop-dashboard stop-openresty
stop-server:
$(MAKE) -C rag-server stop
stop-dashboard:
$(MAKE) -C dashboard stop
start-openresty:
ifeq ($(OS),Darwin)
@brew services start openresty >/dev/null 2>&1 || \
( echo "Creating LaunchAgent for OpenResty..." && \
mkdir -p ~/Library/LaunchAgents && \
printf '%s\n' '<?xml version="1.0" encoding="UTF-8?>' \
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
'<plist version="1.0"><dict>' \
' <key>Label</key><string>homebrew.mxcl.openresty</string>' \
' <key>ProgramArguments</key>' \
' <array>' \
' <string>/opt/homebrew/openresty/nginx/sbin/nginx</string>' \
' <string>-g</string>' \
' <string>daemon off;</string>' \
' </array>' \
' <key>RunAtLoad</key><true/>' \
'</dict></plist>' \
> ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist && \
brew services start ~/Library/LaunchAgents/homebrew.mxcl.openresty.plist )
else
sudo systemctl enable --now openresty || echo "⚠️ openresty.service missing or inactive"
endif
stop-openresty:
ifeq ($(OS),Darwin)
-brew services stop openresty >/dev/null 2>&1
else
-sudo systemctl stop openresty >/dev/null 2>&1
endif
restart: stop start
# -----------------------------------------------------------------------------
# CMS configuration validation
# -----------------------------------------------------------------------------
lint-cms:
python3 scripts/validate_cms_config.py

View File

@ -1,294 +0,0 @@
# =========================================
# 📦 XControl Account Service Makefile
# =========================================
APP_NAME := xcontrol-account
MAIN_FILE := ./cmd/accountsvc/main.go
PORT ?= 8080
OS := $(shell uname -s)
DB_NAME := account
DB_USER := shenlan
DB_PASS := password
DB_HOST := 127.0.0.1
DB_PORT := 5432
DB_URL := postgres://$(DB_USER):$(DB_PASS)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=disable
REPLICATION_MODE ?= pgsync
DB_ADMIN_USER ?= $(DB_USER)
DB_ADMIN_PASS ?= $(DB_PASS)
SCHEMA_FILE := ../sql/schema.sql
PGLOGICAL_INIT_FILE := ../sql/schema_pglogical_init.sql
PGLOGICAL_PATCH_FILE := ../sql/schema_pglogical_patch.sql
PGLOGICAL_REGION_FILE := ../sql/schema_pglogical_region.sql
ACCOUNT_EXPORT_FILE ?= account-export.yaml
ACCOUNT_IMPORT_FILE ?= account-export.yaml
ACCOUNT_EMAIL_KEYWORD ?=
ACCOUNT_SYNC_CONFIG ?= config/sync.yaml
SUPERADMIN_USERNAME ?= Admin
SUPERADMIN_PASSWORD ?= ChangeMe
SUPERADMIN_EMAIL ?= admin@svc.plus
export PATH := /usr/local/go/bin:$(PATH)
# =========================================
# 🧩 基础命令
# =========================================
.PHONY: all init build clean start stop restart dev test help \
init-db-core init-db-replication init-db-pglogical \
reinit-pglogical account-sync-push account-sync-pull account-sync-mirror create-db-user db-reset
all: build
help:
@echo "🧭 XControl Account Service Makefile"
@echo "make init 初始化 Go 环境与数据库"
@echo "make init-db 执行数据库 schema支持 REPLICATION_MODE=pgsync|pglogical"
@echo "make create-db-user 创建数据库用户并授权"
@echo "make db-reset 重置整个 PostgreSQL 集群 (危险操作!)"
@echo "make migrate-db 执行数据库迁移"
@echo "make dump-schema 导出数据库 schema"
@echo "make account-export 导出账号数据为 YAML"
@echo "make account-import 从 YAML 导入账号数据"
@echo "make create-super-admin 创建超级管理员"
@echo "make reinit-db 重置业务 schema (不涉及 pglogical)"
@echo "make reinit-pglogical 重新初始化 pglogical schema"
@echo "make dev 热重载开发模式"
@echo "make clean 清理构建产物"
# =========================================
# 🧰 初始化
# =========================================
init: init-go init-db
init-go:
@if [ ! -f go.mod ]; then \
echo ">>> go.mod not found, initializing module"; \
go mod init account; \
fi
go mod tidy
@echo ">>> 检查 Go 环境"
@if ! command -v go >/dev/null; then \
echo "未安装 Go自动安装中..."; \
([ "$(OS)" = "Darwin" ] && brew install go@1.24 && brew link --overwrite --force go@1.24) || \
(sudo apt-get update && sudo apt-get install -y golang); \
fi
@echo ">>> 配置 Go Proxy"
@(curl -fsSL --max-time 5 https://goproxy.cn >/dev/null && go env -w GOPROXY=https://goproxy.cn,direct) || \
(go env -w GOPROXY=https://proxy.golang.org,direct)
@go mod tidy
init-db:
@echo ">>> 初始化数据库 schema"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@$(MAKE) init-db-core
@$(MAKE) init-db-replication
init-db-core:
@echo ">>> 初始化业务 schema ($(SCHEMA_FILE))"
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(SCHEMA_FILE)
init-db-replication:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
$(MAKE) init-db-pglogical; \
else \
echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=$(REPLICATION_MODE))"; \
fi
init-db-pglogical:
@if [ -f $(PGLOGICAL_INIT_FILE) ]; then \
echo ">>> 初始化 pglogical schema (REPLICATION_MODE=pglogical)"; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" 2>/dev/null | grep -qx 't'; then \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
elif psql "$(DB_URL)" -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" | grep -qx 't'; then \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
else \
echo "⚠️ 当前用户非超级用户,跳过 pglogical 初始化"; \
fi; \
fi; \
if [ -f $(PGLOGICAL_PATCH_FILE) ]; then \
echo ">>> 应用 pglogical 默认值补丁"; \
psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_PATCH_FILE); \
fi
# =========================================
# 🧠 PGLogical 双节点初始化
# =========================================
init-pglogical-region:
@[ -n "$(REGION_DB_URL)" ] || (echo "❌ 缺少 REGION_DB_URL"; exit 1)
@[ -n "$(NODE_NAME)" ] || (echo "❌ 缺少 NODE_NAME"; exit 1)
@[ -n "$(NODE_DSN)" ] || (echo "❌ 缺少 NODE_DSN"; exit 1)
@[ -n "$(SUBSCRIPTION_NAME)" ] || (echo "❌ 缺少 SUBSCRIPTION_NAME"; exit 1)
@[ -n "$(PROVIDER_DSN)" ] || (echo "❌ 缺少 PROVIDER_DSN"; exit 1)
@psql "$(REGION_DB_URL)" -v ON_ERROR_STOP=1 \
-v NODE_NAME="$(NODE_NAME)" \
-v NODE_DSN="$(NODE_DSN)" \
-v SUBSCRIPTION_NAME="$(SUBSCRIPTION_NAME)" \
-v PROVIDER_DSN="$(PROVIDER_DSN)" \
-f $(PGLOGICAL_REGION_FILE)
init-pglogical-region-cn:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_cn" \
NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_global" \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
init-pglogical-region-global:
@$(MAKE) init-pglogical-region \
REGION_DB_URL="$(DB_URL)" \
NODE_NAME="node_global" \
NODE_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
SUBSCRIPTION_NAME="sub_from_cn" \
PROVIDER_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
# =========================================
# 📦 数据库迁移与管理
# =========================================
create-db-user:
@echo ">>> 创建数据库用户 $(DB_USER)"
@command -v psql >/dev/null || (echo "❌ 未检测到 psql请安装 PostgreSQL 客户端" && exit 1)
@echo "正在以 postgres 超级用户身份创建用户..."
@sudo -u postgres psql -c "CREATE USER $(DB_USER) WITH PASSWORD '$(DB_PASS)';" || echo "⚠️ 用户可能已存在"
@sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $(DB_NAME) TO $(DB_USER);"
@echo "✓ 数据库用户创建完成"
migrate-db:
@echo ">>> 执行数据库迁移"
@go run ./cmd/migratectl/main.go migrate --dsn "$(DB_URL)" --dir ../sql/migrations
dump-schema:
@echo ">>> 导出 schema 到 $(SCHEMA_FILE)"
@pg_dump -s -O -x "$(DB_URL)" > $(SCHEMA_FILE)
db-reset:
@echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..."
@read -p "确定要重置数据库集群? 这将删除所有数据! [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 停止 PostgreSQL 服务 ..."; \
sudo systemctl stop postgresql; \
echo ">>> 删除数据库集群 16 main ..."; \
sudo pg_dropcluster --stop 16 main; \
echo ">>> 清理数据目录 ..."; \
sudo rm -rf /var/lib/postgresql/16/main; \
echo ">>> 清理配置目录 ..."; \
sudo rm -rf /etc/postgresql/16/main; \
echo ">>> 创建新的数据库集群 ..."; \
sudo pg_createcluster 16 main --start; \
echo "✓ PostgreSQL 集群重置完成"; \
else \
echo "取消重置"; \
fi
drop-db:
@echo "⚠️ 即将删除数据库 $(DB_NAME) ..."
@read -p "确定要删除数据库 $(DB_NAME)? [y/N] " confirm && \
if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
echo ">>> 强制断开现有连接 ..."; \
if ! PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$(DB_NAME)' AND pid <> pg_backend_pid();"; then \
echo "⚠️ 无法断开所有连接(需要超级用户权限)"; \
fi; \
echo ">>> 清理 pglogical schema ..."; \
PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
-c "DROP SCHEMA IF EXISTS pglogical CASCADE;" >/dev/null 2>&1 || \
echo "⚠️ 无法删除 pglogical schema数据库可能不存在或缺少权限"; \
echo ">>> 删除数据库 $(DB_NAME) ..."; \
if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
-c "DROP DATABASE IF EXISTS $(DB_NAME);"; then \
echo ">>> 数据库已删除"; \
else \
echo ">>> 删除失败"; \
fi; \
else \
echo "取消删除"; \
fi
reset-public-schema:
@psql "$(DB_URL)" -v ON_ERROR_STOP=1 -v db_user="$(DB_USER)" -f ../sql/reset_public_schema.sql
reinit-db:
@echo ">>> 重置业务 schema ($(SCHEMA_FILE))"
@$(MAKE) reset-public-schema
@$(MAKE) init-db-core
reinit-pglogical:
@if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
echo ">>> 重新初始化 pglogical schema"; \
$(MAKE) init-db-pglogical; \
else \
echo ">>> 当前 REPLICATION_MODE=$(REPLICATION_MODE),无需 pglogical 处理"; \
fi
# =========================================
# 💾 账号导入导出
# =========================================
account-export:
@go run ./cmd/migratectl/main.go export --dsn "$(DB_URL)" --output "$(ACCOUNT_EXPORT_FILE)" $(if $(ACCOUNT_EMAIL_KEYWORD),--email "$(ACCOUNT_EMAIL_KEYWORD)")
account-import:
@[ -f "$(ACCOUNT_IMPORT_FILE)" ] || (echo "❌ 未找到文件 $(ACCOUNT_IMPORT_FILE)"; exit 1)
@go run ./cmd/migratectl/main.go import --dsn "$(DB_URL)" --file "$(ACCOUNT_IMPORT_FILE)" \
$(if $(ACCOUNT_IMPORT_MERGE),--merge) \
$(if $(ACCOUNT_IMPORT_MERGE_STRATEGY),--merge-strategy "$(ACCOUNT_IMPORT_MERGE_STRATEGY)") \
$(if $(ACCOUNT_IMPORT_DRY_RUN),--dry-run) \
$(foreach UUID,$(ACCOUNT_IMPORT_MERGE_ALLOWLIST),--merge-allowlist $(UUID)) \
$(ACCOUNT_IMPORT_EXTRA_FLAGS)
account-sync-push:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go push --config "$(ACCOUNT_SYNC_CONFIG)"
account-sync-pull:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go pull --config "$(ACCOUNT_SYNC_CONFIG)"
account-sync-mirror:
@[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
@go run ./cmd/syncctl/main.go mirror --config "$(ACCOUNT_SYNC_CONFIG)"
create-super-admin:
@[ -n "$(SUPERADMIN_USERNAME)" ] && [ -n "$(SUPERADMIN_PASSWORD)" ] || (echo "❌ 请指定用户名与密码"; exit 1)
@go run ./cmd/createadmin/main.go \
--driver postgres \
--dsn "$(DB_URL)" \
--username "$(SUPERADMIN_USERNAME)" \
--password "$(SUPERADMIN_PASSWORD)" \
--email "$(SUPERADMIN_EMAIL)"
# =========================================
# ⚙️ 编译与运行
# =========================================
build: init-go
@go build -o $(APP_NAME) $(MAIN_FILE)
upgrade: build
systemctl stop xcontrol-account
cp xcontrol-account /usr/bin/xcontrol-account
systemctl start xcontrol-account
start: build
@./$(APP_NAME) --config config/account.yaml
stop:
@pkill -f "$(APP_NAME)" || echo "⚠️ 未找到运行进程"
restart: stop start
test:
go test ./...
clean:
rm -f $(APP_NAME) *.pid *.log

View File

@ -1,64 +0,0 @@
module account
go 1.25.1
require (
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.6
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.45.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
xcontrol v0.0.0
)
require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)
replace xcontrol => ..

View File

@ -1,146 +0,0 @@
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

482
cmd/accountsapi/main.go Normal file
View File

@ -0,0 +1,482 @@
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"golang.org/x/crypto/bcrypt"
)
const (
defaultAddr = "127.0.0.1:8080"
defaultBodyLimit = 1 << 20 // 1 MiB
defaultSessionTTL = 24 * time.Hour
defaultRateLimitPerMin = 60
cookieName = "accounts_session"
)
type config struct {
DBUser string
DBPassword string
DBName string
DBSSLMode string
BodyLimit int64
SessionTTL time.Duration
RateLimitRPM int
}
type server struct {
log *slog.Logger
pool *pgxpool.Pool
sessions *sessionStore
bodyLimit int64
limiter *rateLimiter
sessionTTL time.Duration
}
type session struct {
userID int64
expiresAt time.Time
}
type sessionStore struct {
mu sync.RWMutex
data map[string]session
}
type rateLimiter struct {
mu sync.Mutex
limit int
window time.Duration
clients map[string]rateState
disabled bool
}
type rateState struct {
count int
resetAt time.Time
}
type loginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type userResponse struct {
ID int64 `json:"id"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
cfg, err := loadConfig()
if err != nil {
slog.Error("config error", "err", err)
os.Exit(1)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
pool, err := openPool(cfg)
if err != nil {
logger.Error("db connection failed", "err", err)
os.Exit(1)
}
defer pool.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := pool.Ping(ctx); err != nil {
logger.Error("db health check failed", "err", err)
os.Exit(1)
}
srv := &server{
log: logger,
pool: pool,
sessions: newSessionStore(),
bodyLimit: cfg.BodyLimit,
limiter: newRateLimiter(cfg.RateLimitRPM, time.Minute),
sessionTTL: cfg.SessionTTL,
}
httpServer := &http.Server{
Addr: defaultAddr,
Handler: srv.routes(),
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
go func() {
logger.Info("accounts api listening", "addr", defaultAddr)
if err := httpServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
logger.Error("server failed", "err", err)
}
}()
waitForShutdown(logger, httpServer)
}
func loadConfig() (config, error) {
cfg := config{
DBUser: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_USER")),
DBPassword: os.Getenv("ACCOUNTS_DB_PASSWORD"),
DBName: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_NAME")),
DBSSLMode: strings.TrimSpace(os.Getenv("ACCOUNTS_DB_SSLMODE")),
BodyLimit: defaultBodyLimit,
SessionTTL: defaultSessionTTL,
RateLimitRPM: defaultRateLimitPerMin,
}
if cfg.DBSSLMode == "" {
cfg.DBSSLMode = "disable"
}
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_BODY_LIMIT")); v != "" {
n, err := strconv.ParseInt(v, 10, 64)
if err != nil || n <= 0 {
return config{}, fmt.Errorf("invalid ACCOUNTS_BODY_LIMIT: %q", v)
}
cfg.BodyLimit = n
}
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_SESSION_TTL")); v != "" {
d, err := time.ParseDuration(v)
if err != nil || d <= 0 {
return config{}, fmt.Errorf("invalid ACCOUNTS_SESSION_TTL: %q", v)
}
cfg.SessionTTL = d
}
if v := strings.TrimSpace(os.Getenv("ACCOUNTS_RATE_LIMIT_RPM")); v != "" {
if v == "0" {
cfg.RateLimitRPM = 0
} else {
n, err := strconv.Atoi(v)
if err != nil || n < 0 {
return config{}, fmt.Errorf("invalid ACCOUNTS_RATE_LIMIT_RPM: %q", v)
}
cfg.RateLimitRPM = n
}
}
if cfg.DBUser == "" || cfg.DBName == "" {
return config{}, errors.New("ACCOUNTS_DB_USER and ACCOUNTS_DB_NAME are required")
}
return cfg, nil
}
func openPool(cfg config) (*pgxpool.Pool, error) {
dsn := (&url.URL{
Scheme: "postgres",
User: url.UserPassword(cfg.DBUser, cfg.DBPassword),
Host: "127.0.0.1:15432",
Path: cfg.DBName,
RawQuery: "sslmode=" + url.QueryEscape(cfg.DBSSLMode),
}).String()
pgxCfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
pgxCfg.MaxConns = 10
pgxCfg.MinConns = 2
pgxCfg.MaxConnIdleTime = 5 * time.Minute
pgxCfg.MaxConnLifetime = 30 * time.Minute
return pgxpool.NewWithConfig(context.Background(), pgxCfg)
}
func waitForShutdown(logger *slog.Logger, httpServer *http.Server) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
<-signals
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
logger.Info("shutting down")
if err := httpServer.Shutdown(ctx); err != nil {
logger.Error("shutdown error", "err", err)
}
}
func (s *server) routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/api/login", s.handleLogin)
mux.HandleFunc("/api/logout", s.handleLogout)
mux.HandleFunc("/api/me", s.handleMe)
return s.middleware(mux)
}
func (s *server) middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapped := &statusWriter{ResponseWriter: w, status: http.StatusOK}
if s.limiter != nil {
if ok := s.limiter.Allow(clientIP(r)); !ok {
writeJSON(wrapped, http.StatusTooManyRequests, map[string]string{"error": "rate_limited"})
s.logRequest(r, wrapped.status, start)
return
}
}
if r.Body != nil && s.bodyLimit > 0 {
r.Body = http.MaxBytesReader(wrapped, r.Body, s.bodyLimit)
}
next.ServeHTTP(wrapped, r)
s.logRequest(r, wrapped.status, start)
})
}
func (s *server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
var req loginRequest
if err := decodeJSON(r, &req); err != nil {
if isBodyTooLarge(err) {
writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "body_too_large"})
return
}
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid_json"})
return
}
email := strings.ToLower(strings.TrimSpace(req.Email))
if email == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "email_and_password_required"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
var (
userID int64
passwordHash string
)
err := s.pool.QueryRow(ctx, "SELECT id, password_hash FROM users WHERE email=$1", email).Scan(&userID, &passwordHash)
if err != nil {
if !errors.Is(err, pgx.ErrNoRows) {
s.log.Error("login query failed", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
return
}
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid_credentials"})
return
}
if bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)) != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid_credentials"})
return
}
sessionID, err := generateToken(32)
if err != nil {
s.log.Error("session token generation failed", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
return
}
expiresAt := time.Now().Add(s.sessionTTL)
s.sessions.Set(sessionID, session{userID: userID, expiresAt: expiresAt})
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: sessionID,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
Expires: expiresAt,
})
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *server) handleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
if cookie, err := r.Cookie(cookieName); err == nil && cookie.Value != "" {
s.sessions.Delete(cookie.Value)
}
http.SetCookie(w, &http.Cookie{
Name: cookieName,
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}
func (s *server) handleMe(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method_not_allowed"})
return
}
cookie, err := r.Cookie(cookieName)
if err != nil || cookie.Value == "" {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
sess, ok := s.sessions.Get(cookie.Value)
if !ok || time.Now().After(sess.expiresAt) {
s.sessions.Delete(cookie.Value)
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
var user userResponse
err = s.pool.QueryRow(ctx, "SELECT id, email, created_at FROM users WHERE id=$1", sess.userID).
Scan(&user.ID, &user.Email, &user.CreatedAt)
if err != nil {
if !errors.Is(err, pgx.ErrNoRows) {
s.log.Error("me query failed", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "server_error"})
return
}
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"user": user})
}
func decodeJSON(r *http.Request, dst any) error {
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(dst); err != nil {
return err
}
if decoder.More() {
return errors.New("extra json fields")
}
return nil
}
func isBodyTooLarge(err error) bool {
var maxErr *http.MaxBytesError
return errors.As(err, &maxErr)
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if payload != nil {
_ = json.NewEncoder(w).Encode(payload)
}
}
func generateToken(size int) (string, error) {
buf := make([]byte, size)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func newSessionStore() *sessionStore {
return &sessionStore{data: make(map[string]session)}
}
func (s *sessionStore) Get(token string) (session, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
val, ok := s.data[token]
return val, ok
}
func (s *sessionStore) Set(token string, sess session) {
s.mu.Lock()
defer s.mu.Unlock()
s.data[token] = sess
}
func (s *sessionStore) Delete(token string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.data, token)
}
func newRateLimiter(limit int, window time.Duration) *rateLimiter {
if limit <= 0 {
return &rateLimiter{disabled: true}
}
return &rateLimiter{
limit: limit,
window: window,
clients: make(map[string]rateState),
}
}
func (r *rateLimiter) Allow(ip string) bool {
if r == nil || r.disabled {
return true
}
now := time.Now()
r.mu.Lock()
defer r.mu.Unlock()
state := r.clients[ip]
if state.resetAt.IsZero() || now.After(state.resetAt) {
state.resetAt = now.Add(r.window)
state.count = 0
}
if state.count >= r.limit {
r.clients[ip] = state
return false
}
state.count++
r.clients[ip] = state
return true
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
func (s *server) logRequest(r *http.Request, status int, start time.Time) {
s.log.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", status,
"latency", time.Since(start),
"ip", clientIP(r),
)
}
func clientIP(r *http.Request) string {
if r == nil {
return ""
}
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
return strings.TrimSpace(parts[0])
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}

View File

@ -18,7 +18,7 @@ xray:
enabled: true
interval: 5m
outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json"
templatePath: "config/xray.config.template.json"
validateCommand: []
restartCommand:
- "systemctl"

View File

@ -59,7 +59,7 @@ xray:
enabled: false
interval: 5m
outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json"
templatePath: "config/xray.config.template.json"
validateCommand: []
restartCommand:
- "systemctl"

View File

@ -69,7 +69,7 @@ xray:
enabled: false
interval: 5m
outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json"
templatePath: "config/xray.config.template.json"
validateCommand: []
restartCommand:
- "systemctl"

View File

@ -156,12 +156,12 @@ type AgentCredential struct {
}
// Load reads the configuration file at the provided path. When path is empty,
// it defaults to account/config/account.yaml. If the file does not exist an
// it defaults to config/account.yaml. If the file does not exist an
// empty configuration is returned.
func Load(path string) (*Config, error) {
p := path
if p == "" {
p = filepath.Join("account", "config", "account.yaml")
p = filepath.Join("config", "account.yaml")
}
b, err := os.ReadFile(p)

View File

@ -0,0 +1,8 @@
accounts.svc.plus {
@api path /api/*
reverse_proxy @api 127.0.0.1:8080
handle {
reverse_proxy 127.0.0.1:3000
}
}

View File

@ -55,7 +55,7 @@ xray:
enabled: false
interval: 5m
outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json"
templatePath: "config/xray.config.template.json"
validateCommand: []
restartCommand:
- "systemctl"

View File

@ -0,0 +1,25 @@
[Unit]
Description=accounts.svc.plus API
After=network.target
[Service]
Type=simple
User=accounts
Group=accounts
WorkingDirectory=/opt/xcontrol
Environment=ACCOUNTS_DB_USER=accounts
Environment=ACCOUNTS_DB_PASSWORD=change-me
Environment=ACCOUNTS_DB_NAME=accounts
Environment=ACCOUNTS_DB_SSLMODE=disable
Environment=ACCOUNTS_RATE_LIMIT_RPM=60
Environment=ACCOUNTS_SESSION_TTL=24h
ExecStart=/opt/xcontrol/accounts-api
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.target

View File

@ -14,13 +14,13 @@ This document summarizes the new `/api/auth/admin/settings` endpoints for managi
## Storage Model
- The permission matrix is stored in the `admin_settings` table. GORM manages the model via `account/internal/model/admin_setting.go` and a dedicated migration script (`sql/20250305-admin-settings.sql`).
- The permission matrix is stored in the `admin_settings` table. GORM manages the model via `internal/model/admin_setting.go` and a dedicated migration script (`sql/20250305-admin-settings.sql`).
- Each cell records `module_key`, `role`, `enabled`, and a monotonically increasing `version` value. Updates occur inside a single transaction that replaces the existing matrix to guarantee consistency across modules and roles.
- The service layer (`account/internal/service/admin_settings.go`) caches the most recent matrix in-memory and invalidates the cache whenever a write occurs or fails due to a version conflict.
- The service layer (`internal/service/admin_settings.go`) caches the most recent matrix in-memory and invalidates the cache whenever a write occurs or fails due to a version conflict.
## Test Coverage
Integration tests are provided in `account/api/admin_settings_test.go`:
Integration tests are provided in `api/admin_settings_test.go`:
- `TestAdminSettingsReadWrite` exercises a full write followed by a read using the operator role.
- `TestAdminSettingsUnauthorized` verifies that callers without an admin/operator role receive `403 Forbidden` responses for both GET and POST.
@ -29,5 +29,5 @@ Integration tests are provided in `account/api/admin_settings_test.go`:
Run the suite with:
```bash
go test ./account/api -run AdminSettings
go test ./api -run AdminSettings
```

View File

@ -4,19 +4,19 @@
## 1. 配置加载策略
账号服务入口(`account/cmd/accountsvc/main.go`)会调用 `config.Load` 读取 YAML 配置,并允许通过命令行参数覆盖默认路径。当未提供配置文件时,服务会以零值启动,此时可结合环境变量填充关键字段。
账号服务入口(`cmd/accountsvc/main.go`)会调用 `config.Load` 读取 YAML 配置,并允许通过命令行参数覆盖默认路径。当未提供配置文件时,服务会以零值启动,此时可结合环境变量填充关键字段。
当前推荐的覆盖顺序如下:
1. **命令行参数**:用于指定配置文件路径或运行模式。
2. **配置文件**:默认从 `account/config/account.yaml` 读取,适合提交到仓库或挂载到容器内。
2. **配置文件**:默认从 `config/account.yaml` 读取,适合提交到仓库或挂载到容器内。
3. **代码默认值**`config.Config` 结构体中的零值,保证最小可运行。
> 注:目前服务尚未内置环境变量映射逻辑,如需按环境注入配置,可在部署流程中提前生成 YAML 文件或扩展 `config.Load`
## 2. 配置字段参考
`account/config/config.go` 定义了配置结构,主要包含以下几个部分:
`config/config.go` 定义了配置结构,主要包含以下几个部分:
```yaml
log:

View File

@ -17,11 +17,11 @@
```
2. **准备配置**
使用仓库提供的 `account/config/account.yaml`,或根据需要拷贝一份修改端口、数据库连接等字段。
使用仓库提供的 `config/account.yaml`,或根据需要拷贝一份修改端口、数据库连接等字段。
3. **启动服务HTTP**
```bash
go run ./account/cmd/accountsvc --config account/config/account.yaml
go run ./cmd/accountsvc --config config/account.yaml
```
默认监听 `:8080`,可通过 `curl http://127.0.0.1:8080/healthz` 检查服务状态。
@ -101,7 +101,7 @@
启动命令保持不变:
```bash
go run ./account/cmd/accountsvc --config /path/to/secure-account.yaml
go run ./cmd/accountsvc --config /path/to/secure-account.yaml
```
常见验证步骤:
@ -113,7 +113,7 @@ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-subj "/CN=localhost"
# 更新配置后启动服务
ACCOUNT_CONFIG=/tmp/account-secure.yaml go run ./account/cmd/accountsvc --config $ACCOUNT_CONFIG
ACCOUNT_CONFIG=/tmp/account-secure.yaml go run ./cmd/accountsvc --config $ACCOUNT_CONFIG
# 使用 curl 验证 HTTPS开发环境可加 -k 跳过校验)
curl -k https://127.0.0.1:8443/healthz
@ -228,7 +228,7 @@ docker compose -f deploy/docker-compose/caddy-stunnel/docker-compose.db.yaml up
1. **构建镜像(示例)**
```bash
docker build -t xcontrol/account-service -f deploy/account/Dockerfile .
docker build -t xcontrol/account-service -f Dockerfile .
```
2. **运行容器(挂载配置与证书)**
@ -237,7 +237,7 @@ docker compose -f deploy/docker-compose/caddy-stunnel/docker-compose.db.yaml up
--name account-service \
-p 8443:8443 \
-p 8080:8080 \
-v $(pwd)/account.yaml:/etc/xcontrol/account.yaml \
-v $(pwd)/config/account.yaml:/etc/xcontrol/account.yaml \
-v $(pwd)/certs:/etc/ssl/xcontrol \
xcontrol/account-service \
--config /etc/xcontrol/account.yaml
@ -278,7 +278,7 @@ docker compose -f deploy/docker-compose/caddy-stunnel/docker-compose.db.yaml up
- 在容器或集群层启用网络策略,仅开放必要端口。
- 对外提供服务时务必启用 HTTPS保护登录口令与 TOTP 码。
- 对数据库、证书等敏感资源使用最小权限原则,并定期轮换。
- 定期回顾 `account/api/api_test.go` 中的场景测试,确保关键登录链路持续可用。
- 定期回顾 `api/api_test.go` 中的场景测试,确保关键登录链路持续可用。
## 9. 数据库备份、迁移与回滚示例

View File

@ -14,15 +14,15 @@
## 2. 系统架构
账号服务采用 Go 语言实现,入口位于 `account/cmd/accountsvc/main.go`,默认使用 Gin 框架启动 HTTP 服务并注册 REST API 路由。【F:account/cmd/accountsvc/main.go†L1-L12】
账号服务采用 Go 语言实现,入口位于 `cmd/accountsvc/main.go`,默认使用 Gin 框架启动 HTTP 服务并注册 REST API 路由。【F:cmd/accountsvc/main.go†L1-L12】
核心模块划分如下:
- `account/api`: 定义 REST API并实现用户注册、登录、会话维护等业务逻辑。【F:account/api/api.go†L1-L190】
- `account/internal/store`: 提供用户数据的读写接口与内存实现后续可扩展至数据库存储。【F:account/internal/store/store.go†L1-L109】
- `account/internal/auth`: 声明可插拔的第三方认证提供方接口,为接入 LDAP/OIDC 等外部系统提供抽象。【F:account/internal/auth/auth.go†L1-L6】
- `account/internal/cache`: 预留会话缓存接口,便于集成 Redis 等缓存组件。【F:account/internal/cache/cache.go†L1-L6】
- `account/config`: 管理服务配置结构体当前为空定义未来将扩展字段。【F:account/config/config.go†L1-L5】
- `api`: 定义 REST API并实现用户注册、登录、会话维护等业务逻辑。【F:api/api.go†L1-L190】
- `internal/store`: 提供用户数据的读写接口与内存实现后续可扩展至数据库存储。【F:internal/store/store.go†L1-L109】
- `internal/auth`: 声明可插拔的第三方认证提供方接口,为接入 LDAP/OIDC 等外部系统提供抽象。【F:internal/auth/auth.go†L1-L6】
- `internal/cache`: 预留会话缓存接口,便于集成 Redis 等缓存组件。【F:internal/cache/cache.go†L1-L6】
- `config`: 管理服务配置结构体当前为空定义未来将扩展字段。【F:config/config.go†L1-L5】
内部调用关系示意:
@ -56,27 +56,27 @@ Gin Router → API Handler → Store / Session Manager → 数据存储
### 3.3 用户登录
- `POST /v1/login`
- 请求体:`{ "email": string, "password": string }`
- 功能:校验凭据,通过内存存储读取用户并验证哈希密码,成功后生成 24 小时有效的会话 token。【F:account/api/api.go†L65-L136】
- 功能:校验凭据,通过内存存储读取用户并验证哈希密码,成功后生成 24 小时有效的会话 token。【F:api/api.go†L65-L136】
### 3.4 查询会话
- `GET /v1/session`
- Header 中提供 `Authorization: Bearer <token>` 或查询参数 `token`
- 功能:校验 token返回关联用户信息。【F:account/api/api.go†L138-L176】
- 功能:校验 token返回关联用户信息。【F:api/api.go†L138-L176】
### 3.5 注销会话
- `DELETE /v1/session`
- Header 或查询参数传入 token删除内存中的会话记录。【F:account/api/api.go†L178-L190】
- Header 或查询参数传入 token删除内存中的会话记录。【F:api/api.go†L178-L190】
## 4. 数据模型
当前实现使用内存存储,结构体 `store.User` 定义了最小必要字段:`ID`、`Name`、`Email`、`PasswordHash` 与 `CreatedAt` 时间戳。【F:account/internal/store/store.go†L12-L18】
当前实现使用内存存储,结构体 `store.User` 定义了最小必要字段:`ID`、`Name`、`Email`、`PasswordHash` 与 `CreatedAt` 时间戳。【F:internal/store/store.go†L12-L18】
`memoryStore` 负责提供线程安全的增删查能力,并在创建用户时自动生成 UUID 与 UTC 时间,保证多实例场景中的唯一性。未来替换为数据库时,可在 `Store` 接口的基础上新增实现即可。【F:account/internal/store/store.go†L31-L109】
`memoryStore` 负责提供线程安全的增删查能力,并在创建用户时自动生成 UUID 与 UTC 时间,保证多实例场景中的唯一性。未来替换为数据库时,可在 `Store` 接口的基础上新增实现即可。【F:internal/store/store.go†L31-L109】
## 5. 安全与扩展
- **密码存储**:使用 `bcrypt` 哈希防止明文泄露。【F:account/api/api.go†L90-L108】
- **会话管理**:会话 token 为 32 字节随机数生成的十六进制字符串,并设置 24 小时过期过期后自动清理。【F:account/api/api.go†L112-L171】
- **密码存储**:使用 `bcrypt` 哈希防止明文泄露。【F:api/api.go†L90-L108】
- **会话管理**:会话 token 为 32 字节随机数生成的十六进制字符串,并设置 24 小时过期过期后自动清理。【F:api/api.go†L112-L171】
- **扩展点**
- 可在 `Store` 接口层新增 PostgreSQL、MySQL 等实现。
- 可实现 `auth.Provider` 接口以支持外部身份源认证,再与内部用户绑定。

View File

@ -101,10 +101,9 @@ CREATE TABLE IF NOT EXISTS sessions (
## 8. 代码目录规划
后端代码位于根目录`account/` 下:
后端代码位于根目录下:
```
account/
cmd/accountsvc/main.go # 服务入口
api/ # REST 接口
config/ # 配置解析
@ -117,6 +116,7 @@ account/
前端目录扩展:
- `ui/panel/app/`:控制台新增账号模块页面。
- `dashboard/app/login/``dashboard/app/register/`:提供登录/注册页面,登录后根据身份跳转至用户或管理员界面。
## 7. 部署建议

View File

@ -6,9 +6,9 @@
### 1.1 HTTP 接口扩展
仅新增 `POST /api/config/sync`,位于 `account/api` 路由注册:
仅新增 `POST /api/config/sync`,位于 `api` 路由注册:
- **Handler 位置**`account/api/config_sync.go`(新建文件),由 `api.RegisterRoutes` 中挂载到 `auth` 保护下的子路由组。
- **Handler 位置**`api/config_sync.go`(新建文件),由 `api.RegisterRoutes` 中挂载到 `auth` 保护下的子路由组。
- **认证复用**:沿用 `xc_session` Cookie。若桌面端后续需要无 Cookie 调用,可在 `api/auth` 中增加“设备 Token”生成接口但不影响本次实现。
- **请求结构**
```text
@ -51,17 +51,17 @@
]
}
```
- **重放保护**:服务端在 handler 中校验 `timestamp` ±5 分钟及 `nonce` 是否重复。重放窗口可复用现有 Redis/内存缓存(`account/internal/cache`)。
- **重放保护**:服务端在 handler 中校验 `timestamp` ±5 分钟及 `nonce` 是否重复。重放窗口可复用现有 Redis/内存缓存(`internal/cache`)。
### 1.2 加密模块复用
- **Key 派发**:在 `account/internal/store/user.go` 中新增 `SyncSecret` 字段(可选),默认读取已有 `users.sync_secret` 列;若列不存在,可在迁移脚本中与 UUID 一致生成,确保最少改动。
- **算法实现**:在 `account/internal/crypto/syncpayload`(新目录)封装 `Encrypt(payload []byte, secret []byte)``Decrypt`,使用 `XChaCha20-Poly1305`。该算法 Go 侧可复用 `golang.org/x/crypto/chacha20poly1305`
- **Key 派发**:在 `internal/store/user.go` 中新增 `SyncSecret` 字段(可选),默认读取已有 `users.sync_secret` 列;若列不存在,可在迁移脚本中与 UUID 一致生成,确保最少改动。
- **算法实现**:在 `internal/crypto/syncpayload`(新目录)封装 `Encrypt(payload []byte, secret []byte)``Decrypt`,使用 `XChaCha20-Poly1305`。该算法 Go 侧可复用 `golang.org/x/crypto/chacha20poly1305`
- **密钥管理**:管理员通过 `GET/POST /api/auth/admin/settings`(已存在)调整“桌面同步”开关;密钥不在接口返回,仅在数据库存储,客户端登录成功后通过 `/api/config/sync` 解包获得配置。
### 1.3 配置生成复用
- **数据来源**:继续使用 `account/internal/xrayconfig`。根据 `user.UUID` 作为 tenant_id`Generator.Generate()` 获得完整 JSON。
- **数据来源**:继续使用 `internal/xrayconfig`。根据 `user.UUID` 作为 tenant_id`Generator.Generate()` 获得完整 JSON。
- **差异化控制**:在 `xrayconfig` 中新增 `HasDesktopPrivilege(uuid string) bool`(读取管理员设置或用户标记),若返回 false则 handler 返回 `status=NO_PRIVILEGE`,客户端保持现状。
- **审计 & 日志**:复用现有的 `logger.WithContext(ctx)`,记录 `uuid`、`deviceFingerprint`、`configVersion`。
@ -147,7 +147,7 @@ struct SyncResponse {
## 5. 安全与运维要点
- **最小数据面**所有敏感字段都封装在加密包内URL 与 Header 仅携带基础信息Cookie
- **限流**:继续复用 `account/api/middleware/ratelimit`(若已有)或在 handler 中增加 per-device 限流。
- **限流**:继续复用 `api/middleware/ratelimit`(若已有)或在 handler 中增加 per-device 限流。
- **审计**:在服务端日志中记录 `uuid`、`deviceFingerprint` hash、`status`,便于定位问题而不过度存储。
- **滚动升级**:版本字段可确保前后端同时升级;旧客户端仍可解析 version=1。

View File

@ -1,7 +1,7 @@
# Cloud-Neutral 子域统一命名迁移规划
## 1. 背景与目标
现有 Cloud-Neutral 生态的各子系统分别部署在 `svc.plus` 旗下的多个子域中,例如账号中心使用 `accounts.svc.plus`,控制台与公共站点共用 `www.svc.plus`API 入口映射到 `rag-server.svc.plus` 等配置文件仍保留旧命名。【F:account/config/account.yaml†L10-L23】【F:dashboard/config/runtime-service-config.yaml†L1-L27】【F:deploy/openresty/rag-server.svc.plus.conf†L1-L69】
现有 Cloud-Neutral 生态的各子系统分别部署在 `svc.plus` 旗下的多个子域中,例如账号中心使用 `accounts.svc.plus`,控制台与公共站点共用 `www.svc.plus`API 入口映射到 `rag-server.svc.plus` 等配置文件仍保留旧命名。【F:config/account.yaml†L10-L23】【F:dashboard/config/runtime-service-config.yaml†L1-L27】【F:deploy/openresty/rag-server.svc.plus.conf†L1-L69】
为降低证书、路由和跨域配置的复杂度,计划按照统一命名体系:
@ -26,7 +26,7 @@
## 2. 现状盘点
### 2.1 用户中心accounts.svc.plus
- Go 账号服务 `account/` 通过 `server.publicUrl` 指向 `https://accounts.svc.plus` 并在 `allowedOrigins` 中列出旧前端域名列表。【F:account/config/account.yaml†L6-L23】
- Go 账号服务 `` 通过 `server.publicUrl` 指向 `https://accounts.svc.plus` 并在 `allowedOrigins` 中列出旧前端域名列表。【F:config/account.yaml†L6-L23】
- 多份文档(例如 `docs/account-svc-plus.md`、`docs/account-xstream-desktop-integration.md`阐述账号服务部署契约需要同步调整对控制台和下载域的描述。【F:docs/account-svc-plus.md†L1-L37】【F:docs/account-xstream-desktop-integration.md†L1-L52】
### 2.2 控制台console.svc.plus

View File

@ -19,5 +19,5 @@ Related source files:
`NEXT_PUBLIC_REGISTER_URL` and submits the form to that URL.
- `dashboard/app/api/auth/register/route.ts` handles the
`/api/auth/register` requests and forwards them to the account service.
- `account/api/api.go` exposes the `POST /api/auth/register` handler inside the
- `api/api.go` exposes the `POST /api/auth/register` handler inside the
account service.

View File

@ -119,11 +119,11 @@ func SyncXrayClients(ctx context.Context, db *sql.DB, fs afero.Fs, runner comman
The concrete implementation should wire in dependency-injected collaborators for database access, filesystem operations, validation, and command execution to simplify testing.
The initial `Config Generator` module lives at `account/internal/xrayconfig`. It embeds the default Xray template directly in
The initial `Config Generator` module lives at `internal/xrayconfig`. It embeds the default Xray template directly in
the binary, overwrites the VLESS user array with the current database view (setting `flow` to `xtls-rprx-vision` unless callers
request a different value), and writes the merged document to `/usr/local/etc/xray/config.json` using an atomic rename so that Xray always
observes a complete file. Agent deployments can still point `xray.sync.templatePath` at a local template (for example
`account/config/xray.config.template.json`) when they need to override the embedded definition.
`config/xray.config.template.json`) when they need to override the embedded definition.
### Periodic Synchronization
@ -134,7 +134,7 @@ now exposes a background syncer that periodically rebuilds the Xray config by:
2. Regenerating the config with the existing generator.
3. Optionally validating the JSON and restarting Xray using commands defined in configuration.
The cadence and command hooks are controlled through `account/config/account.yaml`:
The cadence and command hooks are controlled through `config/account.yaml`:
```yaml
xray:

86
go.mod
View File

@ -1,83 +1,61 @@
module xcontrol
module account
go 1.23.0
toolchain go1.23.8
go 1.25.1
require (
github.com/gin-contrib/cors v1.6.0
github.com/gin-gonic/gin v1.9.1
github.com/go-git/go-git/v5 v5.16.2
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.19.0
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.5
github.com/pgvector/pgvector-go v0.3.0
github.com/jackc/pgx/v5 v5.7.6
github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.12.0
github.com/spf13/cobra v1.9.1
github.com/yuin/goldmark v1.7.13
golang.org/x/crypto v0.41.0
golang.org/x/net v0.42.0
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.45.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.11.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
)

266
go.sum
View File

@ -1,136 +1,72 @@
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ=
entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.11.2 h1:ywfwo0a/3j9HR8wsYGWsIWl2mvRsI950HyoxiBERw5A=
github.com/bytedance/sonic v1.11.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.6.0 h1:0Z7D/bVhE6ja07lI8CTjTonp6SB07o8bNuFyRbsBUQg=
github.com/gin-contrib/cors v1.6.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0=
github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA=
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
@ -140,125 +76,71 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc=
github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/redis/go-redis/v9 v9.12.0 h1:XlVPGlflh4nxfhsNXPA8Qp6EmEfTo0rp8oaBzPipXnU=
github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo=
mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=

View File

@ -4,7 +4,7 @@
# 用于清理指定文件的历史提交记录,但保留当前版本
#
# 使用示例:
# ./clean_git_history.sh account/sql/schema_pglogical_region.sql
# ./clean_git_history.sh sql/schema_pglogical_region.sql
#
set -euo pipefail

View File

@ -37,7 +37,7 @@ log_error() {
# 配置路径
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DASHBOARD_CONFIG="$ROOT_DIR/dashboard-fresh/config/runtime-service-config.base.yaml"
ACCOUNT_CONFIG="$ROOT_DIR/account/config/account.yaml"
ACCOUNT_CONFIG="$ROOT_DIR/config/account.yaml"
RAG_CONFIG="$ROOT_DIR/rag-server/config/server.yaml"
MANUAL_FILE="$ROOT_DIR/TOKEN_AUTH_MANUAL.md"

View File

@ -0,0 +1,7 @@
-- Minimal schema for accounts.svc.plus API.
CREATE TABLE IF NOT EXISTS public.users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

View File

@ -133,7 +133,7 @@ All test scripts generate JSON results with the following structure:
## Configuration
No configuration files are required. The scripts automatically detect:
- Service directories (`account/`, `rag-server/`, `dashboard-fresh/`)
- Service directories (`/`, `rag-server/`, `dashboard-fresh/`)
- Configuration files in each service
- Authentication implementations

View File

@ -159,8 +159,8 @@ echo "Starting configuration validation tests..."
echo ""
# Account service
validate_configs "account" "account/config"
validate_auth_config "account" "account/internal/auth/token_service.go"
validate_configs "account" "config"
validate_auth_config "account" "internal/auth/token_service.go"
# RAG server
validate_configs "rag-server" "rag-server/config"

View File

@ -179,7 +179,7 @@ echo ""
# test_endpoint "account" "8080" "/health"
# Instead, just validate that the binaries exist
if [ -f "account/xcontrol-account" ]; then
if [ -f "xcontrol-account" ]; then
TOTAL_TESTS=$((TOTAL_TESTS + 1))
echo "✓ Account binary found"
echo "," >> "$RESULTS_FILE"