monorepo/cloud/maplepress-backend/internal/service/site/rotate_apikey.go

114 lines
3.9 KiB
Go

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
}