Files
skybridge/user/internal/services/user_service.go
2025-09-01 17:27:59 -04:00

326 lines
9.2 KiB
Go

package services
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"github.com/RyanCopley/skybridge/user/internal/domain"
"github.com/RyanCopley/skybridge/user/internal/repository/interfaces"
)
// UserService defines the interface for user business logic
type UserService interface {
// Create creates a new user
Create(ctx context.Context, req *domain.CreateUserRequest, actorID string) (*domain.User, error)
// GetByID retrieves a user by ID
GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
// GetByEmail retrieves a user by email
GetByEmail(ctx context.Context, email string) (*domain.User, error)
// Update updates an existing user
Update(ctx context.Context, id uuid.UUID, req *domain.UpdateUserRequest, actorID string) (*domain.User, error)
// Delete deletes a user by ID
Delete(ctx context.Context, id uuid.UUID, actorID string) error
// List retrieves users with filtering and pagination
List(ctx context.Context, req *domain.ListUsersRequest) (*domain.ListUsersResponse, error)
// UpdateLastLogin updates the last login timestamp
UpdateLastLogin(ctx context.Context, id uuid.UUID) error
// ExistsByEmail checks if a user exists with the given email
ExistsByEmail(ctx context.Context, email string) (bool, error)
}
type userService struct {
userRepo interfaces.UserRepository
profileRepo interfaces.UserProfileRepository
auditRepo interfaces.AuditRepository
logger *zap.Logger
}
// NewUserService creates a new user service
func NewUserService(
userRepo interfaces.UserRepository,
profileRepo interfaces.UserProfileRepository,
auditRepo interfaces.AuditRepository,
logger *zap.Logger,
) UserService {
return &userService{
userRepo: userRepo,
profileRepo: profileRepo,
auditRepo: auditRepo,
logger: logger,
}
}
func (s *userService) Create(ctx context.Context, req *domain.CreateUserRequest, actorID string) (*domain.User, error) {
// Validate email uniqueness
exists, err := s.userRepo.ExistsByEmail(ctx, req.Email)
if err != nil {
s.logger.Error("Failed to check email uniqueness", zap.String("email", req.Email), zap.Error(err))
return nil, fmt.Errorf("failed to validate email uniqueness: %w", err)
}
if exists {
return nil, fmt.Errorf("user with email %s already exists", req.Email)
}
// Create user domain object
user := &domain.User{
ID: uuid.New(),
Email: req.Email,
FirstName: req.FirstName,
LastName: req.LastName,
DisplayName: req.DisplayName,
Avatar: req.Avatar,
Role: req.Role,
Status: req.Status,
CreatedBy: actorID,
UpdatedBy: actorID,
}
if user.Status == "" {
user.Status = domain.UserStatusPending
}
// Create user in database
err = s.userRepo.Create(ctx, user)
if err != nil {
s.logger.Error("Failed to create user", zap.String("email", req.Email), zap.Error(err))
return nil, fmt.Errorf("failed to create user: %w", err)
}
// Create default user profile
profile := &domain.UserProfile{
UserID: user.ID,
Bio: "",
Location: "",
Website: "",
Timezone: "UTC",
Language: "en",
Preferences: make(map[string]interface{}),
}
err = s.profileRepo.Create(ctx, profile)
if err != nil {
s.logger.Warn("Failed to create user profile", zap.String("user_id", user.ID.String()), zap.Error(err))
// Don't fail user creation if profile creation fails
}
// Log audit event
if s.auditRepo != nil {
auditEvent := &interfaces.AuditEvent{
ID: uuid.New(),
Type: "user.created",
Severity: "info",
Status: "success",
Timestamp: time.Now().Format(time.RFC3339),
ActorID: actorID,
ActorType: "user",
ResourceID: user.ID.String(),
ResourceType: "user",
Action: "create",
Description: fmt.Sprintf("User %s created", user.Email),
Details: map[string]interface{}{
"user_id": user.ID.String(),
"email": user.Email,
"role": user.Role,
"status": user.Status,
},
}
_ = s.auditRepo.LogEvent(ctx, auditEvent)
}
s.logger.Info("User created successfully",
zap.String("user_id", user.ID.String()),
zap.String("email", user.Email),
zap.String("actor", actorID))
return user, nil
}
func (s *userService) GetByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
s.logger.Debug("Failed to get user by ID", zap.String("id", id.String()), zap.Error(err))
return nil, err
}
return user, nil
}
func (s *userService) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
user, err := s.userRepo.GetByEmail(ctx, email)
if err != nil {
s.logger.Debug("Failed to get user by email", zap.String("email", email), zap.Error(err))
return nil, err
}
return user, nil
}
func (s *userService) Update(ctx context.Context, id uuid.UUID, req *domain.UpdateUserRequest, actorID string) (*domain.User, error) {
// Get existing user
existingUser, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
// Check email uniqueness if email is being updated
if req.Email != nil && *req.Email != existingUser.Email {
exists, err := s.userRepo.ExistsByEmail(ctx, *req.Email)
if err != nil {
s.logger.Error("Failed to check email uniqueness", zap.String("email", *req.Email), zap.Error(err))
return nil, fmt.Errorf("failed to validate email uniqueness: %w", err)
}
if exists {
return nil, fmt.Errorf("user with email %s already exists", *req.Email)
}
}
// Update fields
if req.Email != nil {
existingUser.Email = *req.Email
}
if req.FirstName != nil {
existingUser.FirstName = *req.FirstName
}
if req.LastName != nil {
existingUser.LastName = *req.LastName
}
if req.DisplayName != nil {
existingUser.DisplayName = req.DisplayName
}
if req.Avatar != nil {
existingUser.Avatar = req.Avatar
}
if req.Role != nil {
existingUser.Role = *req.Role
}
if req.Status != nil {
existingUser.Status = *req.Status
}
existingUser.UpdatedBy = actorID
// Update user in database
err = s.userRepo.Update(ctx, existingUser)
if err != nil {
s.logger.Error("Failed to update user", zap.String("id", id.String()), zap.Error(err))
return nil, fmt.Errorf("failed to update user: %w", err)
}
// Log audit event
if s.auditRepo != nil {
auditEvent := &interfaces.AuditEvent{
ID: uuid.New(),
Type: "user.updated",
Severity: "info",
Status: "success",
Timestamp: time.Now().Format(time.RFC3339),
ActorID: actorID,
ActorType: "user",
ResourceID: id.String(),
ResourceType: "user",
Action: "update",
Description: fmt.Sprintf("User %s updated", existingUser.Email),
Details: map[string]interface{}{
"user_id": id.String(),
"email": existingUser.Email,
"role": existingUser.Role,
"status": existingUser.Status,
},
}
_ = s.auditRepo.LogEvent(ctx, auditEvent)
}
s.logger.Info("User updated successfully",
zap.String("user_id", id.String()),
zap.String("email", existingUser.Email),
zap.String("actor", actorID))
return existingUser, nil
}
func (s *userService) Delete(ctx context.Context, id uuid.UUID, actorID string) error {
// Get user for audit logging
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return err
}
// Delete user profile first
_ = s.profileRepo.Delete(ctx, id) // Don't fail if profile doesn't exist
// Delete user
err = s.userRepo.Delete(ctx, id)
if err != nil {
s.logger.Error("Failed to delete user", zap.String("id", id.String()), zap.Error(err))
return fmt.Errorf("failed to delete user: %w", err)
}
// Log audit event
if s.auditRepo != nil {
auditEvent := &interfaces.AuditEvent{
ID: uuid.New(),
Type: "user.deleted",
Severity: "warn",
Status: "success",
Timestamp: time.Now().Format(time.RFC3339),
ActorID: actorID,
ActorType: "user",
ResourceID: id.String(),
ResourceType: "user",
Action: "delete",
Description: fmt.Sprintf("User %s deleted", user.Email),
Details: map[string]interface{}{
"user_id": id.String(),
"email": user.Email,
},
}
_ = s.auditRepo.LogEvent(ctx, auditEvent)
}
s.logger.Info("User deleted successfully",
zap.String("user_id", id.String()),
zap.String("email", user.Email),
zap.String("actor", actorID))
return nil
}
func (s *userService) List(ctx context.Context, req *domain.ListUsersRequest) (*domain.ListUsersResponse, error) {
response, err := s.userRepo.List(ctx, req)
if err != nil {
s.logger.Error("Failed to list users", zap.Error(err))
return nil, fmt.Errorf("failed to list users: %w", err)
}
return response, nil
}
func (s *userService) UpdateLastLogin(ctx context.Context, id uuid.UUID) error {
err := s.userRepo.UpdateLastLogin(ctx, id)
if err != nil {
s.logger.Error("Failed to update last login", zap.String("id", id.String()), zap.Error(err))
return fmt.Errorf("failed to update last login: %w", err)
}
return nil
}
func (s *userService) ExistsByEmail(ctx context.Context, email string) (bool, error) {
exists, err := s.userRepo.ExistsByEmail(ctx, email)
if err != nil {
s.logger.Error("Failed to check email existence", zap.String("email", email), zap.Error(err))
return false, fmt.Errorf("failed to check email existence: %w", err)
}
return exists, nil
}