Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions api-service/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
Expand Down Expand Up @@ -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(
Expand All @@ -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)

Expand Down
8 changes: 4 additions & 4 deletions api-service/internal/dto/request/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion api-service/internal/interface/repository/security.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
56 changes: 52 additions & 4 deletions api-service/internal/repository/permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 != "" {
Expand Down Expand Up @@ -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
}
23 changes: 22 additions & 1 deletion api-service/internal/service/permission.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package service

import (
"api-service/internal/constants"
"api-service/internal/dto/request"
"api-service/internal/dto/response"
"api-service/internal/interface/repository"
"api-service/internal/interface/service"
"api-service/internal/model"
"api-service/pkg/errors"
"api-service/pkg/logger"
"api-service/pkg/redis"
"api-service/pkg/utils"

"context"
"math"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion api-service/internal/service/permission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions api-service/internal/service/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading