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:
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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,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,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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(¤t, "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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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"
|
||||
)
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user