package services import ( "context" "time" "github.com/google/uuid" "go.uber.org/zap" "github.com/kms/api-key-service/internal/config" "github.com/kms/api-key-service/internal/domain" "github.com/kms/api-key-service/internal/errors" "github.com/kms/api-key-service/internal/repository" ) // sessionService implements the SessionService interface type sessionService struct { sessionRepo repository.SessionRepository appRepo repository.ApplicationRepository config config.ConfigProvider logger *zap.Logger } // NewSessionService creates a new session service func NewSessionService( sessionRepo repository.SessionRepository, appRepo repository.ApplicationRepository, config config.ConfigProvider, logger *zap.Logger, ) SessionService { return &sessionService{ sessionRepo: sessionRepo, appRepo: appRepo, config: config, logger: logger, } } // CreateSession creates a new user session func (s *sessionService) CreateSession(ctx context.Context, req *domain.CreateSessionRequest) (*domain.UserSession, error) { s.logger.Debug("Creating new session", zap.String("user_id", req.UserID), zap.String("app_id", req.AppID), zap.String("session_type", string(req.SessionType))) // Validate application exists app, err := s.appRepo.GetByID(ctx, req.AppID) if err != nil { if errors.IsNotFound(err) { return nil, errors.NewValidationError("Application not found") } return nil, err } // Check if application supports user tokens supportsUser := false for _, appType := range app.Type { if appType == domain.ApplicationTypeUser { supportsUser = true break } } if !supportsUser { return nil, errors.NewValidationError("Application does not support user sessions") } // Create session object session := &domain.UserSession{ ID: uuid.New(), UserID: req.UserID, AppID: req.AppID, SessionType: req.SessionType, Status: domain.SessionStatusActive, IPAddress: req.IPAddress, UserAgent: req.UserAgent, ExpiresAt: req.ExpiresAt, Metadata: domain.SessionMetadata{ TenantID: req.TenantID, Permissions: req.Permissions, Claims: req.Claims, LoginMethod: "oauth2", }, } // Create session in repository if err := s.sessionRepo.Create(ctx, session); err != nil { s.logger.Error("Failed to create session", zap.Error(err)) return nil, err } s.logger.Debug("Session created successfully", zap.String("session_id", session.ID.String())) return session, nil } // GetSession retrieves a session by its ID func (s *sessionService) GetSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error) { s.logger.Debug("Getting session", zap.String("session_id", sessionID.String())) session, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return nil, err } return session, nil } // GetUserSessions retrieves all sessions for a user func (s *sessionService) GetUserSessions(ctx context.Context, userID string) ([]*domain.UserSession, error) { s.logger.Debug("Getting user sessions", zap.String("user_id", userID)) sessions, err := s.sessionRepo.GetByUserID(ctx, userID) if err != nil { return nil, err } return sessions, nil } // GetUserAppSessions retrieves sessions for a specific user and application func (s *sessionService) GetUserAppSessions(ctx context.Context, userID, appID string) ([]*domain.UserSession, error) { s.logger.Debug("Getting user app sessions", zap.String("user_id", userID), zap.String("app_id", appID)) sessions, err := s.sessionRepo.GetByUserAndApp(ctx, userID, appID) if err != nil { return nil, err } return sessions, nil } // GetActiveSessions retrieves all active sessions for a user func (s *sessionService) GetActiveSessions(ctx context.Context, userID string) ([]*domain.UserSession, error) { s.logger.Debug("Getting active sessions", zap.String("user_id", userID)) sessions, err := s.sessionRepo.GetActiveByUserID(ctx, userID) if err != nil { return nil, err } return sessions, nil } // ListSessions retrieves sessions with filtering and pagination func (s *sessionService) ListSessions(ctx context.Context, req *domain.SessionListRequest) (*domain.SessionListResponse, error) { s.logger.Debug("Listing sessions", zap.String("user_id", req.UserID), zap.String("app_id", req.AppID), zap.Int("limit", req.Limit), zap.Int("offset", req.Offset)) // Set default pagination if not provided if req.Limit <= 0 { req.Limit = 50 } if req.Limit > 100 { req.Limit = 100 } response, err := s.sessionRepo.List(ctx, req) if err != nil { return nil, err } return response, nil } // UpdateSession updates an existing session func (s *sessionService) UpdateSession(ctx context.Context, sessionID uuid.UUID, updates *domain.UpdateSessionRequest) error { s.logger.Debug("Updating session", zap.String("session_id", sessionID.String())) // Validate session exists _, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return err } // Update session if err := s.sessionRepo.Update(ctx, sessionID, updates); err != nil { return err } s.logger.Debug("Session updated successfully", zap.String("session_id", sessionID.String())) return nil } // UpdateSessionActivity updates the last activity timestamp for a session func (s *sessionService) UpdateSessionActivity(ctx context.Context, sessionID uuid.UUID) error { s.logger.Debug("Updating session activity", zap.String("session_id", sessionID.String())) if err := s.sessionRepo.UpdateActivity(ctx, sessionID); err != nil { return err } return nil } // RevokeSession revokes a specific session func (s *sessionService) RevokeSession(ctx context.Context, sessionID uuid.UUID, revokedBy string) error { s.logger.Debug("Revoking session", zap.String("session_id", sessionID.String()), zap.String("revoked_by", revokedBy)) // Validate session exists and is active session, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return err } if session.Status != domain.SessionStatusActive { return errors.NewValidationError("Session is not active") } // Revoke session if err := s.sessionRepo.Revoke(ctx, sessionID, revokedBy); err != nil { return err } s.logger.Debug("Session revoked successfully", zap.String("session_id", sessionID.String())) return nil } // RevokeUserSessions revokes all sessions for a user func (s *sessionService) RevokeUserSessions(ctx context.Context, userID string, revokedBy string) error { s.logger.Debug("Revoking user sessions", zap.String("user_id", userID), zap.String("revoked_by", revokedBy)) if err := s.sessionRepo.RevokeAllByUser(ctx, userID, revokedBy); err != nil { return err } s.logger.Debug("User sessions revoked successfully", zap.String("user_id", userID)) return nil } // RevokeUserAppSessions revokes all sessions for a user and application func (s *sessionService) RevokeUserAppSessions(ctx context.Context, userID, appID string, revokedBy string) error { s.logger.Debug("Revoking user app sessions", zap.String("user_id", userID), zap.String("app_id", appID), zap.String("revoked_by", revokedBy)) if err := s.sessionRepo.RevokeAllByUserAndApp(ctx, userID, appID, revokedBy); err != nil { return err } s.logger.Debug("User app sessions revoked successfully", zap.String("user_id", userID), zap.String("app_id", appID)) return nil } // ValidateSession validates if a session is active and valid func (s *sessionService) ValidateSession(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error) { s.logger.Debug("Validating session", zap.String("session_id", sessionID.String())) session, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return nil, err } // Check if session is active if !session.IsActive() { if session.IsExpired() { return nil, errors.NewAuthenticationError("Session has expired") } if session.IsRevoked() { return nil, errors.NewAuthenticationError("Session has been revoked") } return nil, errors.NewAuthenticationError("Session is not active") } // Update last activity if err := s.sessionRepo.UpdateActivity(ctx, sessionID); err != nil { s.logger.Warn("Failed to update session activity", zap.Error(err)) // Don't fail validation if we can't update activity } s.logger.Debug("Session validated successfully", zap.String("session_id", sessionID.String())) return session, nil } // RefreshSession refreshes a session's expiration time func (s *sessionService) RefreshSession(ctx context.Context, sessionID uuid.UUID, newExpiration time.Time) error { s.logger.Debug("Refreshing session", zap.String("session_id", sessionID.String()), zap.Time("new_expiration", newExpiration)) // Validate session exists and is active session, err := s.sessionRepo.GetByID(ctx, sessionID) if err != nil { return err } if !session.IsActive() { return errors.NewValidationError("Cannot refresh inactive session") } // Update expiration updates := &domain.UpdateSessionRequest{ ExpiresAt: &newExpiration, } if err := s.sessionRepo.Update(ctx, sessionID, updates); err != nil { return err } s.logger.Debug("Session refreshed successfully", zap.String("session_id", sessionID.String())) return nil } // CleanupExpiredSessions marks expired sessions as expired and optionally deletes old ones func (s *sessionService) CleanupExpiredSessions(ctx context.Context, deleteOlderThan *time.Duration) (expired int, deleted int, err error) { s.logger.Debug("Cleaning up expired sessions") // Mark expired sessions expired, err = s.sessionRepo.ExpireOldSessions(ctx) if err != nil { s.logger.Error("Failed to expire old sessions", zap.Error(err)) return 0, 0, err } // Delete old expired sessions if requested if deleteOlderThan != nil { deleted, err = s.sessionRepo.DeleteExpiredSessions(ctx, *deleteOlderThan) if err != nil { s.logger.Error("Failed to delete expired sessions", zap.Error(err)) return expired, 0, err } } s.logger.Debug("Session cleanup completed", zap.Int("expired", expired), zap.Int("deleted", deleted)) return expired, deleted, nil } // GetSessionStats returns session statistics for a user func (s *sessionService) GetSessionStats(ctx context.Context, userID string) (total int, active int, err error) { s.logger.Debug("Getting session stats", zap.String("user_id", userID)) total, err = s.sessionRepo.GetSessionCount(ctx, userID) if err != nil { return 0, 0, err } active, err = s.sessionRepo.GetActiveSessionCount(ctx, userID) if err != nil { return 0, 0, err } return total, active, nil } // CreateOAuth2Session creates a session from OAuth2 authentication flow func (s *sessionService) CreateOAuth2Session(ctx context.Context, userID, appID string, tokenResponse *domain.TokenResponse, userInfo *domain.UserInfo, sessionType domain.SessionType, ipAddress, userAgent string) (*domain.UserSession, error) { s.logger.Debug("Creating OAuth2 session", zap.String("user_id", userID), zap.String("app_id", appID), zap.String("session_type", string(sessionType))) // Validate application exists app, err := s.appRepo.GetByID(ctx, appID) if err != nil { if errors.IsNotFound(err) { return nil, errors.NewValidationError("Application not found") } return nil, err } // Calculate expiration based on token response expiresAt := time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second) // Use application's max token duration if shorter maxExpiration := time.Now().Add(app.MaxTokenDuration.Duration) if expiresAt.After(maxExpiration) { expiresAt = maxExpiration } // Create session object session := &domain.UserSession{ ID: uuid.New(), UserID: userID, AppID: appID, SessionType: sessionType, Status: domain.SessionStatusActive, AccessToken: tokenResponse.AccessToken, // In production, encrypt this RefreshToken: tokenResponse.RefreshToken, // In production, encrypt this IDToken: tokenResponse.IDToken, // In production, encrypt this IPAddress: ipAddress, UserAgent: userAgent, ExpiresAt: expiresAt, Metadata: domain.SessionMetadata{ LoginMethod: "oauth2", Claims: map[string]string{ "sub": userInfo.Sub, "email": userInfo.Email, "name": userInfo.Name, }, }, } // Create session in repository if err := s.sessionRepo.Create(ctx, session); err != nil { s.logger.Error("Failed to create OAuth2 session", zap.Error(err)) return nil, err } s.logger.Debug("OAuth2 session created successfully", zap.String("session_id", session.ID.String())) return session, nil }