From 3d16851d2079a0660f28f9dc0e23961e6257bcb4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=96=9B=E4=BF=9D=E5=BA=93=EF=BC=88xuebaoku=EF=BC=89?=
Date: Wed, 21 Jan 2026 13:33:28 +0800
Subject: [PATCH 01/11] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=20LDAP?=
=?UTF-8?q?=20=E8=AE=A4=E8=AF=81=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现 LDAP 客户端封装,支持 LDAP/LDAPS/StartTLS 连接
- 添加 LDAP 配置结构和默认值
- 实现 LDAP 认证服务,支持首次登录自动创建用户
- 添加 LDAP 登录 HTTP 端点和路由
- 配置 Wire 依赖注入,支持可选 LDAP 客户端
---
backend/internal/config/config.go | 53 +++++++
backend/internal/handler/auth_handler.go | 28 ++++
backend/internal/pkg/ldap/client.go | 171 +++++++++++++++++++++++
backend/internal/server/routes/auth.go | 1 +
backend/internal/service/auth_service.go | 115 +++++++++++++++
backend/internal/service/wire.go | 10 ++
6 files changed, 378 insertions(+)
create mode 100644 backend/internal/pkg/ldap/client.go
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index 00a784802..6a7895d15 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -62,6 +62,7 @@ type Config struct {
Timezone string `mapstructure:"timezone"` // e.g. "Asia/Shanghai", "UTC"
Gemini GeminiConfig `mapstructure:"gemini"`
Update UpdateConfig `mapstructure:"update"`
+ LDAP LDAPConfig `mapstructure:"ldap"`
}
type GeminiConfig struct {
@@ -93,6 +94,43 @@ type UpdateConfig struct {
ProxyURL string `mapstructure:"proxy_url"`
}
+// LDAPConfig LDAP 认证配置
+type LDAPConfig struct {
+ // Enabled 是否启用 LDAP 认证
+ Enabled bool `mapstructure:"enabled"`
+ // Host LDAP 服务器地址
+ Host string `mapstructure:"host"`
+ // Port LDAP 服务器端口(389 或 636)
+ Port int `mapstructure:"port"`
+ // UseTLS 是否使用 LDAPS(LDAP over SSL/TLS)
+ UseTLS bool `mapstructure:"use_tls"`
+ // UseStartTLS 是否使用 StartTLS 升级连接
+ UseStartTLS bool `mapstructure:"use_start_tls"`
+ // SkipTLSVerify 是否跳过 TLS 证书验证(仅开发环境)
+ SkipTLSVerify bool `mapstructure:"skip_tls_verify"`
+ // BindDN 管理员 DN(用于搜索用户)
+ BindDN string `mapstructure:"bind_dn"`
+ // BindPassword 管理员密码
+ BindPassword string `mapstructure:"bind_password"`
+ // BaseDN 搜索基准 DN
+ BaseDN string `mapstructure:"base_dn"`
+ // UserFilter 用户搜索过滤器(如 "(uid=%s)")
+ UserFilter string `mapstructure:"user_filter"`
+ // Attributes 用户属性映射
+ Attributes LDAPAttributesConfig `mapstructure:"attributes"`
+}
+
+// LDAPAttributesConfig LDAP 用户属性映射配置
+type LDAPAttributesConfig struct {
+ // Username 用户名属性(默认 "uid")
+ Username string `mapstructure:"username"`
+ // Email 邮箱属性(默认 "mail")
+ Email string `mapstructure:"email"`
+ // DisplayName 显示名称属性(默认 "cn")
+ DisplayName string `mapstructure:"display_name"`
+}
+
+
type LinuxDoConnectConfig struct {
Enabled bool `mapstructure:"enabled"`
ClientID string `mapstructure:"client_id"`
@@ -869,6 +907,21 @@ func setDefaults() {
viper.SetDefault("gemini.oauth.client_secret", "")
viper.SetDefault("gemini.oauth.scopes", "")
viper.SetDefault("gemini.quota.policy", "")
+
+ // LDAP
+ viper.SetDefault("ldap.enabled", false)
+ viper.SetDefault("ldap.host", "")
+ viper.SetDefault("ldap.port", 389)
+ viper.SetDefault("ldap.use_tls", false)
+ viper.SetDefault("ldap.use_start_tls", false)
+ viper.SetDefault("ldap.skip_tls_verify", false)
+ viper.SetDefault("ldap.bind_dn", "")
+ viper.SetDefault("ldap.bind_password", "")
+ viper.SetDefault("ldap.base_dn", "")
+ viper.SetDefault("ldap.user_filter", "(uid=%s)")
+ viper.SetDefault("ldap.attributes.username", "uid")
+ viper.SetDefault("ldap.attributes.email", "mail")
+ viper.SetDefault("ldap.attributes.display_name", "cn")
}
func (c *Config) Validate() error {
diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go
index 89f34aae7..04cd3b9e6 100644
--- a/backend/internal/handler/auth_handler.go
+++ b/backend/internal/handler/auth_handler.go
@@ -59,6 +59,12 @@ type LoginRequest struct {
TurnstileToken string `json:"turnstile_token"`
}
+// LDAPLoginRequest represents the LDAP login request payload
+type LDAPLoginRequest struct {
+ Username string `json:"username" binding:"required"`
+ Password string `json:"password" binding:"required"`
+}
+
// AuthResponse 认证响应格式(匹配前端期望)
type AuthResponse struct {
AccessToken string `json:"access_token"`
@@ -247,3 +253,25 @@ func (h *AuthHandler) ValidatePromoCode(c *gin.Context) {
BonusAmount: promoCode.BonusAmount,
})
}
+
+// LDAPLogin handles LDAP user login
+// POST /api/v1/auth/ldap/login
+func (h *AuthHandler) LDAPLogin(c *gin.Context) {
+ var req LDAPLoginRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ response.BadRequest(c, "Invalid request: "+err.Error())
+ return
+ }
+
+ token, user, err := h.authService.AuthenticateWithLDAP(c.Request.Context(), req.Username, req.Password)
+ if err != nil {
+ response.ErrorFrom(c, err)
+ return
+ }
+
+ response.Success(c, AuthResponse{
+ AccessToken: token,
+ TokenType: "Bearer",
+ User: dto.UserFromService(user),
+ })
+}
diff --git a/backend/internal/pkg/ldap/client.go b/backend/internal/pkg/ldap/client.go
new file mode 100644
index 000000000..fa58574a4
--- /dev/null
+++ b/backend/internal/pkg/ldap/client.go
@@ -0,0 +1,171 @@
+// Package ldap provides LDAP authentication client.
+package ldap
+
+import (
+ "crypto/tls"
+ "fmt"
+
+ "github.com/Wei-Shaw/sub2api/backend/internal/config"
+ "github.com/go-ldap/ldap/v3"
+)
+
+// Client LDAP 客户端
+type Client struct {
+ config *config.LDAPConfig
+ conn *ldap.Conn
+}
+
+// UserInfo LDAP 用户信息
+type UserInfo struct {
+ Username string // 用户名
+ Email string // 邮箱
+ DisplayName string // 显示名称
+ DN string // 用户的完整 DN
+}
+
+// NewClient 创建 LDAP 客户端
+func NewClient(cfg *config.LDAPConfig) (*Client, error) {
+ if cfg == nil {
+ return nil, fmt.Errorf("ldap config is nil")
+ }
+ if !cfg.Enabled {
+ return nil, fmt.Errorf("ldap is not enabled")
+ }
+ return &Client{config: cfg}, nil
+}
+
+// Connect 建立 LDAP 连接
+func (c *Client) Connect() error {
+ var conn *ldap.Conn
+ var err error
+
+ addr := fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
+
+ // 根据配置选择连接方式
+ if c.config.UseTLS {
+ // 使用 LDAPS(LDAP over SSL/TLS)
+ tlsConfig := &tls.Config{
+ InsecureSkipVerify: c.config.SkipTLSVerify,
+ ServerName: c.config.Host,
+ }
+ conn, err = ldap.DialTLS("tcp", addr, tlsConfig)
+ if err != nil {
+ return fmt.Errorf("ldap dial tls failed: %w", err)
+ }
+ } else {
+ // 使用普通 LDAP
+ conn, err = ldap.Dial("tcp", addr)
+ if err != nil {
+ return fmt.Errorf("ldap dial failed: %w", err)
+ }
+
+ // 如果配置了 StartTLS,升级连接
+ if c.config.UseStartTLS {
+ tlsConfig := &tls.Config{
+ InsecureSkipVerify: c.config.SkipTLSVerify,
+ ServerName: c.config.Host,
+ }
+ err = conn.StartTLS(tlsConfig)
+ if err != nil {
+ conn.Close()
+ return fmt.Errorf("ldap start tls failed: %w", err)
+ }
+ }
+ }
+
+ c.conn = conn
+ return nil
+}
+
+// Close 关闭 LDAP 连接
+func (c *Client) Close() error {
+ if c.conn != nil {
+ c.conn.Close()
+ c.conn = nil
+ }
+ return nil
+}
+
+// Authenticate 认证用户
+// 返回用户信息,如果认证失败返回错误
+func (c *Client) Authenticate(username, password string) (*UserInfo, error) {
+ // 建立连接
+ if err := c.Connect(); err != nil {
+ return nil, err
+ }
+ defer c.Close()
+
+ // 使用管理员账号绑定,用于搜索用户
+ err := c.conn.Bind(c.config.BindDN, c.config.BindPassword)
+ if err != nil {
+ return nil, fmt.Errorf("admin bind failed: %w", err)
+ }
+
+ // 搜索用户
+ userInfo, err := c.SearchUser(username)
+ if err != nil {
+ return nil, err
+ }
+
+ // 使用用户凭证进行绑定验证
+ err = c.conn.Bind(userInfo.DN, password)
+ if err != nil {
+ return nil, fmt.Errorf("user authentication failed: %w", err)
+ }
+
+ return userInfo, nil
+}
+
+// SearchUser 搜索用户信息
+func (c *Client) SearchUser(username string) (*UserInfo, error) {
+ // 构建搜索过滤器
+ filter := fmt.Sprintf(c.config.UserFilter, ldap.EscapeFilter(username))
+
+ // 构建搜索请求
+ searchRequest := ldap.NewSearchRequest(
+ c.config.BaseDN,
+ ldap.ScopeWholeSubtree,
+ ldap.NeverDerefAliases,
+ 0, // 不限制结果数量
+ 0, // 不限制搜索时间
+ false,
+ filter,
+ []string{
+ c.config.Attributes.Username,
+ c.config.Attributes.Email,
+ c.config.Attributes.DisplayName,
+ },
+ nil,
+ )
+
+ // 执行搜索
+ result, err := c.conn.Search(searchRequest)
+ if err != nil {
+ return nil, fmt.Errorf("ldap search failed: %w", err)
+ }
+
+ // 检查结果
+ if len(result.Entries) == 0 {
+ return nil, fmt.Errorf("user not found: %s", username)
+ }
+ if len(result.Entries) > 1 {
+ return nil, fmt.Errorf("multiple users found: %s", username)
+ }
+
+ entry := result.Entries[0]
+
+ // 提取用户信息
+ userInfo := &UserInfo{
+ DN: entry.DN,
+ Username: entry.GetAttributeValue(c.config.Attributes.Username),
+ Email: entry.GetAttributeValue(c.config.Attributes.Email),
+ DisplayName: entry.GetAttributeValue(c.config.Attributes.DisplayName),
+ }
+
+ // 如果用户名为空,使用搜索的用户名
+ if userInfo.Username == "" {
+ userInfo.Username = username
+ }
+
+ return userInfo, nil
+}
diff --git a/backend/internal/server/routes/auth.go b/backend/internal/server/routes/auth.go
index aa691eba1..d2b6bfc23 100644
--- a/backend/internal/server/routes/auth.go
+++ b/backend/internal/server/routes/auth.go
@@ -26,6 +26,7 @@ func RegisterAuthRoutes(
{
auth.POST("/register", h.Auth.Register)
auth.POST("/login", h.Auth.Login)
+ auth.POST("/ldap/login", h.Auth.LDAPLogin)
auth.POST("/send-verify-code", h.Auth.SendVerifyCode)
// 优惠码验证接口添加速率限制:每分钟最多 10 次(Redis 故障时 fail-close)
auth.POST("/validate-promo-code", rateLimiter.LimitWithOptions("validate-promo", 10, time.Minute, middleware.RateLimitOptions{
diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go
index 854e77329..7192bdc4b 100644
--- a/backend/internal/service/auth_service.go
+++ b/backend/internal/service/auth_service.go
@@ -13,6 +13,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/config"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/ldap"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
@@ -53,6 +54,7 @@ type AuthService struct {
turnstileService *TurnstileService
emailQueueService *EmailQueueService
promoService *PromoService
+ ldapClient *ldap.Client
}
// NewAuthService 创建认证服务实例
@@ -64,6 +66,7 @@ func NewAuthService(
turnstileService *TurnstileService,
emailQueueService *EmailQueueService,
promoService *PromoService,
+ ldapClient *ldap.Client,
) *AuthService {
return &AuthService{
userRepo: userRepo,
@@ -73,6 +76,7 @@ func NewAuthService(
turnstileService: turnstileService,
emailQueueService: emailQueueService,
promoService: promoService,
+ ldapClient: ldapClient,
}
}
@@ -580,3 +584,114 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (
// 生成新token
return s.GenerateToken(user)
}
+
+// AuthenticateWithLDAP 使用 LDAP 认证用户
+// 返回 JWT token 和用户信息
+func (s *AuthService) AuthenticateWithLDAP(ctx context.Context, username, password string) (string, *User, error) {
+ // 检查 LDAP 是否启用
+ if s.ldapClient == nil {
+ return "", nil, infraerrors.BadRequest("LDAP_NOT_ENABLED", "LDAP authentication is not enabled")
+ }
+
+ // 使用 LDAP 认证
+ userInfo, err := s.ldapClient.Authenticate(username, password)
+ if err != nil {
+ log.Printf("[Auth] LDAP authentication failed for user %s: %v", username, err)
+ return "", nil, ErrInvalidCredentials
+ }
+
+ // 查找或创建本地用户
+ user, err := s.FindOrCreateLDAPUser(ctx, userInfo)
+ if err != nil {
+ log.Printf("[Auth] Failed to find or create LDAP user %s: %v", username, err)
+ return "", nil, err
+ }
+
+ // 检查用户状态
+ if !user.IsActive() {
+ return "", nil, ErrUserNotActive
+ }
+
+ // 生成 JWT token
+ token, err := s.GenerateToken(user)
+ if err != nil {
+ return "", nil, err
+ }
+
+ return token, user, nil
+}
+
+// FindOrCreateLDAPUser 查找或创建 LDAP 用户
+func (s *AuthService) FindOrCreateLDAPUser(ctx context.Context, userInfo *ldap.UserInfo) (*User, error) {
+ // 根据用户名查找用户
+ user, err := s.userRepo.GetByUsername(ctx, userInfo.Username)
+ if err == nil {
+ // 用户已存在,更新邮箱和显示名称(如果有变化)
+ needUpdate := false
+ if userInfo.Email != "" && user.Email != userInfo.Email {
+ user.Email = userInfo.Email
+ needUpdate = true
+ }
+ if userInfo.DisplayName != "" && user.Username != userInfo.DisplayName {
+ // 注意:这里不更新 Username,因为它是唯一标识
+ // 如果需要显示名称,应该在 User 模型中添加 DisplayName 字段
+ needUpdate = false // 暂时不更新
+ }
+ if needUpdate {
+ if err := s.userRepo.Update(ctx, user); err != nil {
+ log.Printf("[Auth] Failed to update LDAP user %s: %v", userInfo.Username, err)
+ }
+ }
+ return user, nil
+ }
+
+ // 用户不存在,创建新用户
+ if !errors.Is(err, ErrUserNotFound) {
+ return nil, err
+ }
+
+ // 生成随机密码(LDAP 用户不使用本地密码)
+ randomPassword, err := generateRandomPassword(32)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate random password: %w", err)
+ }
+
+ hashedPassword, err := bcrypt.GenerateFromPassword([]byte(randomPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return nil, fmt.Errorf("failed to hash password: %w", err)
+ }
+
+ // 创建新用户
+ newUser := &User{
+ Username: userInfo.Username,
+ Email: userInfo.Email,
+ PasswordHash: string(hashedPassword),
+ Role: "user", // 默认角色为普通用户
+ Status: "active",
+ Balance: s.cfg.Default.UserBalance,
+ Concurrency: s.cfg.Default.UserConcurrency,
+ TokenVersion: 0,
+ }
+
+ // 如果邮箱为空,使用用户名生成一个占位邮箱
+ if newUser.Email == "" {
+ newUser.Email = fmt.Sprintf("%s@ldap.local", userInfo.Username)
+ }
+
+ if err := s.userRepo.Create(ctx, newUser); err != nil {
+ return nil, fmt.Errorf("failed to create LDAP user: %w", err)
+ }
+
+ log.Printf("[Auth] Created new LDAP user: %s (email: %s)", newUser.Username, newUser.Email)
+ return newUser, nil
+}
+
+// generateRandomPassword 生成随机密码
+func generateRandomPassword(length int) (string, error) {
+ bytes := make([]byte, length)
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ return hex.EncodeToString(bytes), nil
+}
+
diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go
index b210286d3..d02cb556f 100644
--- a/backend/internal/service/wire.go
+++ b/backend/internal/service/wire.go
@@ -6,6 +6,7 @@ import (
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/ldap"
"github.com/google/wire"
"github.com/redis/go-redis/v9"
)
@@ -202,9 +203,18 @@ func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthC
return apiKeyService
}
+// ProvideLDAPClient 提供 LDAP 客户端(如果启用)
+func ProvideLDAPClient(cfg *config.Config) (*ldap.Client, error) {
+ if cfg == nil || !cfg.LDAP.Enabled {
+ return nil, nil
+ }
+ return ldap.NewClient(&cfg.LDAP)
+}
+
// ProviderSet is the Wire provider set for all services
var ProviderSet = wire.NewSet(
// Core services
+ ProvideLDAPClient,
NewAuthService,
NewUserService,
NewAPIKeyService,
From a3bbc1aa3a4ecdada0cd7aaa372975628739e0f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=96=9B=E4=BF=9D=E5=BA=93=EF=BC=88xuebaoku=EF=BC=89?=
Date: Wed, 21 Jan 2026 13:33:54 +0800
Subject: [PATCH 02/11] =?UTF-8?q?feat(frontend):=20=E6=B7=BB=E5=8A=A0=20LD?=
=?UTF-8?q?AP=20=E7=99=BB=E5=BD=95=E5=89=8D=E7=AB=AF=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 添加 LDAPLoginRequest 类型定义
- 实现 ldapLogin API 调用方法
- 扩展登录页面,添加 LDAP 登录逻辑和状态管理
---
frontend/src/api/auth.ts | 17 +++++++
frontend/src/types/index.ts | 6 +++
frontend/src/views/auth/LoginView.vue | 73 ++++++++++++++++++++++++++-
3 files changed, 95 insertions(+), 1 deletion(-)
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
index fddc23efe..37ad54637 100644
--- a/frontend/src/api/auth.ts
+++ b/frontend/src/api/auth.ts
@@ -6,6 +6,7 @@
import { apiClient } from './client'
import type {
LoginRequest,
+ LDAPLoginRequest,
RegisterRequest,
AuthResponse,
CurrentUserResponse,
@@ -51,6 +52,21 @@ export async function login(credentials: LoginRequest): Promise {
return data
}
+/**
+ * LDAP user login
+ * @param credentials - LDAP username and password
+ * @returns Authentication response with token and user data
+ */
+export async function ldapLogin(credentials: LDAPLoginRequest): Promise {
+ const { data } = await apiClient.post('/auth/ldap/login', credentials)
+
+ // Store token and user data
+ setAuthToken(data.access_token)
+ localStorage.setItem('auth_user', JSON.stringify(data.user))
+
+ return data
+}
+
/**
* User registration
* @param userData - Registration data (username, email, password)
@@ -135,6 +151,7 @@ export async function validatePromoCode(code: string): Promise(false)
const errorMessage = ref('')
const showPassword = ref(false)
+const loginType = ref<'email' | 'ldap'>('email')
// Public settings
const turnstileEnabled = ref(false)
@@ -194,12 +195,22 @@ const formData = reactive({
password: ''
})
+const ldapFormData = reactive({
+ username: '',
+ password: ''
+})
+
const errors = reactive({
email: '',
password: '',
turnstile: ''
})
+const ldapErrors = reactive({
+ username: '',
+ password: ''
+})
+
// ==================== Lifecycle ====================
onMounted(async () => {
@@ -326,6 +337,66 @@ async function handleLogin(): Promise {
isLoading.value = false
}
}
+
+// ==================== LDAP Login Handler ====================
+
+async function handleLDAPLogin(): Promise {
+ // Clear previous error
+ errorMessage.value = ''
+ ldapErrors.username = ''
+ ldapErrors.password = ''
+
+ // Validate LDAP form
+ let isValid = true
+ if (!ldapFormData.username.trim()) {
+ ldapErrors.username = t('auth.usernameRequired')
+ isValid = false
+ }
+ if (!ldapFormData.password) {
+ ldapErrors.password = t('auth.passwordRequired')
+ isValid = false
+ }
+
+ if (!isValid) {
+ return
+ }
+
+ isLoading.value = true
+
+ try {
+ // Call LDAP login API directly (it already stores token and user)
+ await ldapLogin({
+ username: ldapFormData.username,
+ password: ldapFormData.password
+ })
+
+ // Refresh auth store state from localStorage
+ authStore.checkAuth()
+
+ // Show success toast
+ appStore.showSuccess(t('auth.loginSuccess'))
+
+ // Redirect to dashboard or intended route
+ const redirectTo = (router.currentRoute.value.query.redirect as string) || '/dashboard'
+ await router.push(redirectTo)
+ } catch (error: unknown) {
+ // Handle login error
+ const err = error as { message?: string; response?: { data?: { detail?: string } } }
+
+ if (err.response?.data?.detail) {
+ errorMessage.value = err.response.data.detail
+ } else if (err.message) {
+ errorMessage.value = err.message
+ } else {
+ errorMessage.value = t('auth.ldapLoginFailed')
+ }
+
+ // Also show error toast
+ appStore.showError(errorMessage.value)
+ } finally {
+ isLoading.value = false
+ }
+}
diff --git a/frontend/src/stores/auth.ts b/frontend/src/stores/auth.ts
index e4612f5e6..216e20855 100644
--- a/frontend/src/stores/auth.ts
+++ b/frontend/src/stores/auth.ts
@@ -115,6 +115,23 @@ export const useAuthStore = defineStore('auth', () => {
}
}
+ /**
+ * LDAP login
+ * @param credentials - LDAP credentials (username and password)
+ * @returns Promise resolving to the auth response
+ * @throws Error if login fails
+ */
+ async function ldapLogin(credentials: { username: string; password: string }): Promise {
+ try {
+ const response = await authAPI.ldapLogin(credentials)
+ setAuthFromResponse(response)
+ return response
+ } catch (error) {
+ clearAuth()
+ throw error
+ }
+ }
+
/**
* Complete login with 2FA code
* @param tempToken - Temporary token from initial login
@@ -285,6 +302,7 @@ export const useAuthStore = defineStore('auth', () => {
// Actions
login,
+ ldapLogin,
login2FA,
register,
setToken,
From 0d5518b90429d972020b0fe4556c4c017881758b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=96=9B=E4=BF=9D=E5=BA=93=EF=BC=88xuebaoku=EF=BC=89?=
Date: Tue, 27 Jan 2026 13:12:41 +0800
Subject: [PATCH 10/11] =?UTF-8?q?feat(auth):=20=E5=AE=8C=E5=96=84LDAP?=
=?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在UserRepository中添加GetByUsername方法支持LDAP用户查询
- 在Wire依赖注入中添加LDAP客户端Provider
- 添加LDAP相关的前端国际化翻译(中英文)
- 在登录页面添加LDAP登录入口
- 在系统设置中添加LDAP配置选项
- 完善LDAP认证的前后端集成
---
backend/internal/handler/dto/settings.go | 1 +
backend/internal/handler/setting_handler.go | 1 +
backend/internal/repository/user_repo.go | 17 +++++++++++++++++
backend/internal/service/setting_service.go | 6 ++++++
backend/internal/service/settings_view.go | 1 +
backend/internal/service/user_service.go | 1 +
backend/internal/service/wire.go | 11 ++++++++---
frontend/src/i18n/locales/en.ts | 13 +++++++++++++
frontend/src/i18n/locales/zh.ts | 13 +++++++++++++
frontend/src/stores/app.ts | 1 +
frontend/src/types/index.ts | 1 +
frontend/src/views/auth/LoginView.vue | 6 ++++++
12 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go
index fc7b1349f..fdcdf58ff 100644
--- a/backend/internal/handler/dto/settings.go
+++ b/backend/internal/handler/dto/settings.go
@@ -73,6 +73,7 @@ type PublicSettings struct {
HomeContent string `json:"home_content"`
HideCcsImportButton bool `json:"hide_ccs_import_button"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
+ LDAPEnabled bool `json:"ldap_enabled"` // LDAP 认证
Version string `json:"version"`
}
diff --git a/backend/internal/handler/setting_handler.go b/backend/internal/handler/setting_handler.go
index 9c0bde332..53628eb8c 100644
--- a/backend/internal/handler/setting_handler.go
+++ b/backend/internal/handler/setting_handler.go
@@ -47,6 +47,7 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
HomeContent: settings.HomeContent,
HideCcsImportButton: settings.HideCcsImportButton,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
+ LDAPEnabled: settings.LDAPEnabled,
Version: h.version,
})
}
diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go
index fe5b645c1..963a45f09 100644
--- a/backend/internal/repository/user_repo.go
+++ b/backend/internal/repository/user_repo.go
@@ -113,6 +113,23 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*service
return out, nil
}
+func (r *userRepository) GetByUsername(ctx context.Context, username string) (*service.User, error) {
+ m, err := r.client.User.Query().Where(dbuser.UsernameEQ(username)).Only(ctx)
+ if err != nil {
+ return nil, translatePersistenceError(err, service.ErrUserNotFound, nil)
+ }
+
+ out := userEntityToService(m)
+ groups, err := r.loadAllowedGroups(ctx, []int64{m.ID})
+ if err != nil {
+ return nil, err
+ }
+ if v, ok := groups[m.ID]; ok {
+ out.AllowedGroups = v
+ }
+ return out, nil
+}
+
func (r *userRepository) Update(ctx context.Context, userIn *service.User) error {
if userIn == nil {
return nil
diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go
index 2a1e7d33a..b81622280 100644
--- a/backend/internal/service/setting_service.go
+++ b/backend/internal/service/setting_service.go
@@ -88,6 +88,9 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
linuxDoEnabled = s.cfg != nil && s.cfg.LinuxDo.Enabled
}
+ // LDAP enabled (read from config)
+ ldapEnabled := s.cfg != nil && s.cfg.LDAP.Enabled
+
// Password reset requires email verification to be enabled
emailVerifyEnabled := settings[SettingKeyEmailVerifyEnabled] == "true"
passwordResetEnabled := emailVerifyEnabled && settings[SettingKeyPasswordResetEnabled] == "true"
@@ -109,6 +112,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
HomeContent: settings[SettingKeyHomeContent],
HideCcsImportButton: settings[SettingKeyHideCcsImportButton] == "true",
LinuxDoOAuthEnabled: linuxDoEnabled,
+ LDAPEnabled: ldapEnabled,
}, nil
}
@@ -149,6 +153,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
HomeContent string `json:"home_content,omitempty"`
HideCcsImportButton bool `json:"hide_ccs_import_button"`
LinuxDoOAuthEnabled bool `json:"linuxdo_oauth_enabled"`
+ LDAPEnabled bool `json:"ldap_enabled"`
Version string `json:"version,omitempty"`
}{
RegistrationEnabled: settings.RegistrationEnabled,
@@ -167,6 +172,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
HomeContent: settings.HomeContent,
HideCcsImportButton: settings.HideCcsImportButton,
LinuxDoOAuthEnabled: settings.LinuxDoOAuthEnabled,
+ LDAPEnabled: settings.LDAPEnabled,
Version: s.version,
}, nil
}
diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go
index f10254e55..8b719f766 100644
--- a/backend/internal/service/settings_view.go
+++ b/backend/internal/service/settings_view.go
@@ -75,6 +75,7 @@ type PublicSettings struct {
HomeContent string
HideCcsImportButton bool
LinuxDoOAuthEnabled bool
+ LDAPEnabled bool // LDAP 认证
Version string
}
diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go
index 99bf7fd0c..03e2ba1ea 100644
--- a/backend/internal/service/user_service.go
+++ b/backend/internal/service/user_service.go
@@ -26,6 +26,7 @@ type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id int64) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
+ GetByUsername(ctx context.Context, username string) (*User, error)
GetFirstAdmin(ctx context.Context) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go
index 7167d3e0a..c4cd0fd9d 100644
--- a/backend/internal/service/wire.go
+++ b/backend/internal/service/wire.go
@@ -3,6 +3,7 @@ package service
import (
"context"
"database/sql"
+ "fmt"
"time"
"github.com/Wei-Shaw/sub2api/internal/config"
@@ -211,11 +212,15 @@ func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthC
}
// ProvideLDAPClient 提供 LDAP 客户端(如果启用)
-func ProvideLDAPClient(cfg *config.Config) (*ldap.Client, error) {
+func ProvideLDAPClient(cfg *config.Config) *ldap.Client {
if cfg == nil || !cfg.LDAP.Enabled {
- return nil, nil
+ return nil
}
- return ldap.NewClient(&cfg.LDAP)
+ client, err := ldap.NewClient(&cfg.LDAP)
+ if err != nil {
+ panic(fmt.Sprintf("failed to create LDAP client: %v", err))
+ }
+ return client
}
// ProviderSet is the Wire provider set for all services
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index 279bcef6d..950ad7877 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -261,6 +261,19 @@ export default {
promoCodeAlreadyUsed: 'You have already used this promo code',
promoCodeValidating: 'Promo code is being validated, please wait',
promoCodeInvalidCannotRegister: 'Invalid promo code. Please check and try again or clear the promo code field',
+ ldap: {
+ usernameLabel: 'Username',
+ usernamePlaceholder: 'Enter your username',
+ usernameRequired: 'Username is required',
+ passwordLabel: 'Password',
+ passwordPlaceholder: 'Enter your password',
+ passwordRequired: 'Password is required',
+ signIn: 'Sign in with LDAP',
+ signingIn: 'Signing in...',
+ orContinue: 'or continue with email',
+ loginSuccess: 'Login successful',
+ loginFailed: 'Login failed. Please check your username and password'
+ },
linuxdo: {
signIn: 'Continue with Linux.do',
orContinue: 'or continue with email',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index ea7ceb61c..db99f50a8 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -258,6 +258,19 @@ export default {
promoCodeAlreadyUsed: '您已使用过此优惠码',
promoCodeValidating: '优惠码正在验证中,请稍候',
promoCodeInvalidCannotRegister: '优惠码无效,请检查后重试或清空优惠码',
+ ldap: {
+ usernameLabel: '用户名',
+ usernamePlaceholder: '请输入用户名',
+ usernameRequired: '请输入用户名',
+ passwordLabel: '密码',
+ passwordPlaceholder: '请输入密码',
+ passwordRequired: '请输入密码',
+ signIn: 'LDAP 登录',
+ signingIn: '登录中...',
+ orContinue: '或使用邮箱密码继续',
+ loginSuccess: '登录成功',
+ loginFailed: '登录失败,请检查用户名和密码'
+ },
linuxdo: {
signIn: '使用 Linux.do 登录',
orContinue: '或使用邮箱密码继续',
diff --git a/frontend/src/stores/app.ts b/frontend/src/stores/app.ts
index c5a1ffc65..e297d102c 100644
--- a/frontend/src/stores/app.ts
+++ b/frontend/src/stores/app.ts
@@ -325,6 +325,7 @@ export const useAppStore = defineStore('app', () => {
home_content: '',
hide_ccs_import_button: false,
linuxdo_oauth_enabled: false,
+ ldap_enabled: false,
version: siteVersion.value
}
}
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index e3d0fc7b1..65051ec85 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -89,6 +89,7 @@ export interface PublicSettings {
home_content: string
hide_ccs_import_button: boolean
linuxdo_oauth_enabled: boolean
+ ldap_enabled: boolean
version: string
}
diff --git a/frontend/src/views/auth/LoginView.vue b/frontend/src/views/auth/LoginView.vue
index 203701081..1100c351f 100644
--- a/frontend/src/views/auth/LoginView.vue
+++ b/frontend/src/views/auth/LoginView.vue
@@ -11,6 +11,9 @@
+
+
+
@@ -180,6 +183,7 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
+import LDAPLoginSection from '@/components/auth/LDAPLoginSection.vue'
import LinuxDoOAuthSection from '@/components/auth/LinuxDoOAuthSection.vue'
import TotpLoginModal from '@/components/auth/TotpLoginModal.vue'
import Icon from '@/components/icons/Icon.vue'
@@ -205,6 +209,7 @@ const showPassword = ref(false)
// Public settings
const turnstileEnabled = ref(false)
const turnstileSiteKey = ref('')
+const ldapEnabled = ref(false)
const linuxdoOAuthEnabled = ref(false)
const passwordResetEnabled = ref(false)
@@ -244,6 +249,7 @@ onMounted(async () => {
const settings = await getPublicSettings()
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
+ ldapEnabled.value = settings.ldap_enabled
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
passwordResetEnabled.value = settings.password_reset_enabled
} catch (error) {
From 48358584f4f97a17be7d2160334823bdd19da55d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=96=9B=E4=BF=9D=E5=BA=93=EF=BC=88xuebaoku=EF=BC=89?=
Date: Tue, 27 Jan 2026 13:13:38 +0800
Subject: [PATCH 11/11] =?UTF-8?q?chore:=20=E6=9B=B4=E6=96=B0=E4=BE=9D?=
=?UTF-8?q?=E8=B5=96=E5=92=8CDocker=E6=9E=84=E5=BB=BA=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在Dockerfile中添加Wire代码生成步骤
- 删除wire_gen.go(改为构建时自动生成)
- 更新Go依赖(LDAP、TOTP、缓存等库)
- 确保Docker构建时依赖注入代码正确生成
---
Dockerfile | 3 +
backend/cmd/server/wire_gen.go | 346 ---------------------------------
backend/go.mod | 17 +-
backend/go.sum | 54 ++++-
4 files changed, 62 insertions(+), 358 deletions(-)
delete mode 100644 backend/cmd/server/wire_gen.go
diff --git a/Dockerfile b/Dockerfile
index 9bdd88f9c..dfcf1eda2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -63,6 +63,9 @@ RUN go mod tidy
# Copy frontend dist from previous stage (must be after backend copy to avoid being overwritten)
COPY --from=frontend-builder /app/backend/internal/web/dist ./internal/web/dist
+# Generate Wire dependency injection code
+RUN go generate ./cmd/server
+
# Build the binary (BuildType=release for CI builds, embed frontend)
RUN CGO_ENABLED=0 GOOS=linux go build \
-tags embed \
diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go
deleted file mode 100644
index 716240912..000000000
--- a/backend/cmd/server/wire_gen.go
+++ /dev/null
@@ -1,346 +0,0 @@
-// Code generated by Wire. DO NOT EDIT.
-
-//go:generate go run -mod=mod github.com/google/wire/cmd/wire
-//go:build !wireinject
-// +build !wireinject
-
-package main
-
-import (
- "context"
- "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/handler"
- "github.com/Wei-Shaw/sub2api/internal/handler/admin"
- "github.com/Wei-Shaw/sub2api/internal/repository"
- "github.com/Wei-Shaw/sub2api/internal/server"
- "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/redis/go-redis/v9"
- "log"
- "net/http"
- "time"
-)
-
-import (
- _ "embed"
- _ "github.com/Wei-Shaw/sub2api/ent/runtime"
-)
-
-// Injectors from wire.go:
-
-func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
- configConfig, err := config.ProvideConfig()
- if err != nil {
- return nil, err
- }
- client, err := repository.ProvideEnt(configConfig)
- if err != nil {
- return nil, err
- }
- db, err := repository.ProvideSQLDB(client)
- if err != nil {
- return nil, err
- }
- userRepository := repository.NewUserRepository(client, db)
- settingRepository := repository.NewSettingRepository(client)
- settingService := service.NewSettingService(settingRepository, configConfig)
- redisClient := repository.ProvideRedis(configConfig)
- emailCache := repository.NewEmailCache(redisClient)
- emailService := service.NewEmailService(settingRepository, emailCache)
- turnstileVerifier := repository.NewTurnstileVerifier()
- turnstileService := service.NewTurnstileService(settingService, turnstileVerifier)
- emailQueueService := service.ProvideEmailQueueService(emailService)
- promoCodeRepository := repository.NewPromoCodeRepository(client)
- billingCache := repository.NewBillingCache(redisClient)
- userSubscriptionRepository := repository.NewUserSubscriptionRepository(client)
- billingCacheService := service.NewBillingCacheService(billingCache, userRepository, userSubscriptionRepository, configConfig)
- apiKeyRepository := repository.NewAPIKeyRepository(client)
- groupRepository := repository.NewGroupRepository(client, db)
- apiKeyCache := repository.NewAPIKeyCache(redisClient)
- apiKeyService := service.NewAPIKeyService(apiKeyRepository, userRepository, groupRepository, userSubscriptionRepository, apiKeyCache, configConfig)
- apiKeyAuthCacheInvalidator := service.ProvideAPIKeyAuthCacheInvalidator(apiKeyService)
- promoService := service.NewPromoService(promoCodeRepository, userRepository, billingCacheService, client, apiKeyAuthCacheInvalidator)
- authService := service.NewAuthService(userRepository, configConfig, settingService, emailService, turnstileService, emailQueueService, promoService)
- userService := service.NewUserService(userRepository, apiKeyAuthCacheInvalidator)
- secretEncryptor, err := repository.NewAESEncryptor(configConfig)
- if err != nil {
- return nil, err
- }
- totpCache := repository.NewTotpCache(redisClient)
- totpService := service.NewTotpService(userRepository, secretEncryptor, totpCache, settingService, emailService, emailQueueService)
- authHandler := handler.NewAuthHandler(configConfig, authService, userService, settingService, promoService, totpService)
- userHandler := handler.NewUserHandler(userService)
- apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService)
- usageLogRepository := repository.NewUsageLogRepository(client, db)
- usageService := service.NewUsageService(usageLogRepository, userRepository, client, apiKeyAuthCacheInvalidator)
- usageHandler := handler.NewUsageHandler(usageService, apiKeyService)
- redeemCodeRepository := repository.NewRedeemCodeRepository(client)
- subscriptionService := service.NewSubscriptionService(groupRepository, userSubscriptionRepository, billingCacheService)
- redeemCache := repository.NewRedeemCache(redisClient)
- redeemService := service.NewRedeemService(redeemCodeRepository, userRepository, subscriptionService, redeemCache, billingCacheService, client, apiKeyAuthCacheInvalidator)
- redeemHandler := handler.NewRedeemHandler(redeemService)
- subscriptionHandler := handler.NewSubscriptionHandler(subscriptionService)
- dashboardAggregationRepository := repository.NewDashboardAggregationRepository(db)
- dashboardStatsCache := repository.NewDashboardCache(redisClient, configConfig)
- dashboardService := service.NewDashboardService(usageLogRepository, dashboardAggregationRepository, dashboardStatsCache, configConfig)
- timingWheelService, err := service.ProvideTimingWheelService()
- if err != nil {
- return nil, err
- }
- dashboardAggregationService := service.ProvideDashboardAggregationService(dashboardAggregationRepository, timingWheelService, configConfig)
- dashboardHandler := admin.NewDashboardHandler(dashboardService, dashboardAggregationService)
- schedulerCache := repository.NewSchedulerCache(redisClient)
- accountRepository := repository.NewAccountRepository(client, db, schedulerCache)
- proxyRepository := repository.NewProxyRepository(client, db)
- proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig)
- proxyLatencyCache := repository.NewProxyLatencyCache(redisClient)
- adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator)
- adminUserHandler := admin.NewUserHandler(adminService)
- groupHandler := admin.NewGroupHandler(adminService)
- claudeOAuthClient := repository.NewClaudeOAuthClient()
- oAuthService := service.NewOAuthService(proxyRepository, claudeOAuthClient)
- openAIOAuthClient := repository.NewOpenAIOAuthClient()
- openAIOAuthService := service.NewOpenAIOAuthService(proxyRepository, openAIOAuthClient)
- geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
- geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
- geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
- antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
- geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
- tempUnschedCache := repository.NewTempUnschedCache(redisClient)
- timeoutCounterCache := repository.NewTimeoutCounterCache(redisClient)
- geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
- compositeTokenCacheInvalidator := service.NewCompositeTokenCacheInvalidator(geminiTokenCache)
- rateLimitService := service.ProvideRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache, timeoutCounterCache, settingService, compositeTokenCacheInvalidator)
- httpUpstream := repository.NewHTTPUpstream(configConfig)
- claudeUsageFetcher := repository.NewClaudeUsageFetcher(httpUpstream)
- antigravityQuotaFetcher := service.NewAntigravityQuotaFetcher(proxyRepository)
- usageCache := service.NewUsageCache()
- identityCache := repository.NewIdentityCache(redisClient)
- accountUsageService := service.NewAccountUsageService(accountRepository, usageLogRepository, claudeUsageFetcher, geminiQuotaService, antigravityQuotaFetcher, usageCache, identityCache)
- geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
- gatewayCache := repository.NewGatewayCache(redisClient)
- antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
- antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
- accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
- concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
- concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
- crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
- sessionLimitCache := repository.ProvideSessionLimitCache(redisClient, configConfig)
- accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService, sessionLimitCache, compositeTokenCacheInvalidator)
- oAuthHandler := admin.NewOAuthHandler(oAuthService)
- openAIOAuthHandler := admin.NewOpenAIOAuthHandler(openAIOAuthService, adminService)
- geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService)
- antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService)
- proxyHandler := admin.NewProxyHandler(adminService)
- adminRedeemHandler := admin.NewRedeemHandler(adminService)
- promoHandler := admin.NewPromoHandler(promoService)
- opsRepository := repository.NewOpsRepository(db)
- schedulerOutboxRepository := repository.NewSchedulerOutboxRepository(db)
- schedulerSnapshotService := service.ProvideSchedulerSnapshotService(schedulerCache, schedulerOutboxRepository, accountRepository, groupRepository, configConfig)
- pricingRemoteClient := repository.ProvidePricingRemoteClient(configConfig)
- pricingService, err := service.ProvidePricingService(configConfig, pricingRemoteClient)
- if err != nil {
- return nil, err
- }
- billingService := service.NewBillingService(configConfig, pricingService)
- identityService := service.NewIdentityService(identityCache)
- deferredService := service.ProvideDeferredService(accountRepository, timingWheelService)
- claudeTokenProvider := service.NewClaudeTokenProvider(accountRepository, geminiTokenCache, oAuthService)
- gatewayService := service.NewGatewayService(accountRepository, groupRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, identityService, httpUpstream, deferredService, claudeTokenProvider, sessionLimitCache)
- openAITokenProvider := service.NewOpenAITokenProvider(accountRepository, geminiTokenCache, openAIOAuthService)
- openAIGatewayService := service.NewOpenAIGatewayService(accountRepository, usageLogRepository, userRepository, userSubscriptionRepository, gatewayCache, configConfig, schedulerSnapshotService, concurrencyService, billingService, rateLimitService, billingCacheService, httpUpstream, deferredService, openAITokenProvider)
- geminiMessagesCompatService := service.NewGeminiMessagesCompatService(accountRepository, groupRepository, gatewayCache, schedulerSnapshotService, geminiTokenProvider, rateLimitService, httpUpstream, antigravityGatewayService, configConfig)
- opsService := service.NewOpsService(opsRepository, settingRepository, configConfig, accountRepository, concurrencyService, gatewayService, openAIGatewayService, geminiMessagesCompatService, antigravityGatewayService)
- settingHandler := admin.NewSettingHandler(settingService, emailService, turnstileService, opsService)
- opsHandler := admin.NewOpsHandler(opsService)
- updateCache := repository.NewUpdateCache(redisClient)
- gitHubReleaseClient := repository.ProvideGitHubReleaseClient(configConfig)
- serviceBuildInfo := provideServiceBuildInfo(buildInfo)
- updateService := service.ProvideUpdateService(updateCache, gitHubReleaseClient, serviceBuildInfo)
- systemHandler := handler.ProvideSystemHandler(updateService)
- adminSubscriptionHandler := admin.NewSubscriptionHandler(subscriptionService)
- usageCleanupRepository := repository.NewUsageCleanupRepository(client, db)
- usageCleanupService := service.ProvideUsageCleanupService(usageCleanupRepository, timingWheelService, dashboardAggregationService, configConfig)
- adminUsageHandler := admin.NewUsageHandler(usageService, apiKeyService, adminService, usageCleanupService)
- userAttributeDefinitionRepository := repository.NewUserAttributeDefinitionRepository(client)
- userAttributeValueRepository := repository.NewUserAttributeValueRepository(client)
- userAttributeService := service.NewUserAttributeService(userAttributeDefinitionRepository, userAttributeValueRepository)
- userAttributeHandler := admin.NewUserAttributeHandler(userAttributeService)
- adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler)
- gatewayHandler := handler.NewGatewayHandler(gatewayService, geminiMessagesCompatService, antigravityGatewayService, userService, concurrencyService, billingCacheService, configConfig)
- openAIGatewayHandler := handler.NewOpenAIGatewayHandler(openAIGatewayService, concurrencyService, billingCacheService, configConfig)
- handlerSettingHandler := handler.ProvideSettingHandler(settingService, buildInfo)
- totpHandler := handler.NewTotpHandler(totpService)
- handlers := handler.ProvideHandlers(authHandler, userHandler, apiKeyHandler, usageHandler, redeemHandler, subscriptionHandler, adminHandlers, gatewayHandler, openAIGatewayHandler, handlerSettingHandler, totpHandler)
- jwtAuthMiddleware := middleware.NewJWTAuthMiddleware(authService, userService)
- adminAuthMiddleware := middleware.NewAdminAuthMiddleware(authService, userService, settingService)
- apiKeyAuthMiddleware := middleware.NewAPIKeyAuthMiddleware(apiKeyService, subscriptionService, configConfig)
- engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService, opsService, settingService, redisClient)
- httpServer := server.ProvideHTTPServer(configConfig, engine)
- opsMetricsCollector := service.ProvideOpsMetricsCollector(opsRepository, settingRepository, accountRepository, concurrencyService, db, redisClient, configConfig)
- opsAggregationService := service.ProvideOpsAggregationService(opsRepository, settingRepository, db, redisClient, configConfig)
- opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig)
- opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig)
- opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig)
- tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, configConfig)
- accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
- subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository)
- v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, schedulerSnapshotService, tokenRefreshService, accountExpiryService, subscriptionExpiryService, usageCleanupService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
- application := &Application{
- Server: httpServer,
- Cleanup: v,
- }
- return application, nil
-}
-
-// wire.go:
-
-type Application struct {
- Server *http.Server
- Cleanup func()
-}
-
-func provideServiceBuildInfo(buildInfo handler.BuildInfo) service.BuildInfo {
- return service.BuildInfo{
- Version: buildInfo.Version,
- BuildType: buildInfo.BuildType,
- }
-}
-
-func provideCleanup(
- entClient *ent.Client,
- rdb *redis.Client,
- opsMetricsCollector *service.OpsMetricsCollector,
- opsAggregation *service.OpsAggregationService,
- opsAlertEvaluator *service.OpsAlertEvaluatorService,
- opsCleanup *service.OpsCleanupService,
- opsScheduledReport *service.OpsScheduledReportService,
- schedulerSnapshot *service.SchedulerSnapshotService,
- tokenRefresh *service.TokenRefreshService,
- accountExpiry *service.AccountExpiryService,
- subscriptionExpiry *service.SubscriptionExpiryService,
- usageCleanup *service.UsageCleanupService,
- pricing *service.PricingService,
- emailQueue *service.EmailQueueService,
- billingCache *service.BillingCacheService,
- oauth *service.OAuthService,
- openaiOAuth *service.OpenAIOAuthService,
- geminiOAuth *service.GeminiOAuthService,
- antigravityOAuth *service.AntigravityOAuthService,
-) func() {
- return func() {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- cleanupSteps := []struct {
- name string
- fn func() error
- }{
- {"OpsScheduledReportService", func() error {
- if opsScheduledReport != nil {
- opsScheduledReport.Stop()
- }
- return nil
- }},
- {"OpsCleanupService", func() error {
- if opsCleanup != nil {
- opsCleanup.Stop()
- }
- return nil
- }},
- {"OpsAlertEvaluatorService", func() error {
- if opsAlertEvaluator != nil {
- opsAlertEvaluator.Stop()
- }
- return nil
- }},
- {"OpsAggregationService", func() error {
- if opsAggregation != nil {
- opsAggregation.Stop()
- }
- return nil
- }},
- {"OpsMetricsCollector", func() error {
- if opsMetricsCollector != nil {
- opsMetricsCollector.Stop()
- }
- return nil
- }},
- {"SchedulerSnapshotService", func() error {
- if schedulerSnapshot != nil {
- schedulerSnapshot.Stop()
- }
- return nil
- }},
- {"UsageCleanupService", func() error {
- if usageCleanup != nil {
- usageCleanup.Stop()
- }
- return nil
- }},
- {"TokenRefreshService", func() error {
- tokenRefresh.Stop()
- return nil
- }},
- {"AccountExpiryService", func() error {
- accountExpiry.Stop()
- return nil
- }},
- {"SubscriptionExpiryService", func() error {
- subscriptionExpiry.Stop()
- return nil
- }},
- {"PricingService", func() error {
- pricing.Stop()
- return nil
- }},
- {"EmailQueueService", func() error {
- emailQueue.Stop()
- return nil
- }},
- {"BillingCacheService", func() error {
- billingCache.Stop()
- return nil
- }},
- {"OAuthService", func() error {
- oauth.Stop()
- return nil
- }},
- {"OpenAIOAuthService", func() error {
- openaiOAuth.Stop()
- return nil
- }},
- {"GeminiOAuthService", func() error {
- geminiOAuth.Stop()
- return nil
- }},
- {"AntigravityOAuthService", func() error {
- antigravityOAuth.Stop()
- return nil
- }},
- {"Redis", func() error {
- return rdb.Close()
- }},
- {"Ent", func() error {
- return entClient.Close()
- }},
- }
-
- for _, step := range cleanupSteps {
- if err := step.fn(); err != nil {
- log.Printf("[Cleanup] %s failed: %v", step.name, err)
-
- } else {
- log.Printf("[Cleanup] %s succeeded", step.name)
- }
- }
-
- select {
- case <-ctx.Done():
- log.Printf("[Cleanup] Warning: cleanup timed out after 10 seconds")
- default:
- log.Printf("[Cleanup] All cleanup steps completed")
- }
- }
-}
diff --git a/backend/go.mod b/backend/go.mod
index 3969eb0e5..a936b1ed8 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -4,15 +4,20 @@ go 1.25.5
require (
entgo.io/ent v0.14.5
+ github.com/DATA-DOG/go-sqlmock v1.5.2
+ github.com/dgraph-io/ristretto v0.2.0
github.com/gin-gonic/gin v1.9.1
- github.com/go-ldap/ldap/v3 v3.4.10
+ github.com/go-ldap/ldap/v3 v3.4.12
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/gorilla/websocket v1.5.3
github.com/imroc/req/v3 v3.57.0
github.com/lib/pq v1.10.9
+ github.com/pquerna/otp v1.5.0
github.com/redis/go-redis/v9 v9.17.2
+ github.com/refraction-networking/utls v1.8.1
+ github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.11.1
@@ -26,13 +31,14 @@ require (
golang.org/x/sync v0.19.0
golang.org/x/term v0.38.0
gopkg.in/yaml.v3 v3.0.1
+ modernc.org/sqlite v1.44.1
)
require (
ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
- github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
@@ -49,7 +55,6 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.1+incompatible // indirect
@@ -62,6 +67,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
+ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
@@ -108,13 +114,10 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
- github.com/pquerna/otp v1.5.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.57.1 // indirect
- github.com/refraction-networking/utls v1.8.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
- github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -150,12 +153,10 @@ require (
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.39.0 // indirect
- golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
- modernc.org/sqlite v1.44.1 // indirect
)
diff --git a/backend/go.sum b/backend/go.sum
index 0addb5bb0..6c94b5afd 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -8,12 +8,16 @@ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8af
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
+github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
+github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
+github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
@@ -55,6 +59,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -83,6 +89,10 @@ 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/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
+github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
+github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -113,6 +123,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -123,6 +135,11 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
+github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
@@ -141,6 +158,18 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
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/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
+github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
+github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
+github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
+github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
+github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
+github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
+github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
+github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
+github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
+github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
+github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
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/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
@@ -345,8 +374,6 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
-golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
-golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
@@ -374,9 +401,8 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
-golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY=
-golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
+golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -399,12 +425,32 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
+modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
+modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
+modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
+modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
+modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
+modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.44.1 h1:qybx/rNpfQipX/t47OxbHmkkJuv2JWifCMH8SVUiDas=
modernc.org/sqlite v1.44.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=