package services import ( "context" "fmt" "time" "github.com/google/uuid" "go.uber.org/zap" "github.com/kms/api-key-service/internal/crypto" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/repository" ) // tokenService implements the TokenService interface type tokenService struct { tokenRepo repository.StaticTokenRepository appRepo repository.ApplicationRepository permRepo repository.PermissionRepository grantRepo repository.GrantedPermissionRepository tokenGen *crypto.TokenGenerator logger *zap.Logger } // NewTokenService creates a new token service func NewTokenService( tokenRepo repository.StaticTokenRepository, appRepo repository.ApplicationRepository, permRepo repository.PermissionRepository, grantRepo repository.GrantedPermissionRepository, hmacKey string, logger *zap.Logger, ) TokenService { return &tokenService{ tokenRepo: tokenRepo, appRepo: appRepo, permRepo: permRepo, grantRepo: grantRepo, tokenGen: crypto.NewTokenGenerator(hmacKey), logger: logger, } } // CreateStaticToken creates a new static token func (s *tokenService) CreateStaticToken(ctx context.Context, req *domain.CreateStaticTokenRequest, userID string) (*domain.CreateStaticTokenResponse, error) { s.logger.Info("Creating static token", zap.String("app_id", req.AppID), zap.String("user_id", userID)) // Validate application exists app, err := s.appRepo.GetByID(ctx, req.AppID) if err != nil { s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID)) return nil, fmt.Errorf("application not found: %w", err) } // Validate permissions exist validPermissions, err := s.permRepo.ValidatePermissionScopes(ctx, req.Permissions) if err != nil { s.logger.Error("Failed to validate permissions", zap.Error(err)) return nil, fmt.Errorf("failed to validate permissions: %w", err) } if len(validPermissions) != len(req.Permissions) { s.logger.Warn("Some permissions are invalid", zap.Strings("requested", req.Permissions), zap.Strings("valid", validPermissions)) return nil, fmt.Errorf("some requested permissions are invalid") } // Generate secure token tokenInfo, err := s.tokenGen.GenerateTokenWithInfo() if err != nil { s.logger.Error("Failed to generate secure token", zap.Error(err)) return nil, fmt.Errorf("failed to generate token: %w", err) } tokenID := uuid.New() now := time.Now() // Create the token entity token := &domain.StaticToken{ ID: tokenID, AppID: req.AppID, Owner: req.Owner, KeyHash: tokenInfo.Hash, Type: "hmac", CreatedAt: now, UpdatedAt: now, } // Save the token to the database err = s.tokenRepo.Create(ctx, token) if err != nil { s.logger.Error("Failed to create token in database", zap.Error(err), zap.String("token_id", tokenID.String())) return nil, fmt.Errorf("failed to create token: %w", err) } // Grant permissions to the token var grants []*domain.GrantedPermission for _, permScope := range validPermissions { // Get permission by scope to get the ID perm, err := s.permRepo.GetAvailablePermissionByScope(ctx, permScope) if err != nil { s.logger.Error("Failed to get permission by scope", zap.Error(err), zap.String("scope", permScope)) continue } grant := &domain.GrantedPermission{ ID: uuid.New(), TokenType: domain.TokenTypeStatic, TokenID: tokenID, PermissionID: perm.ID, Scope: permScope, CreatedBy: userID, } grants = append(grants, grant) } if len(grants) > 0 { err = s.grantRepo.GrantPermissions(ctx, grants) if err != nil { s.logger.Error("Failed to grant permissions", zap.Error(err)) // Clean up the token if permission granting fails s.tokenRepo.Delete(ctx, tokenID) return nil, fmt.Errorf("failed to grant permissions: %w", err) } } response := &domain.CreateStaticTokenResponse{ ID: tokenID, Token: tokenInfo.Token, // Return the actual token only once Permissions: validPermissions, CreatedAt: now, } s.logger.Info("Static token created successfully", zap.String("token_id", tokenID.String()), zap.String("app_id", app.AppID), zap.Strings("permissions", validPermissions)) return response, nil } // ListByApp lists all tokens for an application func (s *tokenService) ListByApp(ctx context.Context, appID string, limit, offset int) ([]*domain.StaticToken, error) { s.logger.Debug("Listing tokens for application", zap.String("app_id", appID)) tokens, err := s.tokenRepo.GetByAppID(ctx, appID) if err != nil { s.logger.Error("Failed to list tokens from repository", zap.Error(err), zap.String("app_id", appID)) return nil, fmt.Errorf("failed to list tokens: %w", err) } // Apply pagination manually since GetByAppID doesn't support it start := offset end := offset + limit if start > len(tokens) { tokens = []*domain.StaticToken{} } else if end > len(tokens) { tokens = tokens[start:] } else { tokens = tokens[start:end] } s.logger.Debug("Listed tokens successfully", zap.String("app_id", appID), zap.Int("count", len(tokens))) return tokens, nil } // Delete deletes a token func (s *tokenService) Delete(ctx context.Context, tokenID uuid.UUID, userID string) error { s.logger.Info("Deleting token", zap.String("token_id", tokenID.String()), zap.String("user_id", userID)) // Check if token exists exists, err := s.tokenRepo.Exists(ctx, tokenID) if err != nil { s.logger.Error("Failed to check token existence", zap.Error(err), zap.String("token_id", tokenID.String())) return err } if !exists { s.logger.Error("Token not found", zap.String("token_id", tokenID.String())) return fmt.Errorf("token with ID '%s' not found", tokenID.String()) } // Delete the token err = s.tokenRepo.Delete(ctx, tokenID) if err != nil { s.logger.Error("Failed to delete token", zap.Error(err), zap.String("token_id", tokenID.String())) return err } // TODO: Revoke associated permissions return nil } // GenerateUserToken generates a user token func (s *tokenService) GenerateUserToken(ctx context.Context, appID, userID string, permissions []string) (string, error) { s.logger.Info("Generating user token", zap.String("app_id", appID), zap.String("user_id", userID)) // TODO: Validate application // TODO: Validate permissions // TODO: Generate JWT token return "user-token-placeholder-" + userID, nil } // VerifyToken verifies a token and returns verification response func (s *tokenService) VerifyToken(ctx context.Context, req *domain.VerifyRequest) (*domain.VerifyResponse, error) { s.logger.Debug("Verifying token", zap.String("app_id", req.AppID), zap.String("type", string(req.Type))) // Validate request if req.Token == "" { return &domain.VerifyResponse{ Valid: false, Error: "Token is required", }, nil } // Validate application exists app, err := s.appRepo.GetByID(ctx, req.AppID) if err != nil { s.logger.Error("Failed to get application", zap.Error(err), zap.String("app_id", req.AppID)) return &domain.VerifyResponse{ Valid: false, Error: "Invalid application", }, nil } switch req.Type { case domain.TokenTypeStatic: return s.verifyStaticToken(ctx, req, app) case domain.TokenTypeUser: return s.verifyUserToken(ctx, req, app) default: return &domain.VerifyResponse{ Valid: false, Error: "Invalid token type", }, nil } } // verifyStaticToken verifies a static token func (s *tokenService) verifyStaticToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) { s.logger.Debug("Verifying static token", zap.String("app_id", req.AppID)) // Check token format if !crypto.IsValidTokenFormat(req.Token) { s.logger.Warn("Invalid token format", zap.String("app_id", req.AppID)) return &domain.VerifyResponse{ Valid: false, Error: "Invalid token format", }, nil } // Try to find token by testing against all stored hashes for this app tokens, err := s.tokenRepo.GetByAppID(ctx, req.AppID) if err != nil { s.logger.Error("Failed to get tokens for app", zap.Error(err), zap.String("app_id", req.AppID)) return &domain.VerifyResponse{ Valid: false, Error: "Token verification failed", }, nil } var matchedToken *domain.StaticToken for _, token := range tokens { if s.tokenGen.VerifyToken(req.Token, token.KeyHash) { matchedToken = token break } } if matchedToken == nil { s.logger.Warn("Token not found or invalid", zap.String("app_id", req.AppID)) return &domain.VerifyResponse{ Valid: false, Error: "Invalid token", }, nil } // Get granted permissions for this token permissions, err := s.grantRepo.GetGrantedPermissionScopes(ctx, domain.TokenTypeStatic, matchedToken.ID) if err != nil { s.logger.Error("Failed to get token permissions", zap.Error(err), zap.String("token_id", matchedToken.ID.String())) return &domain.VerifyResponse{ Valid: false, Error: "Failed to retrieve permissions", }, nil } // Check specific permissions if requested var permissionResults map[string]bool if len(req.Permissions) > 0 { permissionResults, err = s.grantRepo.HasAnyPermission(ctx, domain.TokenTypeStatic, matchedToken.ID, req.Permissions) if err != nil { s.logger.Error("Failed to check specific permissions", zap.Error(err)) return &domain.VerifyResponse{ Valid: false, Error: "Failed to check permissions", }, nil } } s.logger.Info("Static token verified successfully", zap.String("token_id", matchedToken.ID.String()), zap.String("app_id", req.AppID), zap.Strings("permissions", permissions)) return &domain.VerifyResponse{ Valid: true, Permissions: permissions, PermissionResults: permissionResults, TokenType: domain.TokenTypeStatic, }, nil } // verifyUserToken verifies a user token (JWT-based) func (s *tokenService) verifyUserToken(ctx context.Context, req *domain.VerifyRequest, app *domain.Application) (*domain.VerifyResponse, error) { s.logger.Debug("Verifying user token", zap.String("app_id", req.AppID)) // TODO: Implement JWT token verification // For now, return an error since user tokens are not fully implemented return &domain.VerifyResponse{ Valid: false, Error: "User token verification not yet implemented", }, nil } // RenewUserToken renews a user token func (s *tokenService) RenewUserToken(ctx context.Context, req *domain.RenewRequest) (*domain.RenewResponse, error) { s.logger.Info("Renewing user token", zap.String("app_id", req.AppID), zap.String("user_id", req.UserID)) // TODO: Validate current token // TODO: Generate new token with extended expiry but same max valid date response := &domain.RenewResponse{ Token: "renewed-token-placeholder", ExpiresAt: time.Now().Add(7 * 24 * time.Hour), MaxValidAt: time.Now().Add(30 * 24 * time.Hour), } return response, nil }