326 lines
9.2 KiB
Go
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
|
|
} |