-
This commit is contained in:
326
user/internal/services/user_service.go
Normal file
326
user/internal/services/user_service.go
Normal file
@ -0,0 +1,326 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user