29bca402a4
- Updated the notification sending logic in the customer and user service layers to send emails synchronously instead of using goroutines, improving clarity and ensuring immediate feedback on notification processing. - Ensured that all relevant service methods (Register, UpdateStatus, RequestResetPassword, ResetPassword) are updated to reflect these changes, promoting consistency in notification handling across services.
644 lines
17 KiB
Go
644 lines
17 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
"tm/pkg/authorization"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/notification"
|
|
"tm/pkg/response"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Service defines business logic for user operations
|
|
type Service interface {
|
|
// Main CRUD
|
|
Register(ctx context.Context, form *CreateUserForm) (*UserResponse, error)
|
|
Update(ctx context.Context, id string, form *UpdateUserForm) (*UserResponse, error)
|
|
UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error
|
|
GetUserByID(ctx context.Context, userID string) (*UserResponse, error)
|
|
GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error)
|
|
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error)
|
|
Delete(ctx context.Context, id string) error
|
|
|
|
// Profile
|
|
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)
|
|
UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error
|
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
|
ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error
|
|
ResetPassword(ctx context.Context, form *ResetPasswordForm) error
|
|
Logout(ctx context.Context, userID string, accessToken string) error
|
|
}
|
|
|
|
// userService implements the Service interface
|
|
type userService struct {
|
|
repository Repository
|
|
logger logger.Logger
|
|
authService authorization.AuthorizationService
|
|
validator ValidationService
|
|
notification notification.SDK
|
|
}
|
|
|
|
// NewUserService creates a new user service
|
|
func NewUserService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService, notificationSDK notification.SDK) Service {
|
|
return &userService{
|
|
repository: repository,
|
|
logger: logger,
|
|
authService: authService,
|
|
validator: validator,
|
|
notification: notificationSDK,
|
|
}
|
|
}
|
|
|
|
// Register creates a new user
|
|
func (s *userService) Register(ctx context.Context, form *CreateUserForm) (*UserResponse, error) {
|
|
// Check if email already exists
|
|
existingUser, _ := s.repository.GetByEmail(ctx, form.Email)
|
|
if existingUser != nil {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
|
|
// Check if username already exists
|
|
existingUser, _ = s.repository.GetByUsername(ctx, form.Username)
|
|
if existingUser != nil {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
|
|
// Hash password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to process password")
|
|
}
|
|
|
|
// Validate role
|
|
role := UserRole(form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return nil, errors.New("invalid role")
|
|
}
|
|
|
|
// Create user
|
|
user := &User{
|
|
FullName: form.FullName,
|
|
Username: form.Username,
|
|
Email: form.Email,
|
|
Password: string(hashedPassword),
|
|
Role: role,
|
|
Status: UserStatusActive,
|
|
Department: form.Department,
|
|
Position: form.Position,
|
|
Phone: form.Phone,
|
|
ProfileImage: form.ProfileImage,
|
|
IsVerified: true,
|
|
}
|
|
|
|
// Save to database
|
|
err = s.repository.Create(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to register user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"email": form.Email,
|
|
"username": form.Username,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
s.logger.Info("User registered successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"username": user.Username,
|
|
"role": user.Role,
|
|
})
|
|
|
|
s.notification.SendEmail(ctx, notification.Model{
|
|
Recipient: form.Email,
|
|
Title: "Welcome to Opplens",
|
|
Message: fmt.Sprintf("Welcome to Opplens\nUsername: %s\nPassword: %s", form.Username, form.Password),
|
|
Type: "info",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// Update updates user information
|
|
func (s *userService) Update(ctx context.Context, id string, form *UpdateUserForm) (*UserResponse, error) {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.FullName != nil {
|
|
user.FullName = *form.FullName
|
|
}
|
|
|
|
if form.Username != nil {
|
|
// Check if username already exists
|
|
existingUser, _ := s.repository.GetByUsername(ctx, *form.Username)
|
|
if existingUser != nil && existingUser.ID.Hex() != id {
|
|
return nil, errors.New("username already exists")
|
|
}
|
|
user.Username = *form.Username
|
|
}
|
|
|
|
if form.Email != nil {
|
|
// Check if email already exists
|
|
existingUser, _ := s.repository.GetByEmail(ctx, *form.Email)
|
|
if existingUser != nil && existingUser.ID.Hex() != id {
|
|
return nil, errors.New("email already exists")
|
|
}
|
|
user.Email = *form.Email
|
|
}
|
|
|
|
if form.Role != nil {
|
|
role := UserRole(*form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return nil, errors.New("invalid role")
|
|
}
|
|
user.Role = role
|
|
}
|
|
|
|
if form.Department != nil {
|
|
user.Department = form.Department
|
|
}
|
|
|
|
if form.Position != nil {
|
|
user.Position = form.Position
|
|
}
|
|
|
|
if form.Phone != nil {
|
|
user.Phone = form.Phone
|
|
}
|
|
|
|
if form.ProfileImage != nil {
|
|
user.ProfileImage = form.ProfileImage
|
|
}
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return nil, errors.New("failed to update user")
|
|
}
|
|
|
|
s.logger.Info("User updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// UpdateStatus updates user status
|
|
func (s *userService) UpdateStatus(ctx context.Context, id string, form *UpdateUserStatusForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Update status
|
|
user.Status = UserStatus(form.Status)
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user status", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
"status": form.Status,
|
|
})
|
|
return errors.New("failed to update user status")
|
|
}
|
|
|
|
s.logger.Info("User status updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
"status": form.Status,
|
|
})
|
|
|
|
s.notification.SendEmail(ctx, notification.Model{
|
|
Recipient: user.Email,
|
|
Title: "Account Status Updated",
|
|
Message: fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)),
|
|
Type: "warning",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetUserByID retrieves a user by ID
|
|
func (s *userService) GetUserByID(ctx context.Context, id string) (*UserResponse, error) {
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user.ToResponse(), nil
|
|
}
|
|
|
|
// GetUsersByIDs retrieves users by IDs
|
|
func (s *userService) GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error) {
|
|
users, err := s.repository.GetByIDs(ctx, userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var userResponses []*UserResponse
|
|
for _, user := range users {
|
|
userResponses = append(userResponses, user.ToResponse())
|
|
}
|
|
|
|
return &UserListResponse{
|
|
Users: userResponses,
|
|
}, nil
|
|
}
|
|
|
|
// Search searches users with search and filters
|
|
func (s *userService) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error) {
|
|
// Get users
|
|
users, total, err := s.repository.Search(ctx, form, pagination)
|
|
if err != nil {
|
|
s.logger.Error("Failed to list users", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to list users")
|
|
}
|
|
|
|
// Convert to responses
|
|
var userResponses []*UserResponse
|
|
for _, user := range users {
|
|
userResponses = append(userResponses, user.ToResponse())
|
|
}
|
|
|
|
return &UserListResponse{
|
|
Users: userResponses,
|
|
Meta: pagination.Response(total),
|
|
}, nil
|
|
}
|
|
|
|
// Delete deletes a user (hard delete)
|
|
func (s *userService) Delete(ctx context.Context, id string) error {
|
|
err := s.repository.Delete(ctx, id)
|
|
if err != nil {
|
|
s.logger.Error("Failed to delete user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return errors.New("failed to delete user")
|
|
}
|
|
|
|
s.logger.Info("User deleted successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Login authenticates a user
|
|
func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) {
|
|
// Find user by email or username
|
|
var user *User
|
|
var err error
|
|
|
|
if strings.Contains(form.Username, "@") {
|
|
user, err = s.repository.GetByEmail(ctx, form.Username)
|
|
} else {
|
|
user, err = s.repository.GetByUsername(ctx, form.Username)
|
|
}
|
|
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with invalid credentials", map[string]interface{}{
|
|
"email_or_username": form.Username,
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// Check if user is active
|
|
if user.Status != UserStatusActive {
|
|
return nil, errors.New("account is inactive")
|
|
}
|
|
|
|
// Verify password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.Password))
|
|
if err != nil {
|
|
s.logger.Warn("Login attempt with wrong password", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
return nil, errors.New("invalid credentials")
|
|
}
|
|
|
|
// append device token to user if not exists
|
|
if !slices.Contains(user.DeviceToken, form.DeviceToken) {
|
|
user.DeviceToken = append(user.DeviceToken, form.DeviceToken)
|
|
}
|
|
|
|
// Update last login
|
|
err = s.repository.Login(ctx, user.ID.Hex())
|
|
if err != nil {
|
|
s.logger.Error("Failed to update last login", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": user.ID,
|
|
})
|
|
}
|
|
|
|
tokenPair, err := s.authService.GenerateTokenPair(
|
|
user.ID.Hex(),
|
|
user.Email,
|
|
string(user.Role),
|
|
"",
|
|
)
|
|
if err != nil {
|
|
s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": user.ID,
|
|
})
|
|
return nil, errors.New("failed to generate authentication tokens")
|
|
}
|
|
|
|
s.logger.Info("User logged in successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"role": user.Role,
|
|
})
|
|
|
|
return &AuthResponse{
|
|
User: user.ToResponse(),
|
|
AccessToken: tokenPair.AccessToken,
|
|
RefreshToken: tokenPair.RefreshToken,
|
|
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
|
|
}, nil
|
|
}
|
|
|
|
// RefreshToken refreshes the access token
|
|
func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) {
|
|
// Validate refresh token and generate new access token
|
|
newAccessToken, err := s.authService.RefreshAccessToken(refreshToken)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to refresh access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid or expired refresh token")
|
|
}
|
|
|
|
// Parse the new access token to get user information
|
|
validationResult, err := s.authService.ValidateAccessToken(newAccessToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to validate new access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("failed to generate valid access token")
|
|
}
|
|
|
|
if !validationResult.Valid {
|
|
s.logger.Error("New access token validation failed", map[string]interface{}{
|
|
"error": validationResult.Error,
|
|
})
|
|
return nil, errors.New("failed to generate valid access token")
|
|
}
|
|
|
|
// Get user information
|
|
userID, err := bson.ObjectIDFromHex(validationResult.Claims.UserID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to parse user ID from token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, errors.New("invalid token format")
|
|
}
|
|
|
|
user, err := s.repository.GetByID(ctx, userID.Hex())
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user for token refresh", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID.Hex(),
|
|
})
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
// Check if user is still active
|
|
if user.Status != UserStatusActive {
|
|
return nil, errors.New("user account is inactive")
|
|
}
|
|
|
|
s.logger.Info("Access token refreshed successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
|
|
return &AuthResponse{
|
|
User: user.ToResponse(),
|
|
AccessToken: newAccessToken,
|
|
RefreshToken: refreshToken, // Return the same refresh token
|
|
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(), // 15 minutes from now
|
|
}, nil
|
|
}
|
|
|
|
// ChangePassword changes user password
|
|
func (s *userService) ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Verify old password
|
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(form.OldPassword))
|
|
if err != nil {
|
|
return errors.New("invalid old password")
|
|
}
|
|
|
|
// Hash new password
|
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
s.logger.Error("Failed to hash new password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to process new password")
|
|
}
|
|
|
|
// Update password
|
|
user.Password = string(hashedPassword)
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update password", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to update password")
|
|
}
|
|
|
|
s.logger.Info("Password changed successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
s.notification.SendEmail(ctx, notification.Model{
|
|
Recipient: user.Email,
|
|
Title: "Password Reset Success",
|
|
Message: "Your password has been reset successfully",
|
|
Type: "info",
|
|
Priority: "important",
|
|
UserID: user.ID.Hex(),
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// ResetPassword initiates password reset process
|
|
func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm) error {
|
|
// Get user by email
|
|
user, err := s.repository.GetByEmail(ctx, form.Email)
|
|
if err != nil {
|
|
// Don't reveal if user exists or not for security
|
|
s.logger.Info("Password reset requested for non-existent email", map[string]interface{}{
|
|
"email": form.Email,
|
|
})
|
|
return nil // Return success even if user doesn't exist
|
|
}
|
|
|
|
// TODO: Implement password reset logic (send email with reset link)
|
|
s.logger.Info("Password reset requested", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetProfile retrieves the current user's profile
|
|
func (s *userService) GetProfile(ctx context.Context, userID string) (*User, error) {
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user profile", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return nil, errors.New("user not found")
|
|
}
|
|
|
|
s.logger.Info("User profile retrieved successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// UpdateProfile updates the current user's profile
|
|
func (s *userService) UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error {
|
|
user, err := s.repository.GetByID(ctx, userID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get user for profile update", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Update fields if provided
|
|
if form.FullName != nil {
|
|
user.FullName = *form.FullName
|
|
}
|
|
if form.Department != nil {
|
|
user.Department = form.Department
|
|
}
|
|
if form.Position != nil {
|
|
user.Position = form.Position
|
|
}
|
|
if form.Phone != nil {
|
|
user.Phone = form.Phone
|
|
}
|
|
if form.ProfileImage != nil {
|
|
user.ProfileImage = form.ProfileImage
|
|
}
|
|
|
|
// Update timestamp
|
|
user.UpdatedAt = time.Now().Unix()
|
|
|
|
// Save to database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user profile", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
return errors.New("failed to update profile")
|
|
}
|
|
|
|
s.logger.Info("User profile updated successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateUserRole updates user role
|
|
func (s *userService) UpdateRole(ctx context.Context, id string, form *UpdateUserRoleForm) error {
|
|
// Get user
|
|
user, err := s.repository.GetByID(ctx, id)
|
|
if err != nil {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
// Validate role
|
|
role := UserRole(form.Role)
|
|
if !s.validator.IsValidRole(role) {
|
|
return errors.New("invalid role")
|
|
}
|
|
|
|
// Update role
|
|
user.Role = role
|
|
|
|
// Update in database
|
|
err = s.repository.Update(ctx, user)
|
|
if err != nil {
|
|
s.logger.Error("Failed to update user role", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
"role": form.Role,
|
|
})
|
|
return errors.New("failed to update user role")
|
|
}
|
|
|
|
s.logger.Info("User role updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
"role": form.Role,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Logout logs out a user and invalidates tokens
|
|
func (s *userService) Logout(ctx context.Context, userID string, accessToken string) error {
|
|
// Invalidate the access token
|
|
err := s.authService.InvalidateToken(accessToken)
|
|
if err != nil {
|
|
s.logger.Error("Failed to invalidate access token", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": userID,
|
|
})
|
|
// Don't return error here as the user should still be logged out
|
|
}
|
|
|
|
s.logger.Info("User logged out successfully", map[string]interface{}{
|
|
"user_id": userID,
|
|
})
|
|
|
|
return nil
|
|
}
|