diff --git a/api-service/cmd/server/main.go b/api-service/cmd/server/main.go index 6ac4aed..26921d7 100644 --- a/api-service/cmd/server/main.go +++ b/api-service/cmd/server/main.go @@ -21,9 +21,11 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "os" "os/signal" "path/filepath" + "regexp" "strings" "syscall" "time" @@ -229,6 +231,46 @@ func initServices(cfg *config.Config, authConfig *config.AuthConfig, zapLogger l }, nil } +// registerCustomValidators registers all custom validators for request validation +func registerCustomValidators(validatorInstance *validator.Validate) error { + // Register custom validator for RFC3339 datetime format + err := validatorInstance.RegisterValidation("rfc3339", func(fl validator.FieldLevel) bool { + dateStr := fl.Field().String() + if dateStr == "" { + return true // omitempty will handle empty strings + } + + // Handle URL encoding: + // 1. URL decode to handle %3A (colon) and other encoded characters + // 2. Replace spaces with '+' since URL query parameters convert '+' to space + decodedStr, err := url.QueryUnescape(dateStr) + if err != nil { + decodedStr = dateStr + } + + // In URL query parameters, '+' becomes space, so we need to convert back + // Check if the string looks like a datetime with spaces instead of '+' + if strings.Contains(decodedStr, " ") && strings.Count(decodedStr, " ") == 1 { + // Replace the space with '+' for timezone offset + parts := strings.Split(decodedStr, " ") + if len(parts) == 2 && len(parts[1]) >= 5 { + // Check if the second part looks like timezone offset (e.g., "08:00") + if matched, _ := regexp.MatchString(`^\d{2}:\d{2}$`, parts[1]); matched { + decodedStr = parts[0] + "+" + parts[1] + } + } + } + + _, err = time.Parse(time.RFC3339, decodedStr) + return err == nil + }) + if err != nil { + return fmt.Errorf("failed to register RFC3339 validator: %w", err) + } + + return nil +} + // startServer initializes all application components and starts the HTTP server // Handles graceful shutdown when receiving interrupt signals func startServer( @@ -242,6 +284,11 @@ func startServer( // Initialize request validator for input validation validatorInstance := validator.New() + // Register custom validators + if err := registerCustomValidators(validatorInstance); err != nil { + return fmt.Errorf("failed to register custom validators: %w", err) + } + // Initialize data access layer repositories with database connection repos := initRepositories(db) diff --git a/api-service/internal/dto/request/security.go b/api-service/internal/dto/request/security.go index 48f3680..892dccb 100644 --- a/api-service/internal/dto/request/security.go +++ b/api-service/internal/dto/request/security.go @@ -28,8 +28,8 @@ type ListRolesRequest struct { PaginationRequest Search string `form:"search"` Status *int `form:"status" validate:"omitempty,oneof=-1 0 1"` - StartTime string `form:"start_time" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` - EndTime string `form:"end_time" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` + StartTime string `form:"start_time" validate:"omitempty,rfc3339"` + EndTime string `form:"end_time" validate:"omitempty,rfc3339"` } // CreatePermissionRequest creates permission request @@ -61,8 +61,8 @@ type ListPermissionsRequest struct { Module string `form:"module"` Scope string `form:"scope" validate:"omitempty,oneof=platform project"` Status *int `form:"status" validate:"omitempty,oneof=-1 0 1"` - StartTime string `form:"start_time" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` - EndTime string `form:"end_time" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` + StartTime string `form:"start_time" validate:"omitempty,rfc3339"` + EndTime string `form:"end_time" validate:"omitempty,rfc3339"` } // PermissionTreeRequest permission tree query request diff --git a/api-service/internal/interface/repository/security.go b/api-service/internal/interface/repository/security.go index e1197e6..87a96f1 100644 --- a/api-service/internal/interface/repository/security.go +++ b/api-service/internal/interface/repository/security.go @@ -52,7 +52,7 @@ type PermissionRepository interface { Delete(ctx context.Context, id uint) error // Query operations - List(ctx context.Context, req *request.ListPermissionsRequest) ([]*model.Permission, int64, error) + List(ctx context.Context, req *request.ListPermissionsRequest, lang string) ([]*model.Permission, int64, error) GetTree(ctx context.Context, req *request.PermissionTreeRequest) ([]*model.Permission, error) GetWithRoles(ctx context.Context, id uint) (*model.Permission, error) GetChildren(ctx context.Context, parentID uint) ([]*model.Permission, error) diff --git a/api-service/internal/repository/permission.go b/api-service/internal/repository/permission.go index 850cbe9..e572692 100644 --- a/api-service/internal/repository/permission.go +++ b/api-service/internal/repository/permission.go @@ -5,7 +5,9 @@ import ( "api-service/internal/interface/repository" "api-service/internal/model" "api-service/pkg/errors" + "api-service/pkg/i18n" "context" + "strings" "time" "gorm.io/gorm" @@ -144,16 +146,26 @@ func (r *permissionRepository) Delete(ctx context.Context, id uint) error { } // List retrieves a list of permissions -func (r *permissionRepository) List(ctx context.Context, req *request.ListPermissionsRequest) ([]*model.Permission, int64, error) { +func (r *permissionRepository) List(ctx context.Context, req *request.ListPermissionsRequest, lang string) ([]*model.Permission, int64, error) { var permissions []*model.Permission var total int64 query := r.db.WithContext(ctx).Model(&model.Permission{}).Where("status != -1") - // Build query conditions + // Build query conditions with translation support if req.Search != "" { - query = query.Where("name LIKE ? OR code LIKE ? OR description LIKE ?", - "%"+req.Search+"%", "%"+req.Search+"%", "%"+req.Search+"%") + // Find permission codes that match the translated names + matchedCodes, err := r.findMatchingPermissionCodes(ctx, req.Search, lang) + if err != nil { + // If translation matching fails, fallback to original search + query = query.Where("code LIKE ? OR description LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } else if len(matchedCodes) > 0 { + // Use matched codes from translation, also include code and description search + query = query.Where("code IN (?) OR code LIKE ? OR description LIKE ?", matchedCodes, "%"+req.Search+"%", "%"+req.Search+"%") + } else { + // No translation matches found, fallback to original search + query = query.Where("code LIKE ? OR description LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + } } if req.Module != "" { @@ -584,3 +596,39 @@ func (r *permissionRepository) UpdateWithTx(ctx context.Context, tx *gorm.DB, pe return nil } + +// findMatchingPermissionCodes finds permission codes that match the search term after translation +func (r *permissionRepository) findMatchingPermissionCodes(ctx context.Context, searchTerm, lang string) ([]string, error) { + // Create a struct to hold only the fields we need + type PermissionForTranslation struct { + Name string `gorm:"column:name"` + Code string `gorm:"column:code"` + } + + var permissions []PermissionForTranslation + err := r.db.WithContext(ctx).Model(&model.Permission{}). + Select("name, code"). + Where("status != -1"). + Order("id"). + Find(&permissions).Error + + if err != nil { + return nil, errors.WrapError(err, errors.CodeRecordQueryFailed, "failed to get permissions for translation matching") + } + + var matchedCodes []string + searchTermLower := strings.ToLower(searchTerm) + + for _, perm := range permissions { + // Translate the permission name using i18n + translatedName := i18n.T(perm.Name, lang) + translatedNameLower := strings.ToLower(translatedName) + + // Check if translated name contains the search term + if strings.Contains(translatedNameLower, searchTermLower) { + matchedCodes = append(matchedCodes, perm.Code) + } + } + + return matchedCodes, nil +} diff --git a/api-service/internal/service/permission.go b/api-service/internal/service/permission.go index af3b0a9..6861645 100644 --- a/api-service/internal/service/permission.go +++ b/api-service/internal/service/permission.go @@ -1,6 +1,7 @@ package service import ( + "api-service/internal/constants" "api-service/internal/dto/request" "api-service/internal/dto/response" "api-service/internal/interface/repository" @@ -8,6 +9,9 @@ import ( "api-service/internal/model" "api-service/pkg/errors" "api-service/pkg/logger" + "api-service/pkg/redis" + "api-service/pkg/utils" + "context" "math" @@ -191,7 +195,10 @@ func (s *permissionService) ListPermissions(ctx context.Context, req *request.Li logger.String("service", "permission"), logger.String("operation", "ListPermissions")) - permissions, total, err := s.permissionRepo.List(ctx, req) + // Extract language from context for translation support + lang := s.getLanguageFromContext(ctx) + + permissions, total, err := s.permissionRepo.List(ctx, req, lang) if err != nil { s.logger.ErrorContext(ctx, "Failed to list permissions", logger.ErrorField(err)) return nil, err @@ -320,3 +327,17 @@ func (s *permissionService) BatchUpdatePermissionStatus(ctx context.Context, ids return nil } + +// getLanguageFromContext extracts language from context +func (s *permissionService) getLanguageFromContext(ctx context.Context) string { + userID, exists := utils.GetUserIDFromContext(ctx) + if exists { + redisKey := redis.FormatRedisKeyWithID(redis.RK_USER_PREFERENCES, userID) + language, err := redis.HGet(ctx, redisKey, constants.UserLanguage) + + if err == nil && language != "" { + return language + } + } + return constants.DefaultLanguage +} diff --git a/api-service/internal/service/permission_test.go b/api-service/internal/service/permission_test.go index 7b33c9b..4f4a9e3 100644 --- a/api-service/internal/service/permission_test.go +++ b/api-service/internal/service/permission_test.go @@ -164,7 +164,7 @@ func (suite *PermissionServiceTestSuite) TestListPermissions_Success() { } total := int64(2) - suite.mockPermissionRepo.On("List", ctx, req).Return(permissions, total, nil) + suite.mockPermissionRepo.On("List", ctx, req, mock.AnythingOfType("string")).Return(permissions, total, nil) // Execute result, err := suite.service.ListPermissions(ctx, req) diff --git a/api-service/internal/service/role_test.go b/api-service/internal/service/role_test.go index 45528cd..f8260fc 100644 --- a/api-service/internal/service/role_test.go +++ b/api-service/internal/service/role_test.go @@ -159,8 +159,8 @@ func (m *MockPermissionRepository) Delete(ctx context.Context, id uint) error { return args.Error(0) } -func (m *MockPermissionRepository) List(ctx context.Context, req *request.ListPermissionsRequest) ([]*model.Permission, int64, error) { - args := m.Called(ctx, req) +func (m *MockPermissionRepository) List(ctx context.Context, req *request.ListPermissionsRequest, lang string) ([]*model.Permission, int64, error) { + args := m.Called(ctx, req, lang) return args.Get(0).([]*model.Permission), args.Get(1).(int64), args.Error(2) }