优化结构
This commit is contained in:
@@ -37,7 +37,11 @@ type LoginLog struct {
|
||||
ErrorMessage, Agent string
|
||||
UserID uint
|
||||
User *User
|
||||
FilterByStatus bool
|
||||
}
|
||||
|
||||
type LoginLogFilter struct {
|
||||
Username string
|
||||
Status *bool
|
||||
}
|
||||
|
||||
type DataAccessLog struct {
|
||||
@@ -101,7 +105,7 @@ type AuditQueryRepo interface {
|
||||
ListOperations(context.Context, int, int, *OperationRecord) ([]*OperationRecord, int64, error)
|
||||
FindOperation(context.Context, uint) (*OperationRecord, error)
|
||||
DeleteOperations(context.Context, []int) error
|
||||
ListLogins(context.Context, int, int, *LoginLog) ([]*LoginLog, int64, error)
|
||||
ListLogins(context.Context, int, int, *LoginLogFilter) ([]*LoginLog, int64, error)
|
||||
FindLogin(context.Context, uint) (*LoginLog, error)
|
||||
DeleteLogins(context.Context, []int) error
|
||||
ListDataAccess(context.Context, int, int, *DataAccessLog) ([]*DataAccessLog, int64, error)
|
||||
|
||||
@@ -8,6 +8,7 @@ var (
|
||||
ErrUserNotFound = errors.NotFound("USER_NOT_FOUND", "user not found")
|
||||
// ErrInvalidCredentials is returned for any failed login, regardless of
|
||||
// whether the user exists, to avoid leaking which accounts are registered.
|
||||
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
|
||||
ErrUserDisabled = errors.Forbidden("USER_DISABLED", "user disabled")
|
||||
ErrInvalidCredentials = errors.Unauthorized("INVALID_CREDENTIALS", "invalid credentials")
|
||||
ErrAuthoritiesRequired = errors.BadRequest("AUTHORITIES_REQUIRED", "at least one authority is required")
|
||||
ErrUserDisabled = errors.Forbidden("USER_DISABLED", "user disabled")
|
||||
)
|
||||
|
||||
@@ -2,9 +2,21 @@ package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
exportIdentifierPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$`)
|
||||
exportParameterPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
exportColumnPattern = regexp.MustCompile(`(?i)^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?(\s+AS\s+[A-Za-z_][A-Za-z0-9_]*)?$`)
|
||||
exportJoinOnPattern = regexp.MustCompile(`(?i)^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\s*=\s*[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*(\s+AND\s+[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*\s*=\s*[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*)*$`)
|
||||
)
|
||||
|
||||
type ExportCondition struct {
|
||||
ID uint
|
||||
CreatedAt, UpdatedAt time.Time
|
||||
@@ -43,3 +55,81 @@ type ExportRepo interface {
|
||||
type ExportUsecase struct{ ExportRepo }
|
||||
|
||||
func NewExportUsecase(repo ExportRepo) *ExportUsecase { return &ExportUsecase{ExportRepo: repo} }
|
||||
|
||||
func ValidateExportTemplate(value *ExportTemplate) error {
|
||||
if value == nil {
|
||||
return errors.New("导出模板为空")
|
||||
}
|
||||
if strings.TrimSpace(value.Name) == "" || strings.TrimSpace(value.TemplateID) == "" {
|
||||
return errors.New("导出模板名称和模板 ID 不能为空")
|
||||
}
|
||||
if !exportIdentifierPattern.MatchString(strings.TrimSpace(value.TableName)) {
|
||||
return errors.New("导出模板表名不合法")
|
||||
}
|
||||
if strings.TrimSpace(value.DBName) != "" && !exportIdentifierPattern.MatchString(strings.TrimSpace(value.DBName)) {
|
||||
return errors.New("导出模板数据库别名不合法")
|
||||
}
|
||||
if strings.TrimSpace(value.SQL) != "" || strings.TrimSpace(value.ImportSQL) != "" {
|
||||
return errors.New("导出模板不允许自定义 SQL")
|
||||
}
|
||||
var columns map[string]string
|
||||
if err := json.Unmarshal([]byte(value.TemplateInfo), &columns); err != nil {
|
||||
return fmt.Errorf("导出模板列定义无效: %w", err)
|
||||
}
|
||||
if len(columns) == 0 {
|
||||
return errors.New("导出模板列为空")
|
||||
}
|
||||
for column := range columns {
|
||||
if !exportColumnPattern.MatchString(strings.TrimSpace(column)) {
|
||||
return fmt.Errorf("导出模板列不合法: %s", column)
|
||||
}
|
||||
}
|
||||
for _, join := range value.Joins {
|
||||
kind := strings.ToUpper(strings.Join(strings.Fields(join.Join), " "))
|
||||
switch kind {
|
||||
case "JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN":
|
||||
default:
|
||||
return fmt.Errorf("导出模板 Join 类型不合法: %s", join.Join)
|
||||
}
|
||||
if !exportIdentifierPattern.MatchString(strings.TrimSpace(join.Table)) || !exportJoinOnPattern.MatchString(strings.TrimSpace(join.On)) {
|
||||
return errors.New("导出模板 Join 表或条件不合法")
|
||||
}
|
||||
}
|
||||
for _, condition := range value.Conditions {
|
||||
if !exportParameterPattern.MatchString(strings.TrimSpace(condition.From)) || !exportIdentifierPattern.MatchString(strings.TrimSpace(condition.Column)) {
|
||||
return errors.New("导出模板筛选字段不合法")
|
||||
}
|
||||
switch strings.ToUpper(strings.Join(strings.Fields(condition.Operator), " ")) {
|
||||
case "=", "!=", "<>", ">", ">=", "<", "<=", "LIKE", "IN", "NOT IN", "BETWEEN":
|
||||
default:
|
||||
return fmt.Errorf("导出模板筛选运算符不合法: %s", condition.Operator)
|
||||
}
|
||||
}
|
||||
if value.Limit != nil && (*value.Limit < 1 || *value.Limit > 100000) {
|
||||
return errors.New("导出模板行数限制必须在 1 到 100000 之间")
|
||||
}
|
||||
if order := strings.TrimSpace(value.Order); order != "" {
|
||||
parts := strings.Fields(order)
|
||||
if len(parts) > 2 || !exportIdentifierPattern.MatchString(parts[0]) {
|
||||
return errors.New("导出模板排序不合法")
|
||||
}
|
||||
if len(parts) == 2 && !strings.EqualFold(parts[1], "asc") && !strings.EqualFold(parts[1], "desc") {
|
||||
return errors.New("导出模板排序方向不合法")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *ExportUsecase) CreateExportTemplate(ctx context.Context, value *ExportTemplate) error {
|
||||
if err := ValidateExportTemplate(value); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.ExportRepo.CreateExportTemplate(ctx, value)
|
||||
}
|
||||
|
||||
func (uc *ExportUsecase) UpdateExportTemplate(ctx context.Context, value *ExportTemplate) error {
|
||||
if err := ValidateExportTemplate(value); err != nil {
|
||||
return err
|
||||
}
|
||||
return uc.ExportRepo.UpdateExportTemplate(ctx, value)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func validExportTemplateForTest() *ExportTemplate {
|
||||
return &ExportTemplate{Name: "users", TemplateID: "users", TableName: "sys_users", TemplateInfo: `{"id":"ID","username":"用户名"}`}
|
||||
}
|
||||
|
||||
func TestValidateExportTemplateRejectsCustomSQL(t *testing.T) {
|
||||
value := validExportTemplateForTest()
|
||||
value.SQL = "SELECT * FROM sys_users"
|
||||
if err := ValidateExportTemplate(value); err == nil || !strings.Contains(err.Error(), "不允许自定义 SQL") {
|
||||
t.Fatalf("ValidateExportTemplate() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateExportTemplateRejectsUnsafeJoinAndCondition(t *testing.T) {
|
||||
value := validExportTemplateForTest()
|
||||
value.Joins = []ExportJoin{{Join: "LEFT JOIN", Table: "sys_roles", On: "1=1 OR 1=1"}}
|
||||
if err := ValidateExportTemplate(value); err == nil {
|
||||
t.Fatal("ValidateExportTemplate accepted unsafe join")
|
||||
}
|
||||
}
|
||||
@@ -99,13 +99,6 @@ func (uc *SecurityUsecase) Current(ctx context.Context) (*SecurityConfig, error)
|
||||
uc.mu.RUnlock()
|
||||
value, err := uc.repo.SecurityConfig(ctx)
|
||||
if err != nil {
|
||||
// Return the default value together with the database-not-ready
|
||||
// error. Callers such as login/runtime policy consumers intentionally
|
||||
// ignore the error and continue with that default, while the HTTP
|
||||
// settings endpoint still reports the failure.
|
||||
if value != nil {
|
||||
return value, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
uc.mu.Lock()
|
||||
|
||||
@@ -155,7 +155,7 @@ func (uc *UserUsecase) Authorities(ctx context.Context) ([]*Authority, error) {
|
||||
}
|
||||
func (uc *UserUsecase) SetUserAuthorities(ctx context.Context, id uint, authorityIDs []uint) error {
|
||||
if len(authorityIDs) == 0 {
|
||||
return ErrInvalidCredentials
|
||||
return ErrAuthoritiesRequired
|
||||
}
|
||||
return uc.repo.SetUserAuthorities(ctx, id, authorityIDs)
|
||||
}
|
||||
|
||||
@@ -52,3 +52,10 @@ func TestLoginRejectsMissingUserWithoutPanic(t *testing.T) {
|
||||
t.Fatalf("Login() = %#v, %v; want nil, ErrInvalidCredentials", user, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUserAuthoritiesRejectsEmptyListAsBadRequest(t *testing.T) {
|
||||
err := NewUserUsecase(&loginUserRepo{}).SetUserAuthorities(context.Background(), 1, nil)
|
||||
if !errors.Is(err, ErrAuthoritiesRequired) || errors.Is(err, ErrInvalidCredentials) {
|
||||
t.Fatalf("SetUserAuthorities() error = %v, want ErrAuthoritiesRequired", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user