diff --git a/cmd/kratos-admin/wire_gen.go b/cmd/kratos-admin/wire_gen.go index 7f3ac94..1489f17 100644 --- a/cmd/kratos-admin/wire_gen.go +++ b/cmd/kratos-admin/wire_gen.go @@ -42,7 +42,8 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger menuService := service.NewMenuService(menuUsecase) menu := handler.NewMenu(menuService) apiUsecase := biz.NewAPIUsecase(apiRepo) - apiService := service.NewAPIService(apiUsecase, runtime) + runtimeSettings := data.NewRuntimeSettings(runtime) + apiService := service.NewAPIService(apiUsecase, runtimeSettings) api := handler.NewAPI(apiService) permissionRepo := data.NewPermissionRepo(dataData) permissionUsecase := biz.NewPermissionUsecase(permissionRepo) @@ -65,7 +66,6 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger email := handler.NewEmail(emailService) taskRepo := data.NewTaskRepo(dataData) taskUsecase := biz.NewTaskUsecase(taskRepo) - taskService := service.NewTaskService(taskUsecase) mediaRepo := data.NewMediaRepo(dataData) fileStorage, err := data.NewFileStorage(dataData) if err != nil { @@ -75,8 +75,11 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger mediaUsecase := biz.NewMediaUsecase(mediaRepo, fileStorage) taskExecutor := worker.NewTaskExecutor(taskUsecase, mediaUsecase, runtime) taskScheduler := worker.NewTaskScheduler(taskUsecase, taskExecutor, logger) - task := handler.NewTask(taskService, taskScheduler) - mediaService := service.NewMediaService(mediaUsecase, runtime) + taskRuntime := worker.NewTaskRuntime(taskScheduler) + taskApplicationUsecase := biz.NewTaskApplicationUsecase(taskUsecase, taskRuntime) + taskService := service.NewTaskService(taskApplicationUsecase) + task := handler.NewTask(taskService) + mediaService := service.NewMediaService(mediaUsecase, runtimeSettings) media := handler.NewMedia(mediaService) auditQueryRepo := data.NewAuditRepo(dataData) auditUsecase := biz.NewAuditUsecase(auditQueryRepo) @@ -107,24 +110,27 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger parameter := handler.NewParameter(parameterService) apiTokenRepo := data.NewAPITokenRepo(dataData) tokenUsecase := biz.NewTokenUsecase(apiTokenRepo) - tokenService := service.NewTokenService(tokenUsecase, runtime) + tokenIssuer := data.NewTokenIssuer(runtimeSettings) + tokenService := service.NewTokenService(tokenUsecase, tokenIssuer) apiToken := handler.NewAPIToken(tokenService) initializationRepo := data.NewInitializationRepo(dataData) - systemConfigUsecase := biz.NewSystemConfigUsecase(initializationRepo) + systemConfigUsecase := biz.NewSystemConfigUsecase(initializationRepo, taskRuntime) systemConfigService := service.NewSystemConfigService(systemConfigUsecase, runtime) securityRepo := data.NewSecurityRepo(dataData) - securityUsecase := biz.NewSecurityUsecase(securityRepo) - securityService := service.NewSecurityService(securityUsecase, runtime, cache, tokenUsecase) - systemConfig := handler.NewSystemConfig(systemConfigService, securityService, taskScheduler) + securityUsecase := biz.NewSecurityUsecase(securityRepo, cache, runtimeSettings, tokenUsecase) + securityService := service.NewSecurityService(securityUsecase) + systemConfig := handler.NewSystemConfig(systemConfigService, securityService) userRepo := data.NewUserRepo(dataData) userUsecase := biz.NewUserUsecase(userRepo) - authService := service.NewAuthService(userUsecase, runtime, securityService) - public := handler.NewPublic(runtime, authService, systemConfigService, securityService, auditRecorder, taskScheduler) + authenticationUsecase := biz.NewAuthenticationUsecase(userUsecase, securityUsecase, tokenIssuer, auditRecordRepo) + authService := service.NewAuthService(authenticationUsecase) + public := handler.NewPublic(authService, systemConfigService, securityService) userService := service.NewUserService(userUsecase, securityService) user := handler.NewUser(userService, authService) navigation := handler.NewNavigation(userService) session := handler.NewSession(tokenService) - engine := server.NewGinEngine(runtime, accessControlService, authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session, securityService, tokenService, auditRecorder, logger) + set := handler.NewSet(authority, menu, api, permission, organization, announcement, email, task, media, audit, export, version, dictionary, parameter, apiToken, systemConfig, public, user, navigation, session) + engine := server.NewGinEngine(runtime, accessControlService, set, securityService, tokenService, auditRecorder, logger) httpServer := server.NewGinServer(confServer, engine) app := newApp(logger, httpServer, taskScheduler) return app, func() { diff --git a/go.sum b/go.sum index 783c262..9eb7458 100644 --- a/go.sum +++ b/go.sum @@ -177,6 +177,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +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.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -410,6 +412,8 @@ golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -506,6 +510,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/internal/biz/authentication.go b/internal/biz/authentication.go new file mode 100644 index 0000000..d0cb2af --- /dev/null +++ b/internal/biz/authentication.go @@ -0,0 +1,150 @@ +package biz + +import ( + "context" + "errors" + "fmt" + "time" +) + +var ( + ErrAccountLocked = errors.New("account locked") + ErrCaptchaInvalid = errors.New("captcha invalid") + ErrTokenIssue = errors.New("token issue failed") + ErrLoginState = errors.New("login state failed") +) + +type UserDisabledError struct{ UserID uint } + +func (e *UserDisabledError) Error() string { return ErrUserDisabled.Error() } +func (e *UserDisabledError) Unwrap() error { return ErrUserDisabled } + +type AccountLockedError struct{ Minutes int } + +func (e *AccountLockedError) Error() string { return ErrAccountLocked.Error() } +func (e *AccountLockedError) Unwrap() error { return ErrAccountLocked } + +type LoginAttempt struct { + Username string + Password string + CaptchaID string + Captcha string + IP string + Agent string +} + +type AuthenticationResult struct { + User *User + Token string + ExpiresAt time.Time + NeedChangePassword bool +} + +type AuthenticationUsecase struct { + users *UserUsecase + security *SecurityUsecase + issuer TokenIssuer + audit AuditRecordRepo +} + +func NewAuthenticationUsecase(users *UserUsecase, security *SecurityUsecase, issuer TokenIssuer, audit AuditRecordRepo) *AuthenticationUsecase { + return &AuthenticationUsecase{users: users, security: security, issuer: issuer, audit: audit} +} + +func (uc *AuthenticationUsecase) recordLogin(ctx context.Context, attempt *LoginAttempt, status bool, message string, userID uint) { + _ = uc.audit.RecordLogin(ctx, &LoginLog{Username: attempt.Username, IP: attempt.IP, Status: status, ErrorMessage: message, Agent: attempt.Agent, UserID: userID}) +} + +func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttempt) (*AuthenticationResult, error) { + config, _ := uc.security.Current(ctx) + if config != nil && config.LockEnable { + if locked, _ := uc.security.LoginLocked(ctx, attempt.Username); locked { + uc.recordLogin(ctx, attempt, false, "账号已锁定", 0) + return nil, &AccountLockedError{Minutes: config.LockDuration} + } + } + + ipTTL := time.Hour + requireCaptcha := config == nil || config.CaptchaOpen == 0 + if config != nil { + if config.CaptchaTimeout > 0 { + ipTTL = time.Duration(config.CaptchaTimeout) * time.Second + } + failures, _ := uc.security.EnsureLoginIPCounter(ctx, attempt.IP, ipTTL) + requireCaptcha = config.CaptchaOpen == 0 || failures > config.CaptchaOpen + } + if requireCaptcha && !uc.security.VerifyCaptcha(ctx, attempt.CaptchaID, attempt.Captcha, true) { + _, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL) + uc.recordLogin(ctx, attempt, false, "验证码错误", 0) + return nil, ErrCaptchaInvalid + } + + user, err := uc.users.Login(ctx, attempt.Username, attempt.Password) + if err != nil { + _, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL) + if config != nil && config.LockEnable { + lockTTL := time.Duration(config.LockDuration) * time.Minute + failures, _ := uc.security.IncrementLoginFailure(ctx, attempt.Username, lockTTL) + if int(failures) >= config.LockThreshold { + _ = uc.security.LockLogin(ctx, attempt.Username, lockTTL) + } + } + uc.recordLogin(ctx, attempt, false, "用户名不存在或者密码错误", 0) + return nil, ErrInvalidCredentials + } + if user.Enable != 1 { + _, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL) + uc.recordLogin(ctx, attempt, false, "用户被禁止登录", user.ID) + return nil, &UserDisabledError{UserID: user.ID} + } + + uc.security.ClearLoginState(ctx, attempt.Username) + if config != nil && config.PwdExpireEnable && user.PasswordUpdatedAt != nil && time.Since(*user.PasswordUpdatedAt) > time.Duration(config.PwdExpireDays)*24*time.Hour { + user.MustChangePassword = true + } + issued, err := uc.issuer.IssueToken(user, user.AuthorityID, user.MustChangePassword, 0) + if err != nil { + return nil, fmt.Errorf("%w: %v", ErrTokenIssue, err) + } + if uc.security.UseMultipoint() { + oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username) + if cacheErr != nil { + return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr) + } + if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil { + if errors.Is(cacheErr, ErrTokenRevoke) { + return nil, cacheErr + } + return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr) + } + } + uc.recordLogin(ctx, attempt, true, "登录成功", user.ID) + return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil +} + +func (uc *AuthenticationUsecase) SwitchAuthority(ctx context.Context, userID, authorityID uint) (*AuthenticationResult, error) { + if err := uc.users.SetUserAuthority(ctx, userID, authorityID); err != nil { + return nil, err + } + user, err := uc.users.User(ctx, userID) + if err != nil { + return nil, err + } + issued, err := uc.issuer.IssueToken(user, authorityID, user.MustChangePassword, 0) + if err != nil { + return nil, err + } + if uc.security.UseMultipoint() { + oldToken, cacheErr := uc.security.ActiveToken(ctx, user.Username) + if cacheErr != nil { + return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr) + } + if cacheErr = uc.security.RotateActiveToken(ctx, user.Username, oldToken, issued.Value, issued.TTL); cacheErr != nil { + if errors.Is(cacheErr, ErrTokenRevoke) { + return nil, cacheErr + } + return nil, fmt.Errorf("%w: %v", ErrLoginState, cacheErr) + } + } + return &AuthenticationResult{User: user, Token: issued.Value, ExpiresAt: issued.ExpiresAt, NeedChangePassword: user.MustChangePassword}, nil +} diff --git a/internal/biz/biz.go b/internal/biz/biz.go index 2743352..a1a6d58 100644 --- a/internal/biz/biz.go +++ b/internal/biz/biz.go @@ -3,4 +3,4 @@ package biz import "github.com/google/wire" // ProviderSet is biz providers. -var ProviderSet = wire.NewSet(NewUserUsecase, NewSystemConfigUsecase, NewAuthorityUsecase, NewAPIUsecase, NewPermissionUsecase, NewAccessControlUsecase, NewMenuUsecase, NewDepartmentUsecase, NewPositionUsecase, NewDictionaryUsecase, NewParameterUsecase, NewTokenUsecase, NewSecurityUsecase, NewVersionUsecase, NewExportUsecase, NewAuditUsecase, NewAuditRecorderUsecase, NewLogViewerUsecase, NewTaskUsecase, NewMediaUsecase, NewAnnouncementUsecase, NewEmailUsecase) +var ProviderSet = wire.NewSet(NewUserUsecase, NewAuthenticationUsecase, NewSystemConfigUsecase, NewAuthorityUsecase, NewAPIUsecase, NewPermissionUsecase, NewAccessControlUsecase, NewMenuUsecase, NewDepartmentUsecase, NewPositionUsecase, NewDictionaryUsecase, NewParameterUsecase, NewTokenUsecase, NewSecurityUsecase, NewVersionUsecase, NewExportUsecase, NewAuditUsecase, NewAuditRecorderUsecase, NewLogViewerUsecase, NewTaskUsecase, NewTaskApplicationUsecase, NewMediaUsecase, NewAnnouncementUsecase, NewEmailUsecase) diff --git a/internal/biz/infrastructure.go b/internal/biz/infrastructure.go index ac5a72a..1eb8c4f 100644 --- a/internal/biz/infrastructure.go +++ b/internal/biz/infrastructure.go @@ -31,3 +31,43 @@ type FileStorage interface { DeletePrefix(context.Context, string) error List(context.Context, string, string, int) ([]*StoredFile, string, bool, error) } + +type JWTSettings struct { + SigningKey string + Issuer string + Expires time.Duration + Buffer time.Duration +} + +type CaptchaSettings struct { + KeyLong int + ImageWidth int + ImageHeight int + StoreExpiration time.Duration +} + +type MediaSettings struct { + SessionTTL int + MaxFileSize int64 +} + +// RuntimeSettings exposes only the active values needed by the application. +// The data implementation resolves every call from conf.Runtime so hot reloads +// take effect without rebuilding services. +type RuntimeSettings interface { + RouterPrefix() string + JWTSettings() JWTSettings + CaptchaSettings() CaptchaSettings + MediaSettings() MediaSettings + UseMultipoint() bool +} + +type IssuedToken struct { + Value string + ExpiresAt time.Time + TTL time.Duration +} + +type TokenIssuer interface { + IssueToken(*User, uint, bool, time.Duration) (*IssuedToken, error) +} diff --git a/internal/biz/security.go b/internal/biz/security.go index 622b8a5..278e831 100644 --- a/internal/biz/security.go +++ b/internal/biz/security.go @@ -3,6 +3,10 @@ package biz import ( "context" "errors" + "strconv" + "strings" + "sync" + "time" ) type SecurityConfig struct { @@ -33,14 +37,54 @@ type SecurityRepo interface { SaveSecurityConfig(context.Context, *SecurityConfig) error } -type SecurityUsecase struct{ SecurityRepo } +type SecurityUsecase struct { + repo SecurityRepo + cache Cache + settings RuntimeSettings + tokens *TokenUsecase + mu sync.RWMutex + cachedConfig *SecurityConfig +} -func NewSecurityUsecase(repo SecurityRepo) *SecurityUsecase { - return &SecurityUsecase{SecurityRepo: repo} +var ErrTokenRevoke = errors.New("token revoke failed") + +type TokenRevokeError struct{ Err error } + +func (e *TokenRevokeError) Error() string { return e.Err.Error() } +func (e *TokenRevokeError) Is(target error) bool { return target == ErrTokenRevoke } + +func NewSecurityUsecase(repo SecurityRepo, cache Cache, settings RuntimeSettings, tokens *TokenUsecase) *SecurityUsecase { + return &SecurityUsecase{repo: repo, cache: cache, settings: settings, tokens: tokens} } func (uc *SecurityUsecase) UpdateSecurity(ctx context.Context, value *SecurityConfig) error { - return uc.SaveSecurityConfig(ctx, value) + if err := uc.repo.SaveSecurityConfig(ctx, value); err != nil { + return err + } + uc.mu.Lock() + copy := *value + uc.cachedConfig = © + uc.mu.Unlock() + return nil +} + +func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error) { + uc.mu.RLock() + if uc.cachedConfig != nil { + copy := *uc.cachedConfig + uc.mu.RUnlock() + return ©, nil + } + uc.mu.RUnlock() + value, err := uc.repo.SecurityConfig(ctx) + if err != nil { + return nil, err + } + uc.mu.Lock() + copy := *value + uc.cachedConfig = © + uc.mu.Unlock() + return value, nil } func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password string) error { @@ -74,3 +118,113 @@ func (uc *SecurityUsecase) ValidatePassword(value *SecurityConfig, password stri } return nil } + +func (uc *SecurityUsecase) ValidateCurrentPassword(ctx context.Context, password string) error { + config, err := uc.Current(ctx) + if err != nil { + return err + } + return uc.ValidatePassword(config, password) +} + +func (uc *SecurityUsecase) LoginLocked(ctx context.Context, username string) (bool, error) { + _, locked, err := uc.cache.Get(ctx, "login:lock:"+username) + return locked, err +} + +func (uc *SecurityUsecase) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) { + return uc.cache.Increment(ctx, "login:fail:"+username, expiration) +} + +func (uc *SecurityUsecase) LockLogin(ctx context.Context, username string, expiration time.Duration) error { + return uc.cache.Set(ctx, "login:lock:"+username, "1", expiration) +} + +func (uc *SecurityUsecase) ClearLoginState(ctx context.Context, username string) { + _ = uc.cache.Delete(ctx, "login:fail:"+username) + _ = uc.cache.Delete(ctx, "login:lock:"+username) +} + +func (uc *SecurityUsecase) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) { + value, exists, err := uc.cache.Get(ctx, ip) + if err != nil { + return 0, err + } + if exists { + return strconv.Atoi(value) + } + if expiration <= 0 { + expiration = time.Hour + } + if err = uc.cache.Set(ctx, ip, "1", expiration); err != nil { + return 0, err + } + return 1, nil +} + +func (uc *SecurityUsecase) IncrementLoginIP(ctx context.Context, ip string, expiration time.Duration) (int64, error) { + return uc.cache.Increment(ctx, ip, expiration) +} + +func (uc *SecurityUsecase) IncrementRateLimit(ctx context.Context, key string, expiration time.Duration) (int64, error) { + return uc.cache.Increment(ctx, key, expiration) +} + +func (uc *SecurityUsecase) SetCaptcha(ctx context.Context, id, value string, expiration time.Duration) error { + return uc.cache.Set(ctx, "captcha:"+id, value, expiration) +} + +func (uc *SecurityUsecase) GetCaptcha(ctx context.Context, id string) (string, bool, error) { + return uc.cache.Get(ctx, "captcha:"+id) +} + +func (uc *SecurityUsecase) DeleteCaptcha(ctx context.Context, id string) error { + return uc.cache.Delete(ctx, "captcha:"+id) +} + +func (uc *SecurityUsecase) VerifyCaptcha(ctx context.Context, id, answer string, clear bool) bool { + if id == "" || answer == "" { + return false + } + value, ok, err := uc.GetCaptcha(ctx, id) + if err != nil || !ok { + return false + } + if clear { + _ = uc.DeleteCaptcha(ctx, id) + } + return strings.EqualFold(value, answer) +} + +func activeTokenKey(username string) string { return "jwt:active:" + username } + +func (uc *SecurityUsecase) ActiveToken(ctx context.Context, username string) (string, error) { + value, _, err := uc.cache.Get(ctx, activeTokenKey(username)) + return value, err +} + +func (uc *SecurityUsecase) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) { + if !uc.settings.UseMultipoint() { + return true, nil + } + active, ok, err := uc.cache.Get(ctx, activeTokenKey(username)) + return ok && active == token, err +} + +func (uc *SecurityUsecase) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error { + if !uc.settings.UseMultipoint() { + return nil + } + if oldToken != "" && oldToken != newToken { + if err := uc.tokens.BlacklistToken(ctx, oldToken); err != nil { + return &TokenRevokeError{Err: err} + } + } + return uc.cache.Set(ctx, activeTokenKey(username), newToken, expiration) +} + +func (uc *SecurityUsecase) UseMultipoint() bool { return uc.settings.UseMultipoint() } + +func (uc *SecurityUsecase) CaptchaRuntimeSettings() CaptchaSettings { + return uc.settings.CaptchaSettings() +} diff --git a/internal/biz/system_init.go b/internal/biz/system_init.go index c69a310..6e8c563 100644 --- a/internal/biz/system_init.go +++ b/internal/biz/system_init.go @@ -1,6 +1,16 @@ package biz -import "context" +import ( + "context" + "errors" +) + +var ErrTaskRuntimeReload = errors.New("task runtime reload failed") + +type TaskRuntimeReloadError struct{ Err error } + +func (e *TaskRuntimeReloadError) Error() string { return e.Err.Error() } +func (e *TaskRuntimeReloadError) Is(target error) bool { return target == ErrTaskRuntimeReload } type DatabaseConfig struct { Driver, Host, Port, User, Password, Name, Path, Config, Template, AdminPassword string @@ -16,10 +26,13 @@ type InitializationRepo interface { ReloadConfig(context.Context) error } -type SystemConfigUsecase struct{ repo InitializationRepo } +type SystemConfigUsecase struct { + repo InitializationRepo + tasks TaskRuntime +} -func NewSystemConfigUsecase(repo InitializationRepo) *SystemConfigUsecase { - return &SystemConfigUsecase{repo: repo} +func NewSystemConfigUsecase(repo InitializationRepo, tasks TaskRuntime) *SystemConfigUsecase { + return &SystemConfigUsecase{repo: repo, tasks: tasks} } func (uc *SystemConfigUsecase) IsInitialized(ctx context.Context) (bool, error) { @@ -29,7 +42,13 @@ func (uc *SystemConfigUsecase) Initialize(ctx context.Context, config *DatabaseC if config == nil || len(config.AdminPassword) < 6 { return ErrInvalidCredentials } - return uc.repo.Initialize(ctx, config) + if err := uc.repo.Initialize(ctx, config); err != nil { + return err + } + if err := uc.tasks.Reload(ctx); err != nil { + return &TaskRuntimeReloadError{Err: err} + } + return nil } func (uc *SystemConfigUsecase) PersistConfig(ctx context.Context) error { return uc.repo.PersistConfig(ctx) @@ -41,5 +60,11 @@ func (uc *SystemConfigUsecase) PersistRuntimeConfig(ctx context.Context, data, a return uc.repo.PersistRuntimeConfig(ctx, data, admin) } func (uc *SystemConfigUsecase) ReloadConfig(ctx context.Context) error { - return uc.repo.ReloadConfig(ctx) + if err := uc.repo.ReloadConfig(ctx); err != nil { + return err + } + if err := uc.tasks.Reload(ctx); err != nil { + return &TaskRuntimeReloadError{Err: err} + } + return nil } diff --git a/internal/biz/task.go b/internal/biz/task.go index 3eb628c..3312acc 100644 --- a/internal/biz/task.go +++ b/internal/biz/task.go @@ -42,6 +42,13 @@ const ( TaskMethodUploads = "CleanStaleUploads" ) +var ErrTaskSchedule = errors.New("task schedule failed") + +type TaskScheduleError struct{ Err error } + +func (e *TaskScheduleError) Error() string { return e.Err.Error() } +func (e *TaskScheduleError) Is(target error) bool { return target == ErrTaskSchedule } + type TaskMethod struct { Name string Description string @@ -77,6 +84,17 @@ type TaskRepo interface { CleanupLogs(context.Context) error TaskNameExists(context.Context, string, uint) (bool, error) } + +type TaskRuntime interface { + ScheduleID(context.Context, uint) error + Remove(uint) + TriggerID(context.Context, uint) error + NextRuns() map[uint]time.Time + Reload(context.Context) error + Subscribe() chan []byte + Unsubscribe(chan []byte) +} + type TaskUsecase struct{ TaskRepo } func NewTaskUsecase(repo TaskRepo) *TaskUsecase { return &TaskUsecase{TaskRepo: repo} } @@ -149,3 +167,69 @@ func (uc *TaskUsecase) Update(ctx context.Context, value *TimedTask) error { } return uc.UpdateTask(ctx, value) } + +type TaskApplicationUsecase struct { + tasks *TaskUsecase + runtime TaskRuntime +} + +func NewTaskApplicationUsecase(tasks *TaskUsecase, runtime TaskRuntime) *TaskApplicationUsecase { + return &TaskApplicationUsecase{tasks: tasks, runtime: runtime} +} + +func (uc *TaskApplicationUsecase) Create(ctx context.Context, value *TimedTask) error { + if err := uc.tasks.Create(ctx, value); err != nil { + return err + } + if err := uc.runtime.ScheduleID(ctx, value.ID); err != nil { + return &TaskScheduleError{Err: err} + } + return nil +} + +func (uc *TaskApplicationUsecase) Update(ctx context.Context, value *TimedTask) error { + if err := uc.tasks.Update(ctx, value); err != nil { + return err + } + if err := uc.runtime.ScheduleID(ctx, value.ID); err != nil { + return &TaskScheduleError{Err: err} + } + return nil +} + +func (uc *TaskApplicationUsecase) Delete(ctx context.Context, id uint) error { + uc.runtime.Remove(id) + return uc.tasks.DeleteTask(ctx, id) +} + +func (uc *TaskApplicationUsecase) Toggle(ctx context.Context, id uint, enabled bool) error { + if err := uc.tasks.ToggleTask(ctx, id, enabled); err != nil { + return err + } + if err := uc.runtime.ScheduleID(ctx, id); err != nil { + return &TaskScheduleError{Err: err} + } + return nil +} + +func (uc *TaskApplicationUsecase) Trigger(ctx context.Context, id uint) error { + return uc.runtime.TriggerID(ctx, id) +} + +func (uc *TaskApplicationUsecase) List(ctx context.Context, page, size int, filter *TimedTask) ([]*TimedTask, int64, map[uint]time.Time, error) { + items, total, err := uc.tasks.ListTasks(ctx, page, size, filter) + return items, total, uc.runtime.NextRuns(), err +} + +func (uc *TaskApplicationUsecase) Logs(ctx context.Context, page, size int, taskID uint, status string) ([]*TimedTaskLog, int64, error) { + return uc.tasks.ListTaskLogs(ctx, page, size, taskID, status) +} + +func (uc *TaskApplicationUsecase) Reload(ctx context.Context) error { + return uc.runtime.Reload(ctx) +} + +func (uc *TaskApplicationUsecase) Subscribe() chan []byte { return uc.runtime.Subscribe() } +func (uc *TaskApplicationUsecase) Unsubscribe(events chan []byte) { + uc.runtime.Unsubscribe(events) +} diff --git a/internal/data/access.go b/internal/data/access.go index cfd9b5e..0282dae 100644 --- a/internal/data/access.go +++ b/internal/data/access.go @@ -2,10 +2,14 @@ package data import "kra/internal/biz" -type accessRepo struct{ data *Data } +type authorityAccessRepo struct{ data *Data } +type apiRepo struct{ data *Data } +type permissionRepo struct{ data *Data } -func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo { return &accessRepo{data: data} } +func NewAuthorityAccessRepo(data *Data) biz.AuthorityAccessRepo { + return &authorityAccessRepo{data: data} +} -func NewAPIRepo(data *Data) biz.APIRepo { return &accessRepo{data: data} } +func NewAPIRepo(data *Data) biz.APIRepo { return &apiRepo{data: data} } -func NewPermissionRepo(data *Data) biz.PermissionRepo { return &accessRepo{data: data} } +func NewPermissionRepo(data *Data) biz.PermissionRepo { return &permissionRepo{data: data} } diff --git a/internal/data/api.go b/internal/data/api.go index 20b644a..b727b2d 100644 --- a/internal/data/api.go +++ b/internal/data/api.go @@ -44,7 +44,7 @@ func (authorityAPIPO) TableName() string { return "sys_authority_apis" } func apiFromPO(po apiPO) *biz.API { return &biz.API{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Path: po.Path, Description: po.Description, APIGroup: po.APIGroup, Method: po.Method} } -func (r *accessRepo) CreateAPI(ctx context.Context, v *biz.API) error { +func (r *apiRepo) CreateAPI(ctx context.Context, v *biz.API) error { po := apiPO{Path: v.Path, Description: v.Description, APIGroup: v.APIGroup, Method: strings.ToUpper(v.Method)} var count int64 if err := r.data.gormDB.WithContext(ctx).Model(&apiPO{}).Where("path = ? AND method = ?", po.Path, po.Method).Count(&count).Error; err != nil { @@ -59,7 +59,7 @@ func (r *accessRepo) CreateAPI(ctx context.Context, v *biz.API) error { v.ID, v.CreatedAt, v.UpdatedAt, v.Method = po.ID, po.CreatedAt, po.UpdatedAt, po.Method return nil } -func (r *accessRepo) UpdateAPI(ctx context.Context, v *biz.API) error { +func (r *apiRepo) UpdateAPI(ctx context.Context, v *biz.API) error { db := r.data.gormDB.WithContext(ctx) var old apiPO if err := db.First(&old, v.ID).Error; err != nil { @@ -77,7 +77,7 @@ func (r *accessRepo) UpdateAPI(ctx context.Context, v *biz.API) error { } return db.Model(&old).Updates(map[string]any{"path": v.Path, "description": v.Description, "api_group": v.APIGroup, "method": method}).Error } -func (r *accessRepo) DeleteAPIs(ctx context.Context, ids []uint) error { +func (r *apiRepo) DeleteAPIs(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("api_id IN ?", ids).Delete(&authorityAPIPO{}).Error; err != nil { return err @@ -85,14 +85,14 @@ func (r *accessRepo) DeleteAPIs(ctx context.Context, ids []uint) error { return tx.Delete(&apiPO{}, ids).Error }) } -func (r *accessRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) { +func (r *apiRepo) FindAPI(ctx context.Context, id uint) (*biz.API, error) { var po apiPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return apiFromPO(po), nil } -func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) { +func (r *apiRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ([]*biz.API, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&apiPO{}) if q != nil && q.StrictAll { config := r.data.runtime.Admin() @@ -149,7 +149,7 @@ func (r *accessRepo) ListAPIs(ctx context.Context, page, size int, q *biz.API) ( } return out, total, nil } -func (r *accessRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) { +func (r *apiRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Table("sys_authority_apis"). Joins("JOIN sys_apis ON sys_apis.id = sys_authority_apis.api_id"). @@ -157,7 +157,7 @@ func (r *accessRepo) APIRoleIDs(ctx context.Context, path, method string) ([]uin Pluck("sys_authority_apis.authority_id", &ids).Error return ids, err } -func (r *accessRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error { +func (r *apiRepo) SetAPIRoles(ctx context.Context, path, method string, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var api apiPO if err := tx.Where("path = ? AND method = ?", path, strings.ToUpper(method)).First(&api).Error; err != nil { @@ -176,7 +176,7 @@ func (r *accessRepo) SetAPIRoles(ctx context.Context, path, method string, ids [ return nil }) } -func (r *accessRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) { +func (r *apiRepo) Authorize(ctx context.Context, aid uint, path, method string) (bool, error) { policies, err := r.PolicyPaths(ctx, aid) if err != nil { return false, err @@ -204,7 +204,7 @@ m = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)`) } return enforcer.Enforce(subject, path, strings.ToUpper(method)) } -func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) { +func (r *apiRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, error) { var pos []apiPO err := r.data.gormDB.WithContext(ctx).Table("sys_apis").Joins("JOIN sys_authority_apis ON sys_authority_apis.api_id = sys_apis.id").Where("sys_authority_apis.authority_id = ? AND sys_apis.deleted_at IS NULL", aid).Order("sys_apis.path,sys_apis.method").Find(&pos).Error if err != nil { @@ -216,8 +216,8 @@ func (r *accessRepo) PolicyPaths(ctx context.Context, aid uint) ([]*biz.API, err } return out, nil } -func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error { - if err := r.checkAuthorityIDAuth(ctx, aid); err != nil { +func (r *apiRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz.API) error { + if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, aid); err != nil { return err } config := r.data.runtime.Admin() @@ -267,7 +267,7 @@ func (r *accessRepo) SetPolicyPaths(ctx context.Context, aid uint, paths []*biz. }) } -func (r *accessRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) { +func (r *apiRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) { var pos []ignoredAPIPO if err := r.data.gormDB.WithContext(ctx).Order("path,method").Find(&pos).Error; err != nil { return nil, err @@ -278,14 +278,14 @@ func (r *accessRepo) IgnoredAPIs(ctx context.Context) ([]*biz.API, error) { } return out, nil } -func (r *accessRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error { +func (r *apiRepo) SetAPIIgnored(ctx context.Context, path, method string, ignored bool) error { po := ignoredAPIPO{Path: path, Method: strings.ToUpper(method)} if ignored { return r.data.gormDB.WithContext(ctx).FirstOrCreate(&po, po).Error } return r.data.gormDB.WithContext(ctx).Where("path = ? AND method = ?", po.Path, po.Method).Delete(&ignoredAPIPO{}).Error } -func (r *accessRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error { +func (r *apiRepo) ApplyAPISync(ctx context.Context, added, deleted []*biz.API) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for _, item := range added { po := apiPO{Path: item.Path, Method: strings.ToUpper(item.Method), Description: item.Description, APIGroup: item.APIGroup} diff --git a/internal/data/api_token.go b/internal/data/api_token.go index 023d281..4e13b50 100644 --- a/internal/data/api_token.go +++ b/internal/data/api_token.go @@ -35,7 +35,7 @@ type jwtBlacklistPO struct { func (jwtBlacklistPO) TableName() string { return "jwt_blacklists" } -func (r *settingsRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) { +func (r *apiTokenRepo) UserHasAuthority(ctx context.Context, userID, authorityID uint) (*biz.User, bool, error) { var po userPO if err := r.data.gormDB.WithContext(ctx).First(&po, userID).Error; err != nil { return nil, false, err @@ -45,10 +45,10 @@ func (r *settingsRepo) UserHasAuthority(ctx context.Context, userID, authorityID if err != nil { return nil, false, err } - user, err := (&systemRepo{data: r.data}).toBizUser(ctx, &po) + user, err := (&userRepo{data: r.data}).toBizUser(ctx, &po) return user, count > 0 || po.AuthorityID == authorityID, err } -func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error { +func (r *apiTokenRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) error { po := apiTokenPO{UserID: v.UserID, AuthorityID: v.AuthorityID, Token: v.Token, Status: v.Status, ExpiresAt: v.ExpiresAt, Remark: v.Remark} if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { return err @@ -57,7 +57,7 @@ func (r *settingsRepo) CreateAPIToken(ctx context.Context, v *biz.APIToken) erro v.CreatedAt = po.CreatedAt return nil } -func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) { +func (r *apiTokenRepo) ListAPITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]*biz.APIToken, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&apiTokenPO{}) if userID != 0 { db = db.Where("user_id = ?", userID) @@ -94,14 +94,14 @@ func (r *settingsRepo) ListAPITokens(ctx context.Context, page, size int, userID } return out, total, nil } -func (r *settingsRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) { +func (r *apiTokenRepo) DisableAPIToken(ctx context.Context, id uint) (string, error) { var po apiTokenPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return "", err } return po.Token, r.data.gormDB.WithContext(ctx).Model(&po).Update("status", false).Error } -func (r *settingsRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error { +func (r *apiTokenRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var po apiTokenPO if err := tx.First(&po, id).Error; err != nil { @@ -114,13 +114,13 @@ func (r *settingsRepo) DisableAndBlacklistAPIToken(ctx context.Context, id uint) return tx.Model(&po).Update("status", false).Error }) } -func (r *settingsRepo) BlacklistToken(ctx context.Context, token string) error { +func (r *apiTokenRepo) BlacklistToken(ctx context.Context, token string) error { if token == "" { return errors.New("token is empty") } return r.data.gormDB.WithContext(ctx).Create(&jwtBlacklistPO{JWT: token}).Error } -func (r *settingsRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) { +func (r *apiTokenRepo) IsTokenDisabled(ctx context.Context, token string) (bool, error) { var blacklistCount int64 if err := r.data.gormDB.WithContext(ctx).Model(&jwtBlacklistPO{}).Where("jwt = ?", token).Count(&blacklistCount).Error; err != nil { return false, err diff --git a/internal/data/audit.go b/internal/data/audit.go index 4944244..4fa449a 100644 --- a/internal/data/audit.go +++ b/internal/data/audit.go @@ -2,10 +2,12 @@ package data import "kra/internal/biz" -type auditRepo struct{ data *Data } +type auditQueryRepo struct{ data *Data } +type auditRecorderRepo struct{ data *Data } +type logFileRepo struct{ data *Data } -func NewAuditRepo(data *Data) biz.AuditQueryRepo { return &auditRepo{data: data} } +func NewAuditRepo(data *Data) biz.AuditQueryRepo { return &auditQueryRepo{data: data} } -func NewAuditRecorderRepo(data *Data) biz.AuditRecordRepo { return &auditRepo{data: data} } +func NewAuditRecorderRepo(data *Data) biz.AuditRecordRepo { return &auditRecorderRepo{data: data} } -func NewLogFileRepo(data *Data) biz.LogFileRepo { return &auditRepo{data: data} } +func NewLogFileRepo(data *Data) biz.LogFileRepo { return &logFileRepo{data: data} } diff --git a/internal/data/authority.go b/internal/data/authority.go index 27ff989..e47bf78 100644 --- a/internal/data/authority.go +++ b/internal/data/authority.go @@ -11,7 +11,7 @@ import ( "gorm.io/gorm" ) -func (r *accessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) { +func (r *authorityAccessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[uint]bool, error) { allowed := make(map[uint]bool) if actorID == 0 { return allowed, nil @@ -46,7 +46,7 @@ func (r *accessRepo) strictAuthorityIDs(ctx context.Context, actorID uint) (map[ return allowed, nil } -func (r *accessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error { +func (r *authorityAccessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) error { config := r.data.runtime.Admin() if config == nil || config.System == nil || !config.System.UseStrictAuth { return nil @@ -65,7 +65,7 @@ func (r *accessRepo) checkAuthorityIDAuth(ctx context.Context, targetID uint) er return nil } -func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error { +func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) error { config := r.data.runtime.Admin() if config != nil && config.System != nil && config.System.UseStrictAuth && (value.ParentID == nil || *value.ParentID == 0) { if actor, ok := biz.ActorFromContext(ctx); ok { @@ -104,7 +104,7 @@ func (r *accessRepo) CreateAuthority(ctx context.Context, value *biz.Authority) return nil }) } -func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error { +func (r *authorityAccessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *biz.Authority) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { config := r.data.runtime.Admin() actor, hasActor := biz.ActorFromContext(ctx) @@ -179,10 +179,10 @@ func (r *accessRepo) CopyAuthority(ctx context.Context, sourceID uint, value *bi return nil }) } -func (r *accessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error { +func (r *authorityAccessRepo) UpdateAuthority(ctx context.Context, value *biz.Authority) error { return r.data.gormDB.WithContext(ctx).Model(&authorityPO{}).Where("authority_id = ?", value.AuthorityID).Updates(map[string]any{"authority_name": value.AuthorityName, "parent_id": value.ParentID, "data_scope": value.DataScope, "default_router": value.DefaultRouter}).Error } -func (r *accessRepo) DeleteAuthority(ctx context.Context, id uint) error { +func (r *authorityAccessRepo) DeleteAuthority(ctx context.Context, id uint) error { if id == 888 { return errors.New("不能删除超级管理员") } @@ -221,7 +221,7 @@ func (r *accessRepo) DeleteAuthority(ctx context.Context, id uint) error { return tx.Delete(&authorityPO{}, "authority_id = ?", id).Error }) } -func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { +func (r *authorityAccessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { var pos []authorityPO if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil { return nil, err @@ -245,7 +245,7 @@ func (r *accessRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err } return out, nil } -func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error { +func (r *authorityAccessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var oldIDs []uint if err := tx.Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &oldIDs).Error; err != nil { @@ -287,12 +287,12 @@ func (r *accessRepo) SetAuthorityUsers(ctx context.Context, id uint, ids []uint) return nil }) } -func (r *accessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) { +func (r *authorityAccessRepo) AuthorityUserIDs(ctx context.Context, id uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&userAuthorityPO{}).Where("sys_authority_authority_id = ?", id).Pluck("sys_user_id", &ids).Error return ids, err } -func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error { +func (r *authorityAccessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptIDs []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Model(&authorityPO{}).Where("authority_id = ?", id).Update("data_scope", scope).Error; err != nil { return err @@ -312,12 +312,12 @@ func (r *accessRepo) SetDataScope(ctx context.Context, id uint, scope int, deptI return nil }) } -func (r *accessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) { +func (r *authorityAccessRepo) DataScopeDepartmentIDs(ctx context.Context, id uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&authorityDepartmentPO{}).Where("authority_id = ?", id).Pluck("department_id", &ids).Error return ids, err } -func (r *accessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) { +func (r *authorityAccessRepo) ResolveDataScope(ctx context.Context, authorityID, userID uint) (biz.DataScope, error) { identity := biz.DataScope{UserID: userID, AuthorityID: authorityID} var user userPO _ = r.data.gormDB.WithContext(ctx).Select("id", "dept_id").First(&user, userID).Error diff --git a/internal/data/data.go b/internal/data/data.go index 8ba6b16..75f50ef 100644 --- a/internal/data/data.go +++ b/internal/data/data.go @@ -13,7 +13,7 @@ import ( "kra/internal/conf" ) -var ProviderSet = wire.NewSet(NewData, NewUserRepo, NewInitializationRepo, NewAuthorityAccessRepo, NewAPIRepo, NewPermissionRepo, NewMenuRepo, NewDepartmentRepo, NewPositionRepo, NewDictionaryRepo, NewParameterRepo, NewAPITokenRepo, NewSecurityRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewAuditRecorderRepo, NewLogFileRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage) +var ProviderSet = wire.NewSet(NewData, NewRuntimeSettings, NewTokenIssuer, NewUserRepo, NewInitializationRepo, NewAuthorityAccessRepo, NewAPIRepo, NewPermissionRepo, NewMenuRepo, NewDepartmentRepo, NewPositionRepo, NewDictionaryRepo, NewParameterRepo, NewAPITokenRepo, NewSecurityRepo, NewVersionRepo, NewExportRepo, NewAuditRepo, NewAuditRecorderRepo, NewLogFileRepo, NewTaskRepo, NewMediaRepo, NewAnnouncementRepo, NewEmailRepo, NewCache, NewFileStorage) type Data struct { initMu sync.Mutex diff --git a/internal/data/data_access_log.go b/internal/data/data_access_log.go index ea756ae..ee48a21 100644 --- a/internal/data/data_access_log.go +++ b/internal/data/data_access_log.go @@ -22,13 +22,13 @@ type dataAccessLogPO struct { func (dataAccessLogPO) TableName() string { return "sys_data_access_logs" } -func (r *auditRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error { +func (r *auditRecorderRepo) RecordDataAccess(ctx context.Context, v *biz.DataAccessLog) error { return r.data.gormDB.WithContext(ctx).Create(&dataAccessLogPO{EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail}).Error } func dataAccessFromPO(v dataAccessLogPO) *biz.DataAccessLog { return &biz.DataAccessLog{ID: v.ID, CreatedAt: v.CreatedAt, EventType: v.EventType, TargetTable: v.TargetTable, Operation: v.Operation, UserID: v.UserID, AuthorityID: v.AuthorityID, Scope: v.Scope, RequestID: v.RequestID, Method: v.Method, Path: v.Path, Detail: v.Detail} } -func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) { +func (r *auditQueryRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.DataAccessLog) ([]*biz.DataAccessLog, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&dataAccessLogPO{}) if q != nil { if q.EventType != "" { @@ -55,6 +55,6 @@ func (r *auditRepo) ListDataAccess(ctx context.Context, page, size int, q *biz.D } return out, total, nil } -func (r *auditRepo) DeleteDataAccess(ctx context.Context, ids []uint) error { +func (r *auditQueryRepo) DeleteDataAccess(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Delete(&dataAccessLogPO{}, ids).Error } diff --git a/internal/data/department.go b/internal/data/department.go index be3104d..dd1c027 100644 --- a/internal/data/department.go +++ b/internal/data/department.go @@ -45,7 +45,7 @@ func deptFromPO(po departmentPO) *biz.Department { return &biz.Department{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, ParentID: po.ParentID, Ancestors: po.Ancestors, Sort: po.Sort, LeaderID: po.LeaderID, Status: po.Status} } -func (r *organizationRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error { +func (r *departmentRepo) attachDepartmentLeaders(ctx context.Context, departments []*biz.Department) error { leaderIDs := make([]uint, 0, len(departments)) for _, department := range departments { if department.LeaderID != 0 { @@ -68,7 +68,7 @@ func (r *organizationRepo) attachDepartmentLeaders(ctx context.Context, departme } return nil } -func (r *organizationRepo) CreateDepartment(ctx context.Context, v *biz.Department) error { +func (r *departmentRepo) CreateDepartment(ctx context.Context, v *biz.Department) error { v.Ancestors = "0" if v.ParentID != 0 { var parent departmentPO @@ -79,7 +79,7 @@ func (r *organizationRepo) CreateDepartment(ctx context.Context, v *biz.Departme } return r.data.gormDB.WithContext(ctx).Create(&departmentPO{Name: v.Name, ParentID: v.ParentID, Ancestors: v.Ancestors, Sort: v.Sort, LeaderID: v.LeaderID, Status: v.Status}).Error } -func (r *organizationRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error { +func (r *departmentRepo) UpdateDepartment(ctx context.Context, v *biz.Department) error { if v.ParentID == v.ID { return errors.New("父部门不能是自己") } @@ -93,7 +93,7 @@ func (r *organizationRepo) UpdateDepartment(ctx context.Context, v *biz.Departme } return r.data.gormDB.WithContext(ctx).Model(&departmentPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "parent_id": v.ParentID, "ancestors": v.Ancestors, "sort": v.Sort, "leader_id": v.LeaderID, "status": v.Status}).Error } -func (r *organizationRepo) DeleteDepartment(ctx context.Context, id uint) error { +func (r *departmentRepo) DeleteDepartment(ctx context.Context, id uint) error { if id == 0 { return errors.New("部门ID不能为空") } @@ -118,7 +118,7 @@ func (r *organizationRepo) DeleteDepartment(ctx context.Context, id uint) error } return r.data.gormDB.WithContext(ctx).Delete(&departmentPO{}, id).Error } -func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) { +func (r *departmentRepo) FindDepartment(ctx context.Context, id uint) (*biz.Department, error) { var po departmentPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err @@ -129,7 +129,7 @@ func (r *organizationRepo) FindDepartment(ctx context.Context, id uint) (*biz.De } return department, nil } -func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) { +func (r *departmentRepo) ListDepartments(ctx context.Context, name string) ([]*biz.Department, error) { var pos []departmentPO db := r.data.gormDB.WithContext(ctx).Order("sort,id") if name != "" { @@ -162,15 +162,15 @@ func (r *organizationRepo) ListDepartments(ctx context.Context, name string) ([] } return roots, nil } -func (r *organizationRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) { +func (r *departmentRepo) DepartmentUserIDs(ctx context.Context, id uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&userDepartmentPO{}).Where("sys_department_id = ?", id).Pluck("sys_user_id", &ids).Error return ids, err } -func (r *organizationRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error { +func (r *departmentRepo) SetDepartmentUsers(ctx context.Context, id uint, ids []uint) error { return r.replaceUserDepartments(ctx, id, ids) } -func (r *organizationRepo) replaceUserDepartments(ctx context.Context, id uint, ids []uint) error { +func (r *departmentRepo) replaceUserDepartments(ctx context.Context, id uint, ids []uint) error { if id == 0 { return errors.New("部门ID不能为空") } @@ -214,7 +214,7 @@ func (r *organizationRepo) replaceUserDepartments(ctx context.Context, id uint, return nil }) } -func (r *organizationRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error { +func (r *departmentRepo) SetUserDepartments(ctx context.Context, uid uint, ids []uint, primary uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var user userPO if err := tx.First(&user, uid).Error; err != nil { diff --git a/internal/data/dictionary.go b/internal/data/dictionary.go index 88628d6..b61a7cd 100644 --- a/internal/data/dictionary.go +++ b/internal/data/dictionary.go @@ -54,7 +54,7 @@ func parameterFromPO(po parameterPO) *biz.SystemParameter { return &biz.SystemParameter{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Key: po.Key, Value: po.Value, Desc: po.Desc} } -func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error { +func (r *dictionaryRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) error { po := dictionaryPO{Name: v.Name, Type: v.Type, Status: v.Status, Desc: v.Desc, ParentID: v.ParentID} if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { return err @@ -63,7 +63,7 @@ func (r *settingsRepo) CreateDictionary(ctx context.Context, v *biz.Dictionary) return nil } -func (r *settingsRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error { +func (r *dictionaryRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dictionary, details []*biz.DictionaryDetail) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { po := dictionaryPO{Name: dictionary.Name, Type: dictionary.Type, Status: dictionary.Status, Desc: dictionary.Desc, ParentID: dictionary.ParentID} if err := tx.Create(&po).Error; err != nil { @@ -103,10 +103,10 @@ func (r *settingsRepo) ImportDictionary(ctx context.Context, dictionary *biz.Dic return nil }) } -func (r *settingsRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error { +func (r *dictionaryRepo) UpdateDictionary(ctx context.Context, v *biz.Dictionary) error { return r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "type": v.Type, "status": v.Status, "desc": v.Desc, "parent_id": v.ParentID}).Error } -func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error { +func (r *dictionaryRepo) DeleteDictionary(ctx context.Context, id uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var count int64 if err := tx.Model(&dictionaryPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { @@ -121,7 +121,7 @@ func (r *settingsRepo) DeleteDictionary(ctx context.Context, id uint) error { return tx.Delete(&dictionaryPO{}, id).Error }) } -func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) { +func (r *dictionaryRepo) FindDictionary(ctx context.Context, id uint, typ string, status *bool, details bool) (*biz.Dictionary, error) { var po dictionaryPO db := r.data.gormDB.WithContext(ctx) active := true @@ -148,7 +148,7 @@ func (r *settingsRepo) FindDictionary(ctx context.Context, id uint, typ string, return out, err } -func (r *settingsRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) { +func (r *dictionaryRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dictionary, error) { var po dictionaryPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err @@ -161,7 +161,7 @@ func (r *settingsRepo) ExportDictionary(ctx context.Context, id uint) (*biz.Dict value.Details = details return value, nil } -func (r *settingsRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) { +func (r *dictionaryRepo) ListDictionaries(ctx context.Context, page, size int, name, typ string, details bool) ([]*biz.Dictionary, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&dictionaryPO{}) if name != "" { like := "%" + name + "%" @@ -219,7 +219,7 @@ func (r *settingsRepo) ListDictionaries(ctx context.Context, page, size int, nam return out, total, nil } -func (r *settingsRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { +func (r *dictionaryRepo) CreateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { po := dictionaryDetailPO{Label: v.Label, Value: v.Value, Extend: v.Extend, Status: v.Status, Sort: v.Sort, DictionaryID: v.DictionaryID, ParentID: v.ParentID} po.Level = 0 po.Path = "" @@ -239,7 +239,7 @@ func (r *settingsRepo) CreateDictionaryDetail(ctx context.Context, v *biz.Dictio v.Path = po.Path return nil } -func (r *settingsRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { +func (r *dictionaryRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.DictionaryDetail) error { var po dictionaryDetailPO if err := r.data.gormDB.WithContext(ctx).First(&po, v.ID).Error; err != nil { return err @@ -268,7 +268,7 @@ func (r *settingsRepo) UpdateDictionaryDetail(ctx context.Context, v *biz.Dictio return tx.Model(&dictionaryDetailPO{}).Where("path = ? OR path LIKE ?", oldPrefix, oldPrefix+",%").Update("path", gorm.Expr("REPLACE(path, ?, ?)", oldPrefix, newPrefix)).Error }) } -func (r *settingsRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error { +func (r *dictionaryRepo) DeleteDictionaryDetail(ctx context.Context, id uint) error { var count int64 if err := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}).Where("parent_id = ?", id).Count(&count).Error; err != nil { return err @@ -278,14 +278,14 @@ func (r *settingsRepo) DeleteDictionaryDetail(ctx context.Context, id uint) erro } return r.data.gormDB.WithContext(ctx).Delete(&dictionaryDetailPO{}, id).Error } -func (r *settingsRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) { +func (r *dictionaryRepo) FindDictionaryDetail(ctx context.Context, id uint) (*biz.DictionaryDetail, error) { var po dictionaryDetailPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return detailFromPO(po), nil } -func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) { +func (r *dictionaryRepo) ListDictionaryDetails(ctx context.Context, page, size int, filter biz.DictionaryDetailFilter) ([]*biz.DictionaryDetail, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&dictionaryDetailPO{}) if filter.DictionaryID != 0 { db = db.Where("sys_dictionary_id = ?", filter.DictionaryID) @@ -319,7 +319,7 @@ func (r *settingsRepo) ListDictionaryDetails(ctx context.Context, page, size int } return out, total, nil } -func (r *settingsRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) { +func (r *dictionaryRepo) DictionaryDetailTree(ctx context.Context, dictionaryID uint, typ string) ([]*biz.DictionaryDetail, error) { if dictionaryID == 0 { active := true dictionary, err := r.FindDictionary(ctx, 0, typ, &active, false) diff --git a/internal/data/error_record.go b/internal/data/error_record.go index f474ca9..8a08f90 100644 --- a/internal/data/error_record.go +++ b/internal/data/error_record.go @@ -28,26 +28,26 @@ func (errorRecordPO) TableName() string { return "sys_error" } func errorFromPO(v errorRecordPO) *biz.ErrorRecord { return &biz.ErrorRecord{ID: v.ID, CreatedAt: v.CreatedAt, Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status} } -func (r *auditRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error { +func (r *auditRecorderRepo) CreateError(ctx context.Context, v *biz.ErrorRecord) error { if v.Status == "" { v.Status = "未处理" } return r.data.gormDB.WithContext(ctx).Create(&errorRecordPO{Form: v.Form, Info: v.Info, Level: v.Level, RequestID: v.RequestID, TraceID: v.TraceID, Solution: v.Solution, Status: v.Status}).Error } -func (r *auditRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error { +func (r *auditQueryRepo) UpdateError(ctx context.Context, v *biz.ErrorRecord) error { return r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}).Where("id = ?", v.ID).Updates(map[string]any{"form": v.Form, "info": v.Info, "level": v.Level, "solution": v.Solution, "status": v.Status}).Error } -func (r *auditRepo) DeleteErrors(ctx context.Context, ids []uint) error { +func (r *auditQueryRepo) DeleteErrors(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Delete(&errorRecordPO{}, ids).Error } -func (r *auditRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) { +func (r *auditQueryRepo) FindError(ctx context.Context, id uint) (*biz.ErrorRecord, error) { var po errorRecordPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return errorFromPO(po), nil } -func (r *auditRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) { +func (r *auditQueryRepo) ListErrors(ctx context.Context, page, size int, q *biz.ErrorRecord) ([]*biz.ErrorRecord, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&errorRecordPO{}) if q != nil { if len(q.CreatedAtRange) == 2 { diff --git a/internal/data/log_file.go b/internal/data/log_file.go index 0d28839..ae7c093 100644 --- a/internal/data/log_file.go +++ b/internal/data/log_file.go @@ -14,7 +14,7 @@ import ( "kra/internal/biz" ) -func (r *auditRepo) logRoot() (string, error) { +func (r *logFileRepo) logRoot() (string, error) { admin := r.data.runtime.Admin() if admin == nil || admin.Zap == nil || strings.TrimSpace(admin.Zap.Director) == "" { return "", biz.ErrLogRootUnavailable @@ -35,7 +35,7 @@ func (r *auditRepo) logRoot() (string, error) { } return root, nil } -func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) { +func (r *logFileRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, error) { if parsed, err := time.Parse("2006-01", month); err != nil || parsed.Format("2006-01") != month { return nil, biz.ErrInvalidLogMonth } @@ -75,7 +75,7 @@ func (r *auditRepo) LogDates(ctx context.Context, month string) ([]biz.LogDate, sort.Slice(out, func(i, j int) bool { return out[i].Date < out[j].Date }) return out, nil } -func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) { +func (r *logFileRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, error) { if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date { return nil, biz.ErrInvalidLogDate } @@ -115,7 +115,7 @@ func (r *auditRepo) LogFiles(ctx context.Context, date string) ([]biz.LogFile, e sort.Slice(out, func(i, j int) bool { return out[i].Path < out[j].Path }) return out, err } -func (r *auditRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) { +func (r *logFileRepo) LogContent(ctx context.Context, date, path string, cursor *int64) (*biz.LogContent, error) { if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Format("2006-01-02") != date { return nil, biz.ErrInvalidLogDate } diff --git a/internal/data/login_log.go b/internal/data/login_log.go index 01a464d..0337101 100644 --- a/internal/data/login_log.go +++ b/internal/data/login_log.go @@ -22,13 +22,13 @@ type loginLogPO struct { func (loginLogPO) TableName() string { return "sys_login_logs" } -func (r *auditRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error { +func (r *auditRecorderRepo) RecordLogin(ctx context.Context, v *biz.LoginLog) error { return r.data.gormDB.WithContext(ctx).Create(&loginLogPO{Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID}).Error } func loginFromPO(v loginLogPO) *biz.LoginLog { return &biz.LoginLog{ID: v.ID, CreatedAt: v.CreatedAt, Username: v.Username, IP: v.IP, Status: v.Status, ErrorMessage: v.ErrorMessage, Agent: v.Agent, UserID: v.UserID} } -func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) { +func (r *auditQueryRepo) ListLogins(ctx context.Context, page, size int, q *biz.LoginLog) ([]*biz.LoginLog, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&loginLogPO{}) if q != nil { if q.Username != "" { @@ -61,13 +61,13 @@ func (r *auditRepo) ListLogins(ctx context.Context, page, size int, q *biz.Login } return out, total, nil } -func (r *auditRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) { +func (r *auditQueryRepo) FindLogin(ctx context.Context, id uint) (*biz.LoginLog, error) { var po loginLogPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return loginFromPO(po), nil } -func (r *auditRepo) DeleteLogins(ctx context.Context, ids []uint) error { +func (r *auditQueryRepo) DeleteLogins(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Delete(&loginLogPO{}, ids).Error } diff --git a/internal/data/menu.go b/internal/data/menu.go index c75147d..0a671b2 100644 --- a/internal/data/menu.go +++ b/internal/data/menu.go @@ -242,7 +242,7 @@ func (r *menuRepo) List(ctx context.Context) ([]*biz.Menu, error) { } func (r *menuRepo) SetAuthorityMenus(ctx context.Context, id uint, ids []uint) error { - if err := (&accessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil { + if err := (&authorityAccessRepo{data: r.data}).checkAuthorityIDAuth(ctx, id); err != nil { return err } config := r.data.runtime.Admin() diff --git a/internal/data/operation_log.go b/internal/data/operation_log.go index 8642225..36a49af 100644 --- a/internal/data/operation_log.go +++ b/internal/data/operation_log.go @@ -29,13 +29,13 @@ type operationPO struct { func (operationPO) TableName() string { return "sys_operation_records" } -func (r *auditRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error { +func (r *auditRecorderRepo) RecordOperation(ctx context.Context, v *biz.OperationRecord) error { return r.data.gormDB.WithContext(ctx).Create(&operationPO{IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID}).Error } func opFromPO(v operationPO) *biz.OperationRecord { return &biz.OperationRecord{ID: v.ID, CreatedAt: v.CreatedAt, IP: v.IP, Method: v.Method, Path: v.Path, Status: v.Status, LatencyMS: v.LatencyMS, Agent: v.Agent, ErrorMessage: v.ErrorMessage, Body: v.Body, Response: v.Response, UserID: v.UserID, RequestID: v.RequestID, TraceID: v.TraceID, DeviceID: v.DeviceID} } -func (r *auditRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) { +func (r *auditQueryRepo) ListOperations(ctx context.Context, page, size int, q *biz.OperationRecord) ([]*biz.OperationRecord, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&operationPO{}) if q != nil { if q.Path != "" { @@ -90,13 +90,13 @@ func auditUsers(ctx context.Context, db *gorm.DB, ids []uint) map[uint]*biz.User } return result } -func (r *auditRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) { +func (r *auditQueryRepo) FindOperation(ctx context.Context, id uint) (*biz.OperationRecord, error) { var po operationPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return opFromPO(po), nil } -func (r *auditRepo) DeleteOperations(ctx context.Context, ids []uint) error { +func (r *auditQueryRepo) DeleteOperations(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Delete(&operationPO{}, ids).Error } diff --git a/internal/data/organization.go b/internal/data/organization.go index 8088c79..2dd86e8 100644 --- a/internal/data/organization.go +++ b/internal/data/organization.go @@ -2,8 +2,9 @@ package data import "kra/internal/biz" -type organizationRepo struct{ data *Data } +type departmentRepo struct{ data *Data } +type positionRepo struct{ data *Data } -func NewDepartmentRepo(data *Data) biz.DepartmentRepo { return &organizationRepo{data: data} } +func NewDepartmentRepo(data *Data) biz.DepartmentRepo { return &departmentRepo{data: data} } -func NewPositionRepo(data *Data) biz.PositionRepo { return &organizationRepo{data: data} } +func NewPositionRepo(data *Data) biz.PositionRepo { return &positionRepo{data: data} } diff --git a/internal/data/parameter.go b/internal/data/parameter.go index 469d592..00ea504 100644 --- a/internal/data/parameter.go +++ b/internal/data/parameter.go @@ -23,7 +23,7 @@ type parameterPO struct { func (parameterPO) TableName() string { return "sys_params" } -func (r *settingsRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error { +func (r *parameterRepo) CreateParameter(ctx context.Context, v *biz.SystemParameter) error { po := parameterPO{Name: v.Name, Key: v.Key, Value: v.Value, Desc: v.Desc} if err := r.data.gormDB.WithContext(ctx).Create(&po).Error; err != nil { return err @@ -31,13 +31,13 @@ func (r *settingsRepo) CreateParameter(ctx context.Context, v *biz.SystemParamet v.ID = po.ID return nil } -func (r *settingsRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error { +func (r *parameterRepo) UpdateParameter(ctx context.Context, v *biz.SystemParameter) error { return r.data.gormDB.WithContext(ctx).Model(¶meterPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "key": v.Key, "value": v.Value, "desc": v.Desc}).Error } -func (r *settingsRepo) DeleteParameters(ctx context.Context, ids []uint) error { +func (r *parameterRepo) DeleteParameters(ctx context.Context, ids []uint) error { return r.data.gormDB.WithContext(ctx).Delete(¶meterPO{}, ids).Error } -func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) (*biz.SystemParameter, error) { +func (r *parameterRepo) FindParameter(ctx context.Context, id uint, key string) (*biz.SystemParameter, error) { var po parameterPO db := r.data.gormDB.WithContext(ctx) var err error @@ -51,7 +51,7 @@ func (r *settingsRepo) FindParameter(ctx context.Context, id uint, key string) ( } return parameterFromPO(po), nil } -func (r *settingsRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) { +func (r *parameterRepo) ListParameters(ctx context.Context, page, size int, q *biz.SystemParameter) ([]*biz.SystemParameter, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(¶meterPO{}) if q != nil { if q.StartCreatedAt != nil && q.EndCreatedAt != nil { diff --git a/internal/data/permission.go b/internal/data/permission.go index d324ca8..d7b8a3a 100644 --- a/internal/data/permission.go +++ b/internal/data/permission.go @@ -25,7 +25,7 @@ type authorityButtonPO struct { func (authorityButtonPO) TableName() string { return "sys_authority_btns" } -func (r *accessRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) { +func (r *permissionRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButton, error) { var pos []menuButtonPO if err := r.data.gormDB.WithContext(ctx).Where("sys_base_menu_id = ?", menuID).Find(&pos).Error; err != nil { return nil, err @@ -36,7 +36,7 @@ func (r *accessRepo) Buttons(ctx context.Context, menuID uint) ([]*biz.MenuButto } return out, nil } -func (r *accessRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error { +func (r *permissionRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons map[uint][]uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("authority_id = ?", aid).Delete(&authorityButtonPO{}).Error; err != nil { return err @@ -53,12 +53,12 @@ func (r *accessRepo) SetAuthorityButtons(ctx context.Context, aid uint, buttons return nil }) } -func (r *accessRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) { +func (r *permissionRepo) SelectedButtons(ctx context.Context, aid, menuID uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Pluck("sys_base_menu_btn_id", &ids).Error return ids, err } -func (r *accessRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error { +func (r *permissionRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("authority_id = ? AND sys_menu_id = ?", aid, menuID).Delete(&authorityButtonPO{}).Error; err != nil { return err @@ -73,12 +73,12 @@ func (r *accessRepo) SetSelectedButtons(ctx context.Context, aid, menuID uint, i return nil }) } -func (r *accessRepo) CanRemoveButton(ctx context.Context, id uint) (bool, error) { +func (r *permissionRepo) CanRemoveButton(ctx context.Context, id uint) (bool, error) { var count int64 err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("sys_base_menu_btn_id = ?", id).Count(&count).Error return count == 0, err } -func (r *accessRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) { +func (r *permissionRepo) AuthorityButtonIDs(ctx context.Context, aid uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&authorityButtonPO{}).Where("authority_id = ?", aid).Pluck("sys_base_menu_btn_id", &ids).Error return ids, err diff --git a/internal/data/position.go b/internal/data/position.go index e4d68c1..eaf6ae3 100644 --- a/internal/data/position.go +++ b/internal/data/position.go @@ -34,13 +34,13 @@ func (userPositionPO) TableName() string { return "sys_user_positions" } func posFromPO(po positionPO) *biz.Position { return &biz.Position{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, Name: po.Name, Code: po.Code, Sort: po.Sort, Status: po.Status, Remark: po.Remark} } -func (r *organizationRepo) CreatePosition(ctx context.Context, v *biz.Position) error { +func (r *positionRepo) CreatePosition(ctx context.Context, v *biz.Position) error { return r.data.gormDB.WithContext(ctx).Create(&positionPO{Name: v.Name, Code: v.Code, Sort: v.Sort, Status: v.Status, Remark: v.Remark}).Error } -func (r *organizationRepo) UpdatePosition(ctx context.Context, v *biz.Position) error { +func (r *positionRepo) UpdatePosition(ctx context.Context, v *biz.Position) error { return r.data.gormDB.WithContext(ctx).Model(&positionPO{}).Where("id = ?", v.ID).Updates(map[string]any{"name": v.Name, "code": v.Code, "sort": v.Sort, "status": v.Status, "remark": v.Remark}).Error } -func (r *organizationRepo) DeletePosition(ctx context.Context, id uint) error { +func (r *positionRepo) DeletePosition(ctx context.Context, id uint) error { if id == 0 { return errors.New("岗位ID不能为空") } @@ -53,14 +53,14 @@ func (r *organizationRepo) DeletePosition(ctx context.Context, id uint) error { } return r.data.gormDB.WithContext(ctx).Delete(&positionPO{}, id).Error } -func (r *organizationRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) { +func (r *positionRepo) FindPosition(ctx context.Context, id uint) (*biz.Position, error) { var po positionPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { return nil, err } return posFromPO(po), nil } -func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) { +func (r *positionRepo) ListPositions(ctx context.Context, page, size int, q *biz.PositionListFilter) ([]*biz.Position, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&positionPO{}) if q != nil { if q.Name != "" { @@ -87,12 +87,12 @@ func (r *organizationRepo) ListPositions(ctx context.Context, page, size int, q } return out, total, nil } -func (r *organizationRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) { +func (r *positionRepo) PositionUserIDs(ctx context.Context, id uint) ([]uint, error) { var ids []uint err := r.data.gormDB.WithContext(ctx).Model(&userPositionPO{}).Where("sys_position_id = ?", id).Pluck("sys_user_id", &ids).Error return ids, err } -func (r *organizationRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error { +func (r *positionRepo) SetPositionUsers(ctx context.Context, id uint, ids []uint) error { if id == 0 { return errors.New("岗位ID不能为空") } @@ -110,7 +110,7 @@ func (r *organizationRepo) SetPositionUsers(ctx context.Context, id uint, ids [] return nil }) } -func (r *organizationRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error { +func (r *positionRepo) SetUserPositions(ctx context.Context, uid uint, ids []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("sys_user_id = ?", uid).Delete(&userPositionPO{}).Error; err != nil { return err diff --git a/internal/data/runtime_settings.go b/internal/data/runtime_settings.go new file mode 100644 index 0000000..71a5b8c --- /dev/null +++ b/internal/data/runtime_settings.go @@ -0,0 +1,94 @@ +package data + +import ( + "time" + + "kra/internal/biz" + "kra/internal/conf" + "kra/pkg/adminauth" +) + +type runtimeSettings struct{ runtime *conf.Runtime } + +func NewRuntimeSettings(runtime *conf.Runtime) biz.RuntimeSettings { + return &runtimeSettings{runtime: runtime} +} + +func (s *runtimeSettings) RouterPrefix() string { + config := s.runtime.Admin() + if config == nil { + return "" + } + return config.RouterPrefix +} + +func (s *runtimeSettings) JWTSettings() biz.JWTSettings { + value := biz.JWTSettings{Issuer: "kra", Expires: 7 * 24 * time.Hour, Buffer: 24 * time.Hour} + config := s.runtime.Admin() + if config == nil || config.Jwt == nil { + return value + } + value.SigningKey = config.Jwt.SigningKey + if config.Jwt.Issuer != "" { + value.Issuer = config.Jwt.Issuer + } + if config.Jwt.ExpiresTime != nil { + value.Expires = config.Jwt.ExpiresTime.AsDuration() + } + if config.Jwt.BufferTime != nil { + value.Buffer = config.Jwt.BufferTime.AsDuration() + } + return value +} + +func (s *runtimeSettings) CaptchaSettings() biz.CaptchaSettings { + value := biz.CaptchaSettings{KeyLong: 6, ImageWidth: 240, ImageHeight: 80, StoreExpiration: 3 * time.Minute} + config := s.runtime.Admin() + if config == nil || config.Captcha == nil { + return value + } + if config.Captcha.KeyLong > 0 { + value.KeyLong = int(config.Captcha.KeyLong) + } + if config.Captcha.ImgWidth > 0 { + value.ImageWidth = int(config.Captcha.ImgWidth) + } + if config.Captcha.ImgHeight > 0 { + value.ImageHeight = int(config.Captcha.ImgHeight) + } + if config.Captcha.StoreExpiration != nil { + value.StoreExpiration = config.Captcha.StoreExpiration.AsDuration() + } + return value +} + +func (s *runtimeSettings) MediaSettings() biz.MediaSettings { + config := s.runtime.Admin() + if config == nil || config.Media == nil { + return biz.MediaSettings{} + } + return biz.MediaSettings{SessionTTL: int(config.Media.SessionTtl), MaxFileSize: config.Media.MaxFileSize} +} + +func (s *runtimeSettings) UseMultipoint() bool { + config := s.runtime.Admin() + return config != nil && config.System != nil && config.System.UseMultipoint +} + +type tokenIssuer struct{ settings biz.RuntimeSettings } + +func NewTokenIssuer(settings biz.RuntimeSettings) biz.TokenIssuer { + return &tokenIssuer{settings: settings} +} + +func (i *tokenIssuer) IssueToken(user *biz.User, authorityID uint, mustChangePassword bool, expires time.Duration) (*biz.IssuedToken, error) { + settings := i.settings.JWTSettings() + if expires <= 0 { + expires = settings.Expires + } + token, claims, err := adminauth.Generate(settings.SigningKey, settings.Issuer, expires, settings.Buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, mustChangePassword) + if err != nil { + return nil, err + } + return &biz.IssuedToken{Value: token, ExpiresAt: claims.ExpiresAt.Time, TTL: expires}, nil +} diff --git a/internal/data/security.go b/internal/data/security.go index 62da3ca..985b072 100644 --- a/internal/data/security.go +++ b/internal/data/security.go @@ -44,7 +44,7 @@ func securityFromPO(v securityConfigPO) *biz.SecurityConfig { func securityToPO(v *biz.SecurityConfig) securityConfigPO { return securityConfigPO{ID: 1, CaptchaOpen: v.CaptchaOpen, CaptchaTimeout: v.CaptchaTimeout, KeyLong: v.KeyLong, ImgWidth: v.ImgWidth, ImgHeight: v.ImgHeight, PwdMinLength: v.PwdMinLength, PwdRequireUpper: v.PwdRequireUpper, PwdRequireLower: v.PwdRequireLower, PwdRequireDigit: v.PwdRequireDigit, PwdRequireSpecial: v.PwdRequireSpecial, LimitEnable: v.LimitEnable, LimitWindow: v.LimitWindow, LimitCount: v.LimitCount, LockEnable: v.LockEnable, LockThreshold: v.LockThreshold, LockDuration: v.LockDuration, PwdExpireEnable: v.PwdExpireEnable, PwdExpireDays: v.PwdExpireDays, ForceNewUserChangePassword: v.ForceNewUserChangePassword} } -func (r *settingsRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) { +func (r *securityRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, error) { var po securityConfigPO err := r.data.gormDB.WithContext(ctx).First(&po, 1).Error if errors.Is(err, gorm.ErrRecordNotFound) { @@ -56,7 +56,7 @@ func (r *settingsRepo) SecurityConfig(ctx context.Context) (*biz.SecurityConfig, } return securityFromPO(po), nil } -func (r *settingsRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error { +func (r *securityRepo) SaveSecurityConfig(ctx context.Context, v *biz.SecurityConfig) error { previous, err := r.SecurityConfig(ctx) if err != nil { return err diff --git a/internal/data/settings.go b/internal/data/settings.go index 3b02aed..9ecfd1b 100644 --- a/internal/data/settings.go +++ b/internal/data/settings.go @@ -2,12 +2,15 @@ package data import "kra/internal/biz" -type settingsRepo struct{ data *Data } +type dictionaryRepo struct{ data *Data } +type parameterRepo struct{ data *Data } +type apiTokenRepo struct{ data *Data } +type securityRepo struct{ data *Data } -func NewDictionaryRepo(data *Data) biz.DictionaryRepo { return &settingsRepo{data: data} } +func NewDictionaryRepo(data *Data) biz.DictionaryRepo { return &dictionaryRepo{data: data} } -func NewParameterRepo(data *Data) biz.ParameterRepo { return &settingsRepo{data: data} } +func NewParameterRepo(data *Data) biz.ParameterRepo { return ¶meterRepo{data: data} } -func NewAPITokenRepo(data *Data) biz.APITokenRepo { return &settingsRepo{data: data} } +func NewAPITokenRepo(data *Data) biz.APITokenRepo { return &apiTokenRepo{data: data} } -func NewSecurityRepo(data *Data) biz.SecurityRepo { return &settingsRepo{data: data} } +func NewSecurityRepo(data *Data) biz.SecurityRepo { return &securityRepo{data: data} } diff --git a/internal/data/system.go b/internal/data/system.go index c6fd56c..898b802 100644 --- a/internal/data/system.go +++ b/internal/data/system.go @@ -90,8 +90,9 @@ type menuParameterPO struct { func (menuParameterPO) TableName() string { return "sys_base_menu_parameters" } -type systemRepo struct{ data *Data } +type userRepo struct{ data *Data } +type initializationRepo struct{ data *Data } -func NewUserRepo(data *Data) biz.UserRepo { return &systemRepo{data: data} } +func NewUserRepo(data *Data) biz.UserRepo { return &userRepo{data: data} } -func NewInitializationRepo(data *Data) biz.InitializationRepo { return &systemRepo{data: data} } +func NewInitializationRepo(data *Data) biz.InitializationRepo { return &initializationRepo{data: data} } diff --git a/internal/data/system_init.go b/internal/data/system_init.go index 6b7cb61..ae257a7 100644 --- a/internal/data/system_init.go +++ b/internal/data/system_init.go @@ -17,8 +17,8 @@ import ( "gorm.io/gorm" ) -func (r *systemRepo) PersistConfig(context.Context) error { return r.data.persistConfig() } -func (r *systemRepo) PersistAdminConfig(_ context.Context, raw []byte) error { +func (r *initializationRepo) PersistConfig(context.Context) error { return r.data.persistConfig() } +func (r *initializationRepo) PersistAdminConfig(_ context.Context, raw []byte) error { currentData, currentAdmin := r.data.runtime.Values() next := proto.Clone(currentAdmin).(*conf.AdminBackend) if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(raw, next); err != nil { @@ -33,7 +33,7 @@ func (r *systemRepo) PersistAdminConfig(_ context.Context, raw []byte) error { r.data.runtime.Replace(currentData, next) return nil } -func (r *systemRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw []byte) error { +func (r *initializationRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw []byte) error { currentData, currentAdmin := r.data.runtime.Values() nextData := proto.Clone(currentData).(*conf.Data) nextAdmin := proto.Clone(currentAdmin).(*conf.AdminBackend) @@ -51,11 +51,11 @@ func (r *systemRepo) PersistRuntimeConfig(_ context.Context, dataRaw, adminRaw [ r.data.runtime.Replace(nextData, nextAdmin) return nil } -func (r *systemRepo) ReloadConfig(ctx context.Context) error { +func (r *initializationRepo) ReloadConfig(ctx context.Context) error { return r.data.reloadConfig(ctx) } -func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) { +func (r *initializationRepo) IsInitialized(ctx context.Context) (bool, error) { db := r.data.gormDB.WithContext(ctx) if !db.Migrator().HasTable(&userPO{}) { return false, nil @@ -67,7 +67,7 @@ func (r *systemRepo) IsInitialized(ctx context.Context) (bool, error) { return count > 0, nil } -func (r *systemRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error { +func (r *initializationRepo) Initialize(ctx context.Context, input *biz.DatabaseConfig) error { config := &conf.Data_Database{Driver: input.Driver, Host: input.Host, Port: input.Port, User: input.User, Password: input.Password, Name: input.Name, Path: input.Path, Config: input.Config} r.data.initMu.Lock() defer r.data.initMu.Unlock() diff --git a/internal/data/transactions_test.go b/internal/data/transactions_test.go index e2d4f86..1326c8d 100644 --- a/internal/data/transactions_test.go +++ b/internal/data/transactions_test.go @@ -30,7 +30,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) { if err := data.gormDB.WithContext(ctx).Create(&[]authorityPO{{AuthorityID: 888, AuthorityName: "admin"}, {AuthorityID: 999, AuthorityName: "operator"}}).Error; err != nil { t.Fatal(err) } - repo := &systemRepo{data: data} + repo := &userRepo{data: data} user := &biz.User{Username: "atomic", Password: "hash", NickName: "before", AuthorityID: 888, Enable: 1} created, err := repo.CreateUserWithAuthorities(ctx, user, []uint{888, 999}) if err != nil { @@ -66,7 +66,7 @@ func TestUserAuthorityWritesAreAtomic(t *testing.T) { func TestDictionaryImportKeepsHierarchyInOneTransaction(t *testing.T) { data := newTransactionTestData(t) ctx := context.Background() - repo := &settingsRepo{data: data} + repo := &dictionaryRepo{data: data} parentID := uint(10) dictionary := &biz.Dictionary{Name: "status", Type: "status", Status: true} details := []*biz.DictionaryDetail{{ID: 10, Label: "parent", Value: "1", Status: true}, {ID: 11, Label: "child", Value: "2", ParentID: &parentID, Status: true}} diff --git a/internal/data/user.go b/internal/data/user.go index bcdf018..d84c8a2 100644 --- a/internal/data/user.go +++ b/internal/data/user.go @@ -14,7 +14,7 @@ import ( "gorm.io/gorm" ) -func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) { +func (r *userRepo) FindUserByUsername(ctx context.Context, username string) (*biz.User, error) { var po userPO if err := r.data.gormDB.WithContext(ctx).Where("username = ?", username).First(&po).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -25,7 +25,7 @@ func (r *systemRepo) FindUserByUsername(ctx context.Context, username string) (* return r.toBizUser(ctx, &po) } -func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) { +func (r *userRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, error) { var po userPO if err := r.data.gormDB.WithContext(ctx).First(&po, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -36,7 +36,7 @@ func (r *systemRepo) FindUserByID(ctx context.Context, id uint) (*biz.User, erro return r.toBizUser(ctx, &po) } -func (r *systemRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) { +func (r *userRepo) toBizUser(ctx context.Context, po *userPO) (*biz.User, error) { var authority authorityPO if err := r.data.gormDB.WithContext(ctx).First(&authority, "authority_id = ?", po.AuthorityID).Error; err != nil { return nil, err @@ -90,7 +90,7 @@ func baseBizUser(po *userPO) *biz.User { return &biz.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt} } -func (r *systemRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) { +func (r *userRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, error) { if len(pos) == 0 { return []*biz.User{}, nil } @@ -195,7 +195,7 @@ func (r *systemRepo) toBizUsers(ctx context.Context, pos []userPO) ([]*biz.User, return users, nil } -func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) { +func (r *userRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([]*biz.Menu, error) { var pos []menuPO err := r.data.gormDB.WithContext(ctx).Table("sys_base_menus"). Joins("JOIN sys_authority_menus ON sys_authority_menus.sys_base_menu_id = sys_base_menus.id"). @@ -243,7 +243,7 @@ func (r *systemRepo) MenusByAuthority(ctx context.Context, authorityID uint) ([] return roots, nil } -func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) { +func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int, filter *biz.UserListFilter) ([]*biz.User, int64, error) { db := r.data.gormDB.WithContext(ctx).Model(&userPO{}) order := "id desc" if filter != nil { @@ -279,11 +279,11 @@ func (r *systemRepo) ListUsers(ctx context.Context, page, pageSize int, filter * return users, total, err } -func (r *systemRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) { +func (r *userRepo) CreateUser(ctx context.Context, user *biz.User) (*biz.User, error) { return r.CreateUserWithAuthorities(ctx, user, nil) } -func (r *systemRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) { +func (r *userRepo) CreateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) (*biz.User, error) { if user.UUID == "" { user.UUID = uuid.NewString() } @@ -332,11 +332,11 @@ func validateAuthorities(tx *gorm.DB, authorityIDs []uint) error { return nil } -func (r *systemRepo) UpdateUser(ctx context.Context, user *biz.User) error { +func (r *userRepo) UpdateUser(ctx context.Context, user *biz.User) error { return r.updateUser(r.data.gormDB.WithContext(ctx), user) } -func (r *systemRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error { +func (r *userRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error { updates := make(map[string]any) if user.NickName != "" { updates["nick_name"] = user.NickName @@ -359,7 +359,7 @@ func (r *systemRepo) UpdateSelfUser(ctx context.Context, user *biz.User) error { return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error } -func (r *systemRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error { +func (r *userRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User) error { if user == nil || len(user.Departments) == 0 { return nil } @@ -408,7 +408,7 @@ func (r *systemRepo) FillDepartmentNamePaths(ctx context.Context, user *biz.User return nil } -func (r *systemRepo) updateUser(tx *gorm.DB, user *biz.User) error { +func (r *userRepo) updateUser(tx *gorm.DB, user *biz.User) error { updates := map[string]any{"nick_name": user.NickName, "header_img": user.HeaderImg, "phone": user.Phone, "email": user.Email, "enable": user.Enable} if user.AuthorityID != 0 { updates["authority_id"] = user.AuthorityID @@ -416,7 +416,7 @@ func (r *systemRepo) updateUser(tx *gorm.DB, user *biz.User) error { return tx.Model(&userPO{}).Where("id = ?", user.ID).Updates(updates).Error } -func (r *systemRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error { +func (r *userRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.User, authorityIDs []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := r.updateUser(tx, user); err != nil { return err @@ -427,7 +427,7 @@ func (r *systemRepo) UpdateUserWithAuthorities(ctx context.Context, user *biz.Us return setUserAuthorities(tx, user.ID, authorityIDs) }) } -func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error { +func (r *userRepo) DeleteUser(ctx context.Context, id uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Where("sys_user_id = ?", id).Delete(&userAuthorityPO{}).Error; err != nil { return err @@ -441,7 +441,7 @@ func (r *systemRepo) DeleteUser(ctx context.Context, id uint) error { return tx.Delete(&userPO{}, id).Error }) } -func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error { +func (r *userRepo) UpdatePassword(ctx context.Context, id uint, password string, clearMustChange bool) error { now := time.Now() updates := map[string]any{"password": password, "password_updated_at": now} if clearMustChange { @@ -449,7 +449,7 @@ func (r *systemRepo) UpdatePassword(ctx context.Context, id uint, password strin } return r.data.gormDB.WithContext(ctx).Model(&userPO{}).Where("id = ?", id).Updates(updates).Error } -func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { +func (r *userRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, error) { var pos []authorityPO if err := r.data.gormDB.WithContext(ctx).Order("authority_id").Find(&pos).Error; err != nil { return nil, err @@ -462,9 +462,9 @@ func (r *systemRepo) ListAuthorities(ctx context.Context) ([]*biz.Authority, err return result, nil } -func (r *systemRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error { +func (r *userRepo) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error { return r.data.gormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - access := &accessRepo{data: r.data} + access := &authorityAccessRepo{data: r.data} for _, authorityID := range authorityIDs { if err := access.checkAuthorityIDAuth(ctx, authorityID); err != nil { return err @@ -494,7 +494,7 @@ func setUserAuthorities(tx *gorm.DB, id uint, authorityIDs []uint) error { return tx.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityIDs[0]).Error } -func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error { +func (r *userRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) error { db := r.data.gormDB.WithContext(ctx) var count int64 if err := db.Model(&userAuthorityPO{}).Where("sys_user_id = ? AND sys_authority_authority_id = ?", id, authorityID).Count(&count).Error; err != nil { @@ -520,7 +520,7 @@ func (r *systemRepo) SetUserAuthority(ctx context.Context, id, authorityID uint) return db.Model(&userPO{}).Where("id = ?", id).Update("authority_id", authorityID).Error } -func (r *systemRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error { +func (r *userRepo) SetUserSetting(ctx context.Context, id uint, setting map[string]any) error { value, err := json.Marshal(setting) if err != nil { return err diff --git a/internal/data/version.go b/internal/data/version.go index 8f45dfd..1d7456b 100644 --- a/internal/data/version.go +++ b/internal/data/version.go @@ -147,11 +147,11 @@ func (r *versionRepo) BuildVersionBundle(ctx context.Context, menuIDs, apiIDs, d return nil, err } } - settings := &settingsRepo{data: r.data} + dictionariesRepo := &dictionaryRepo{data: r.data} for _, po := range dictionaries { v := dictionaryFromPO(po) v.ID = 0 - details, _, err := settings.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID}) + details, _, err := dictionariesRepo.ListDictionaryDetails(ctx, 0, 0, biz.DictionaryDetailFilter{DictionaryID: po.ID}) if err != nil { return nil, err } diff --git a/internal/server/gin.go b/internal/server/gin.go index e5ede82..644a192 100644 --- a/internal/server/gin.go +++ b/internal/server/gin.go @@ -20,7 +20,7 @@ import ( kratoshttp "github.com/go-kratos/kratos/v3/transport/http" ) -func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, authority *handler.Authority, menu *handler.Menu, api *handler.API, permission *handler.Permission, organization *handler.Organization, announcement *handler.Announcement, email *handler.Email, task *handler.Task, media *handler.Media, auditHandler *handler.Audit, export *handler.Export, version *handler.Version, dictionary *handler.Dictionary, parameter *handler.Parameter, apiToken *handler.APIToken, systemConfig *handler.SystemConfig, publicHandler *handler.Public, user *handler.User, navigation *handler.Navigation, session *handler.Session, security *service.SecurityService, tokens *service.TokenService, audit *service.AuditRecorder, logger *slog.Logger) *gin.Engine { +func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, security *service.SecurityService, tokens *service.TokenService, audit *service.AuditRecorder, logger *slog.Logger) *gin.Engine { gin.SetMode(gin.ReleaseMode) engine := gin.New() engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(security), servermiddleware.OperationAudit(runtime, audit)) @@ -32,29 +32,29 @@ func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, a } public := engine.Group(prefix) public.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, "ok") }) - serverrouter.RegisterPublic(public, engine, publicHandler) + serverrouter.RegisterPublic(public, engine, handlers.Public) private := engine.Group(prefix) private.Use(servermiddleware.Auth(runtime, security, tokens), servermiddleware.MustChangePassword(), servermiddleware.AccessControl(runtime, access, audit)) - serverrouter.RegisterUser(private, user) - serverrouter.RegisterNavigation(private, navigation) - serverrouter.RegisterSession(private, session) - serverrouter.RegisterAuthority(private, authority) - serverrouter.RegisterMenu(private, menu) - serverrouter.RegisterAPI(private, public, engine, api) - serverrouter.RegisterPermission(private, permission) - serverrouter.RegisterOrganization(private, organization) - serverrouter.RegisterDictionary(private, dictionary) - serverrouter.RegisterParameter(private, parameter) - serverrouter.RegisterAPIToken(private, apiToken) - serverrouter.RegisterSystemConfig(private, systemConfig) - serverrouter.RegisterVersion(private, version) - serverrouter.RegisterExport(private, public, export) - serverrouter.RegisterAudit(private, public, auditHandler) - serverrouter.RegisterTask(private, task) - serverrouter.RegisterMedia(private, media) - serverrouter.RegisterAnnouncement(private, public, announcement) - serverrouter.RegisterEmail(private, email) + serverrouter.RegisterUser(private, handlers.User) + serverrouter.RegisterNavigation(private, handlers.Navigation) + serverrouter.RegisterSession(private, handlers.Session) + serverrouter.RegisterAuthority(private, handlers.Authority) + serverrouter.RegisterMenu(private, handlers.Menu) + serverrouter.RegisterAPI(private, public, engine, handlers.API) + serverrouter.RegisterPermission(private, handlers.Permission) + serverrouter.RegisterOrganization(private, handlers.Organization) + serverrouter.RegisterDictionary(private, handlers.Dictionary) + serverrouter.RegisterParameter(private, handlers.Parameter) + serverrouter.RegisterAPIToken(private, handlers.APIToken) + serverrouter.RegisterSystemConfig(private, handlers.SystemConfig) + serverrouter.RegisterVersion(private, handlers.Version) + serverrouter.RegisterExport(private, public, handlers.Export) + serverrouter.RegisterAudit(private, public, handlers.Audit) + serverrouter.RegisterTask(private, handlers.Task) + serverrouter.RegisterMedia(private, handlers.Media) + serverrouter.RegisterAnnouncement(private, public, handlers.Announcement) + serverrouter.RegisterEmail(private, handlers.Email) engine.NoRoute(func(c *gin.Context) { if serveLocalStorage(c, runtime) { diff --git a/internal/server/handler/media.go b/internal/server/handler/media.go index fd1bf5a..7cd90bd 100644 --- a/internal/server/handler/media.go +++ b/internal/server/handler/media.go @@ -173,7 +173,7 @@ func (h *Media) InitUpload(c *gin.Context) { httpx.Fail(c, "参数错误") return } - if config := h.service.MediaConfig(); config != nil && config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize { + if config := h.service.MediaConfig(); config.MaxFileSize > 0 && req.FileSize > config.MaxFileSize { httpx.Fail(c, "文件超过大小上限") return } diff --git a/internal/server/handler/public.go b/internal/server/handler/public.go index 2968d4c..4d4566f 100644 --- a/internal/server/handler/public.go +++ b/internal/server/handler/public.go @@ -8,45 +8,28 @@ import ( "time" "kra/internal/biz" - "kra/internal/conf" "kra/internal/server/httpx" "kra/internal/service" "kra/internal/service/dto" - "kra/internal/worker" "github.com/gin-gonic/gin" "github.com/mojocn/base64Captcha" ) type Public struct { - auth *service.AuthService - system *service.SystemConfigService - settings *service.SecurityService - audit *service.AuditRecorder - scheduler *worker.TaskScheduler - runtime *conf.Runtime - store *captchaStore + auth *service.AuthService + system *service.SystemConfigService + settings *service.SecurityService + store *captchaStore } -func NewPublic(runtime *conf.Runtime, auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService, audit *service.AuditRecorder, scheduler *worker.TaskScheduler) *Public { - return &Public{auth: auth, system: system, settings: settings, audit: audit, scheduler: scheduler, runtime: runtime, store: &captchaStore{service: settings, runtime: runtime}} +func NewPublic(auth *service.AuthService, system *service.SystemConfigService, settings *service.SecurityService) *Public { + return &Public{auth: auth, system: system, settings: settings, store: &captchaStore{service: settings}} } func (h *Public) captchaConfig() (int, int, int) { - keyLong, width, height := 6, 240, 80 - config := h.runtime.Admin() - if config != nil && config.Captcha != nil { - if config.Captcha.KeyLong > 0 { - keyLong = int(config.Captcha.KeyLong) - } - if config.Captcha.ImgWidth > 0 { - width = int(config.Captcha.ImgWidth) - } - if config.Captcha.ImgHeight > 0 { - height = int(config.Captcha.ImgHeight) - } - } - return keyLong, width, height + config := h.settings.CaptchaSettings() + return config.KeyLong, config.ImageWidth, config.ImageHeight } func (h *Public) Captcha(c *gin.Context) { @@ -74,57 +57,32 @@ func (h *Public) Login(c *gin.Context) { httpx.Fail(c, "请输入用户名和密码") return } - security, _ := h.settings.CurrentSecurity(c.Request.Context()) - if security != nil && security.LockEnable { - if locked, _ := h.settings.LoginLocked(c.Request.Context(), req.Username); locked { - httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(security.LockDuration)+" 分钟后再试") - _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "账号已锁定", Agent: c.Request.UserAgent()}) - return - } - } - requireCaptcha := security == nil || security.CaptchaOpen == 0 - ipTTL := time.Hour - if security != nil { - if security.CaptchaTimeout > 0 { - ipTTL = time.Duration(security.CaptchaTimeout) * time.Second - } - failures, _ := h.settings.EnsureLoginIPCounter(c.Request.Context(), c.ClientIP(), ipTTL) - requireCaptcha = security.CaptchaOpen == 0 || failures > security.CaptchaOpen - } - if requireCaptcha && (req.CaptchaID == "" || req.Captcha == "" || !h.store.Verify(req.CaptchaID, req.Captcha, true)) { - _, _ = h.settings.IncrementLoginIP(c.Request.Context(), c.ClientIP(), ipTTL) - _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "验证码错误", Agent: c.Request.UserAgent()}) - httpx.Fail(c, "验证码错误") - return - } - result, err := h.auth.Login(c.Request.Context(), req.Username, req.Password) + result, err := h.auth.Login(c.Request.Context(), &req, c.ClientIP(), c.Request.UserAgent()) if err != nil { - _, _ = h.settings.IncrementLoginIP(c.Request.Context(), c.ClientIP(), ipTTL) - if errors.Is(err, biz.ErrUserDisabled) { - var disabled *service.UserDisabledError - errors.As(err, &disabled) - userID := uint(0) - if disabled != nil { - userID = disabled.UserID + switch { + case errors.Is(err, biz.ErrAccountLocked): + var locked *biz.AccountLockedError + errors.As(err, &locked) + minutes := 0 + if locked != nil { + minutes = locked.Minutes } - _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户被禁止登录", Agent: c.Request.UserAgent(), UserID: userID}) + httpx.Fail(c, "账号已锁定,请 "+strconv.Itoa(minutes)+" 分钟后再试") + case errors.Is(err, biz.ErrCaptchaInvalid): + httpx.Fail(c, "验证码错误") + case errors.Is(err, biz.ErrUserDisabled): httpx.Fail(c, "用户被禁止登录") - return + case errors.Is(err, biz.ErrTokenIssue): + httpx.Fail(c, "获取token失败") + case errors.Is(err, biz.ErrLoginState): + httpx.Fail(c, "设置登录状态失败") + case errors.Is(err, biz.ErrTokenRevoke): + httpx.Fail(c, "jwt作废失败") + default: + httpx.Fail(c, "用户名不存在或者密码错误") } - _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: false, ErrorMessage: "用户名不存在或者密码错误", Agent: c.Request.UserAgent()}) - if security != nil && security.LockEnable { - lockTTL := time.Duration(security.LockDuration) * time.Minute - failures, _ := h.settings.IncrementLoginFailure(c.Request.Context(), req.Username, lockTTL) - if int(failures) >= security.LockThreshold { - _ = h.settings.LockLogin(c.Request.Context(), req.Username, lockTTL) - } - } - httpx.Fail(c, "用户名不存在或者密码错误") return } - userID, _ := result.User["ID"].(uint) - _ = h.audit.RecordLoginRequest(c.Request.Context(), &dto.LoginLogRequest{Username: req.Username, IP: c.ClientIP(), Status: true, Agent: c.Request.UserAgent(), UserID: userID}) - h.settings.ClearLoginState(c.Request.Context(), req.Username) maxAge := int(time.Until(time.UnixMilli(result.ExpiresAt)).Seconds()) httpx.SetTokenCookie(c, result.Token, maxAge) httpx.Write(c, httpx.CodeSuccess, result, "登录成功") @@ -165,28 +123,23 @@ func (h *Public) InitializeDatabase(engine *gin.Engine) gin.HandlerFunc { values = append(values, dto.Route{Path: route.Path, Method: route.Method}) } if err := h.system.InitializeRoutes(c.Request.Context(), &input, values); err != nil { + if errors.Is(err, biz.ErrTaskRuntimeReload) { + httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error()) + return + } httpx.Fail(c, "自动创建数据库失败: "+err.Error()) return } - if err := h.scheduler.Reload(c.Request.Context()); err != nil { - httpx.Fail(c, "数据库已初始化,但定时任务加载失败:"+err.Error()) - return - } httpx.Write(c, httpx.CodeSuccess, gin.H{}, "自动创建数据库成功") } } type captchaStore struct { service *service.SecurityService - runtime *conf.Runtime } func (s *captchaStore) Set(id, value string) error { - expiration := 3 * time.Minute - config := s.runtime.Admin() - if config != nil && config.Captcha != nil && config.Captcha.StoreExpiration != nil { - expiration = config.Captcha.StoreExpiration.AsDuration() - } + expiration := s.service.CaptchaSettings().StoreExpiration return s.service.SetCaptcha(context.Background(), id, value, expiration) } func (s *captchaStore) Get(id string, clear bool) string { diff --git a/internal/server/handler/set.go b/internal/server/handler/set.go new file mode 100644 index 0000000..9e65113 --- /dev/null +++ b/internal/server/handler/set.go @@ -0,0 +1,28 @@ +package handler + +type Set struct { + Authority *Authority + Menu *Menu + API *API + Permission *Permission + Organization *Organization + Announcement *Announcement + Email *Email + Task *Task + Media *Media + Audit *Audit + Export *Export + Version *Version + Dictionary *Dictionary + Parameter *Parameter + APIToken *APIToken + SystemConfig *SystemConfig + Public *Public + User *User + Navigation *Navigation + Session *Session +} + +func NewSet(authority *Authority, menu *Menu, api *API, permission *Permission, organization *Organization, announcement *Announcement, email *Email, task *Task, media *Media, audit *Audit, export *Export, version *Version, dictionary *Dictionary, parameter *Parameter, apiToken *APIToken, systemConfig *SystemConfig, public *Public, user *User, navigation *Navigation, session *Session) *Set { + return &Set{Authority: authority, Menu: menu, API: api, Permission: permission, Organization: organization, Announcement: announcement, Email: email, Task: task, Media: media, Audit: audit, Export: export, Version: version, Dictionary: dictionary, Parameter: parameter, APIToken: apiToken, SystemConfig: systemConfig, Public: public, User: user, Navigation: navigation, Session: session} +} diff --git a/internal/server/handler/system_config.go b/internal/server/handler/system_config.go index c511eed..997e3ea 100644 --- a/internal/server/handler/system_config.go +++ b/internal/server/handler/system_config.go @@ -1,22 +1,23 @@ package handler import ( + "errors" + + "kra/internal/biz" "kra/internal/server/httpx" "kra/internal/service" "kra/internal/service/dto" - "kra/internal/worker" "github.com/gin-gonic/gin" ) type SystemConfig struct { - system *service.SystemConfigService - settings *service.SecurityService - scheduler *worker.TaskScheduler + system *service.SystemConfigService + settings *service.SecurityService } -func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService, scheduler *worker.TaskScheduler) *SystemConfig { - return &SystemConfig{system: system, settings: settings, scheduler: scheduler} +func NewSystemConfig(system *service.SystemConfigService, settings *service.SecurityService) *SystemConfig { + return &SystemConfig{system: system, settings: settings} } func (h *SystemConfig) GetSecurity(c *gin.Context) { @@ -61,13 +62,13 @@ func (h *SystemConfig) Set(c *gin.Context) { func (h *SystemConfig) Reload(c *gin.Context) { if err := h.system.ReloadConfig(c.Request.Context()); err != nil { + if errors.Is(err, biz.ErrTaskRuntimeReload) { + httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error()) + return + } httpx.Fail(c, "重载系统失败:"+err.Error()) return } - if err := h.scheduler.Reload(c.Request.Context()); err != nil { - httpx.Fail(c, "系统配置已重载,但定时任务重载失败:"+err.Error()) - return - } httpx.Write(c, httpx.CodeSuccess, gin.H{}, "重载系统成功") } diff --git a/internal/server/handler/task.go b/internal/server/handler/task.go index 391eeab..f69b75a 100644 --- a/internal/server/handler/task.go +++ b/internal/server/handler/task.go @@ -1,26 +1,26 @@ package handler import ( + "errors" "fmt" "net/http" "strconv" "time" + "kra/internal/biz" "kra/internal/server/httpx" "kra/internal/service" "kra/internal/service/dto" - "kra/internal/worker" "github.com/gin-gonic/gin" ) type Task struct { - service *service.TaskService - scheduler *worker.TaskScheduler + service *service.TaskService } -func NewTask(service *service.TaskService, scheduler *worker.TaskScheduler) *Task { - return &Task{service: service, scheduler: scheduler} +func NewTask(service *service.TaskService) *Task { + return &Task{service: service} } func (h *Task) Create(c *gin.Context) { @@ -29,15 +29,17 @@ func (h *Task) Create(c *gin.Context) { httpx.Fail(c, "参数错误") return } - id, err := h.service.CreateRequest(c.Request.Context(), &req) + _, err := h.service.CreateRequest(c.Request.Context(), &req) if err != nil { + if errors.Is(err, biz.ErrTaskSchedule) { + var scheduleErr *biz.TaskScheduleError + errors.As(err, &scheduleErr) + httpx.Fail(c, "调度失败:"+scheduleErr.Error()) + return + } httpx.Fail(c, "创建失败:"+err.Error()) return } - if err = h.scheduler.ScheduleID(c.Request.Context(), id); err != nil { - httpx.Fail(c, "调度失败:"+err.Error()) - return - } httpx.Write(c, httpx.CodeSuccess, gin.H{}, "创建成功") } @@ -48,13 +50,15 @@ func (h *Task) Update(c *gin.Context) { return } if err := h.service.UpdateRequest(c.Request.Context(), &req); err != nil { + if errors.Is(err, biz.ErrTaskSchedule) { + var scheduleErr *biz.TaskScheduleError + errors.As(err, &scheduleErr) + httpx.Fail(c, "调度失败:"+scheduleErr.Error()) + return + } httpx.Fail(c, "更新失败:"+err.Error()) return } - if err := h.scheduler.ScheduleID(c.Request.Context(), req.ID); err != nil { - httpx.Fail(c, "调度失败:"+err.Error()) - return - } httpx.Write(c, httpx.CodeSuccess, gin.H{}, "更新成功") } @@ -64,7 +68,6 @@ func (h *Task) Delete(c *gin.Context) { httpx.Fail(c, "参数错误") return } - h.scheduler.Remove(req.ID) if err := h.service.Delete(c.Request.Context(), req.ID); err != nil { httpx.Fail(c, "删除失败") return @@ -82,10 +85,6 @@ func (h *Task) Toggle(c *gin.Context) { httpx.Fail(c, "操作失败: "+err.Error()) return } - if err := h.scheduler.ScheduleID(c.Request.Context(), req.ID); err != nil { - httpx.Fail(c, "操作失败: "+err.Error()) - return - } httpx.Write(c, httpx.CodeSuccess, gin.H{}, "操作成功") } @@ -95,7 +94,7 @@ func (h *Task) Trigger(c *gin.Context) { httpx.Fail(c, "参数错误") return } - if err := h.scheduler.TriggerID(c.Request.Context(), req.ID); err != nil { + if err := h.service.Trigger(c.Request.Context(), req.ID); err != nil { httpx.Fail(c, "触发失败: "+err.Error()) return } @@ -110,7 +109,7 @@ func (h *Task) List(c *gin.Context) { value, _ := strconv.ParseBool(raw) enabled = &value } - items, total, err := h.service.ListRequest(c.Request.Context(), page, size, c.Query("name"), c.Query("executorType"), enabled, h.scheduler.NextRuns()) + items, total, err := h.service.ListRequest(c.Request.Context(), page, size, c.Query("name"), c.Query("executorType"), enabled) if err != nil { httpx.Fail(c, "获取失败") return @@ -146,8 +145,8 @@ func (h *Task) AlertStream(c *gin.Context) { } // Remove the normal server deadline for long-lived SSE connections. _ = http.NewResponseController(c.Writer).SetWriteDeadline(time.Time{}) - events := h.scheduler.Subscribe() - defer h.scheduler.Unsubscribe(events) + events := h.service.Subscribe() + defer h.service.Unsubscribe(events) ticker := time.NewTicker(20 * time.Second) defer ticker.Stop() _, _ = fmt.Fprint(c.Writer, "event: connected\ndata: {}\n\n") diff --git a/internal/server/server.go b/internal/server/server.go index d4df517..00cbfce 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -8,4 +8,4 @@ import ( ) // ProviderSet is server providers. -var ProviderSet = wire.NewSet(NewGinEngine, NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewTask, handler.NewMedia, handler.NewAudit, handler.NewExport, handler.NewVersion, handler.NewDictionary, handler.NewParameter, handler.NewAPIToken, handler.NewSystemConfig, handler.NewPublic, handler.NewUser, handler.NewNavigation, handler.NewSession, worker.NewTaskExecutor, worker.NewTaskScheduler) +var ProviderSet = wire.NewSet(NewGinEngine, NewGinServer, handler.NewAuthority, handler.NewMenu, handler.NewAPI, handler.NewPermission, handler.NewOrganization, handler.NewAnnouncement, handler.NewEmail, handler.NewTask, handler.NewMedia, handler.NewAudit, handler.NewExport, handler.NewVersion, handler.NewDictionary, handler.NewParameter, handler.NewAPIToken, handler.NewSystemConfig, handler.NewPublic, handler.NewUser, handler.NewNavigation, handler.NewSession, handler.NewSet, worker.NewTaskExecutor, worker.NewTaskScheduler, worker.NewTaskRuntime) diff --git a/internal/service/api.go b/internal/service/api.go index 010b88d..8bdffc5 100644 --- a/internal/service/api.go +++ b/internal/service/api.go @@ -5,25 +5,24 @@ import ( "strings" "kra/internal/biz" - "kra/internal/conf" "kra/internal/service/dto" ) type APIService struct { - uc *biz.APIUsecase - runtime *conf.Runtime + uc *biz.APIUsecase + settings biz.RuntimeSettings } -func NewAPIService(uc *biz.APIUsecase, runtime *conf.Runtime) *APIService { - return &APIService{uc: uc, runtime: runtime} +func NewAPIService(uc *biz.APIUsecase, settings biz.RuntimeSettings) *APIService { + return &APIService{uc: uc, settings: settings} } func (s *APIService) NormalizeRoutePath(path string) string { - config := s.runtime.Admin() - if config == nil || config.RouterPrefix == "" { + routerPrefix := s.settings.RouterPrefix() + if routerPrefix == "" { return path } - prefix := strings.TrimSuffix(config.RouterPrefix, "/") + prefix := strings.TrimSuffix(routerPrefix, "/") normalized := strings.TrimPrefix(path, prefix) if normalized == "" { return "/" diff --git a/internal/service/api_token.go b/internal/service/api_token.go index 5d51332..ab2b514 100644 --- a/internal/service/api_token.go +++ b/internal/service/api_token.go @@ -5,17 +5,15 @@ import ( "time" "kra/internal/biz" - "kra/internal/conf" - "kra/pkg/adminauth" ) type TokenService struct { - uc *biz.TokenUsecase - runtime *conf.Runtime + uc *biz.TokenUsecase + issuer biz.TokenIssuer } -func NewTokenService(uc *biz.TokenUsecase, runtime *conf.Runtime) *TokenService { - return &TokenService{uc: uc, runtime: runtime} +func NewTokenService(uc *biz.TokenUsecase, issuer biz.TokenIssuer) *TokenService { + return &TokenService{uc: uc, issuer: issuer} } func tokenDTO(v *biz.APIToken) map[string]any { @@ -31,27 +29,15 @@ func (s *TokenService) CreateAPIToken(ctx context.Context, userID, authorityID u if err != nil { return "", err } - secret, issuer := "", "kra" - buffer := 24 * time.Hour - config := s.runtime.Admin() - if config != nil && config.Jwt != nil { - secret = config.Jwt.SigningKey - if config.Jwt.Issuer != "" { - issuer = config.Jwt.Issuer - } - if config.Jwt.BufferTime != nil { - buffer = config.Jwt.BufferTime.AsDuration() - } - } - token, _, err := adminauth.Generate(secret, issuer, duration, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, false) + issued, err := s.issuer.IssueToken(user, authorityID, false, duration) if err != nil { return "", err } - record := &biz.APIToken{UserID: userID, AuthorityID: authorityID, Token: token, Status: true, ExpiresAt: time.Now().Add(duration), Remark: remark} + record := &biz.APIToken{UserID: userID, AuthorityID: authorityID, Token: issued.Value, Status: true, ExpiresAt: time.Now().Add(duration), Remark: remark} if err = s.uc.CreateAPIToken(ctx, record); err != nil { return "", err } - return token, nil + return issued.Value, nil } func (s *TokenService) APITokens(ctx context.Context, page, size int, userID uint, status *bool) ([]map[string]any, int64, error) { items, total, err := s.uc.ListAPITokens(ctx, page, size, userID, status) diff --git a/internal/service/authentication.go b/internal/service/authentication.go index aaae648..93c6fb5 100644 --- a/internal/service/authentication.go +++ b/internal/service/authentication.go @@ -2,18 +2,11 @@ package service import ( "context" - "time" "kra/internal/biz" - "kra/internal/conf" - "kra/pkg/adminauth" + "kra/internal/service/dto" ) -type UserDisabledError struct{ UserID uint } - -func (e *UserDisabledError) Error() string { return biz.ErrUserDisabled.Error() } -func (e *UserDisabledError) Unwrap() error { return biz.ErrUserDisabled } - type LoginResult struct { User map[string]any `json:"user"` Token string `json:"token"` @@ -22,65 +15,29 @@ type LoginResult struct { } type AuthService struct { - uc *biz.UserUsecase - runtime *conf.Runtime - settings *SecurityService + uc *biz.AuthenticationUsecase } -func NewAuthService(uc *biz.UserUsecase, runtime *conf.Runtime, settings *SecurityService) *AuthService { - return &AuthService{uc: uc, runtime: runtime, settings: settings} +func NewAuthService(uc *biz.AuthenticationUsecase) *AuthService { + return &AuthService{uc: uc} } -func (s *AuthService) Login(ctx context.Context, username, password string) (*LoginResult, error) { - u, err := s.uc.Login(ctx, username, password) +func loginResult(value *biz.AuthenticationResult) *LoginResult { + return &LoginResult{User: convertUser(value.User), Token: value.Token, ExpiresAt: value.ExpiresAt.UnixMilli(), NeedChangePassword: value.NeedChangePassword} +} + +func (s *AuthService) Login(ctx context.Context, req *dto.LoginRequest, ip, agent string) (*LoginResult, error) { + value, err := s.uc.Login(ctx, &biz.LoginAttempt{Username: req.Username, Password: req.Password, CaptchaID: req.CaptchaID, Captcha: req.Captcha, IP: ip, Agent: agent}) if err != nil { return nil, err } - if u.Enable != 1 { - return nil, &UserDisabledError{UserID: u.ID} - } - if security, securityErr := s.settings.CurrentSecurity(ctx); securityErr == nil && security.PwdExpireEnable && u.PasswordUpdatedAt != nil && time.Since(*u.PasswordUpdatedAt) > time.Duration(security.PwdExpireDays)*24*time.Hour { - u.MustChangePassword = true - } - return s.issueLogin(ctx, u, u.AuthorityID) -} - -func (s *AuthService) issueLogin(ctx context.Context, user *biz.User, authorityID uint) (*LoginResult, error) { - expires, buffer := 7*24*time.Hour, 24*time.Hour - secret, issuer := "", "kra" - config := s.runtime.Admin() - if config != nil && config.Jwt != nil { - secret, issuer = config.Jwt.SigningKey, config.Jwt.Issuer - if config.Jwt.ExpiresTime != nil { - expires = config.Jwt.ExpiresTime.AsDuration() - } - if config.Jwt.BufferTime != nil { - buffer = config.Jwt.BufferTime.AsDuration() - } - } - token, claims, err := adminauth.Generate(secret, issuer, expires, buffer, user.ID, authorityID, user.UUID, user.Username, user.NickName, user.MustChangePassword) - if err != nil { - return nil, err - } - if s.settings.UseMultipoint() { - oldToken, cacheErr := s.settings.ActiveToken(ctx, user.Username) - if cacheErr != nil { - return nil, cacheErr - } - if cacheErr = s.settings.RotateActiveToken(ctx, user.Username, oldToken, token, expires); cacheErr != nil { - return nil, cacheErr - } - } - return &LoginResult{User: convertUser(user), Token: token, ExpiresAt: claims.ExpiresAt.UnixMilli(), NeedChangePassword: user.MustChangePassword}, nil + return loginResult(value), nil } func (s *AuthService) SwitchAuthority(ctx context.Context, id, authorityID uint) (*LoginResult, error) { - if err := s.uc.SetUserAuthority(ctx, id, authorityID); err != nil { - return nil, err - } - user, err := s.uc.User(ctx, id) + value, err := s.uc.SwitchAuthority(ctx, id, authorityID) if err != nil { return nil, err } - return s.issueLogin(ctx, user, authorityID) + return loginResult(value), nil } diff --git a/internal/service/media.go b/internal/service/media.go index cfcf93b..73a7d92 100644 --- a/internal/service/media.go +++ b/internal/service/media.go @@ -5,23 +5,19 @@ import ( "io" "kra/internal/biz" - "kra/internal/conf" "kra/internal/service/dto" ) type MediaService struct { - uc *biz.MediaUsecase - runtime *conf.Runtime + uc *biz.MediaUsecase + settings biz.RuntimeSettings } -func NewMediaService(uc *biz.MediaUsecase, runtime *conf.Runtime) *MediaService { - return &MediaService{uc: uc, runtime: runtime} +func NewMediaService(uc *biz.MediaUsecase, settings biz.RuntimeSettings) *MediaService { + return &MediaService{uc: uc, settings: settings} } -func (s *MediaService) MediaConfig() *conf.AdminBackend_Media { - if config := s.runtime.Admin(); config != nil { - return config.Media - } - return nil +func (s *MediaService) MediaConfig() biz.MediaSettings { + return s.settings.MediaSettings() } func mediaDTO(v *biz.MediaFile) map[string]any { return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "classId": v.CategoryID, "url": v.URL, "tag": v.Tag, "key": v.Key, "size": v.Size, "mime": v.Mime, "md5": v.MD5, "userId": v.UserID} diff --git a/internal/service/security.go b/internal/service/security.go index 3f95b51..b7bbd8b 100644 --- a/internal/service/security.go +++ b/internal/service/security.go @@ -11,22 +11,7 @@ func securityDTO(v *biz.SecurityConfig) map[string]any { return map[string]any{"ID": v.ID, "captchaOpen": v.CaptchaOpen, "captchaTimeout": v.CaptchaTimeout, "keyLong": v.KeyLong, "imgWidth": v.ImgWidth, "imgHeight": v.ImgHeight, "pwdMinLength": v.PwdMinLength, "pwdRequireUpper": v.PwdRequireUpper, "pwdRequireLower": v.PwdRequireLower, "pwdRequireDigit": v.PwdRequireDigit, "pwdRequireSpecial": v.PwdRequireSpecial, "limitEnable": v.LimitEnable, "limitWindow": v.LimitWindow, "limitCount": v.LimitCount, "lockEnable": v.LockEnable, "lockThreshold": v.LockThreshold, "lockDuration": v.LockDuration, "pwdExpireEnable": v.PwdExpireEnable, "pwdExpireDays": v.PwdExpireDays, "forceNewUserChangePassword": v.ForceNewUserChangePassword} } func (s *SecurityService) CurrentSecurity(ctx context.Context) (*biz.SecurityConfig, error) { - s.securityMu.RLock() - if s.securityCache != nil { - copy := *s.securityCache - s.securityMu.RUnlock() - return ©, nil - } - s.securityMu.RUnlock() - value, err := s.uc.SecurityConfig(ctx) - if err != nil { - return nil, err - } - s.securityMu.Lock() - s.securityCache = value - copy := *value - s.securityMu.Unlock() - return ©, nil + return s.uc.Current(ctx) } func (s *SecurityService) Security(ctx context.Context) (map[string]any, error) { value, err := s.CurrentSecurity(ctx) @@ -39,9 +24,6 @@ func (s *SecurityService) SaveSecurity(ctx context.Context, value *biz.SecurityC if err := s.uc.UpdateSecurity(ctx, value); err != nil { return nil, err } - s.securityMu.Lock() - s.securityCache = value - s.securityMu.Unlock() return securityDTO(value), nil } func (s *SecurityService) SaveSecurityRequest(ctx context.Context, value *dto.SecurityConfigRequest) (map[string]any, error) { diff --git a/internal/service/security_session.go b/internal/service/security_session.go index a752cee..2473a32 100644 --- a/internal/service/security_session.go +++ b/internal/service/security_session.go @@ -2,126 +2,73 @@ package service import ( "context" - "strconv" - "sync" "time" "kra/internal/biz" - "kra/internal/conf" ) type SecurityService struct { - uc *biz.SecurityUsecase - runtime *conf.Runtime - securityMu sync.RWMutex - securityCache *biz.SecurityConfig - cache biz.Cache - tokens *biz.TokenUsecase + uc *biz.SecurityUsecase } -func NewSecurityService(uc *biz.SecurityUsecase, runtime *conf.Runtime, cache biz.Cache, tokens *biz.TokenUsecase) *SecurityService { - return &SecurityService{uc: uc, runtime: runtime, cache: cache, tokens: tokens} -} - -func (s *SecurityService) cacheGet(ctx context.Context, key string) (string, bool, error) { - return s.cache.Get(ctx, key) -} - -func (s *SecurityService) cacheSet(ctx context.Context, key, value string, expiration time.Duration) error { - return s.cache.Set(ctx, key, value, expiration) -} - -func (s *SecurityService) cacheDelete(ctx context.Context, key string) error { - return s.cache.Delete(ctx, key) -} - -func (s *SecurityService) cacheIncrement(ctx context.Context, key string, expiration time.Duration) (int64, error) { - return s.cache.Increment(ctx, key, expiration) +func NewSecurityService(uc *biz.SecurityUsecase) *SecurityService { + return &SecurityService{uc: uc} } func (s *SecurityService) ActiveToken(ctx context.Context, username string) (string, error) { - value, _, err := s.cacheGet(ctx, activeTokenKey(username)) - return value, err + return s.uc.ActiveToken(ctx, username) } func (s *SecurityService) LoginLocked(ctx context.Context, username string) (bool, error) { - _, locked, err := s.cacheGet(ctx, "login:lock:"+username) - return locked, err + return s.uc.LoginLocked(ctx, username) } func (s *SecurityService) IncrementLoginFailure(ctx context.Context, username string, expiration time.Duration) (int64, error) { - return s.cacheIncrement(ctx, "login:fail:"+username, expiration) + return s.uc.IncrementLoginFailure(ctx, username, expiration) } func (s *SecurityService) LockLogin(ctx context.Context, username string, expiration time.Duration) error { - return s.cacheSet(ctx, "login:lock:"+username, "1", expiration) + return s.uc.LockLogin(ctx, username, expiration) } func (s *SecurityService) ClearLoginState(ctx context.Context, username string) { - _ = s.cacheDelete(ctx, "login:fail:"+username) - _ = s.cacheDelete(ctx, "login:lock:"+username) + s.uc.ClearLoginState(ctx, username) } func (s *SecurityService) EnsureLoginIPCounter(ctx context.Context, ip string, expiration time.Duration) (int, error) { - value, exists, err := s.cacheGet(ctx, ip) - if err != nil { - return 0, err - } - if exists { - return strconv.Atoi(value) - } - if expiration <= 0 { - expiration = time.Hour - } - if err = s.cacheSet(ctx, ip, "1", expiration); err != nil { - return 0, err - } - return 1, nil + return s.uc.EnsureLoginIPCounter(ctx, ip, expiration) } func (s *SecurityService) IncrementLoginIP(ctx context.Context, ip string, expiration time.Duration) (int64, error) { - return s.cacheIncrement(ctx, ip, expiration) + return s.uc.IncrementLoginIP(ctx, ip, expiration) } func (s *SecurityService) IncrementRateLimit(ctx context.Context, key string, expiration time.Duration) (int64, error) { - return s.cacheIncrement(ctx, key, expiration) + return s.uc.IncrementRateLimit(ctx, key, expiration) } func (s *SecurityService) SetCaptcha(ctx context.Context, id, value string, expiration time.Duration) error { - return s.cacheSet(ctx, "captcha:"+id, value, expiration) + return s.uc.SetCaptcha(ctx, id, value, expiration) } func (s *SecurityService) GetCaptcha(ctx context.Context, id string) (string, bool, error) { - return s.cacheGet(ctx, "captcha:"+id) + return s.uc.GetCaptcha(ctx, id) } func (s *SecurityService) DeleteCaptcha(ctx context.Context, id string) error { - return s.cacheDelete(ctx, "captcha:"+id) + return s.uc.DeleteCaptcha(ctx, id) } -func (s *SecurityService) UseMultipoint() bool { - config := s.runtime.Admin() - return config != nil && config.System != nil && config.System.UseMultipoint -} +func (s *SecurityService) UseMultipoint() bool { return s.uc.UseMultipoint() } -func activeTokenKey(username string) string { return "jwt:active:" + username } +func (s *SecurityService) CaptchaSettings() biz.CaptchaSettings { + return s.uc.CaptchaRuntimeSettings() +} func (s *SecurityService) ActiveTokenMatches(ctx context.Context, username, token string) (bool, error) { - if !s.UseMultipoint() { - return true, nil - } - active, ok, err := s.cacheGet(ctx, activeTokenKey(username)) - return ok && active == token, err + return s.uc.ActiveTokenMatches(ctx, username, token) } func (s *SecurityService) RotateActiveToken(ctx context.Context, username, oldToken, newToken string, expiration time.Duration) error { - if !s.UseMultipoint() { - return nil - } - if oldToken != "" && oldToken != newToken { - if err := s.tokens.BlacklistToken(ctx, oldToken); err != nil { - return err - } - } - return s.cacheSet(ctx, activeTokenKey(username), newToken, expiration) + return s.uc.RotateActiveToken(ctx, username, oldToken, newToken, expiration) } diff --git a/internal/service/task.go b/internal/service/task.go index 30cfaf9..5a6efc5 100644 --- a/internal/service/task.go +++ b/internal/service/task.go @@ -10,10 +10,10 @@ import ( ) type TaskService struct { - uc *biz.TaskUsecase + uc *biz.TaskApplicationUsecase } -func NewTaskService(uc *biz.TaskUsecase) *TaskService { +func NewTaskService(uc *biz.TaskApplicationUsecase) *TaskService { return &TaskService{uc: uc} } func taskDomain(v *dto.TaskRequest) *biz.TimedTask { @@ -29,8 +29,8 @@ func (s *TaskService) CreateRequest(ctx context.Context, req *dto.TaskRequest) ( func (s *TaskService) UpdateRequest(ctx context.Context, req *dto.TaskRequest) error { return s.Update(ctx, taskDomain(req)) } -func (s *TaskService) ListRequest(ctx context.Context, page, size int, name, executorType string, enabled *bool, next map[uint]time.Time) ([]map[string]any, int64, error) { - return s.Tasks(ctx, page, size, &biz.TimedTask{Name: name, ExecutorType: executorType, EnabledFilter: enabled}, next) +func (s *TaskService) ListRequest(ctx context.Context, page, size int, name, executorType string, enabled *bool) ([]map[string]any, int64, error) { + return s.Tasks(ctx, page, size, &biz.TimedTask{Name: name, ExecutorType: executorType, EnabledFilter: enabled}) } func (s *TaskService) Create(ctx context.Context, v *biz.TimedTask) error { return s.uc.Create(ctx, v) @@ -39,16 +39,16 @@ func (s *TaskService) Update(ctx context.Context, v *biz.TimedTask) error { return s.uc.Update(ctx, v) } func (s *TaskService) Delete(ctx context.Context, id uint) error { - return s.uc.DeleteTask(ctx, id) + return s.uc.Delete(ctx, id) } func (s *TaskService) Toggle(ctx context.Context, id uint, enabled bool) error { - return s.uc.ToggleTask(ctx, id, enabled) + return s.uc.Toggle(ctx, id, enabled) } func taskDTO(v *biz.TimedTask, next *time.Time) map[string]any { return map[string]any{"ID": v.ID, "CreatedAt": v.CreatedAt, "UpdatedAt": v.UpdatedAt, "DeletedAt": nil, "name": v.Name, "description": v.Description, "spec": v.Spec, "withSeconds": v.WithSeconds, "executorType": v.ExecutorType, "methodName": v.MethodName, "params": json.RawMessage(v.Params), "httpUrl": v.HTTPURL, "httpMethod": v.HTTPMethod, "httpHeader": json.RawMessage(v.HTTPHeader), "httpBody": v.HTTPBody, "httpAllowPrivate": v.HTTPAllowPrivate, "enabled": v.Enabled, "nextRunAt": next} } -func (s *TaskService) Tasks(ctx context.Context, page, size int, q *biz.TimedTask, next map[uint]time.Time) ([]map[string]any, int64, error) { - items, total, err := s.uc.ListTasks(ctx, page, size, q) +func (s *TaskService) Tasks(ctx context.Context, page, size int, q *biz.TimedTask) ([]map[string]any, int64, error) { + items, total, next, err := s.uc.List(ctx, page, size, q) if err != nil { return nil, 0, err } @@ -65,7 +65,7 @@ func (s *TaskService) Tasks(ctx context.Context, page, size int, q *biz.TimedTas } func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, status string) ([]map[string]any, int64, error) { - items, total, err := s.uc.ListTaskLogs(ctx, page, size, taskID, status) + items, total, err := s.uc.Logs(ctx, page, size, taskID, status) if err != nil { return nil, 0, err } @@ -76,6 +76,11 @@ func (s *TaskService) Logs(ctx context.Context, page, size int, taskID uint, sta return out, total, nil } +func (s *TaskService) Trigger(ctx context.Context, id uint) error { return s.uc.Trigger(ctx, id) } +func (s *TaskService) Reload(ctx context.Context) error { return s.uc.Reload(ctx) } +func (s *TaskService) Subscribe() chan []byte { return s.uc.Subscribe() } +func (s *TaskService) Unsubscribe(events chan []byte) { s.uc.Unsubscribe(events) } + func (s *TaskService) RegisteredMethods() []map[string]any { methods := biz.RegisteredTaskMethods() out := make([]map[string]any, 0, len(methods)) diff --git a/internal/worker/task_scheduler.go b/internal/worker/task_scheduler.go index 4436010..905a02d 100644 --- a/internal/worker/task_scheduler.go +++ b/internal/worker/task_scheduler.go @@ -36,6 +36,8 @@ func NewTaskScheduler(tasks *biz.TaskUsecase, executor *TaskExecutor, logger *sl return &TaskScheduler{tasks: tasks, executor: executor, logger: logger, standard: cron.New(), seconds: cron.New(cron.WithSeconds()), entries: map[uint]scheduledEntry{}, subscribers: map[chan []byte]struct{}{}} } +func NewTaskRuntime(scheduler *TaskScheduler) biz.TaskRuntime { return scheduler } + func (s *TaskScheduler) Start(ctx context.Context) error { runContext, cancel := context.WithCancel(ctx) s.ctxMu.Lock()