diff --git a/.gitignore b/.gitignore
index 48172982c..f73e1a00d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -118,6 +118,8 @@ tests
CLAUDE.md
AGENTS.md
.claude
+.spec-workflow
+.zcf
scripts
.code-review-state
openspec/
diff --git a/Dockerfile b/Dockerfile
index b33203009..dfcf1eda2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -57,9 +57,15 @@ RUN go mod download
# Copy backend source first
COPY backend/ ./
+# Ensure go.sum is up to date
+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 ad7d76b67..a936b1ed8 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -4,14 +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.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
@@ -25,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
@@ -48,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
@@ -61,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
@@ -107,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
@@ -149,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=
diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go
index 477cb59d5..297b6f663 100644
--- a/backend/internal/config/config.go
+++ b/backend/internal/config/config.go
@@ -63,6 +63,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 {
@@ -94,6 +95,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"`
@@ -897,6 +935,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 3522407db..1c6a1076e 100644
--- a/backend/internal/handler/auth_handler.go
+++ b/backend/internal/handler/auth_handler.go
@@ -63,6 +63,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"`
@@ -427,3 +433,25 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
Message: "Your password has been reset successfully. You can now log in with your new password.",
})
}
+
+// 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/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/pkg/ldap/client.go b/backend/internal/pkg/ldap/client.go
new file mode 100644
index 000000000..3d28e9fe7
--- /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/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/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/server/routes/auth.go b/backend/internal/server/routes/auth.go
index 33a88e82f..9dc4013c4 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("/login/2fa", h.Auth.Login2FA)
auth.POST("/send-verify-code", h.Auth.SendVerifyCode)
// 优惠码验证接口添加速率限制:每分钟最多 10 次(Redis 故障时 fail-close)
diff --git a/backend/internal/service/auth_service.go b/backend/internal/service/auth_service.go
index f51fae243..d02646aea 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,
}
}
@@ -581,6 +585,117 @@ func (s *AuthService) RefreshToken(ctx context.Context, oldTokenString string) (
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
+ }
+
+ log.Printf("[Auth] LDAP login successful for user: %s (email: %s)", user.Username, user.Email)
+ 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
+}
+
// IsPasswordResetEnabled 检查是否启用密码重置功能
// 要求:必须同时开启邮件验证且 SMTP 配置正确
func (s *AuthService) IsPasswordResetEnabled(ctx context.Context) bool {
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 df86b2e7d..c4cd0fd9d 100644
--- a/backend/internal/service/wire.go
+++ b/backend/internal/service/wire.go
@@ -3,9 +3,11 @@ package service
import (
"context"
"database/sql"
+ "fmt"
"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"
)
@@ -209,9 +211,22 @@ func ProvideAPIKeyAuthCacheInvalidator(apiKeyService *APIKeyService) APIKeyAuthC
return apiKeyService
}
+// ProvideLDAPClient 提供 LDAP 客户端(如果启用)
+func ProvideLDAPClient(cfg *config.Config) *ldap.Client {
+ if cfg == nil || !cfg.LDAP.Enabled {
+ return nil
+ }
+ 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
var ProviderSet = wire.NewSet(
// Core services
+ ProvideLDAPClient,
NewAuthService,
NewUserService,
NewAPIKeyService,
diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml
index 98aba8f5a..8f95ddd70 100644
--- a/deploy/config.example.yaml
+++ b/deploy/config.example.yaml
@@ -443,6 +443,54 @@ linuxdo_connect:
userinfo_id_path: ""
userinfo_username_path: ""
+# =============================================================================
+# LDAP Authentication Configuration
+# LDAP 认证配置
+# =============================================================================
+ldap:
+ # Enable LDAP authentication
+ # 启用 LDAP 认证
+ enabled: false
+ # LDAP server address
+ # LDAP 服务器地址
+ host: "ldap.example.com"
+ # LDAP server port (389 for LDAP, 636 for LDAPS)
+ # LDAP 服务器端口(389 用于 LDAP,636 用于 LDAPS)
+ port: 389
+ # Use LDAPS (LDAP over SSL/TLS)
+ # 使用 LDAPS(LDAP over SSL/TLS)
+ use_tls: false
+ # Use StartTLS to upgrade connection
+ # 使用 StartTLS 升级连接
+ use_start_tls: true
+ # Skip TLS certificate verification (development only)
+ # 跳过 TLS 证书验证(仅开发环境)
+ skip_tls_verify: false
+ # Admin DN for user search
+ # 管理员 DN(用于搜索用户)
+ bind_dn: "cn=admin,dc=example,dc=com"
+ # Admin password
+ # 管理员密码
+ bind_password: "admin_password"
+ # Base DN for user search
+ # 搜索基准 DN
+ base_dn: "ou=users,dc=example,dc=com"
+ # User search filter (%s will be replaced with username)
+ # 用户搜索过滤器(%s 将被替换为用户名)
+ user_filter: "(uid=%s)"
+ # User attribute mapping
+ # 用户属性映射
+ attributes:
+ # Username attribute (default: "uid")
+ # 用户名属性(默认:"uid")
+ username: "uid"
+ # Email attribute (default: "mail")
+ # 邮箱属性(默认:"mail")
+ email: "mail"
+ # Display name attribute (default: "cn")
+ # 显示名称属性(默认:"cn")
+ display_name: "cn"
+
# =============================================================================
# Default Settings
# 默认设置
diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts
index bbd5ed74f..305f1e836 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,
@@ -82,6 +83,21 @@ export async function login2FA(request: TotpLogin2FARequest): Promise