refactor(auth): delegate access control to be-system

Remove Agent Desk users, roles, login sessions, tokens, and local permission persistence. Expose the backend as an embeddable ai-agent module with host-provided subject lookup and operation authorization callbacks, and complete the frontend/backend repository split.
This commit is contained in:
t
2026-08-21 00:41:07 +08:00
parent 3d47227fbd
commit 2bbf42b741
447 changed files with 1901 additions and 8920 deletions
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
)
var AgentEvaluationService = newAgentEvaluationService()
@@ -4,8 +4,8 @@ import (
"context"
"testing"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
)
func TestAgentEvaluationServiceValidatesAndCallsRunner(t *testing.T) {
+8 -8
View File
@@ -1,17 +1,17 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+6 -6
View File
@@ -7,12 +7,12 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
@@ -5,8 +5,8 @@ import (
"strings"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+8 -8
View File
@@ -7,14 +7,14 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/httpx/params"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
+5 -5
View File
@@ -5,11 +5,11 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -1,20 +1,20 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"slices"
"strings"
"sync"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
@@ -5,11 +5,11 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+8 -8
View File
@@ -1,17 +1,17 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -4,8 +4,8 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -4,7 +4,7 @@ import (
"strings"
"testing"
"agent-desk/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -3,7 +3,7 @@ package services
import (
"testing"
"agent-desk/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
)
func TestValidateMCPToolRiskPolicyRejectsTrustedToolOverride(t *testing.T) {
+10 -10
View File
@@ -6,17 +6,17 @@ import (
"strings"
"time"
aitooling "agent-desk/internal/ai/tooling"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/toolx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
aitooling "code.tczkiot.com/wlw/ai-agent/internal/ai/tooling"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"strings"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -3,13 +3,13 @@ package services
import (
"strings"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
+8 -8
View File
@@ -4,15 +4,15 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+1 -1
View File
@@ -1,5 +1,5 @@
package services
import "agent-desk/internal/models"
import "code.tczkiot.com/wlw/ai-agent/internal/models"
var TriggerAIReplyAsyncHook func(conversation models.Conversation, message models.Message)
+11 -11
View File
@@ -7,17 +7,17 @@ import (
"strings"
"time"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
workflowvalidator "agent-desk/internal/ai/workflow/validator"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/httpx/params"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
workflowregistry "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/registry"
workflowvalidator "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/validator"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -6,12 +6,12 @@ import (
"testing"
"time"
"agent-desk/internal/ai/workflow/dsl"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+8 -8
View File
@@ -1,14 +1,14 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"agent-desk/internal/services/storage"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/services/storage"
"bytes"
"io"
"mime/multipart"
+35 -399
View File
@@ -1,436 +1,72 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"crypto/rand"
"encoding/hex"
"slices"
"sort"
"strings"
"time"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/constants"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
const (
authPrincipalContextKey = "authPrincipal"
)
const authPrincipalContextKey = "externalAuthPrincipal"
var AuthService = newAuthService()
// AuthService adapts identity data authenticated by the host application and
// delegates every operation authorization back to that host.
var AuthService = &externalPrincipalService{}
func newAuthService() *authService {
return &authService{}
}
type externalPrincipalService struct{}
type authService struct {
}
func (s *authService) GetAuthPrincipal(ctx *gin.Context) *dto.AuthPrincipal {
func (s *externalPrincipalService) GetAuthPrincipal(ctx *gin.Context) *dto.AuthPrincipal {
if ctx == nil {
return nil
}
v, _ := ctx.Get(authPrincipalContextKey)
if principal, ok := v.(*dto.AuthPrincipal); ok {
return principal
}
return nil
}
func (s *authService) setAuthPrincipal(ctx *gin.Context, user *models.User, roles, permissions []string) *dto.AuthPrincipal {
principal := &dto.AuthPrincipal{
UserID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Avatar: user.Avatar,
Status: user.Status,
Roles: roles,
Permissions: permissions,
}
ctx.Set(authPrincipalContextKey, principal)
value, _ := ctx.Get(authPrincipalContextKey)
principal, _ := value.(*dto.AuthPrincipal)
return principal
}
func (s *authService) RequirePermission(ctx *gin.Context, permission constants.Permission) (principal *dto.AuthPrincipal, err error) {
if principal = s.GetAuthPrincipal(ctx); principal == nil {
if principal, err = s.Authenticate(ctx); err != nil {
return nil, err
}
}
if principal == nil {
return nil, errorsx.ForbiddenI18n("error.e0225")
}
if !s.HasPermission(ctx, permission.Code) {
return principal, errorsx.ForbiddenI18n("error.e0225")
}
return principal, nil
}
func (s *authService) Login(req request.LoginRequest, authCfg config.AuthConfig, clientIP, userAgent string) (*response.LoginResponse, error) {
username := strings.TrimSpace(req.Username)
principal := normalizeLoginPrincipal(username)
password := req.Password
if username == "" || strings.TrimSpace(password) == "" {
return nil, errorsx.InvalidParamI18n("error.e0258")
}
if s.isCredentialLocked(principal, authCfg) {
_ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "credential locked")
return nil, errorsx.CredentialLockedI18n("error.e0270")
}
user := UserService.GetByUsername(username)
if user == nil || user.Status != enums.StatusOk {
_ = s.createLoginCredentialLog(principal, 0, false, clientIP, userAgent, "user not found")
return nil, errorsx.InvalidAccountI18n("error.e0260")
}
if strs.IsBlank(user.Password) || bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) != nil {
_ = s.createLoginCredentialLog(principal, user.ID, false, clientIP, userAgent, "password mismatch")
return nil, errorsx.InvalidAccountI18n("error.e0260")
}
var ret *response.LoginResponse
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
var dbErr error
ret, dbErr = s.issueTokens(ctx, user, clientIP, userAgent, authCfg)
if dbErr != nil {
return dbErr
}
if dbErr = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{
"last_login_at": time.Now(),
"last_login_ip": clientIP,
"update_user_id": user.ID,
"update_user_name": user.Username,
"updated_at": time.Now(),
}); dbErr != nil {
return dbErr
}
return nil
}); err != nil {
return nil, err
}
_ = s.createLoginCredentialLog(principal, user.ID, true, clientIP, userAgent, "")
return ret, nil
}
func (s *authService) Logout(accessToken string) error {
accessToken = s.extractBearerToken(accessToken)
now := time.Now()
if accessToken != "" {
if session := LoginSessionService.FindOne(sqls.NewCnd().Eq("token", accessToken)); session != nil && session.RevokedAt == nil {
if err := LoginSessionService.Updates(session.ID, map[string]any{
"revoked_at": now,
"updated_at": now,
}); err != nil {
return err
}
}
}
return nil
}
func (s *authService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error) {
func (s *externalPrincipalService) Authenticate(ctx *gin.Context) (*dto.AuthPrincipal, error) {
if principal := s.GetAuthPrincipal(ctx); principal != nil {
return principal, nil
}
token := s.extractBearerToken(ctx.GetHeader("Authorization"))
if token == "" {
token = strings.TrimSpace(ctx.Query("accessToken"))
}
if token == "" {
if ctx == nil || ctx.Request == nil {
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
}
session, err := s.validateSessionToken(token)
if err != nil {
return nil, err
subject, err := SubjectService.Current(ctx.Request.Context())
if err != nil || subject == nil || subject.Category != identity.CategorySystem || !subject.Enabled {
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
}
user := UserService.Get(session.UserID)
if user == nil || user.Status != enums.StatusOk {
return nil, errorsx.UnauthorizedI18n("error.e0256")
principal := &dto.AuthPrincipal{
SubjectType: subject.Type,
UserID: subject.ID,
Username: subject.Username,
Nickname: subject.Name,
Avatar: subject.Avatar,
Status: enums.StatusOk,
Roles: []string{string(subject.Type)},
}
roles, permissions, err := s.loadUserAuthScope(sqls.DB(), user.ID)
if err != nil {
return nil, err
}
principal := s.setAuthPrincipal(ctx, user, roles, permissions)
now := time.Now()
_ = LoginSessionService.Updates(session.ID, map[string]any{
"last_seen_at": now,
"updated_at": now,
})
ctx.Set(authPrincipalContextKey, principal)
return principal, nil
}
func (s *authService) HasPermission(ctx *gin.Context, permissionCode string) bool {
principal := s.GetAuthPrincipal(ctx)
if principal == nil {
return false
}
return slices.Contains(principal.Permissions, permissionCode)
}
func (s *authService) CurrentProfile(ctx *gin.Context) (*response.LoginResponse, error) {
func (s *externalPrincipalService) RequirePermission(ctx *gin.Context, permission constants.Permission) (*dto.AuthPrincipal, error) {
principal, err := s.Authenticate(ctx)
if err != nil {
return nil, err
}
return &response.LoginResponse{
User: &response.AuthUserResponse{
ID: principal.UserID,
Username: principal.Username,
Nickname: principal.Nickname,
Avatar: principal.Avatar,
Status: principal.Status,
Roles: principal.Roles,
},
Permissions: principal.Permissions,
Roles: principal.Roles,
}, nil
if err := SubjectService.Authorize(ctx.Request.Context(), permission.Code); err != nil {
return nil, errorsx.ForbiddenI18n("error.auth.forbidden")
}
return principal, nil
}
func (s *authService) GetUserRoles(userID int64) ([]models.Role, error) {
return s.loadUserRoles(sqls.DB(), userID)
}
func (s *authService) GetUserPermissions(userID int64) ([]string, error) {
return s.loadUserPermissionCodes(sqls.DB(), userID)
}
func (s *authService) issueTokens(ctx *sqls.TxContext, user *models.User, clientIP, userAgent string, authCfg config.AuthConfig) (*response.LoginResponse, error) {
roles, permissions, err := s.loadUserAuthScope(ctx.Tx, user.ID)
if err != nil {
return nil, err
}
tokenTTL := s.resolveTokenTTL(authCfg)
accessToken, err := randomToken(constants.AuthTokenPrefix)
if err != nil {
return nil, err
}
now := time.Now()
if err := repositories.LoginSessionRepository.Create(ctx.Tx, &models.LoginSession{
UserID: user.ID,
Token: accessToken,
ClientType: constants.ClientTypeAdminWeb,
ClientIP: clientIP,
UserAgent: userAgent,
ExpiredAt: now.Add(tokenTTL),
LastSeenAt: &now,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: user.ID,
CreateUserName: user.Username,
UpdatedAt: now,
UpdateUserID: user.ID,
UpdateUserName: user.Username,
},
}); err != nil {
return nil, err
}
return &response.LoginResponse{
AccessToken: accessToken,
ExpiresAt: now.Add(tokenTTL).Format(time.DateTime),
User: &response.AuthUserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Avatar: user.Avatar,
Status: user.Status,
Roles: roles,
},
Permissions: permissions,
Roles: roles,
}, nil
}
func (s *authService) resolveTokenTTL(authCfg config.AuthConfig) time.Duration {
tokenTTL := 12 * time.Hour
if authCfg.TokenTTLHours > 0 {
tokenTTL = time.Duration(authCfg.TokenTTLHours) * time.Hour
}
return tokenTTL
}
func (s *authService) validateSessionToken(token string) (*models.LoginSession, error) {
if strings.TrimSpace(token) == "" {
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
}
session := LoginSessionService.FindOne(sqls.NewCnd().Eq("token", token))
if session == nil {
return nil, errorsx.InvalidTokenI18n("error.e0269")
}
if session.RevokedAt != nil {
return nil, errorsx.InvalidTokenI18n("error.e0267")
}
if time.Now().After(session.ExpiredAt) {
return nil, errorsx.InvalidTokenI18n("error.e0268")
}
return session, nil
}
func (s *authService) loadUserAuthScope(tx *gorm.DB, userID int64) ([]string, []string, error) {
roleCodes, err := s.loadUserRoleCodes(tx, userID)
if err != nil {
return nil, nil, err
}
permissionCodes, err := s.loadUserPermissionCodes(tx, userID)
if err != nil {
return nil, nil, err
}
return roleCodes, permissionCodes, nil
}
func (s *authService) loadUserRoleCodes(tx *gorm.DB, userID int64) ([]string, error) {
roles, err := s.loadUserRoles(tx, userID)
if err != nil {
return nil, err
}
roleCodes := make([]string, 0, len(roles))
for _, role := range roles {
roleCodes = append(roleCodes, role.Code)
}
return roleCodes, nil
}
func (s *authService) loadUserRoles(tx *gorm.DB, userID int64) ([]models.Role, error) {
roles := make([]models.Role, 0)
if err := tx.
Table("t_role AS r").
Select("r.*").
Joins("JOIN t_user_role AS ur ON ur.role_id = r.id").
Where("ur.user_id = ? AND r.status = ?", userID, enums.StatusOk).
Order("r.sort_no ASC, r.id ASC").
Scan(&roles).Error; err != nil {
return nil, err
}
return roles, nil
}
func (s *authService) loadUserPermissionCodes(tx *gorm.DB, userID int64) ([]string, error) {
permissionRows := make([]struct {
Code string
}, 0)
db := tx.Table("t_permission AS p").
Select("DISTINCT p.code").
Joins("JOIN t_role_permission AS rp ON rp.permission_id = p.id").
Joins("JOIN t_user_role AS ur ON ur.role_id = rp.role_id").
Where("ur.user_id = ?", userID).
Where("p.status = ?", enums.StatusOk)
if err := db.Scan(&permissionRows).Error; err != nil {
return nil, err
}
permissionCodes := make([]string, 0, len(permissionRows))
for _, permission := range permissionRows {
permissionCodes = append(permissionCodes, permission.Code)
}
overrideRows := make([]struct {
Code string
Effect int
}, 0)
if err := tx.
Table("t_user_permission AS up").
Select("p.code, up.effect").
Joins("JOIN t_permission AS p ON p.id = up.permission_id").
Where("up.user_id = ? AND (up.expired_at IS NULL OR up.expired_at > ?)", userID, time.Now()).
Scan(&overrideRows).Error; err != nil {
return nil, err
}
permissionSet := make(map[string]bool, len(permissionCodes))
for _, code := range permissionCodes {
permissionSet[code] = true
}
for _, override := range overrideRows {
if override.Effect < 0 {
delete(permissionSet, override.Code)
continue
}
permissionSet[override.Code] = true
}
permissionCodes = permissionCodes[:0]
for code := range permissionSet {
permissionCodes = append(permissionCodes, code)
}
sort.Strings(permissionCodes)
return permissionCodes, nil
}
func (s *authService) extractBearerToken(header string) string {
header = strings.TrimSpace(header)
if header == "" {
return ""
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return ""
}
return strings.TrimSpace(parts[1])
}
func (s *authService) createLoginCredentialLog(principal string, userID int64, success bool, clientIP, userAgent, reason string) error {
return LoginCredentialLogService.Create(&models.LoginCredentialLog{
Principal: principal,
UserID: userID,
Success: success,
ClientIP: clientIP,
UserAgent: userAgent,
Reason: reason,
CreatedAt: time.Now(),
})
}
func (s *authService) isCredentialLocked(principal string, authCfg config.AuthConfig) bool {
maxFailedAttempts := authCfg.MaxFailedAttempts
if maxFailedAttempts <= 0 {
func (s *externalPrincipalService) HasPermission(ctx *gin.Context, operation string) bool {
if _, err := s.Authenticate(ctx); err != nil {
return false
}
lockMinute := authCfg.CredentialLockMinute
if lockMinute <= 0 {
lockMinute = 15
}
since := time.Now().Add(-time.Duration(lockMinute) * time.Minute)
return LoginCredentialLogService.Count(sqls.NewCnd().
Eq("principal", normalizeLoginPrincipal(principal)).
Eq("success", false).
NotEq("reason", "credential locked").
Where("created_at >= ?", since)) >= int64(maxFailedAttempts)
}
func normalizeLoginPrincipal(principal string) string {
return strings.ToLower(strings.TrimSpace(principal))
}
func randomToken(prefix string) (string, error) {
buf := make([]byte, 24)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return prefix + hex.EncodeToString(buf), nil
return SubjectService.Authorize(ctx.Request.Context(), operation) == nil
}
+49 -469
View File
@@ -1,488 +1,68 @@
package services
import (
"context"
"errors"
"strings"
"net/http/httptest"
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/constants"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"github.com/mlogclub/simple/web"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"gorm.io/gorm/schema"
"github.com/gin-gonic/gin"
)
func TestExtractBearerToken(t *testing.T) {
svc := newAuthService()
if got := svc.extractBearerToken("Bearer token_123"); got != "token_123" {
t.Fatalf("expected bearer token to be extracted, got %q", got)
}
if got := svc.extractBearerToken("token_123"); got != "" {
t.Fatalf("expected raw token to be rejected by bearer extractor, got %q", got)
}
}
func TestAuthServiceLoginCreatesSingleAccessSession(t *testing.T) {
db := setupAuthServiceTestDB(t)
user := createAuthTestUser(t, db, "admin", "secret")
svc := newAuthService()
ret, err := svc.Login(request.LoginRequest{
Username: " admin ",
Password: "secret",
}, config.AuthConfig{TokenTTLHours: 2, MaxFailedAttempts: 5, CredentialLockMinute: 15}, "127.0.0.1", "go-test")
if err != nil {
t.Fatalf("login failed: %v", err)
}
if ret.AccessToken == "" || !strings.HasPrefix(ret.AccessToken, "ak_") {
t.Fatalf("expected ak_ access token, got %q", ret.AccessToken)
}
if ret.ExpiresAt == "" {
t.Fatal("expected expiresAt to be returned")
}
var sessions []models.LoginSession
if err := db.Find(&sessions).Error; err != nil {
t.Fatalf("query login sessions: %v", err)
}
if len(sessions) != 1 {
t.Fatalf("expected exactly one session, got %d", len(sessions))
}
if sessions[0].Token != ret.AccessToken {
t.Fatalf("expected session token %q, got %q", ret.AccessToken, sessions[0].Token)
}
if sessions[0].UserID != user.ID {
t.Fatalf("expected session user %d, got %d", user.ID, sessions[0].UserID)
}
if sessions[0].ClientType != "admin_web" {
t.Fatalf("expected admin_web client type, got %q", sessions[0].ClientType)
}
logs := findCredentialLogs(t, db)
if len(logs) != 1 {
t.Fatalf("expected one credential log, got %d", len(logs))
}
if !logs[0].Success || logs[0].Principal != "admin" || logs[0].UserID != user.ID {
t.Fatalf("unexpected success credential log: %+v", logs[0])
}
}
func TestAuthServiceLoginFailureWritesCredentialLogs(t *testing.T) {
db := setupAuthServiceTestDB(t)
createAuthTestUser(t, db, "admin", "secret")
svc := newAuthService()
authCfg := config.AuthConfig{TokenTTLHours: 2, MaxFailedAttempts: 5, CredentialLockMinute: 15}
if _, err := svc.Login(request.LoginRequest{Username: "missing", Password: "secret"}, authCfg, "127.0.0.1", "go-test"); !hasCode(err, errorsx.CodeAuthInvalidAccount) {
t.Fatalf("expected invalid account for missing user, got %v", err)
}
if _, err := svc.Login(request.LoginRequest{Username: "admin", Password: "wrong"}, authCfg, "127.0.0.1", "go-test"); !hasCode(err, errorsx.CodeAuthInvalidAccount) {
t.Fatalf("expected invalid account for password mismatch, got %v", err)
}
logs := findCredentialLogs(t, db)
if len(logs) != 2 {
t.Fatalf("expected two credential logs, got %d", len(logs))
}
if logs[0].Reason != "user not found" || logs[0].Success {
t.Fatalf("unexpected missing-user log: %+v", logs[0])
}
if logs[1].Reason != "password mismatch" || logs[1].Success {
t.Fatalf("unexpected password-mismatch log: %+v", logs[1])
}
}
func TestAuthServiceLoginCredentialLockout(t *testing.T) {
db := setupAuthServiceTestDB(t)
user := createAuthTestUser(t, db, "admin", "secret")
now := time.Now()
for i := 0; i < 2; i++ {
if err := db.Create(&models.LoginCredentialLog{
Principal: "admin",
UserID: user.ID,
Success: false,
Reason: "password mismatch",
CreatedAt: now.Add(-time.Duration(i+1) * time.Minute),
}).Error; err != nil {
t.Fatalf("seed credential log: %v", err)
func TestExternalAuthDelegatesOperationToHost(t *testing.T) {
SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
if !query.Current {
return nil, nil
}
}
if err := db.Create(&models.LoginCredentialLog{
Principal: "admin",
UserID: user.ID,
Success: false,
Reason: "password mismatch",
CreatedAt: now.Add(-30 * time.Minute),
}).Error; err != nil {
t.Fatalf("seed old credential log: %v", err)
}
svc := newAuthService()
_, err := svc.Login(request.LoginRequest{Username: "admin", Password: "secret"}, config.AuthConfig{
TokenTTLHours: 2,
MaxFailedAttempts: 2,
CredentialLockMinute: 15,
}, "127.0.0.1", "go-test")
if !hasCode(err, errorsx.CodeAuthCredentialLocked) {
t.Fatalf("expected credential locked error, got %v", err)
}
var lockedLog models.LoginCredentialLog
if err := db.Order("id DESC").Take(&lockedLog).Error; err != nil {
t.Fatalf("query latest credential log: %v", err)
}
if lockedLog.Reason != "credential locked" || lockedLog.Success {
t.Fatalf("unexpected locked credential log: %+v", lockedLog)
}
var sessionCount int64
if err := db.Model(&models.LoginSession{}).Count(&sessionCount).Error; err != nil {
t.Fatalf("count sessions: %v", err)
}
if sessionCount != 0 {
t.Fatalf("expected no session while credential locked, got %d", sessionCount)
}
}
func TestAuthServiceCredentialLockoutDoesNotExtendWhileLocked(t *testing.T) {
db := setupAuthServiceTestDB(t)
createAuthTestUser(t, db, "admin", "secret")
now := time.Now()
entries := []models.LoginCredentialLog{
{
Principal: "admin",
UserID: 1,
Success: false,
Reason: "password mismatch",
CreatedAt: now.Add(-2 * time.Minute),
},
{
Principal: "admin",
UserID: 0,
Success: false,
Reason: "credential locked",
CreatedAt: now.Add(-1 * time.Minute),
},
}
if err := db.Create(&entries).Error; err != nil {
t.Fatalf("seed credential logs: %v", err)
}
ret, err := newAuthService().Login(request.LoginRequest{Username: "admin", Password: "secret"}, config.AuthConfig{
TokenTTLHours: 2,
MaxFailedAttempts: 2,
CredentialLockMinute: 15,
}, "127.0.0.1", "go-test")
if err != nil {
t.Fatalf("expected locked attempt logs not to extend lockout, got %v", err)
}
if ret == nil || !strings.HasPrefix(ret.AccessToken, "ak_") {
t.Fatalf("expected login response with ak_ token, got %+v", ret)
}
}
func TestAuthServiceCredentialLockoutNormalizesPrincipalCase(t *testing.T) {
db := setupAuthServiceTestDB(t)
createAuthTestUser(t, db, "admin", "secret")
if err := db.Create(&models.LoginCredentialLog{
Principal: "admin",
UserID: 1,
Success: false,
Reason: "password mismatch",
CreatedAt: time.Now().Add(-time.Minute),
}).Error; err != nil {
t.Fatalf("seed credential log: %v", err)
}
_, err := newAuthService().Login(request.LoginRequest{Username: "ADMIN", Password: "secret"}, config.AuthConfig{
TokenTTLHours: 2,
MaxFailedAttempts: 1,
CredentialLockMinute: 15,
}, "127.0.0.1", "go-test")
if !hasCode(err, errorsx.CodeAuthCredentialLocked) {
t.Fatalf("expected normalized principal to be locked, got %v", err)
}
var lockedLog models.LoginCredentialLog
if err := db.Order("id DESC").Take(&lockedLog).Error; err != nil {
t.Fatalf("query latest credential log: %v", err)
}
if lockedLog.Principal != "admin" || lockedLog.Reason != "credential locked" {
t.Fatalf("unexpected locked log: %+v", lockedLog)
}
}
func TestAuthServiceCredentialLockoutDisabledWhenMaxAttemptsNonPositive(t *testing.T) {
db := setupAuthServiceTestDB(t)
user := createAuthTestUser(t, db, "admin", "secret")
now := time.Now()
for i := 0; i < 3; i++ {
if err := db.Create(&models.LoginCredentialLog{
Principal: "admin",
UserID: user.ID,
Success: false,
Reason: "credential locked",
CreatedAt: now.Add(-time.Duration(i+1) * time.Minute),
}).Error; err != nil {
t.Fatalf("seed credential log: %v", err)
}
}
ret, err := newAuthService().Login(request.LoginRequest{Username: "admin", Password: "secret"}, config.AuthConfig{
TokenTTLHours: 2,
MaxFailedAttempts: 0,
CredentialLockMinute: 15,
}, "127.0.0.1", "go-test")
if err != nil {
t.Fatalf("expected lockout to be disabled, got %v", err)
}
if ret == nil || ret.AccessToken == "" {
t.Fatalf("expected login response with access token, got %+v", ret)
}
}
func TestValidateSessionTokenStates(t *testing.T) {
db := setupAuthServiceTestDB(t)
svc := newAuthService()
now := time.Now()
if _, err := svc.validateSessionToken(" "); !hasCode(err, errorsx.CodeAuthUnauthorized) {
t.Fatalf("expected unauthorized for empty token, got %v", err)
}
if _, err := svc.validateSessionToken("missing"); !hasCode(err, errorsx.CodeAuthInvalidToken) {
t.Fatalf("expected invalid token for missing session, got %v", err)
}
revokedAt := now
if err := db.Create(&models.LoginSession{
UserID: 1,
Token: "ak_revoked",
ClientType: "admin_web",
ExpiredAt: now.Add(time.Hour),
RevokedAt: &revokedAt,
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
}).Error; err != nil {
t.Fatalf("seed revoked session: %v", err)
}
if _, err := svc.validateSessionToken("ak_revoked"); !hasCode(err, errorsx.CodeAuthInvalidToken) {
t.Fatalf("expected invalid token for revoked session, got %v", err)
}
if err := db.Create(&models.LoginSession{
UserID: 1,
Token: "ak_expired",
ClientType: "admin_web",
ExpiredAt: now.Add(-time.Hour),
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
}).Error; err != nil {
t.Fatalf("seed expired session: %v", err)
}
if _, err := svc.validateSessionToken("ak_expired"); !hasCode(err, errorsx.CodeAuthInvalidToken) {
t.Fatalf("expected invalid token for expired session, got %v", err)
}
if err := db.Create(&models.LoginSession{
UserID: 1,
Token: "ak_valid",
ClientType: "admin_web",
ExpiredAt: now.Add(time.Hour),
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
}).Error; err != nil {
t.Fatalf("seed valid session: %v", err)
}
session, err := svc.validateSessionToken("ak_valid")
if err != nil {
t.Fatalf("expected valid session token, got %v", err)
}
if session.Token != "ak_valid" {
t.Fatalf("expected valid session token ak_valid, got %q", session.Token)
}
}
func TestAuthServiceLogoutRevokesCurrentTokenOnly(t *testing.T) {
db := setupAuthServiceTestDB(t)
now := time.Now()
sessions := []models.LoginSession{
{
UserID: 1,
Token: "ak_current",
ClientType: "admin_web",
ExpiredAt: now.Add(time.Hour),
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
},
{
UserID: 1,
Token: "ak_other",
ClientType: "admin_web",
ExpiredAt: now.Add(time.Hour),
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
},
}
if err := db.Create(&sessions).Error; err != nil {
t.Fatalf("seed sessions: %v", err)
}
if err := newAuthService().Logout("Bearer ak_current"); err != nil {
t.Fatalf("logout failed: %v", err)
}
var current models.LoginSession
if err := db.Take(&current, "token = ?", "ak_current").Error; err != nil {
t.Fatalf("query current session: %v", err)
}
if current.RevokedAt == nil {
t.Fatal("expected current session to be revoked")
}
var other models.LoginSession
if err := db.Take(&other, "token = ?", "ak_other").Error; err != nil {
t.Fatalf("query other session: %v", err)
}
if other.RevokedAt != nil {
t.Fatal("expected other session to remain active")
}
}
func TestLoadUserPermissionCodesReturnsSortedDistinctCodes(t *testing.T) {
db := setupAuthServiceTestDB(t)
user := createAuthTestUser(t, db, "admin", "secret")
now := time.Now()
auditFields := models.AuditFields{CreatedAt: now, UpdatedAt: now}
permissions := []models.Permission{
{Name: "Zulu", Code: "z.read", SortNo: 1, Status: enums.StatusOk, AuditFields: auditFields},
{Name: "Alpha", Code: "a.read", SortNo: 2, Status: enums.StatusOk, AuditFields: auditFields},
}
if err := db.Create(&permissions).Error; err != nil {
t.Fatalf("seed permissions: %v", err)
}
roles := []models.Role{
{Name: "Role One", Code: "role_one", Status: enums.StatusOk, AuditFields: auditFields},
{Name: "Role Two", Code: "role_two", Status: enums.StatusOk, AuditFields: auditFields},
}
if err := db.Create(&roles).Error; err != nil {
t.Fatalf("seed roles: %v", err)
}
userRoles := []models.UserRole{
{UserID: user.ID, RoleID: roles[0].ID, AuditFields: auditFields},
{UserID: user.ID, RoleID: roles[1].ID, AuditFields: auditFields},
}
if err := db.Create(&userRoles).Error; err != nil {
t.Fatalf("seed user roles: %v", err)
}
rolePermissions := []models.RolePermission{
{RoleID: roles[0].ID, PermissionID: permissions[0].ID, AuditFields: auditFields},
{RoleID: roles[0].ID, PermissionID: permissions[1].ID, AuditFields: auditFields},
{RoleID: roles[1].ID, PermissionID: permissions[0].ID, AuditFields: auditFields},
}
if err := db.Create(&rolePermissions).Error; err != nil {
t.Fatalf("seed role permissions: %v", err)
}
codes, err := newAuthService().loadUserPermissionCodes(db, user.ID)
if err != nil {
t.Fatalf("load user permission codes: %v", err)
}
if got, want := strings.Join(codes, ","), "a.read,z.read"; got != want {
t.Fatalf("permission codes = %q, want %q", got, want)
}
}
func setupAuthServiceTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_",
SingularTable: true,
},
return []identity.Subject{{
Type: identity.SubjectAdmin,
Category: identity.CategorySystem,
ID: 9,
Username: "admin",
Name: "Admin",
Enabled: true,
}}, nil
})
var gotOperation string
SetAuthorize(func(_ context.Context, operation string) error {
gotOperation = operation
return nil
})
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = httptest.NewRequest("GET", "/api/dashboard/conversation/list", nil)
principal, err := AuthService.RequirePermission(ctx, constants.PermissionConversationView)
if err != nil {
t.Fatalf("open sqlite db: %v", err)
t.Fatalf("RequirePermission() error = %v", err)
}
if err := db.AutoMigrate(
&models.User{},
&models.UserIdentity{},
&models.Role{},
&models.Permission{},
&models.UserRole{},
&models.RolePermission{},
&models.UserPermission{},
&models.LoginSession{},
&models.LoginCredentialLog{},
); err != nil {
t.Fatalf("migrate auth tables: %v", err)
if principal.UserID != 9 || principal.SubjectType != identity.SubjectAdmin {
t.Fatalf("principal = %#v", principal)
}
if gotOperation != constants.PermissionConversationView.Code {
t.Fatalf("operation = %q, want %q", gotOperation, constants.PermissionConversationView.Code)
}
sqls.SetDB(db)
return db
}
func createAuthTestUser(t *testing.T, db *gorm.DB, username, password string) *models.User {
t.Helper()
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
t.Fatalf("hash password: %v", err)
}
now := time.Now()
user := &models.User{
Username: username,
Nickname: username,
Password: string(passwordHash),
Status: enums.StatusOk,
AuditFields: models.AuditFields{
CreatedAt: now,
UpdatedAt: now,
},
}
if err := db.Create(user).Error; err != nil {
t.Fatalf("create auth test user: %v", err)
}
return user
}
func TestExternalAuthRejectsHostDeniedOperation(t *testing.T) {
SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
if !query.Current {
return nil, nil
}
return []identity.Subject{{
Type: identity.SubjectAgent, Category: identity.CategorySystem, ID: 10, Enabled: true,
}}, nil
})
SetAuthorize(func(_ context.Context, _ string) error {
return errors.New("denied by host")
})
func findCredentialLogs(t *testing.T, db *gorm.DB) []models.LoginCredentialLog {
t.Helper()
var logs []models.LoginCredentialLog
if err := db.Order("id ASC").Find(&logs).Error; err != nil {
t.Fatalf("query credential logs: %v", err)
ctx, _ := gin.CreateTestContext(httptest.NewRecorder())
ctx.Request = httptest.NewRequest("POST", "/api/dashboard/ai-config/delete", nil)
if _, err := AuthService.RequirePermission(ctx, constants.PermissionAIConfigDelete); err == nil {
t.Fatal("RequirePermission() error = nil, want forbidden")
}
return logs
}
func hasCode(err error, code int) bool {
if err == nil {
return false
}
var codeErr *web.CodeError
if errors.As(err, &codeErr) {
return codeErr.Code == code
}
return false
}
+5 -5
View File
@@ -6,11 +6,11 @@ import (
"fmt"
"strings"
aitooling "agent-desk/internal/ai/tooling"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/toolx"
aitooling "code.tczkiot.com/wlw/ai-agent/internal/ai/tooling"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
)
// BusinessToolExecutor is the write boundary for built-in business tools.
@@ -1,16 +1,16 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"encoding/json"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+11 -110
View File
@@ -1,23 +1,21 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/httpx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"agent-desk/internal/wxwork"
"crypto/rand"
"encoding/base64"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/wxwork"
"encoding/json"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/gin-gonic/gin"
"github.com/mlogclub/simple/common/strs"
@@ -271,7 +269,6 @@ func (s *channelService) ParseWebChannelConfig(raw string) (*dto.WebChannelConfi
if cfg.Width == "" {
cfg.Width = "380px"
}
cfg.UserTokenSecret = strings.TrimSpace(cfg.UserTokenSecret)
return cfg, nil
}
@@ -296,91 +293,9 @@ func (s *channelService) ParseWechatMPChannelConfig(raw string) (*dto.WechatMPCh
if cfg.ThemeColor == "" {
cfg.ThemeColor = "#2563eb"
}
cfg.UserTokenSecret = strings.TrimSpace(cfg.UserTokenSecret)
return cfg, nil
}
func (s *channelService) GetUserTokenSecret(channel *models.Channel) string {
if channel == nil {
return ""
}
switch channel.ChannelType {
case enums.ChannelTypeWeb:
cfg, err := s.ParseWebChannelConfig(channel.ConfigJSON)
if err != nil {
return ""
}
return strings.TrimSpace(cfg.UserTokenSecret)
case enums.ChannelTypeWechatMP:
cfg, err := s.ParseWechatMPChannelConfig(channel.ConfigJSON)
if err != nil {
return ""
}
return strings.TrimSpace(cfg.UserTokenSecret)
default:
return ""
}
}
func (s *channelService) ResetUserTokenSecret(channelID int64, operator *dto.AuthPrincipal) (string, error) {
if operator == nil {
return "", errorsx.UnauthorizedI18n("error.auth.expired")
}
channel := s.Get(channelID)
if channel == nil || channel.Status == enums.StatusDeleted {
return "", errorsx.InvalidParamI18n("error.e0208")
}
if channel.ChannelType != enums.ChannelTypeWeb && channel.ChannelType != enums.ChannelTypeWechatMP {
return "", errorsx.InvalidParamI18n("error.e0196")
}
secret, err := generateUserTokenSecret()
if err != nil {
return "", err
}
var configJSON string
switch channel.ChannelType {
case enums.ChannelTypeWeb:
cfg, err := s.ParseWebChannelConfig(channel.ConfigJSON)
if err != nil {
return "", err
}
cfg.UserTokenSecret = secret
raw, err := json.Marshal(cfg)
if err != nil {
return "", err
}
configJSON = string(raw)
case enums.ChannelTypeWechatMP:
cfg, err := s.ParseWechatMPChannelConfig(channel.ConfigJSON)
if err != nil {
return "", err
}
cfg.UserTokenSecret = secret
raw, err := json.Marshal(cfg)
if err != nil {
return "", err
}
configJSON = string(raw)
}
if err := repositories.ChannelRepository.Updates(sqls.DB(), channelID, map[string]any{
"config_json": configJSON,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
}); err != nil {
return "", err
}
return secret, nil
}
func generateUserTokenSecret() (string, error) {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(buf), nil
}
func (s *channelService) GetEnabledWxWorkKFChannelByOpenKfID(openKfID string) *models.Channel {
openKfID = strings.TrimSpace(openKfID)
if openKfID == "" {
@@ -467,13 +382,6 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe
if err != nil {
return nil, errorsx.InvalidParamI18n("error.e0060")
}
if strings.TrimSpace(cfg.UserTokenSecret) == "" {
secret, err := generateUserTokenSecret()
if err != nil {
return nil, err
}
cfg.UserTokenSecret = secret
}
configBytes, err := json.Marshal(cfg)
if err != nil {
return nil, err
@@ -490,13 +398,6 @@ func (s *channelService) buildChannelModel(id int64, req request.CreateChannelRe
if err != nil {
return nil, errorsx.InvalidParamI18n("error.e0201")
}
if strings.TrimSpace(cfg.UserTokenSecret) == "" {
secret, err := generateUserTokenSecret()
if err != nil {
return nil, err
}
cfg.UserTokenSecret = secret
}
configBytes, err := json.Marshal(cfg)
if err != nil {
return nil, err
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"strings"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+8 -8
View File
@@ -1,17 +1,17 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,14 +1,14 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -10,13 +10,13 @@ import (
"sync/atomic"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -310,9 +310,7 @@ func (s *conversationDispatchService) filterEnabledDispatchProfiles(profiles []m
return nil, nil, "no_profile_with_capacity_config"
}
enabledUsers := UserService.Find(sqls.NewCnd().
In("id", userIDs).
Eq("status", enums.StatusOk))
enabledUsers := UserService.FindByIds(userIDs)
if len(enabledUsers) == 0 {
return nil, nil, "no_enabled_user"
}
@@ -1,14 +1,14 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/tracex"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/tracex"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,13 +1,15 @@
package services
import (
"context"
"encoding/json"
"strings"
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -141,7 +143,6 @@ func setupHumanDispatchRealtimeTestDB(t *testing.T) *gorm.DB {
}
})
if err := db.AutoMigrate(
&models.User{},
&models.Notification{},
&models.Customer{},
&models.CustomerIdentity{},
@@ -200,14 +201,16 @@ func createHumanDispatchRealtimeActiveSchedule(t *testing.T, db *gorm.DB, teamID
func createHumanDispatchRealtimeAgentProfile(t *testing.T, db *gorm.DB, userID, teamID int64) {
t.Helper()
if err := db.Create(&models.User{
ID: userID,
Username: "agent",
Nickname: "客服",
Status: enums.StatusOk,
}).Error; err != nil {
t.Fatalf("create user error = %v", err)
}
SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
if len(query.IDs) > 0 && query.IDs[0] != userID {
return nil, nil
}
return []identity.Subject{{
Type: identity.SubjectAgent, Category: identity.CategorySystem,
ID: userID, Username: "agent", Name: "客服", Enabled: true,
}}, nil
})
SetAuthorize(func(_ context.Context, _ string) error { return nil })
if err := db.Create(&models.AgentProfile{
UserID: userID,
TeamID: teamID,
@@ -6,13 +6,13 @@ import (
"strings"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -5,11 +5,11 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -200,7 +200,6 @@ func setupConversationHumanDispatchTestDB(t *testing.T) *gorm.DB {
}
})
if err := db.AutoMigrate(
&models.User{},
&models.Customer{},
&models.CustomerIdentity{},
&models.AIAgent{},
@@ -257,14 +256,7 @@ func createHumanDispatchActiveSchedule(t *testing.T, db *gorm.DB, teamID int64)
func createHumanDispatchAgentProfile(t *testing.T, db *gorm.DB, userID, teamID int64, serviceStatus enums.ServiceStatus, maxConcurrent int, autoAssign bool, status enums.Status) {
t.Helper()
if err := db.Create(&models.User{
ID: userID,
Username: "agent",
Nickname: "客服",
Status: enums.StatusOk,
}).Error; err != nil {
t.Fatalf("create user error = %v", err)
}
registerTestExternalSubject(userID, "agent", "客服", enums.StatusOk)
if err := db.Create(&models.AgentProfile{
UserID: userID,
TeamID: teamID,
@@ -5,8 +5,8 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -1,14 +1,14 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,16 +1,16 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
+12 -13
View File
@@ -5,18 +5,17 @@ import (
"encoding/json"
"log/slog"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"slices"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
@@ -738,7 +737,7 @@ func (s *conversationService) isAdmin(operator *dto.AuthPrincipal) bool {
if operator == nil {
return false
}
return slices.Contains(operator.Roles, constants.RoleCodeSuperAdmin) || slices.Contains(operator.Roles, constants.RoleCodeAdmin)
return operator.SubjectType == identity.SubjectAdmin
}
func (s *conversationService) buildEventPayload(payload map[string]any) string {
@@ -1,15 +1,15 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+1 -1
View File
@@ -1,7 +1,7 @@
package cronx
import (
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"fmt"
"log/slog"
@@ -5,15 +5,15 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+9 -9
View File
@@ -5,18 +5,18 @@ import (
"encoding/hex"
"log/slog"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -1,252 +0,0 @@
package services
import (
"errors"
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/mlogclub/simple/sqls"
)
const (
customerSessionTokenType = "customer_session"
customerSessionHeader = "X-Customer-Session-Token"
customerSessionExpHeader = "X-Customer-Session-Expires-At"
)
var CustomerSessionService = newCustomerSessionService()
func newCustomerSessionService() *customerSessionService {
return &customerSessionService{}
}
type customerSessionService struct {
}
type customerSessionClaims struct {
TokenType string `json:"typ"`
ChannelID int64 `json:"channelId"`
ChannelCode string `json:"channelCode"`
CustomerID int64 `json:"customerId"`
CustomerName string `json:"customerName"`
IdentityKey string `json:"identityKey"`
jwt.RegisteredClaims
}
type CustomerSessionVerifyResult struct {
ExternalUser *openidentity.ExternalUser
Token string
ExpiresAt time.Time
Refreshed bool
}
func (s *customerSessionService) Exchange(channel *models.Channel, externalUser openidentity.ExternalUser) (*response.CustomerSessionExchangeResponse, error) {
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
var customerID int64
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
id, err := CustomerService.EnsureExternalCustomer(ctx, externalUser)
if err != nil {
return err
}
customerID = id
return nil
}); err != nil {
return nil, err
}
customer := CustomerService.Get(customerID)
if customer == nil || customer.Status == enums.StatusDeleted {
return nil, errorsx.InvalidParamI18n("error.e0155")
}
token, expiresAt, err := s.Sign(channel, customer, externalUser)
if err != nil {
return nil, err
}
return &response.CustomerSessionExchangeResponse{
CustomerSessionToken: token,
ExpiresAt: expiresAt.Format(time.DateTime),
IdentityKey: s.identityKey(externalUser),
Customer: response.CustomerSessionCustomerResponse{
ID: customer.ID,
Name: strings.TrimSpace(customer.Name),
},
}, nil
}
func (s *customerSessionService) Sign(channel *models.Channel, customer *models.Customer, externalUser openidentity.ExternalUser) (string, time.Time, error) {
cfg := config.Current().CustomerSession
secret := strings.TrimSpace(cfg.Secret)
if secret == "" {
return "", time.Time{}, errorsx.BusinessErrorI18n(1, "error.customerSession.secretMissing")
}
if channel == nil || customer == nil {
return "", time.Time{}, errorsx.InvalidParamI18n("error.e0158")
}
now := time.Now()
expiresAt := now.Add(time.Duration(cfg.TTL()) * time.Minute)
claims := customerSessionClaims{
TokenType: customerSessionTokenType,
ChannelID: channel.ID,
ChannelCode: strings.TrimSpace(channel.ChannelID),
CustomerID: customer.ID,
CustomerName: strings.TrimSpace(customer.Name),
IdentityKey: s.identityKey(externalUser),
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(expiresAt),
},
}
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
if err != nil {
return "", time.Time{}, err
}
return token, expiresAt, nil
}
func (s *customerSessionService) VerifyRequest(ctx *gin.Context, channel *models.Channel) (*CustomerSessionVerifyResult, error) {
token := s.getCustomerSessionToken(ctx)
if token == "" {
return nil, errorsx.UnauthorizedI18n("error.e0157")
}
claims, err := s.verifyToken(token)
if err != nil {
return nil, err
}
if channel == nil || channel.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0209")
}
if claims.ChannelID != channel.ID || strings.TrimSpace(claims.ChannelCode) != strings.TrimSpace(channel.ChannelID) {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
customer := CustomerService.Get(claims.CustomerID)
if customer == nil || customer.Status == enums.StatusDeleted {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
external, err := s.externalUserFromClaims(claims, customer)
if err != nil {
return nil, err
}
result := &CustomerSessionVerifyResult{
ExternalUser: external,
Token: token,
ExpiresAt: claims.ExpiresAt.Time,
}
if s.shouldRefresh(claims.ExpiresAt.Time) {
newToken, expiresAt, err := s.Sign(channel, customer, *external)
if err != nil {
return nil, err
}
result.Token = newToken
result.ExpiresAt = expiresAt
result.Refreshed = true
}
return result, nil
}
func (s *customerSessionService) SetRefreshHeaders(ctx *gin.Context, result *CustomerSessionVerifyResult) {
if ctx == nil || result == nil || !result.Refreshed {
return
}
ctx.Header(customerSessionHeader, result.Token)
ctx.Header(customerSessionExpHeader, result.ExpiresAt.Format(time.DateTime))
}
func (s *customerSessionService) verifyToken(rawToken string) (*customerSessionClaims, error) {
cfg := config.Current().CustomerSession
secret := strings.TrimSpace(cfg.Secret)
if secret == "" {
return nil, errorsx.BusinessErrorI18n(1, "error.customerSession.secretMissing")
}
claims := &customerSessionClaims{}
token, err := jwt.ParseWithClaims(rawToken, claims, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unsupported signing method")
}
return []byte(secret), nil
}, jwt.WithExpirationRequired(), jwt.WithValidMethods([]string{
jwt.SigningMethodHS256.Alg(),
jwt.SigningMethodHS384.Alg(),
jwt.SigningMethodHS512.Alg(),
}))
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, errorsx.UnauthorizedI18n("error.e0160")
}
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
if token == nil || !token.Valid || claims.TokenType != customerSessionTokenType || claims.ExpiresAt == nil {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
if claims.ChannelID <= 0 || strings.TrimSpace(claims.ChannelCode) == "" || claims.CustomerID <= 0 || strings.TrimSpace(claims.IdentityKey) == "" {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
return claims, nil
}
func (s *customerSessionService) externalUserFromClaims(claims *customerSessionClaims, customer *models.Customer) (*openidentity.ExternalUser, error) {
identityKey := strings.TrimSpace(claims.IdentityKey)
parts := strings.SplitN(identityKey, ":", 2)
if len(parts) != 2 || strings.TrimSpace(parts[1]) == "" {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
var source enums.ExternalSource
switch parts[0] {
case "user":
source = enums.ExternalSourceUser
case "guest":
source = enums.ExternalSourceGuest
default:
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), source, parts[1])
if identity == nil || identity.CustomerID != claims.CustomerID {
return nil, errorsx.UnauthorizedI18n("error.e0161")
}
name := strings.TrimSpace(claims.CustomerName)
if customer != nil && strings.TrimSpace(customer.Name) != "" {
name = strings.TrimSpace(customer.Name)
}
return &openidentity.ExternalUser{
ExternalSource: source,
ExternalID: parts[1],
ExternalName: name,
}, nil
}
func (s *customerSessionService) shouldRefresh(expiresAt time.Time) bool {
threshold := config.Current().CustomerSession.RefreshThreshold()
return time.Until(expiresAt) <= time.Duration(threshold)*time.Minute
}
func (s *customerSessionService) identityKey(externalUser openidentity.ExternalUser) string {
switch externalUser.ExternalSource {
case enums.ExternalSourceUser:
return "user:" + strings.TrimSpace(externalUser.ExternalID)
default:
return "guest:" + strings.TrimSpace(externalUser.ExternalID)
}
}
func (s *customerSessionService) getCustomerSessionToken(ctx *gin.Context) string {
auth := strings.TrimSpace(ctx.GetHeader("Authorization"))
if len(auth) > 7 && strings.EqualFold(auth[:7], "Bearer ") {
if token := strings.TrimSpace(auth[7:]); token != "" {
return token
}
}
token, _ := params.Get(ctx, "customerSessionToken")
return strings.TrimSpace(token)
}
+5 -5
View File
@@ -1,11 +1,11 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"fmt"
"sort"
"strings"
+2 -2
View File
@@ -1,8 +1,8 @@
package services
import (
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"testing"
)
@@ -6,12 +6,12 @@ import (
"strings"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/mlogclub/simple/common/strs"
)
@@ -6,11 +6,11 @@ import (
"log/slog"
"strings"
"agent-desk/internal/events"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/mlogclub/simple/common/strs"
)
@@ -5,10 +5,10 @@ import (
"testing"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -1,12 +1,12 @@
package event_handlers
import (
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"context"
"fmt"
"strings"
@@ -1,11 +1,11 @@
package event_handlers
import (
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"context"
"fmt"
"strings"
@@ -0,0 +1,41 @@
package services_test
import (
"context"
"slices"
"sync"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/services"
)
var testExternalSubjects sync.Map
func registerTestExternalSubject(id int64, username, name string, status enums.Status) {
testExternalSubjects.Store(id, identity.Subject{
Type: identity.SubjectAgent,
Category: identity.CategorySystem,
ID: id,
Username: username,
Name: name,
Identifier: username,
Enabled: status == enums.StatusOk,
})
services.SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
results := make([]identity.Subject, 0)
testExternalSubjects.Range(func(_, value any) bool {
subject := value.(identity.Subject)
if len(query.IDs) > 0 && !slices.Contains(query.IDs, subject.ID) {
return true
}
if query.EnabledOnly && !subject.Enabled {
return true
}
results = append(results, subject)
return true
})
return results, nil
})
services.SetAuthorize(func(_ context.Context, _ string) error { return nil })
}
+4 -4
View File
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/services/storage"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/services/storage"
"encoding/json"
"strings"
)
+11 -11
View File
@@ -7,18 +7,18 @@ import (
"strings"
"time"
"agent-desk/internal/ai/rag"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/rag"
"code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
workflowregistry "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/registry"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -5,12 +5,12 @@ import (
"strings"
"testing"
"agent-desk/internal/ai/workflow/dsl"
workflowregistry "agent-desk/internal/ai/workflow/registry"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/dsl"
workflowregistry "code.tczkiot.com/wlw/ai-agent/internal/ai/workflow/registry"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -4,13 +4,13 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -3,11 +3,11 @@ package services
import (
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
@@ -7,16 +7,16 @@ import (
"log/slog"
"time"
"agent-desk/internal/ai/rag"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/rag"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
+10 -10
View File
@@ -11,16 +11,16 @@ import (
"strings"
"time"
"agent-desk/internal/ai/rag"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/rag"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
"github.com/xuri/excelize/v2"
@@ -5,11 +5,11 @@ import (
"encoding/json"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+9 -9
View File
@@ -6,16 +6,16 @@ import (
"log/slog"
"time"
"agent-desk/internal/ai/rag"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/ai/rag"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,9 +1,9 @@
package services
import (
"agent-desk/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,67 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var LoginCredentialLogService = newLoginCredentialLogService()
func newLoginCredentialLogService() *loginCredentialLogService {
return &loginCredentialLogService{}
}
type loginCredentialLogService struct {
}
func (s *loginCredentialLogService) Get(id int64) *models.LoginCredentialLog {
return repositories.LoginCredentialLogRepository.Get(sqls.DB(), id)
}
func (s *loginCredentialLogService) Take(where ...interface{}) *models.LoginCredentialLog {
return repositories.LoginCredentialLogRepository.Take(sqls.DB(), where...)
}
func (s *loginCredentialLogService) Find(cnd *sqls.Cnd) []models.LoginCredentialLog {
return repositories.LoginCredentialLogRepository.Find(sqls.DB(), cnd)
}
func (s *loginCredentialLogService) FindOne(cnd *sqls.Cnd) *models.LoginCredentialLog {
return repositories.LoginCredentialLogRepository.FindOne(sqls.DB(), cnd)
}
func (s *loginCredentialLogService) FindPageByParams(params *params.QueryParams) (list []models.LoginCredentialLog, paging *sqls.Paging) {
return repositories.LoginCredentialLogRepository.FindPageByParams(sqls.DB(), params)
}
func (s *loginCredentialLogService) FindPageByCnd(cnd *sqls.Cnd) (list []models.LoginCredentialLog, paging *sqls.Paging) {
return repositories.LoginCredentialLogRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *loginCredentialLogService) Count(cnd *sqls.Cnd) int64 {
return repositories.LoginCredentialLogRepository.Count(sqls.DB(), cnd)
}
func (s *loginCredentialLogService) Create(t *models.LoginCredentialLog) error {
return repositories.LoginCredentialLogRepository.Create(sqls.DB(), t)
}
func (s *loginCredentialLogService) Update(t *models.LoginCredentialLog) error {
return repositories.LoginCredentialLogRepository.Update(sqls.DB(), t)
}
func (s *loginCredentialLogService) Updates(id int64, columns map[string]interface{}) error {
return repositories.LoginCredentialLogRepository.Updates(sqls.DB(), id, columns)
}
func (s *loginCredentialLogService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.LoginCredentialLogRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *loginCredentialLogService) Delete(id int64) {
repositories.LoginCredentialLogRepository.Delete(sqls.DB(), id)
}
@@ -1,95 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"time"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var LoginSessionService = newLoginSessionService()
func newLoginSessionService() *loginSessionService {
return &loginSessionService{}
}
type loginSessionService struct {
}
func (s *loginSessionService) Get(id int64) *models.LoginSession {
return repositories.LoginSessionRepository.Get(sqls.DB(), id)
}
func (s *loginSessionService) Take(where ...interface{}) *models.LoginSession {
return repositories.LoginSessionRepository.Take(sqls.DB(), where...)
}
func (s *loginSessionService) Find(cnd *sqls.Cnd) []models.LoginSession {
return repositories.LoginSessionRepository.Find(sqls.DB(), cnd)
}
func (s *loginSessionService) FindOne(cnd *sqls.Cnd) *models.LoginSession {
return repositories.LoginSessionRepository.FindOne(sqls.DB(), cnd)
}
func (s *loginSessionService) FindPageByParams(params *params.QueryParams) (list []models.LoginSession, paging *sqls.Paging) {
return repositories.LoginSessionRepository.FindPageByParams(sqls.DB(), params)
}
func (s *loginSessionService) FindPageByCnd(cnd *sqls.Cnd) (list []models.LoginSession, paging *sqls.Paging) {
return repositories.LoginSessionRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *loginSessionService) Count(cnd *sqls.Cnd) int64 {
return repositories.LoginSessionRepository.Count(sqls.DB(), cnd)
}
func (s *loginSessionService) Create(t *models.LoginSession) error {
return repositories.LoginSessionRepository.Create(sqls.DB(), t)
}
func (s *loginSessionService) Update(t *models.LoginSession) error {
return repositories.LoginSessionRepository.Update(sqls.DB(), t)
}
func (s *loginSessionService) Updates(id int64, columns map[string]interface{}) error {
return repositories.LoginSessionRepository.Updates(sqls.DB(), id, columns)
}
func (s *loginSessionService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.LoginSessionRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *loginSessionService) Delete(id int64) {
repositories.LoginSessionRepository.Delete(sqls.DB(), id)
}
func (s *loginSessionService) Revoke(id int64, operatorID int64, operatorName string) error {
session := s.Get(id)
if session == nil {
return errorsx.InvalidParamI18n("error.e0116")
}
now := time.Now()
return s.Updates(id, map[string]any{
"revoked_at": now,
"update_user_id": operatorID,
"update_user_name": operatorName,
"updated_at": now,
})
}
func (s *loginSessionService) RevokeByUser(userID int64, operatorID int64, operatorName string) error {
now := time.Now()
return sqls.DB().Model(&models.LoginSession{}).
Where("user_id = ? AND revoked_at IS NULL", userID).
Updates(map[string]any{
"revoked_at": now,
"update_user_id": operatorID,
"update_user_name": operatorName,
"updated_at": now,
}).Error
}
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"strings"
"time"
"agent-desk/internal/ai/mcps"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
)
var MCPDebugService = newMCPDebugService()
+9 -9
View File
@@ -1,20 +1,20 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/pkg/tracex"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/tracex"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"log/slog"
"slices"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
+4 -4
View File
@@ -7,10 +7,10 @@ import (
"testing"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
+3 -3
View File
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+7 -7
View File
@@ -4,13 +4,13 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
@@ -3,9 +3,9 @@ package services_test
import (
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
-245
View File
@@ -1,245 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/oidcclient"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"context"
"crypto/sha256"
"encoding/hex"
"strconv"
"strings"
"time"
"unicode"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
)
var OIDCLoginService = newOIDCLoginService()
type oidcLoginService struct {
}
type oidcLoginProfile = oidcclient.Profile
func newOIDCLoginService() *oidcLoginService {
return &oidcLoginService{}
}
func (s *oidcLoginService) BuildOIDCLoginURL(next string) (string, error) {
return oidcclient.BuildAuthCodeURL(next)
}
func (s *oidcLoginService) LoginByOIDC(ctx context.Context, code, state string, authCfg config.AuthConfig, clientIP, userAgent string) (string, string, error) {
next, err := oidcclient.ParseState(state)
if err != nil {
return "", "", err
}
profile, err := oidcclient.ExchangeCode(ctx, code)
if err != nil {
return "", "", err
}
loginResp, err := s.loginWithOIDCProfile(profile, authCfg, clientIP, userAgent)
if err != nil {
return "", "", err
}
ticket, err := oidcclient.IssueLoginTicket(loginResp)
if err != nil {
return "", "", err
}
return ticket, next, nil
}
func (s *oidcLoginService) ExchangeOIDCLoginTicket(ticket string) (*response.LoginResponse, error) {
return oidcclient.ConsumeLoginTicket(ticket)
}
func (s *oidcLoginService) loginWithOIDCProfile(profile *oidcLoginProfile, authCfg config.AuthConfig, clientIP, userAgent string) (*response.LoginResponse, error) {
if profile == nil || strings.TrimSpace(profile.Subject) == "" {
return nil, errorsx.BusinessErrorI18n(2, "error.oidc.profileMissing")
}
var ret *response.LoginResponse
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
var (
identity = repositories.UserIdentityRepository.GetBy(ctx.Tx, enums.ThirdProviderOIDC, "", profile.Subject)
user *models.User
err error
)
if identity == nil {
user, identity, err = s.createOIDCUser(ctx, profile)
if err != nil {
return err
}
} else {
if identity.Status != enums.StatusOk {
return errorsx.BusinessErrorI18n(3, "error.oidc.bindingDisabled")
}
user = repositories.UserRepository.Get(ctx.Tx, identity.UserID)
if user == nil {
return errorsx.BusinessErrorI18n(4, "error.oidc.boundUserMissing")
}
}
if user.Status != enums.StatusOk {
return errorsx.UnauthorizedI18n("error.e0200")
}
if err = repositories.UserRepository.Updates(ctx.Tx, user.ID, map[string]any{
"nickname": s.resolveOIDCNickname(user.Nickname, profile),
"avatar": s.resolveOIDCAvatar(user.Avatar, profile),
"last_login_at": time.Now(),
"last_login_ip": clientIP,
"update_user_id": user.ID,
"update_user_name": user.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
if err = repositories.UserIdentityRepository.Updates(ctx.Tx, identity.ID, map[string]any{
"provider_name": enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
"raw_profile": profile.RawProfile,
"last_auth_at": time.Now(),
"status": enums.StatusOk,
"update_user_id": user.ID,
"update_user_name": user.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
ret, err = AuthService.issueTokens(ctx, user, clientIP, userAgent, authCfg)
return err
})
if err != nil {
return nil, err
}
return ret, nil
}
func (s *oidcLoginService) createOIDCUser(ctx *sqls.TxContext, profile *oidcLoginProfile) (*models.User, *models.UserIdentity, error) {
now := time.Now()
email := s.availableEmail(ctx.Tx, profile.Email)
username := s.availableUsername(ctx.Tx, profile)
user := &models.User{
Username: username,
Nickname: s.resolveOIDCNickname("", profile),
Avatar: s.resolveOIDCAvatar("", profile),
Email: email,
Password: "",
PasswordSalt: "",
Status: enums.StatusOk,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: 0,
CreateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
UpdatedAt: now,
UpdateUserID: 0,
UpdateUserName: enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
},
}
if err := repositories.UserRepository.Create(ctx.Tx, user); err != nil {
return nil, nil, err
}
identity := &models.UserIdentity{
UserID: user.ID,
Provider: enums.ThirdProviderOIDC,
ProviderUserID: strings.TrimSpace(profile.Subject),
ProviderCorpID: "",
ProviderName: enums.GetThirdProviderLabel(enums.ThirdProviderOIDC),
RawProfile: profile.RawProfile,
Status: enums.StatusOk,
LastAuthAt: &now,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: user.ID,
CreateUserName: user.Username,
UpdatedAt: now,
UpdateUserID: user.ID,
UpdateUserName: user.Username,
},
}
if err := repositories.UserIdentityRepository.Create(ctx.Tx, identity); err != nil {
return nil, nil, err
}
return user, identity, nil
}
func (s *oidcLoginService) availableEmail(tx *gorm.DB, email string) *string {
email = strings.TrimSpace(strings.ToLower(email))
if email == "" || repositories.UserRepository.GetByEmail(tx, email) != nil {
return nil
}
return &email
}
func (s *oidcLoginService) availableUsername(tx *gorm.DB, profile *oidcLoginProfile) string {
for _, candidate := range []string{
profile.PreferredUsername,
strings.Split(strings.TrimSpace(profile.Email), "@")[0],
} {
username := normalizeOIDCUsername(candidate)
if username != "" && repositories.UserRepository.GetByUsername(tx, username) == nil {
return username
}
}
base := "oidc_" + shortSubjectHash(profile.Subject)
if repositories.UserRepository.GetByUsername(tx, base) == nil {
return base
}
for i := 1; i < 100; i++ {
username := base + "_" + strconv.Itoa(i)
if repositories.UserRepository.GetByUsername(tx, username) == nil {
return username
}
}
return base + "_" + shortSubjectHash(time.Now().String())
}
func (s *oidcLoginService) resolveOIDCNickname(current string, profile *oidcLoginProfile) string {
if profile != nil {
for _, candidate := range []string{profile.Name, profile.PreferredUsername, profile.Email, profile.Subject} {
if candidate = strings.TrimSpace(candidate); candidate != "" {
return candidate
}
}
}
return strings.TrimSpace(current)
}
func (s *oidcLoginService) resolveOIDCAvatar(current string, profile *oidcLoginProfile) string {
if profile != nil {
if picture := strings.TrimSpace(profile.Picture); picture != "" {
return picture
}
}
return strings.TrimSpace(current)
}
func normalizeOIDCUsername(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
var b strings.Builder
for _, r := range value {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' || r == '.' {
b.WriteRune(r)
}
}
ret := strings.Trim(b.String(), "._-")
if len(ret) > 100 {
ret = ret[:100]
}
return ret
}
func shortSubjectHash(subject string) string {
sum := sha256.Sum256([]byte(strings.TrimSpace(subject)))
return hex.EncodeToString(sum[:])[:16]
}
@@ -1,96 +0,0 @@
package services
import (
"strings"
"testing"
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
)
func TestOIDCLoginAutoCreatesSystemUser(t *testing.T) {
db := setupAuthServiceTestDB(t)
svc := newOIDCLoginService()
ret, err := svc.loginWithOIDCProfile(&oidcLoginProfile{
Subject: "sub-123",
Email: "ada@example.com",
PreferredUsername: "ada",
Name: "Ada Lovelace",
Picture: "https://example.com/ada.png",
RawProfile: `{"sub":"sub-123"}`,
}, config.AuthConfig{TokenTTLHours: 2}, "127.0.0.1", "go-test")
if err != nil {
t.Fatalf("loginWithOIDCProfile() error = %v", err)
}
if ret == nil || !strings.HasPrefix(ret.AccessToken, "ak_") {
t.Fatalf("expected ak_ access token, got %+v", ret)
}
var user models.User
if err := db.Take(&user, "username = ?", "ada").Error; err != nil {
t.Fatalf("expected OIDC user to be created: %v", err)
}
if user.Nickname != "Ada Lovelace" || user.Avatar != "https://example.com/ada.png" {
t.Fatalf("unexpected created user profile: %+v", user)
}
if user.Email == nil || *user.Email != "ada@example.com" {
t.Fatalf("expected email to be stored, got %+v", user.Email)
}
if user.Password != "" {
t.Fatalf("expected OIDC-created user password to be empty, got %q", user.Password)
}
var identity models.UserIdentity
if err := db.Take(&identity, "provider = ? AND provider_user_id = ?", enums.ThirdProviderOIDC, "sub-123").Error; err != nil {
t.Fatalf("expected OIDC identity to be created: %v", err)
}
if identity.UserID != user.ID || identity.ProviderName != "OIDC" || identity.Status != enums.StatusOk {
t.Fatalf("unexpected OIDC identity: %+v", identity)
}
var sessions []models.LoginSession
if err := db.Find(&sessions).Error; err != nil {
t.Fatalf("query login sessions: %v", err)
}
if len(sessions) != 1 || sessions[0].UserID != user.ID || sessions[0].Token != ret.AccessToken {
t.Fatalf("unexpected login sessions: %+v", sessions)
}
}
func TestOIDCLoginReusesExistingIdentity(t *testing.T) {
db := setupAuthServiceTestDB(t)
user := createAuthTestUser(t, db, "existing", "secret")
if err := db.Create(&models.UserIdentity{
UserID: user.ID,
Provider: enums.ThirdProviderOIDC,
ProviderUserID: "sub-123",
ProviderName: "OIDC",
Status: enums.StatusOk,
}).Error; err != nil {
t.Fatalf("seed OIDC identity: %v", err)
}
ret, err := newOIDCLoginService().loginWithOIDCProfile(&oidcLoginProfile{
Subject: "sub-123",
PreferredUsername: "ignored",
Name: "Updated Name",
Picture: "https://example.com/updated.png",
RawProfile: `{"sub":"sub-123"}`,
}, config.AuthConfig{TokenTTLHours: 2}, "127.0.0.1", "go-test")
if err != nil {
t.Fatalf("loginWithOIDCProfile() error = %v", err)
}
if ret == nil || ret.User == nil || ret.User.ID != user.ID {
t.Fatalf("expected existing user login response, got %+v", ret)
}
var count int64
if err := db.Model(&models.User{}).Count(&count).Error; err != nil {
t.Fatalf("count users: %v", err)
}
if count != 1 {
t.Fatalf("expected existing identity to reuse user, got %d users", count)
}
}
-144
View File
@@ -1,144 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/repositories"
"fmt"
"time"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var PermissionService = newPermissionService()
func newPermissionService() *permissionService {
return &permissionService{}
}
type permissionService struct {
}
func (s *permissionService) Get(id int64) *models.Permission {
return repositories.PermissionRepository.Get(sqls.DB(), id)
}
func (s *permissionService) Take(where ...interface{}) *models.Permission {
return repositories.PermissionRepository.Take(sqls.DB(), where...)
}
func (s *permissionService) Find(cnd *sqls.Cnd) []models.Permission {
return repositories.PermissionRepository.Find(sqls.DB(), cnd)
}
func (s *permissionService) FindOne(cnd *sqls.Cnd) *models.Permission {
return repositories.PermissionRepository.FindOne(sqls.DB(), cnd)
}
func (s *permissionService) FindPageByParams(params *params.QueryParams) (list []models.Permission, paging *sqls.Paging) {
return repositories.PermissionRepository.FindPageByParams(sqls.DB(), params)
}
func (s *permissionService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Permission, paging *sqls.Paging) {
return repositories.PermissionRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *permissionService) Count(cnd *sqls.Cnd) int64 {
return repositories.PermissionRepository.Count(sqls.DB(), cnd)
}
func (s *permissionService) Create(t *models.Permission) error {
return repositories.PermissionRepository.Create(sqls.DB(), t)
}
func (s *permissionService) Update(t *models.Permission) error {
return repositories.PermissionRepository.Update(sqls.DB(), t)
}
func (s *permissionService) Updates(id int64, columns map[string]interface{}) error {
return repositories.PermissionRepository.Updates(sqls.DB(), id, columns)
}
func (s *permissionService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.PermissionRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *permissionService) Delete(id int64) {
repositories.PermissionRepository.Delete(sqls.DB(), id)
}
func (s *permissionService) SyncBuiltinPermissions() (*response.PermissionSyncResponse, error) {
result := &response.PermissionSyncResponse{}
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
permissions := make(map[string]*models.Permission, len(constants.Permissions))
now := time.Now()
for _, spec := range constants.Permissions {
permission := repositories.PermissionRepository.FindOne(ctx.Tx, sqls.NewCnd().Eq("code", spec.Code))
if permission == nil {
permission = &models.Permission{
Name: spec.Name, Code: spec.Code, Type: spec.Type, GroupName: spec.GroupName,
Method: spec.Method, APIPath: spec.APIPath, SortNo: spec.SortNo,
Status: enums.StatusOk, IsBuiltin: true,
AuditFields: systemPermissionAuditFields(now),
}
if err := repositories.PermissionRepository.Create(ctx.Tx, permission); err != nil {
return err
}
result.Created++
} else {
if err := repositories.PermissionRepository.Updates(ctx.Tx, permission.ID, map[string]any{
"name": spec.Name, "type": spec.Type, "group_name": spec.GroupName,
"method": spec.Method, "api_path": spec.APIPath, "sort_no": spec.SortNo,
"status": enums.StatusOk, "is_builtin": true,
"update_user_id": constants.SystemAuditUserID,
"update_user_name": constants.SystemAuditUserName, "updated_at": now,
}); err != nil {
return err
}
permission = repositories.PermissionRepository.Get(ctx.Tx, permission.ID)
result.Updated++
}
permissions[spec.Code] = permission
}
for roleCode, specs := range constants.RolePermissions {
role := repositories.RoleRepository.GetByCode(ctx.Tx, roleCode)
if role == nil {
return fmt.Errorf("builtin role not found: %s", roleCode)
}
for _, spec := range specs {
permission := permissions[spec.Code]
if permission == nil {
return fmt.Errorf("builtin permission not found: %s", spec.Code)
}
if repositories.RolePermissionRepository.FindOne(ctx.Tx, sqls.NewCnd().Eq("role_id", role.ID).Eq("permission_id", permission.ID)) != nil {
continue
}
if err := repositories.RolePermissionRepository.Create(ctx.Tx, &models.RolePermission{
RoleID: role.ID, PermissionID: permission.ID,
AuditFields: systemPermissionAuditFields(now),
}); err != nil {
return err
}
result.RolePermissionsAdded++
}
}
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func systemPermissionAuditFields(now time.Time) models.AuditFields {
return models.AuditFields{
CreatedAt: now, CreateUserID: constants.SystemAuditUserID, CreateUserName: constants.SystemAuditUserName,
UpdatedAt: now, UpdateUserID: constants.SystemAuditUserID, UpdateUserName: constants.SystemAuditUserName,
}
}
@@ -1,100 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/constants"
"agent-desk/internal/pkg/enums"
"testing"
"time"
"github.com/glebarez/sqlite"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func TestPermissionServiceSyncBuiltinPermissions(t *testing.T) {
db := setupPermissionServiceTestDB(t)
now := time.Now()
for _, spec := range constants.Roles {
if err := db.Create(&models.Role{
Name: spec.Name, Code: spec.Code, Status: enums.StatusOk, IsSystem: true, SortNo: spec.SortNo,
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
}).Error; err != nil {
t.Fatalf("create role %s: %v", spec.Code, err)
}
}
customPermission := &models.Permission{
Name: "Custom permission", Code: "custom.keep", Type: "api", GroupName: "custom",
Status: enums.StatusOk, AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
}
if err := db.Create(customPermission).Error; err != nil {
t.Fatalf("create custom permission: %v", err)
}
superAdmin := &models.Role{}
if err := db.First(superAdmin, "code = ?", constants.RoleCodeSuperAdmin).Error; err != nil {
t.Fatalf("find super admin role: %v", err)
}
if err := db.Create(&models.RolePermission{
RoleID: superAdmin.ID, PermissionID: customPermission.ID,
AuditFields: models.AuditFields{CreatedAt: now, UpdatedAt: now},
}).Error; err != nil {
t.Fatalf("create custom role permission: %v", err)
}
first, err := PermissionService.SyncBuiltinPermissions()
if err != nil {
t.Fatalf("first sync: %v", err)
}
if first.Created != len(constants.Permissions) || first.Updated != 0 {
t.Fatalf("unexpected first sync result: %+v", first)
}
wantRolePermissions := 0
for _, permissions := range constants.RolePermissions {
wantRolePermissions += len(permissions)
}
if first.RolePermissionsAdded != wantRolePermissions {
t.Fatalf("role permissions added=%d want=%d", first.RolePermissionsAdded, wantRolePermissions)
}
second, err := PermissionService.SyncBuiltinPermissions()
if err != nil {
t.Fatalf("second sync: %v", err)
}
if second.Created != 0 || second.Updated != len(constants.Permissions) || second.RolePermissionsAdded != 0 {
t.Fatalf("sync is not idempotent: %+v", second)
}
var permissionCount int64
if err := db.Model(&models.Permission{}).Count(&permissionCount).Error; err != nil {
t.Fatalf("count permissions: %v", err)
}
if permissionCount != int64(len(constants.Permissions)+1) {
t.Fatalf("permission count=%d want=%d", permissionCount, len(constants.Permissions)+1)
}
var customRolePermissionCount int64
if err := db.Model(&models.RolePermission{}).
Where("role_id = ? AND permission_id = ?", superAdmin.ID, customPermission.ID).
Count(&customRolePermissionCount).Error; err != nil {
t.Fatalf("count custom role permission: %v", err)
}
if customRolePermissionCount != 1 {
t.Fatalf("custom role permission was removed")
}
}
func setupPermissionServiceTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{
NamingStrategy: schema.NamingStrategy{TablePrefix: "t_", SingularTable: true},
})
if err != nil {
t.Fatalf("open sqlite db: %v", err)
}
if err := db.AutoMigrate(&models.Role{}, &models.Permission{}, &models.RolePermission{}); err != nil {
t.Fatalf("migrate permission tables: %v", err)
}
sqls.SetDB(db)
return db
}
+7 -7
View File
@@ -1,16 +1,16 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
@@ -1,67 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var RolePermissionService = newRolePermissionService()
func newRolePermissionService() *rolePermissionService {
return &rolePermissionService{}
}
type rolePermissionService struct {
}
func (s *rolePermissionService) Get(id int64) *models.RolePermission {
return repositories.RolePermissionRepository.Get(sqls.DB(), id)
}
func (s *rolePermissionService) Take(where ...interface{}) *models.RolePermission {
return repositories.RolePermissionRepository.Take(sqls.DB(), where...)
}
func (s *rolePermissionService) Find(cnd *sqls.Cnd) []models.RolePermission {
return repositories.RolePermissionRepository.Find(sqls.DB(), cnd)
}
func (s *rolePermissionService) FindOne(cnd *sqls.Cnd) *models.RolePermission {
return repositories.RolePermissionRepository.FindOne(sqls.DB(), cnd)
}
func (s *rolePermissionService) FindPageByParams(params *params.QueryParams) (list []models.RolePermission, paging *sqls.Paging) {
return repositories.RolePermissionRepository.FindPageByParams(sqls.DB(), params)
}
func (s *rolePermissionService) FindPageByCnd(cnd *sqls.Cnd) (list []models.RolePermission, paging *sqls.Paging) {
return repositories.RolePermissionRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *rolePermissionService) Count(cnd *sqls.Cnd) int64 {
return repositories.RolePermissionRepository.Count(sqls.DB(), cnd)
}
func (s *rolePermissionService) Create(t *models.RolePermission) error {
return repositories.RolePermissionRepository.Create(sqls.DB(), t)
}
func (s *rolePermissionService) Update(t *models.RolePermission) error {
return repositories.RolePermissionRepository.Update(sqls.DB(), t)
}
func (s *rolePermissionService) Updates(id int64, columns map[string]interface{}) error {
return repositories.RolePermissionRepository.Updates(sqls.DB(), id, columns)
}
func (s *rolePermissionService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.RolePermissionRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *rolePermissionService) Delete(id int64) {
repositories.RolePermissionRepository.Delete(sqls.DB(), id)
}
-200
View File
@@ -1,200 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"slices"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var RoleService = newRoleService()
func newRoleService() *roleService {
return &roleService{}
}
type roleService struct {
}
func (s *roleService) Get(id int64) *models.Role {
return repositories.RoleRepository.Get(sqls.DB(), id)
}
func (s *roleService) Take(where ...interface{}) *models.Role {
return repositories.RoleRepository.Take(sqls.DB(), where...)
}
func (s *roleService) Find(cnd *sqls.Cnd) []models.Role {
return repositories.RoleRepository.Find(sqls.DB(), cnd)
}
func (s *roleService) FindOne(cnd *sqls.Cnd) *models.Role {
return repositories.RoleRepository.FindOne(sqls.DB(), cnd)
}
func (s *roleService) FindPageByParams(params *params.QueryParams) (list []models.Role, paging *sqls.Paging) {
return repositories.RoleRepository.FindPageByParams(sqls.DB(), params)
}
func (s *roleService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Role, paging *sqls.Paging) {
return repositories.RoleRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *roleService) Count(cnd *sqls.Cnd) int64 {
return repositories.RoleRepository.Count(sqls.DB(), cnd)
}
func (s *roleService) Create(t *models.Role) error {
return repositories.RoleRepository.Create(sqls.DB(), t)
}
func (s *roleService) Update(t *models.Role) error {
return repositories.RoleRepository.Update(sqls.DB(), t)
}
func (s *roleService) Updates(id int64, columns map[string]interface{}) error {
return repositories.RoleRepository.Updates(sqls.DB(), id, columns)
}
func (s *roleService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.RoleRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *roleService) Delete(id int64) {
repositories.RoleRepository.Delete(sqls.DB(), id)
}
func (s *roleService) CreateRole(req request.CreateRoleRequest, operator *dto.AuthPrincipal) (*models.Role, error) {
name := strings.TrimSpace(req.Name)
code := strings.TrimSpace(req.Code)
if name == "" || code == "" {
return nil, errorsx.InvalidParamI18n("error.e0306")
}
if s.Take("code = ?", code) != nil {
return nil, errorsx.InvalidParamI18n("error.e0308")
}
role := &models.Role{
Name: name,
Code: code,
Status: enums.StatusOk,
IsSystem: false,
SortNo: s.NextSortNo(),
Remark: strings.TrimSpace(req.Remark),
AuditFields: utils.BuildAuditFields(operator),
}
if err := s.Create(role); err != nil {
return nil, err
}
return role, nil
}
func (s *roleService) UpdateRole(req request.UpdateRoleRequest, operator *dto.AuthPrincipal) error {
role := s.Get(req.ID)
if role == nil {
return errorsx.InvalidParamI18n("error.e0305")
}
now := time.Now()
return s.Updates(req.ID, map[string]any{
"name": strings.TrimSpace(req.Name),
"sort_no": req.SortNo,
"remark": strings.TrimSpace(req.Remark),
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": now,
})
}
func (s *roleService) NextSortNo() int {
if latest := s.FindOne(sqls.NewCnd().Desc("sort_no").Desc("id")); latest != nil {
return latest.SortNo + 1
}
return 0
}
func (s *roleService) UpdateSort(ids []int64) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
for i, id := range ids {
if err := repositories.RoleRepository.UpdateColumn(ctx.Tx, id, "sort_no", i); err != nil {
return err
}
}
return nil
})
}
func (s *roleService) DeleteRole(id int64) error {
role := s.Get(id)
if role == nil {
return errorsx.InvalidParamI18n("error.e0305")
}
if role.IsSystem {
return errorsx.ForbiddenI18n("error.e0293")
}
if UserRoleService.Take("role_id = ?", id) != nil {
return errorsx.ForbiddenI18n("error.e0307")
}
s.Delete(id)
return nil
}
func (s *roleService) UpdateStatus(id int64, status enums.Status, operator *dto.AuthPrincipal) error {
role := s.Get(id)
if role == nil {
return errorsx.InvalidParamI18n("error.e0305")
}
if !slices.Contains(enums.StatusValues, status) {
return errorsx.InvalidParamI18n("error.e0254")
}
if err := s.Updates(id, map[string]any{
"status": status,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
return nil
}
func (s *roleService) AssignPermissions(roleID int64, permissionIDs []int64, operator *dto.AuthPrincipal) error {
role := s.Get(roleID)
if role == nil {
return errorsx.InvalidParamI18n("error.e0305")
}
return s.replaceRolePermissions(roleID, permissionIDs, operator)
}
func (s *roleService) replaceRolePermissions(roleID int64, permissionIDs []int64, operator *dto.AuthPrincipal) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
if err := ctx.Tx.Where("role_id = ?", roleID).Delete(&models.RolePermission{}).Error; err != nil {
return err
}
for _, permissionID := range permissionIDs {
permission := PermissionService.Get(permissionID)
if permission == nil {
return errorsx.InvalidParamI18n("error.e0236")
}
relation := &models.RolePermission{
RoleID: roleID,
PermissionID: permissionID,
AuditFields: utils.BuildAuditFields(operator),
}
if err := ctx.Tx.Create(relation).Error; err != nil {
return err
}
}
return nil
})
}
@@ -5,16 +5,16 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/toolx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+3 -3
View File
@@ -5,9 +5,9 @@ import (
"fmt"
"strings"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
)
var SkillRuntimeService = newSkillRuntimeService()
+2 -2
View File
@@ -1,8 +1,8 @@
package storage
import (
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
type UploadInfo struct {
+2 -2
View File
@@ -1,8 +1,8 @@
package storage
import (
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"io"
"os"
"path/filepath"
+3 -3
View File
@@ -1,9 +1,9 @@
package storage
import (
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"fmt"
"io"
"net/url"
+3 -3
View File
@@ -1,9 +1,9 @@
package storage
import (
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"io"
)
+111
View File
@@ -0,0 +1,111 @@
package services
import (
"context"
"errors"
"fmt"
"log/slog"
"sync"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
)
var SubjectService = &subjectService{}
type subjectService struct {
mu sync.RWMutex
query identity.QuerySubjectsFunc
authorize identity.AuthorizeFunc
}
func SetQuerySubjects(query identity.QuerySubjectsFunc) {
SubjectService.mu.Lock()
defer SubjectService.mu.Unlock()
SubjectService.query = query
}
func SetAuthorize(authorize identity.AuthorizeFunc) {
SubjectService.mu.Lock()
defer SubjectService.mu.Unlock()
SubjectService.authorize = authorize
}
func (s *subjectService) Authorize(ctx context.Context, operation string) error {
s.mu.RLock()
fn := s.authorize
s.mu.RUnlock()
if fn == nil {
return errors.New("agent-desk: Authorize is not initialized")
}
return fn(ctx, operation)
}
func (s *subjectService) Query(ctx context.Context, query identity.Query) ([]identity.Subject, error) {
s.mu.RLock()
fn := s.query
s.mu.RUnlock()
if fn == nil {
return nil, errors.New("agent-desk: QuerySubjects is not initialized")
}
return fn(ctx, query)
}
func (s *subjectService) Current(ctx context.Context) (*identity.Subject, error) {
items, err := s.Query(ctx, identity.Query{Current: true, EnabledOnly: true})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, errors.New("agent-desk: current subject not found")
}
return &items[0], nil
}
func (s *subjectService) CurrentExternal(ctx context.Context) (*openidentity.ExternalUser, error) {
subject, err := s.Current(ctx)
if err != nil {
return nil, err
}
if subject.Category != identity.CategoryUser || !subject.Enabled {
return nil, errors.New("agent-desk: current subject is not a customer identity")
}
return &openidentity.ExternalUser{
ExternalSource: enums.ExternalSourceUser,
ExternalID: fmt.Sprintf("%s:%d", subject.Type, subject.ID),
ExternalName: subject.Name,
}, nil
}
func (s *subjectService) Get(id int64) *identity.Subject {
items, err := s.Query(context.Background(), identity.Query{
Types: []identity.SubjectType{identity.SubjectAgent},
IDs: []int64{id},
EnabledOnly: true,
})
if err != nil {
slog.Warn("query external subject failed", "id", id, "error", err)
return nil
}
if len(items) == 0 {
return nil
}
return &items[0]
}
func (s *subjectService) FindByIDs(ids []int64) []identity.Subject {
if len(ids) == 0 {
return nil
}
items, err := s.Query(context.Background(), identity.Query{
Types: []identity.SubjectType{identity.SubjectAgent},
IDs: ids,
EnabledOnly: true,
})
if err != nil {
slog.Warn("query external subjects failed", "error", err)
return nil
}
return items
}
+3 -3
View File
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+8 -8
View File
@@ -1,17 +1,17 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"strings"
"time"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+2 -2
View File
@@ -1,8 +1,8 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"fmt"
"strings"
"sync"
+3 -3
View File
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
+22 -22
View File
@@ -5,18 +5,18 @@ import (
"strings"
"time"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
@@ -33,7 +33,7 @@ type TicketDetailAggregate struct {
Tags []models.Tag
Customer *models.Customer
Progresses []models.TicketProgress
Users map[int64]*models.User
Users map[int64]*ExternalUser
}
type TicketSummaryAggregate struct {
@@ -50,7 +50,7 @@ type TicketListAggregate struct {
List []models.Ticket
Paging *sqls.Paging
TagsByTicketID map[int64][]models.Tag
Users map[int64]*models.User
Users map[int64]*ExternalUser
Customers map[int64]*models.Customer
}
@@ -66,7 +66,7 @@ func normalizeTicketStaleHours(staleHours int) int {
}
}
func buildTicketAssignmentProgressContent(fromUser *models.User, toUser *models.User, reason string) string {
func buildTicketAssignmentProgressContent(fromUser *ExternalUser, toUser *ExternalUser, reason string) string {
fromName := ticketAssignmentUserDisplayName(fromUser)
if fromName == "" {
fromName = "未分配"
@@ -82,7 +82,7 @@ func buildTicketAssignmentProgressContent(fromUser *models.User, toUser *models.
return content
}
func ticketAssignmentUserDisplayName(user *models.User) string {
func ticketAssignmentUserDisplayName(user *ExternalUser) string {
if user == nil {
return ""
}
@@ -445,7 +445,7 @@ func (s *ticketService) GetDetail(id int64) (*TicketDetailAggregate, error) {
Ticket: ticket,
Tags: s.GetTags(id),
Progresses: repositories.TicketProgressRepository.Find(sqls.DB(), sqls.NewCnd().Eq("ticket_id", id).Asc("id")),
Users: make(map[int64]*models.User),
Users: make(map[int64]*ExternalUser),
}
if ticket.CustomerID > 0 {
aggregate.Customer = CustomerService.Get(ticket.CustomerID)
@@ -467,7 +467,7 @@ func (s *ticketService) GetDetail(id int64) (*TicketDetailAggregate, error) {
addUserID(aggregate.Progresses[i].AuthorID)
}
if len(userIDs) > 0 {
users := repositories.UserRepository.FindByIds(sqls.DB(), userIDs)
users := UserService.FindByIds(userIDs)
for i := range users {
item := users[i]
aggregate.Users[item.ID] = &item
@@ -503,13 +503,13 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ
if err := s.validateRequiredAssignee(req.ToUserID); err != nil {
return nil, err
}
toUser := repositories.UserRepository.Get(tx, req.ToUserID)
toUser := UserService.Get(req.ToUserID)
if toUser == nil || toUser.Status != enums.StatusOk {
return nil, errorsx.InvalidParamI18n("error.e0334")
}
var fromUser *models.User
var fromUser *ExternalUser
if ticket.CurrentAssigneeID > 0 {
fromUser = repositories.UserRepository.Get(tx, ticket.CurrentAssigneeID)
fromUser = UserService.Get(ticket.CurrentAssigneeID)
}
now := time.Now()
if err := repositories.TicketRepository.Updates(tx, ticket.ID, map[string]any{
@@ -542,7 +542,7 @@ func (s *ticketService) buildTicketListAggregate(db *gorm.DB, list []models.Tick
List: list,
Paging: paging,
TagsByTicketID: make(map[int64][]models.Tag),
Users: make(map[int64]*models.User),
Users: make(map[int64]*ExternalUser),
Customers: make(map[int64]*models.Customer),
}
if len(list) == 0 {
@@ -575,7 +575,7 @@ func (s *ticketService) buildTicketListAggregate(db *gorm.DB, list []models.Tick
}
s.enrichTicketTags(db, aggregate, ticketIDs)
if len(userIDs) > 0 {
users := repositories.UserRepository.FindByIds(db, userIDs)
users := UserService.FindByIds(userIDs)
for i := range users {
item := users[i]
aggregate.Users[item.ID] = &item
+14 -29
View File
@@ -9,16 +9,16 @@ import (
"testing"
"time"
"agent-desk/internal/bootstrap"
"agent-desk/internal/events"
"agent-desk/internal/models"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/eventbus"
"agent-desk/internal/repositories"
"agent-desk/internal/services"
"code.tczkiot.com/wlw/ai-agent/internal/bootstrap"
"code.tczkiot.com/wlw/ai-agent/internal/events"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/eventbus"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/services"
"github.com/mlogclub/simple/sqls"
)
@@ -632,25 +632,10 @@ func createTestUser(t *testing.T, prefix string) int64 {
func createTestUserWithStatus(t *testing.T, prefix string, status enums.Status) int64 {
t.Helper()
now := time.Now()
username := fmt.Sprintf("%s_%d", prefix, now.UnixNano())
user := &models.User{
Username: username,
Nickname: prefix,
Status: status,
AuditFields: models.AuditFields{
CreatedAt: now,
CreateUserID: 1,
CreateUserName: "admin",
UpdatedAt: now,
UpdateUserID: 1,
UpdateUserName: "admin",
},
}
if err := repositories.UserRepository.Create(sqls.DB(), user); err != nil {
t.Fatalf("create user error = %v", err)
}
return user.ID
id := time.Now().UnixNano()
username := fmt.Sprintf("%s_%d", prefix, id)
registerTestExternalSubject(id, username, prefix, status)
return id
}
func createTestConversation(t *testing.T, customerID int64, prefix string) int64 {
+5 -5
View File
@@ -1,11 +1,11 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"time"
"github.com/mlogclub/simple/sqls"
+6 -6
View File
@@ -5,12 +5,12 @@ import (
"strings"
"time"
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"code.tczkiot.com/wlw/ai-agent/internal/models"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/request"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
"code.tczkiot.com/wlw/ai-agent/internal/repositories"
"github.com/mlogclub/simple/sqls"
)
+6 -6
View File
@@ -5,12 +5,12 @@ import (
"slices"
"strings"
"agent-desk/internal/ai/mcps"
"agent-desk/internal/pkg/config"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/i18nx"
"agent-desk/internal/pkg/toolx"
"code.tczkiot.com/wlw/ai-agent/internal/ai/mcps"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/toolx"
)
var ToolCatalogService = newToolCatalogService()
@@ -1,67 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var UserIdentityService = newUserIdentityService()
func newUserIdentityService() *userIdentityService {
return &userIdentityService{}
}
type userIdentityService struct {
}
func (s *userIdentityService) Get(id int64) *models.UserIdentity {
return repositories.UserIdentityRepository.Get(sqls.DB(), id)
}
func (s *userIdentityService) Take(where ...interface{}) *models.UserIdentity {
return repositories.UserIdentityRepository.Take(sqls.DB(), where...)
}
func (s *userIdentityService) Find(cnd *sqls.Cnd) []models.UserIdentity {
return repositories.UserIdentityRepository.Find(sqls.DB(), cnd)
}
func (s *userIdentityService) FindOne(cnd *sqls.Cnd) *models.UserIdentity {
return repositories.UserIdentityRepository.FindOne(sqls.DB(), cnd)
}
func (s *userIdentityService) FindPageByParams(params *params.QueryParams) (list []models.UserIdentity, paging *sqls.Paging) {
return repositories.UserIdentityRepository.FindPageByParams(sqls.DB(), params)
}
func (s *userIdentityService) FindPageByCnd(cnd *sqls.Cnd) (list []models.UserIdentity, paging *sqls.Paging) {
return repositories.UserIdentityRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *userIdentityService) Count(cnd *sqls.Cnd) int64 {
return repositories.UserIdentityRepository.Count(sqls.DB(), cnd)
}
func (s *userIdentityService) Create(t *models.UserIdentity) error {
return repositories.UserIdentityRepository.Create(sqls.DB(), t)
}
func (s *userIdentityService) Update(t *models.UserIdentity) error {
return repositories.UserIdentityRepository.Update(sqls.DB(), t)
}
func (s *userIdentityService) Updates(id int64, columns map[string]interface{}) error {
return repositories.UserIdentityRepository.Updates(sqls.DB(), id, columns)
}
func (s *userIdentityService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.UserIdentityRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *userIdentityService) Delete(id int64) {
repositories.UserIdentityRepository.Delete(sqls.DB(), id)
}
@@ -1,67 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var UserPermissionService = newUserPermissionService()
func newUserPermissionService() *userPermissionService {
return &userPermissionService{}
}
type userPermissionService struct {
}
func (s *userPermissionService) Get(id int64) *models.UserPermission {
return repositories.UserPermissionRepository.Get(sqls.DB(), id)
}
func (s *userPermissionService) Take(where ...interface{}) *models.UserPermission {
return repositories.UserPermissionRepository.Take(sqls.DB(), where...)
}
func (s *userPermissionService) Find(cnd *sqls.Cnd) []models.UserPermission {
return repositories.UserPermissionRepository.Find(sqls.DB(), cnd)
}
func (s *userPermissionService) FindOne(cnd *sqls.Cnd) *models.UserPermission {
return repositories.UserPermissionRepository.FindOne(sqls.DB(), cnd)
}
func (s *userPermissionService) FindPageByParams(params *params.QueryParams) (list []models.UserPermission, paging *sqls.Paging) {
return repositories.UserPermissionRepository.FindPageByParams(sqls.DB(), params)
}
func (s *userPermissionService) FindPageByCnd(cnd *sqls.Cnd) (list []models.UserPermission, paging *sqls.Paging) {
return repositories.UserPermissionRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *userPermissionService) Count(cnd *sqls.Cnd) int64 {
return repositories.UserPermissionRepository.Count(sqls.DB(), cnd)
}
func (s *userPermissionService) Create(t *models.UserPermission) error {
return repositories.UserPermissionRepository.Create(sqls.DB(), t)
}
func (s *userPermissionService) Update(t *models.UserPermission) error {
return repositories.UserPermissionRepository.Update(sqls.DB(), t)
}
func (s *userPermissionService) Updates(id int64, columns map[string]interface{}) error {
return repositories.UserPermissionRepository.Updates(sqls.DB(), id, columns)
}
func (s *userPermissionService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.UserPermissionRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *userPermissionService) Delete(id int64) {
repositories.UserPermissionRepository.Delete(sqls.DB(), id)
}
-67
View File
@@ -1,67 +0,0 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/repositories"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
)
var UserRoleService = newUserRoleService()
func newUserRoleService() *userRoleService {
return &userRoleService{}
}
type userRoleService struct {
}
func (s *userRoleService) Get(id int64) *models.UserRole {
return repositories.UserRoleRepository.Get(sqls.DB(), id)
}
func (s *userRoleService) Take(where ...interface{}) *models.UserRole {
return repositories.UserRoleRepository.Take(sqls.DB(), where...)
}
func (s *userRoleService) Find(cnd *sqls.Cnd) []models.UserRole {
return repositories.UserRoleRepository.Find(sqls.DB(), cnd)
}
func (s *userRoleService) FindOne(cnd *sqls.Cnd) *models.UserRole {
return repositories.UserRoleRepository.FindOne(sqls.DB(), cnd)
}
func (s *userRoleService) FindPageByParams(params *params.QueryParams) (list []models.UserRole, paging *sqls.Paging) {
return repositories.UserRoleRepository.FindPageByParams(sqls.DB(), params)
}
func (s *userRoleService) FindPageByCnd(cnd *sqls.Cnd) (list []models.UserRole, paging *sqls.Paging) {
return repositories.UserRoleRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *userRoleService) Count(cnd *sqls.Cnd) int64 {
return repositories.UserRoleRepository.Count(sqls.DB(), cnd)
}
func (s *userRoleService) Create(t *models.UserRole) error {
return repositories.UserRoleRepository.Create(sqls.DB(), t)
}
func (s *userRoleService) Update(t *models.UserRole) error {
return repositories.UserRoleRepository.Update(sqls.DB(), t)
}
func (s *userRoleService) Updates(id int64, columns map[string]interface{}) error {
return repositories.UserRoleRepository.Updates(sqls.DB(), id, columns)
}
func (s *userRoleService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.UserRoleRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *userRoleService) Delete(id int64) {
repositories.UserRoleRepository.Delete(sqls.DB(), id)
}
+57 -276
View File
@@ -1,299 +1,80 @@
package services
import (
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/request"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/utils"
"agent-desk/internal/repositories"
"slices"
"strings"
"time"
"context"
"agent-desk/internal/pkg/httpx/params"
"github.com/mlogclub/simple/sqls"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"code.tczkiot.com/wlw/ai-agent/identity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
)
var UserService = newUserService()
func newUserService() *userService {
return &userService{}
// ExternalUser is a non-persistent display adapter for a system identity owned
// by be-system.
type ExternalUser struct {
ID int64
SubjectType identity.SubjectType
Username string
Nickname string
Avatar string
Status enums.Status
}
type userService struct {
}
var UserService = &externalUserService{}
func (s *userService) Get(id int64) *models.User {
return repositories.UserRepository.Get(sqls.DB(), id)
}
type externalUserService struct{}
func (s *userService) Take(where ...interface{}) *models.User {
return repositories.UserRepository.Take(sqls.DB(), where...)
}
func (s *userService) Find(cnd *sqls.Cnd) []models.User {
return repositories.UserRepository.Find(sqls.DB(), cnd)
}
func (s *userService) FindOne(cnd *sqls.Cnd) *models.User {
return repositories.UserRepository.FindOne(sqls.DB(), cnd)
}
func (s *userService) FindPageByParams(params *params.QueryParams) (list []models.User, paging *sqls.Paging) {
return repositories.UserRepository.FindPageByParams(sqls.DB(), params)
}
func (s *userService) FindPageByCnd(cnd *sqls.Cnd) (list []models.User, paging *sqls.Paging) {
return repositories.UserRepository.FindPageByCnd(sqls.DB(), cnd)
}
func (s *userService) Count(cnd *sqls.Cnd) int64 {
return repositories.UserRepository.Count(sqls.DB(), cnd)
}
func (s *userService) FindByIds(ids []int64) []models.User {
return repositories.UserRepository.FindByIds(sqls.DB(), ids)
}
func (s *userService) Create(t *models.User) error {
return repositories.UserRepository.Create(sqls.DB(), t)
}
func (s *userService) Update(t *models.User) error {
return repositories.UserRepository.Update(sqls.DB(), t)
}
func (s *userService) Updates(id int64, columns map[string]interface{}) error {
return repositories.UserRepository.Updates(sqls.DB(), id, columns)
}
func (s *userService) UpdateColumn(id int64, name string, value interface{}) error {
return repositories.UserRepository.UpdateColumn(sqls.DB(), id, name, value)
}
func (s *userService) GetByUsername(username string) *models.User {
return repositories.UserRepository.GetByUsername(sqls.DB(), username)
}
func (s *userService) GetByMobile(mobile string) *models.User {
return repositories.UserRepository.GetByMobile(sqls.DB(), mobile)
}
func (s *userService) GetByEmail(email string) *models.User {
return repositories.UserRepository.GetByEmail(sqls.DB(), email)
}
func (s *userService) CreateUser(req request.CreateUserRequest, operator *dto.AuthPrincipal) (*models.User, string, error) {
username := strings.TrimSpace(req.Username)
if username == "" {
return nil, "", errorsx.InvalidParamI18n("error.e0257")
}
if s.GetByUsername(username) != nil {
return nil, "", errorsx.InvalidParamI18n("error.e0259")
func (s *externalUserService) Get(id int64) *ExternalUser {
items := s.FindByIds([]int64{id})
if len(items) == 0 {
return nil
}
return &items[0]
}
mobile := utils.NormalizeNullableString(req.Mobile)
email := utils.NormalizeNullableString(req.Email)
if mobile != nil && s.GetByMobile(*mobile) != nil {
return nil, "", errorsx.InvalidParamI18n("error.e0206")
func (s *externalUserService) FindByIds(ids []int64) []ExternalUser {
if len(ids) == 0 {
return nil
}
if email != nil && s.GetByEmail(*email) != nil {
return nil, "", errorsx.InvalidParamI18n("error.e0338")
}
plain, err := utils.GenerateRandomPassword(12)
if err != nil {
return nil, "", err
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
if err != nil {
return nil, "", err
}
user := &models.User{
Username: username,
Nickname: strings.TrimSpace(req.Nickname),
Password: string(passwordHash),
Avatar: strings.TrimSpace(req.Avatar),
Mobile: mobile,
Email: email,
Status: enums.StatusOk,
Remark: strings.TrimSpace(req.Remark),
PasswordSalt: "",
AuditFields: utils.BuildAuditFields(operator),
}
if user.Nickname == "" {
user.Nickname = username
}
err = sqls.WithTransaction(func(ctx *sqls.TxContext) error {
if err := repositories.UserRepository.Create(ctx.Tx, user); err != nil {
return err
}
return s.replaceUserRolesDB(ctx.Tx, user.ID, req.RoleIDs, operator)
subjects, err := SubjectService.Query(context.Background(), identity.Query{
Types: []identity.SubjectType{identity.SubjectAgent},
IDs: ids,
EnabledOnly: true,
})
if err != nil {
return nil, "", err
return nil
}
return user, plain, nil
users := make([]ExternalUser, 0, len(subjects))
for _, subject := range subjects {
status := enums.StatusDisabled
if subject.Enabled {
status = enums.StatusOk
}
users = append(users, ExternalUser{
ID: subject.ID,
SubjectType: subject.Type,
Username: subject.Username,
Nickname: subject.Name,
Avatar: subject.Avatar,
Status: status,
})
}
return users
}
func (s *userService) UpdateUser(req request.UpdateUserRequest, operator *dto.AuthPrincipal) error {
user := s.Get(req.ID)
if user == nil || user.DeletedAt != nil {
return errorsx.InvalidParamI18n("error.e0255")
}
mobile := utils.NormalizeNullableString(req.Mobile)
email := utils.NormalizeNullableString(req.Email)
if mobile != nil {
if existed := s.GetByMobile(*mobile); existed != nil && existed.ID != req.ID {
return errorsx.InvalidParamI18n("error.e0206")
}
}
if email != nil {
if existed := s.GetByEmail(*email); existed != nil && existed.ID != req.ID {
return errorsx.InvalidParamI18n("error.e0338")
}
}
return s.Updates(req.ID, map[string]any{
"nickname": strings.TrimSpace(req.Nickname),
"avatar": strings.TrimSpace(req.Avatar),
"mobile": mobile,
"email": email,
"remark": strings.TrimSpace(req.Remark),
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
func (s *externalUserService) Find(keyword string) []ExternalUser {
subjects, err := SubjectService.Query(context.Background(), identity.Query{
Types: []identity.SubjectType{identity.SubjectAgent},
Keyword: keyword,
EnabledOnly: true,
})
}
func (s *userService) DeleteUser(id int64, operator *dto.AuthPrincipal) error {
user := s.Get(id)
if user == nil {
return errorsx.InvalidParamI18n("error.e0255")
}
if err := s.Updates(id, map[string]any{
"status": enums.StatusDisabled,
"deleted_at": time.Now(),
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
return LoginSessionService.RevokeByUser(id, operator.UserID, operator.Username)
}
func (s *userService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error {
user := s.Get(id)
if user == nil {
return errorsx.InvalidParamI18n("error.e0255")
}
if !slices.Contains(enums.StatusValues, enums.Status(status)) {
return errorsx.InvalidParamI18n("error.e0254")
}
if err := s.Updates(id, map[string]any{
"status": status,
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": time.Now(),
}); err != nil {
return err
}
if status == int(enums.StatusDisabled) || status == int(enums.StatusDeleted) {
return LoginSessionService.RevokeByUser(id, operator.UserID, operator.Username)
}
return nil
}
func (s *userService) ResetPassword(userID int64, operator *dto.AuthPrincipal) (string, error) {
password, err := utils.GenerateRandomPassword(12)
if err != nil {
return "", err
return nil
}
if err = s.changePassword(userID, password, operator); err != nil {
return "", err
users := make([]ExternalUser, 0, len(subjects))
for _, subject := range subjects {
users = append(users, ExternalUser{
ID: subject.ID, SubjectType: subject.Type, Username: subject.Username,
Nickname: subject.Name, Avatar: subject.Avatar, Status: enums.StatusOk,
})
}
return password, nil
}
func (s *userService) ChangeOwnPassword(password string, operator *dto.AuthPrincipal) error {
if operator == nil || operator.UserID <= 0 {
return errorsx.UnauthorizedI18n("error.auth.expired")
}
return s.changePassword(operator.UserID, password, operator)
}
func (s *userService) AssignRoles(userID int64, roleIDs []int64, operator *dto.AuthPrincipal) error {
user := s.Get(userID)
if user == nil || user.DeletedAt != nil {
return errorsx.InvalidParamI18n("error.e0255")
}
if err := s.replaceUserRoles(userID, roleIDs, operator); err != nil {
return err
}
return LoginSessionService.RevokeByUser(userID, operator.UserID, operator.Username)
}
func (s *userService) replaceUserRoles(userID int64, roleIDs []int64, operator *dto.AuthPrincipal) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
return s.replaceUserRolesDB(ctx.Tx, userID, roleIDs, operator)
})
}
func (s *userService) replaceUserRolesDB(db *gorm.DB, userID int64, roleIDs []int64, operator *dto.AuthPrincipal) error {
if err := db.Where("user_id = ?", userID).Delete(&models.UserRole{}).Error; err != nil {
return err
}
for _, roleID := range roleIDs {
role := RoleService.Get(roleID)
if role == nil {
return errorsx.InvalidParamI18n("error.e0305")
}
if role.Status != enums.StatusOk {
return errorsx.InvalidParamI18n("error.e0291")
}
relation := &models.UserRole{
UserID: userID,
RoleID: roleID,
AuditFields: utils.BuildAuditFields(operator),
}
if err := db.Create(relation).Error; err != nil {
return err
}
}
return nil
}
func (s *userService) changePassword(userID int64, password string, operator *dto.AuthPrincipal) error {
user := s.Get(userID)
if user == nil || user.DeletedAt != nil {
return errorsx.InvalidParamI18n("error.e0255")
}
if strings.TrimSpace(password) == "" {
return errorsx.InvalidParamI18n("error.e0220")
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
now := time.Now()
if err = s.Updates(userID, map[string]any{
"password": string(passwordHash),
"update_user_id": operator.UserID,
"update_user_name": operator.Username,
"updated_at": now,
}); err != nil {
return err
}
return LoginSessionService.RevokeByUser(userID, operator.UserID, operator.Username)
return users
}
+4 -23
View File
@@ -1,10 +1,10 @@
package services
import (
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/dto/response"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/openidentity"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
"code.tczkiot.com/wlw/ai-agent/internal/pkg/openidentity"
"encoding/json"
"sync"
"sync/atomic"
@@ -238,25 +238,6 @@ func (e RealtimeNotificationCreatedEvent) EventPayload() RealtimeEventPayload {
return e.Payload
}
type RealtimeCustomerSessionRefreshPayload struct {
CustomerSessionToken string `json:"customerSessionToken"`
ExpiresAt string `json:"expiresAt"`
}
func (RealtimeCustomerSessionRefreshPayload) realtimeEventPayload() {}
type RealtimeCustomerSessionRefreshEvent struct {
Payload RealtimeCustomerSessionRefreshPayload
}
func (e RealtimeCustomerSessionRefreshEvent) EventType() string {
return enums.IMRealtimeEventCustomerSessionRefresh
}
func (e RealtimeCustomerSessionRefreshEvent) EventPayload() RealtimeEventPayload {
return e.Payload
}
type realtimeClientMessage struct {
Type string `json:"type"`
Topics []string `json:"topics,omitempty"`

Some files were not shown because too many files have changed in this diff Show More