This commit is contained in:
2026-08-15 02:35:23 +08:00
parent bd5dbf2522
commit a34133cce5
49 changed files with 929 additions and 504 deletions
+7 -8
View File
@@ -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 "/"
+7 -21
View File
@@ -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)
+13 -56
View File
@@ -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
}
+6 -10
View File
@@ -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}
+1 -19
View File
@@ -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 &copy, 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 &copy, 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) {
+20 -73
View File
@@ -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)
}
+14 -9
View File
@@ -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))