298 lines
8.1 KiB
Go
298 lines
8.1 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"errors"
|
|
|
|
"knowlege-lsxd/internal/logger"
|
|
"knowlege-lsxd/internal/types"
|
|
"knowlege-lsxd/internal/types/interfaces"
|
|
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var apiKeySecret = getApiKeySecret()
|
|
|
|
// ListTenantsParams defines parameters for listing tenants with filtering and pagination
|
|
type ListTenantsParams struct {
|
|
Page int // Page number for pagination
|
|
PageSize int // Number of items per page
|
|
Status string // Filter by tenant status
|
|
Name string // Filter by tenant name
|
|
}
|
|
|
|
// tenantService implements the TenantService interface
|
|
type tenantService struct {
|
|
repo interfaces.TenantRepository // Repository for tenant data operations
|
|
}
|
|
|
|
// NewTenantService creates a new tenant service instance
|
|
func NewTenantService(repo interfaces.TenantRepository) interfaces.TenantService {
|
|
return &tenantService{repo: repo}
|
|
}
|
|
|
|
// CreateTenant creates a new tenant
|
|
func (s *tenantService) CreateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error) {
|
|
logger.Info(ctx, "Start creating tenant")
|
|
|
|
if tenant.Name == "" {
|
|
logger.Error(ctx, "Tenant name cannot be empty")
|
|
return nil, errors.New("tenant name cannot be empty")
|
|
}
|
|
|
|
logger.Infof(ctx, "Creating tenant, name: %s", tenant.Name)
|
|
|
|
// Create tenant with initial values
|
|
tenant.APIKey = s.generateApiKey(0)
|
|
tenant.Status = "active"
|
|
tenant.CreatedAt = time.Now()
|
|
tenant.UpdatedAt = time.Now()
|
|
|
|
logger.Info(ctx, "Saving tenant information to database")
|
|
if err := s.repo.CreateTenant(ctx, tenant); err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_name": tenant.Name,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant created successfully, ID: %d, generating official API Key", tenant.ID)
|
|
tenant.APIKey = s.generateApiKey(tenant.ID)
|
|
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": tenant.ID,
|
|
"tenant_name": tenant.Name,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant creation and update completed, ID: %d, name: %s", tenant.ID, tenant.Name)
|
|
return tenant, nil
|
|
}
|
|
|
|
// GetTenantByID retrieves a tenant by their ID
|
|
func (s *tenantService) GetTenantByID(ctx context.Context, id uint) (*types.Tenant, error) {
|
|
logger.Info(ctx, "Start retrieving tenant")
|
|
|
|
if id == 0 {
|
|
logger.Error(ctx, "Tenant ID cannot be 0")
|
|
return nil, errors.New("tenant ID cannot be 0")
|
|
}
|
|
|
|
logger.Infof(ctx, "Retrieving tenant, ID: %d", id)
|
|
|
|
tenant, err := s.repo.GetTenantByID(ctx, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant retrieved successfully, ID: %d, name: %s", tenant.ID, tenant.Name)
|
|
return tenant, nil
|
|
}
|
|
|
|
// ListTenants retrieves a list of all tenants
|
|
func (s *tenantService) ListTenants(ctx context.Context) ([]*types.Tenant, error) {
|
|
logger.Info(ctx, "Start retrieving tenant list")
|
|
|
|
tenants, err := s.repo.ListTenants(ctx)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, nil)
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant list retrieved successfully, total: %d", len(tenants))
|
|
return tenants, nil
|
|
}
|
|
|
|
// UpdateTenant updates an existing tenant's information
|
|
func (s *tenantService) UpdateTenant(ctx context.Context, tenant *types.Tenant) (*types.Tenant, error) {
|
|
if tenant.ID == 0 {
|
|
logger.Error(ctx, "Tenant ID cannot be 0")
|
|
return nil, errors.New("tenant ID cannot be 0")
|
|
}
|
|
|
|
logger.Infof(ctx, "Updating tenant, ID: %d, name: %s", tenant.ID, tenant.Name)
|
|
|
|
// Generate new API key if empty
|
|
if tenant.APIKey == "" {
|
|
logger.Info(ctx, "API Key is empty, generating new API Key")
|
|
tenant.APIKey = s.generateApiKey(tenant.ID)
|
|
}
|
|
|
|
tenant.UpdatedAt = time.Now()
|
|
logger.Info(ctx, "Saving tenant information to database")
|
|
|
|
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": tenant.ID,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant updated successfully, ID: %d", tenant.ID)
|
|
return tenant, nil
|
|
}
|
|
|
|
// DeleteTenant removes a tenant by their ID
|
|
func (s *tenantService) DeleteTenant(ctx context.Context, id uint) error {
|
|
logger.Info(ctx, "Start deleting tenant")
|
|
|
|
if id == 0 {
|
|
logger.Error(ctx, "Tenant ID cannot be 0")
|
|
return errors.New("tenant ID cannot be 0")
|
|
}
|
|
|
|
logger.Infof(ctx, "Deleting tenant, ID: %d", id)
|
|
|
|
// Get tenant information for logging
|
|
tenant, err := s.repo.GetTenantByID(ctx, id)
|
|
if err != nil {
|
|
if err.Error() == "record not found" {
|
|
logger.Warnf(ctx, "Tenant to be deleted does not exist, ID: %d", id)
|
|
} else {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": id,
|
|
})
|
|
return err
|
|
}
|
|
} else {
|
|
logger.Infof(ctx, "Deleting tenant, ID: %d, name: %s", id, tenant.Name)
|
|
}
|
|
|
|
err = s.repo.DeleteTenant(ctx, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": id,
|
|
})
|
|
return err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant deleted successfully, ID: %d", id)
|
|
return nil
|
|
}
|
|
|
|
// UpdateAPIKey updates the API key for a specific tenant
|
|
func (s *tenantService) UpdateAPIKey(ctx context.Context, id uint) (string, error) {
|
|
logger.Info(ctx, "Start updating tenant API Key")
|
|
|
|
if id == 0 {
|
|
logger.Error(ctx, "Tenant ID cannot be 0")
|
|
return "", errors.New("tenant ID cannot be 0")
|
|
}
|
|
|
|
logger.Infof(ctx, "Retrieving tenant information, ID: %d", id)
|
|
|
|
tenant, err := s.repo.GetTenantByID(ctx, id)
|
|
if err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": id,
|
|
})
|
|
return "", err
|
|
}
|
|
|
|
logger.Infof(ctx, "Generating new API Key for tenant, ID: %d", id)
|
|
tenant.APIKey = s.generateApiKey(tenant.ID)
|
|
|
|
if err := s.repo.UpdateTenant(ctx, tenant); err != nil {
|
|
logger.ErrorWithFields(ctx, err, map[string]interface{}{
|
|
"tenant_id": id,
|
|
})
|
|
return "", err
|
|
}
|
|
|
|
logger.Infof(ctx, "Tenant API Key updated successfully, ID: %d", id)
|
|
return tenant.APIKey, nil
|
|
}
|
|
|
|
// generateApiKey generates a secure API key for tenant authentication
|
|
func (r *tenantService) generateApiKey(tenantID uint) string {
|
|
// 1. Convert tenant_id to bytes
|
|
idBytes := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(idBytes, uint64(tenantID))
|
|
|
|
// 2. Encrypt tenant_id using AES-GCM
|
|
block, err := aes.NewCipher(apiKeySecret)
|
|
if err != nil {
|
|
panic("Failed to create AES cipher: " + err.Error())
|
|
}
|
|
|
|
nonce := make([]byte, 12)
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
panic("Failed to create GCM cipher: " + err.Error())
|
|
}
|
|
|
|
ciphertext := aesgcm.Seal(nil, nonce, idBytes, nil)
|
|
|
|
// 3. Combine nonce and ciphertext, then encode with base64
|
|
combined := append(nonce, ciphertext...)
|
|
encoded := base64.RawURLEncoding.EncodeToString(combined)
|
|
|
|
// Create final API Key in format: sk-{encrypted_part}
|
|
return "sk-" + encoded
|
|
}
|
|
|
|
// ExtractTenantIDFromAPIKey extracts the tenant ID from an API key
|
|
func (r *tenantService) ExtractTenantIDFromAPIKey(apiKey string) (uint, error) {
|
|
// 1. Validate format and extract encrypted part
|
|
parts := strings.SplitN(apiKey, "-", 2)
|
|
if len(parts) != 2 || parts[0] != "sk" {
|
|
return 0, errors.New("invalid API key format")
|
|
}
|
|
|
|
// 2. Decode the base64 part
|
|
encryptedData, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return 0, errors.New("invalid API key encoding")
|
|
}
|
|
|
|
// 3. Separate nonce and ciphertext
|
|
if len(encryptedData) < 12 {
|
|
return 0, errors.New("invalid API key length")
|
|
}
|
|
nonce, ciphertext := encryptedData[:12], encryptedData[12:]
|
|
|
|
// 4. Decrypt
|
|
block, err := aes.NewCipher(apiKeySecret)
|
|
if err != nil {
|
|
return 0, errors.New("decryption error")
|
|
}
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return 0, errors.New("decryption error")
|
|
}
|
|
|
|
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return 0, errors.New("API key is invalid or has been tampered with")
|
|
}
|
|
|
|
// 5. Convert back to tenant_id
|
|
tenantID := binary.LittleEndian.Uint64(plaintext)
|
|
|
|
return uint(tenantID), nil
|
|
}
|
|
|
|
func getApiKeySecret() []byte {
|
|
secret := os.Getenv("TENANT_AES_KEY")
|
|
hashArray := sha256.Sum256([]byte(secret))
|
|
return hashArray[:]
|
|
}
|