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 b2cc2b1dad
commit 07e31ff6bd
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 # 📦 XControl Account Service Makefile
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) APP_NAME := xcontrol-account
SUDO := MAIN_FILE := ./cmd/accountsvc/main.go
else PORT ?= 8080
SUDO ?= sudo OS := $(shell uname -s)
endif
HOSTS_FILE ?= /etc/hosts DB_NAME := account
HOSTS_IP ?= 127.0.0.1 DB_USER := shenlan
HOSTS_DOMAINS ?= dev-accounts.svc.plus dev-api.svc.plus 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) REPLICATION_MODE ?= pgsync
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 DB_ADMIN_USER ?= $(DB_USER)
NGINX_CONF_DIR ?= $(NGINX_CONF_ROOT)/conf.d DB_ADMIN_PASS ?= $(DB_PASS)
NGINX_MAIN_CONF ?= $(NGINX_CONF_ROOT)/nginx.conf
NGINX_SIT_CONFIGS := example/sit/nginx/nginx.conf SCHEMA_FILE := ./sql/schema.sql
NGINX_SIT_CONFIGS += example/sit/nginx/dev.svc.plus.conf PGLOGICAL_INIT_FILE := ./sql/schema_pglogical_init.sql
NGINX_SIT_CONFIGS += example/sit/nginx/dev-api.svc.plus.conf PGLOGICAL_PATCH_FILE := ./sql/schema_pglogical_patch.sql
NGINX_SIT_CONFIGS := example/sit/nginx/dev-accounts.svc.plus.conf PGLOGICAL_REGION_FILE := ./sql/schema_pglogical_region.sql
NGINX_PROD_CONFIGS := example/prod/nginx/nginx.conf ACCOUNT_EXPORT_FILE ?= account-export.yaml
NGINX_PROD_CONFIGS := example/prod/nginx/dev.svc.plus.conf ACCOUNT_IMPORT_FILE ?= account-export.yaml
NGINX_PROD_CONFIGS := example/prod/nginx/api.svc.plus.conf ACCOUNT_EMAIL_KEYWORD ?=
NGINX_PROD_CONFIGS := example/prod/nginx/accounts.svc.plus.conf 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) # =========================================
# 🧩 基础命令
# =========================================
# ----------------------------------------------------------------------------- .PHONY: all init build clean start stop restart dev test help \
# Environment bootstrap (hosts & services) 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: init: init-go init-db
@set -e; \
if [ ! -f "$(HOSTS_FILE)" ]; then \ init-go:
echo "⚠️ Hosts file $(HOSTS_FILE) not found; skipping host configuration."; \ @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 \ else \
for domain in $(HOSTS_DOMAINS); do \ echo ">>> 跳过 pglogical 初始化 (REPLICATION_MODE=$(REPLICATION_MODE))"; \
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 fi
init-nginx: init-db-pglogical:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)" @if [ -f $(PGLOGICAL_INIT_FILE) ]; then \
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \ echo ">>> 初始化 pglogical schema (REPLICATION_MODE=pglogical)"; \
if [ -f "$(NGINX_MAIN_CONF)" ]; then \ if PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
if cmp -s "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; then \ -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" 2>/dev/null | grep -qx 't'; then \
echo "$(NGINX_MAIN_CONF) already up to date"; \ PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d $(DB_NAME) \
else \ -v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
echo "⬆️ Updating $(NGINX_MAIN_CONF) from template"; \ elif psql "$(DB_URL)" -Atc "SELECT rolsuper FROM pg_roles WHERE rolname = current_user" | grep -qx 't'; then \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \ psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_INIT_FILE); \
fi; \ else \
else \ echo "⚠️ 当前用户非超级用户,跳过 pglogical 初始化"; \
echo " Installing $(NGINX_MAIN_CONF)"; \ fi; \
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \ fi; \
fi; \ if [ -f $(PGLOGICAL_PATCH_FILE) ]; then \
fi echo ">>> 应用 pglogical 默认值补丁"; \
@for file in $(NGINX_ALL_CONFIGS); do \ psql "$(DB_URL)" -v ON_ERROR_STOP=1 -f $(PGLOGICAL_PATCH_FILE); \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \ fi
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 # =========================================
# 🧠 PGLogical 双节点初始化
# =========================================
upgrade-nginx: init-pglogical-region:
@$(SUDO) mkdir -p "$(NGINX_CONF_DIR)" @[ -n "$(REGION_DB_URL)" ] || (echo "❌ 缺少 REGION_DB_URL"; exit 1)
@if [ -n "$(NGINX_MAIN_TEMPLATE)" ]; then \ @[ -n "$(NODE_NAME)" ] || (echo "❌ 缺少 NODE_NAME"; exit 1)
echo "⬆️ Updating $(NGINX_MAIN_CONF)"; \ @[ -n "$(NODE_DSN)" ] || (echo "❌ 缺少 NODE_DSN"; exit 1)
$(SUDO) install -m 0644 "$(NGINX_MAIN_TEMPLATE)" "$(NGINX_MAIN_CONF)"; \ @[ -n "$(SUBSCRIPTION_NAME)" ] || (echo "❌ 缺少 SUBSCRIPTION_NAME"; exit 1)
fi @[ -n "$(PROVIDER_DSN)" ] || (echo "❌ 缺少 PROVIDER_DSN"; exit 1)
@for file in $(NGINX_ALL_CONFIGS); do \ @psql "$(REGION_DB_URL)" -v ON_ERROR_STOP=1 \
dest="$(NGINX_CONF_DIR)/$$(basename $$file)"; \ -v NODE_NAME="$(NODE_NAME)" \
echo "⬆️ Updating $$dest"; \ -v NODE_DSN="$(NODE_DSN)" \
$(SUDO) install -m 0644 "$$file" "$$dest"; \ -v SUBSCRIPTION_NAME="$(SUBSCRIPTION_NAME)" \
done -v PROVIDER_DSN="$(PROVIDER_DSN)" \
@$(MAKE) reload-openresty -f $(PGLOGICAL_REGION_FILE)
reload-openresty: init-pglogical-region-cn:
@echo "🔄 Reloading OpenResty/Nginx if available..." @$(MAKE) init-pglogical-region \
@command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files | grep -q '^openresty.service' && { \ REGION_DB_URL="$(DB_URL)" \
$(SUDO) systemctl reload openresty 2>/dev/null || $(SUDO) systemctl restart openresty 2>/dev/null || true; \ NODE_NAME="node_cn" \
echo "✅ openresty.service reloaded"; \ NODE_DSN="host=cn-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx" \
} || echo " openresty.service not managed by systemd or systemctl missing; please reload manually." SUBSCRIPTION_NAME="sub_from_global" \
PROVIDER_DSN="host=global-homepage.svc.plus port=5432 dbname=account user=pglogical password=xxxx"
init-account: init-pglogical-region-global:
@$(MAKE) -C account init @$(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: create-db-user:
@$(MAKE) -C account upgrade @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: migrate-db:
@$(MAKE) -C rag-server init @echo ">>> 执行数据库迁移"
@go run ./cmd/migratectl/main.go migrate --dsn "$(DB_URL)" --dir sql/migrations
install-rag-server: dump-schema:
@$(MAKE) -C rag-server build @echo ">>> 导出 schema 到 $(SCHEMA_FILE)"
@pg_dump -s -O -x "$(DB_URL)" > $(SCHEMA_FILE)
upgrade-rag-server: db-reset:
@$(MAKE) -C rag-server build @echo "⚠️ 即将重置整个 PostgreSQL 数据库集群 ..."
@$(MAKE) -C rag-server restart @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 \ drop-db:
build update-dashboard-manifests build-server build-dashboard \ @echo "⚠️ 即将删除数据库 $(DB_NAME) ..."
start start-openresty start-server start-dashboard \ @read -p "确定要删除数据库 $(DB_NAME)? [y/N] " confirm && \
stop stop-server stop-dashboard stop-openresty restart lint-cms \ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
init init-nginx install-nginx upgrade-nginx reload-openresty \ echo ">>> 强制断开现有连接 ..."; \
init-account install-account upgrade-account \ if ! PGPASSWORD="$(DB_ADMIN_PASS)" psql -h $(DB_HOST) -U $(DB_ADMIN_USER) -d postgres \
init-rag-server install-rag-server upgrade-rag-server \ -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='$(DB_NAME)' AND pid <> pg_backend_pid();"; then \
configure-hosts install-services upgrade-services \ echo "⚠️ 无法断开所有连接(需要超级用户权限)"; \
build-base-images docker-openresty-geoip docker-postgres-extensions \ fi; \
docker-node-builder docker-node-runtime docker-go-builder docker-go-runtime 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:
# Dependency installation @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 --------------------------------------------------------------- reinit-pglogical:
install-nodejs: @if [ "$(REPLICATION_MODE)" = "pglogical" ]; then \
ifeq ($(OS),Darwin) echo ">>> 重新初始化 pglogical schema"; \
( brew install node@22 && brew link --overwrite --force node@22 ) || brew install node $(MAKE) init-db-pglogical; \
corepack enable || true else \
corepack prepare yarn@stable --activate || true echo ">>> 当前 REPLICATION_MODE=$(REPLICATION_MODE),无需 pglogical 处理"; \
@echo "✅ Node: $$(node -v)"; echo "✅ Yarn: $$(yarn -v 2>/dev/null || echo n/a)" fi
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 ------------------------------------------------------------- account-export:
install-openresty: @go run ./cmd/migratectl/main.go export --dsn "$(DB_URL)" --output "$(ACCOUNT_EXPORT_FILE)" $(if $(ACCOUNT_EMAIL_KEYWORD),--email "$(ACCOUNT_EMAIL_KEYWORD)")
@echo "🚀 Installing OpenResty using external script..."
@bash scripts/install-openresty.sh; \
# --- Redis ----------------------------------------------------------------- account-import:
install-redis: @[ -f "$(ACCOUNT_IMPORT_FILE)" ] || (echo "❌ 未找到文件 $(ACCOUNT_IMPORT_FILE)"; exit 1)
ifeq ($(OS),Darwin) @go run ./cmd/migratectl/main.go import --dsn "$(DB_URL)" --file "$(ACCOUNT_IMPORT_FILE)" \
brew install redis && brew services start redis $(if $(ACCOUNT_IMPORT_MERGE),--merge) \
else $(if $(ACCOUNT_IMPORT_MERGE_STRATEGY),--merge-strategy "$(ACCOUNT_IMPORT_MERGE_STRATEGY)") \
@echo "🟥 Installing Redis via setup_ubuntu_2204.sh..." $(if $(ACCOUNT_IMPORT_DRY_RUN),--dry-run) \
bash scripts/setup_ubuntu_2204.sh install-redis $(foreach UUID,$(ACCOUNT_IMPORT_MERGE_ALLOWLIST),--merge-allowlist $(UUID)) \
endif $(ACCOUNT_IMPORT_EXTRA_FLAGS)
# --- PostgreSQL ------------------------------------------------------------ account-sync-push:
install-postgresql: @[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
ifeq ($(OS),Darwin) @go run ./cmd/syncctl/main.go push --config "$(ACCOUNT_SYNC_CONFIG)"
@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-pull:
# Base container images @[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
# ----------------------------------------------------------------------------- @go run ./cmd/syncctl/main.go pull --config "$(ACCOUNT_SYNC_CONFIG)"
build-base-images: account-sync-mirror:
@OPENRESTY_IMAGE=$(OPENRESTY_IMAGE) POSTGRES_EXT_IMAGE=$(POSTGRES_EXT_IMAGE) \ @[ -f "$(ACCOUNT_SYNC_CONFIG)" ] || (echo "❌ 未找到配置文件 $(ACCOUNT_SYNC_CONFIG)"; exit 1)
NODE_BUILDER_IMAGE=$(NODE_BUILDER_IMAGE) NODE_RUNTIME_IMAGE=$(NODE_RUNTIME_IMAGE) \ @go run ./cmd/syncctl/main.go mirror --config "$(ACCOUNT_SYNC_CONFIG)"
GO_BUILDER_IMAGE=$(GO_BUILDER_IMAGE) GO_RUNTIME_IMAGE=$(GO_RUNTIME_IMAGE) \
bash scripts/build-base-images.sh
docker-openresty-geoip: create-super-admin:
docker build -f $(BASE_IMAGE_DIR)/openresty-geoip.Dockerfile -t $(OPENRESTY_IMAGE) $(BASE_IMAGE_DIR) @[ -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: build: init-go
docker build -f $(BASE_IMAGE_DIR)/node-builder.Dockerfile -t $(NODE_BUILDER_IMAGE) $(BASE_IMAGE_DIR) @go build -o $(APP_NAME) $(MAIN_FILE)
docker-node-runtime: upgrade: build
docker build -f $(BASE_IMAGE_DIR)/node-runtime.Dockerfile -t $(NODE_RUNTIME_IMAGE) $(BASE_IMAGE_DIR) systemctl stop xcontrol-account
cp xcontrol-account /usr/bin/xcontrol-account
systemctl start xcontrol-account
docker-go-builder: start: build
docker build -f $(BASE_IMAGE_DIR)/go-builder.Dockerfile -t $(GO_BUILDER_IMAGE) $(BASE_IMAGE_DIR) @./$(APP_NAME) --config config/account.yaml
docker-go-runtime: stop:
docker build -f $(BASE_IMAGE_DIR)/go-runtime.Dockerfile -t $(GO_RUNTIME_IMAGE) $(BASE_IMAGE_DIR) @pkill -f "$(APP_NAME)" || echo "⚠️ 未找到运行进程"
# -----------------------------------------------------------------------------
# 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 restart: stop start
# ----------------------------------------------------------------------------- test:
# CMS configuration validation go test ./...
# -----------------------------------------------------------------------------
lint-cms: clean:
python3 scripts/validate_cms_config.py 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 enabled: true
interval: 5m interval: 5m
outputPath: "/usr/local/etc/xray/config.json" outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json" templatePath: "config/xray.config.template.json"
validateCommand: [] validateCommand: []
restartCommand: restartCommand:
- "systemctl" - "systemctl"

View File

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

View File

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

View File

@ -156,12 +156,12 @@ type AgentCredential struct {
} }
// Load reads the configuration file at the provided path. When path is empty, // 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. // empty configuration is returned.
func Load(path string) (*Config, error) { func Load(path string) (*Config, error) {
p := path p := path
if p == "" { if p == "" {
p = filepath.Join("account", "config", "account.yaml") p = filepath.Join("config", "account.yaml")
} }
b, err := os.ReadFile(p) 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 enabled: false
interval: 5m interval: 5m
outputPath: "/usr/local/etc/xray/config.json" outputPath: "/usr/local/etc/xray/config.json"
templatePath: "account/config/xray.config.template.json" templatePath: "config/xray.config.template.json"
validateCommand: [] validateCommand: []
restartCommand: restartCommand:
- "systemctl" - "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=YOUR_PASSWORD
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 ## 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. - 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 ## 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. - `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. - `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: Run the suite with:
```bash ```bash
go test ./account/api -run AdminSettings go test ./api -run AdminSettings
``` ```

View File

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

View File

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

View File

@ -14,15 +14,15 @@
## 2. 系统架构 ## 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】 - `api`: 定义 REST API并实现用户注册、登录、会话维护等业务逻辑。【F:api/api.go†L1-L190】
- `account/internal/store`: 提供用户数据的读写接口与内存实现后续可扩展至数据库存储。【F:account/internal/store/store.go†L1-L109】 - `internal/store`: 提供用户数据的读写接口与内存实现后续可扩展至数据库存储。【F:internal/store/store.go†L1-L109】
- `account/internal/auth`: 声明可插拔的第三方认证提供方接口,为接入 LDAP/OIDC 等外部系统提供抽象。【F:account/internal/auth/auth.go†L1-L6】 - `internal/auth`: 声明可插拔的第三方认证提供方接口,为接入 LDAP/OIDC 等外部系统提供抽象。【F:internal/auth/auth.go†L1-L6】
- `account/internal/cache`: 预留会话缓存接口,便于集成 Redis 等缓存组件。【F:account/internal/cache/cache.go†L1-L6】 - `internal/cache`: 预留会话缓存接口,便于集成 Redis 等缓存组件。【F:internal/cache/cache.go†L1-L6】
- `account/config`: 管理服务配置结构体当前为空定义未来将扩展字段。【F:account/config/config.go†L1-L5】 - `config`: 管理服务配置结构体当前为空定义未来将扩展字段。【F:config/config.go†L1-L5】
内部调用关系示意: 内部调用关系示意:
@ -56,27 +56,27 @@ Gin Router → API Handler → Store / Session Manager → 数据存储
### 3.3 用户登录 ### 3.3 用户登录
- `POST /v1/login` - `POST /v1/login`
- 请求体:`{ "email": string, "password": string }` - 请求体:`{ "email": string, "password": string }`
- 功能:校验凭据,通过内存存储读取用户并验证哈希密码,成功后生成 24 小时有效的会话 token。【F:account/api/api.go†L65-L136】 - 功能:校验凭据,通过内存存储读取用户并验证哈希密码,成功后生成 24 小时有效的会话 token。【F:api/api.go†L65-L136】
### 3.4 查询会话 ### 3.4 查询会话
- `GET /v1/session` - `GET /v1/session`
- Header 中提供 `Authorization: Bearer <token>` 或查询参数 `token` - Header 中提供 `Authorization: Bearer <token>` 或查询参数 `token`
- 功能:校验 token返回关联用户信息。【F:account/api/api.go†L138-L176】 - 功能:校验 token返回关联用户信息。【F:api/api.go†L138-L176】
### 3.5 注销会话 ### 3.5 注销会话
- `DELETE /v1/session` - `DELETE /v1/session`
- Header 或查询参数传入 token删除内存中的会话记录。【F:account/api/api.go†L178-L190】 - Header 或查询参数传入 token删除内存中的会话记录。【F:api/api.go†L178-L190】
## 4. 数据模型 ## 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. 安全与扩展 ## 5. 安全与扩展
- **密码存储**:使用 `bcrypt` 哈希防止明文泄露。【F:account/api/api.go†L90-L108】 - **密码存储**:使用 `bcrypt` 哈希防止明文泄露。【F:api/api.go†L90-L108】
- **会话管理**:会话 token 为 32 字节随机数生成的十六进制字符串,并设置 24 小时过期过期后自动清理。【F:account/api/api.go†L112-L171】 - **会话管理**:会话 token 为 32 字节随机数生成的十六进制字符串,并设置 24 小时过期过期后自动清理。【F:api/api.go†L112-L171】
- **扩展点** - **扩展点**
- 可在 `Store` 接口层新增 PostgreSQL、MySQL 等实现。 - 可在 `Store` 接口层新增 PostgreSQL、MySQL 等实现。
- 可实现 `auth.Provider` 接口以支持外部身份源认证,再与内部用户绑定。 - 可实现 `auth.Provider` 接口以支持外部身份源认证,再与内部用户绑定。

View File

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

View File

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

View File

@ -1,7 +1,7 @@
# Cloud-Neutral 子域统一命名迁移规划 # Cloud-Neutral 子域统一命名迁移规划
## 1. 背景与目标 ## 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. 现状盘点
### 2.1 用户中心accounts.svc.plus ### 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】 - 多份文档(例如 `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 ### 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. `NEXT_PUBLIC_REGISTER_URL` and submits the form to that URL.
- `dashboard/app/api/auth/register/route.ts` handles the - `dashboard/app/api/auth/register/route.ts` handles the
`/api/auth/register` requests and forwards them to the account service. `/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. 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 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 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 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 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 ### 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. 2. Regenerating the config with the existing generator.
3. Optionally validating the JSON and restarting Xray using commands defined in configuration. 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 ```yaml
xray: xray:

86
go.mod
View File

@ -1,83 +1,61 @@
module xcontrol module account
go 1.23.0 go 1.25.1
toolchain go1.23.8
require ( require (
github.com/gin-contrib/cors v1.6.0 github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.11.0
github.com/go-git/go-git/v5 v5.16.2
github.com/golang-jwt/jwt/v5 v5.3.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/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.5 github.com/jackc/pgx/v5 v5.7.6
github.com/pgvector/pgvector-go v0.3.0
github.com/pkg/sftp v1.13.10 github.com/pkg/sftp v1.13.10
github.com/pquerna/otp v1.5.0 github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.12.0 github.com/spf13/cobra v1.10.2
github.com/spf13/cobra v1.9.1 golang.org/x/crypto v0.45.0
github.com/yuin/goldmark v1.7.13
golang.org/x/crypto v0.41.0
golang.org/x/net v0.42.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.5.7 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde gorm.io/gorm v1.31.1
) )
require ( 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/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/bytedance/sonic v1.11.2 // indirect github.com/bytedance/sonic v1.14.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/gin-contrib/sse v1.1.0 // 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/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // 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/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // 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.3.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/kr/fs v0.1.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/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/quic-go/quic-go v0.57.1 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect github.com/spf13/pflag v1.0.9 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.31.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/warnings.v0 v0.1.2 // 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 h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 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/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-contrib/cors v1.6.0 h1:0Z7D/bVhE6ja07lI8CTjTonp6SB07o8bNuFyRbsBUQg= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-contrib/cors v1.6.0/go.mod h1:cI+h6iOAyxKRtUtC6iF/Si1KSFvGm/gK+kshxlCi8ro= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 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/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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 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 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 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/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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 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 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 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.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 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/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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= 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/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.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 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= 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/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/redis/go-redis/v9 v9.12.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 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/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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.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.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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 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/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.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde h1:9DShaph9qhkIYw7QF91I/ynrr4cOO2PZra2PFD7Mfeg= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
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=

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 set -euo pipefail

View File

@ -37,7 +37,7 @@ log_error() {
# 配置路径 # 配置路径
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DASHBOARD_CONFIG="$ROOT_DIR/dashboard-fresh/config/runtime-service-config.base.yaml" 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" RAG_CONFIG="$ROOT_DIR/rag-server/config/server.yaml"
MANUAL_FILE="$ROOT_DIR/TOKEN_AUTH_MANUAL.md" 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 ## Configuration
No configuration files are required. The scripts automatically detect: 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 - Configuration files in each service
- Authentication implementations - Authentication implementations

View File

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

View File

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