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=