package site import ( "context" "fmt" "time" "github.com/gocql/gocql" "go.uber.org/zap" siteusecase "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/usecase/site" ) // RotateAPIKeyService handles API key rotation operations type RotateAPIKeyService interface { RotateAPIKey(ctx context.Context, tenantID gocql.UUID, input *siteusecase.RotateAPIKeyInput) (*siteusecase.RotateAPIKeyOutput, error) } type rotateAPIKeyService struct { // Focused usecases getSiteUC *siteusecase.GetSiteUseCase generateAPIKeyUC *siteusecase.GenerateAPIKeyUseCase updateSiteAPIKeyUC *siteusecase.UpdateSiteAPIKeyUseCase updateSiteAPIKeyToRepoUC *siteusecase.UpdateSiteAPIKeyToRepoUseCase logger *zap.Logger } // NewRotateAPIKeyService creates a new RotateAPIKeyService func NewRotateAPIKeyService( getSiteUC *siteusecase.GetSiteUseCase, generateAPIKeyUC *siteusecase.GenerateAPIKeyUseCase, updateSiteAPIKeyUC *siteusecase.UpdateSiteAPIKeyUseCase, updateSiteAPIKeyToRepoUC *siteusecase.UpdateSiteAPIKeyToRepoUseCase, logger *zap.Logger, ) RotateAPIKeyService { return &rotateAPIKeyService{ getSiteUC: getSiteUC, generateAPIKeyUC: generateAPIKeyUC, updateSiteAPIKeyUC: updateSiteAPIKeyUC, updateSiteAPIKeyToRepoUC: updateSiteAPIKeyToRepoUC, logger: logger.Named("rotate-apikey-service"), } } // RotateAPIKey orchestrates the API key rotation workflow func (s *rotateAPIKeyService) RotateAPIKey(ctx context.Context, tenantID gocql.UUID, input *siteusecase.RotateAPIKeyInput) (*siteusecase.RotateAPIKeyOutput, error) { s.logger.Info("rotating API key", zap.String("tenant_id", tenantID.String()), zap.String("site_id", input.SiteID)) // Step 1: Get current site siteOutput, err := s.getSiteUC.Execute(ctx, tenantID, &siteusecase.GetSiteInput{ ID: input.SiteID, }) if err != nil { s.logger.Error("failed to get site", zap.String("site_id", input.SiteID), zap.Error(err)) return nil, err } site := siteOutput.Site // Step 2: Store old key info for response and rotation oldKeyLastFour := site.APIKeyLastFour oldAPIKeyHash := site.APIKeyHash // Step 3: Determine test mode from existing API key prefix // If current key starts with "test_", generate a test key; otherwise generate live key testMode := len(site.APIKeyPrefix) >= 5 && site.APIKeyPrefix[:5] == "test_" s.logger.Info("generating new API key", zap.Bool("test_mode", testMode), zap.String("current_key_prefix", site.APIKeyPrefix), zap.String("site_id", input.SiteID)) apiKeyResult, err := s.generateAPIKeyUC.Execute(testMode) if err != nil { s.logger.Error("failed to generate new API key", zap.Error(err)) return nil, fmt.Errorf("failed to generate API key: %w", err) } // Step 4: Update site entity with new key details s.updateSiteAPIKeyUC.Execute(&siteusecase.UpdateSiteAPIKeyInput{ Site: site, NewAPIKeyHash: apiKeyResult.HashedKey, NewKeyPrefix: apiKeyResult.Prefix, NewKeyLastFour: apiKeyResult.LastFour, }) // Step 5: Update site API key in repository (all tables) // Use UpdateSiteAPIKeyToRepoUC to properly handle sites_by_apikey table (delete old + insert new) if err := s.updateSiteAPIKeyToRepoUC.Execute(ctx, &siteusecase.UpdateSiteAPIKeyToRepoInput{ Site: site, OldAPIKeyHash: oldAPIKeyHash, }); err != nil { s.logger.Error("failed to update site with new API key", zap.Error(err)) return nil, err } // Step 6: Build output rotatedAt := time.Now() s.logger.Info("API key rotated successfully", zap.String("site_id", input.SiteID), zap.String("old_key_last_four", oldKeyLastFour), zap.String("new_key_prefix", apiKeyResult.Prefix), zap.String("new_key_last_four", apiKeyResult.LastFour)) return &siteusecase.RotateAPIKeyOutput{ NewAPIKey: apiKeyResult.PlaintextKey, // PLAINTEXT - only shown once! OldKeyLastFour: oldKeyLastFour, RotatedAt: rotatedAt, }, nil }