Files
tm_back/internal/user/service.go
T
n.nakhostin dfcdef60d6 Add Device Token Support for Customer and User Entities
- Introduced a new DeviceToken field in both Customer and User entities to store device tokens for authentication and notifications.
- Updated LoginForm structures for both Customer and User to include an optional DeviceToken field, allowing clients to send device tokens during login.
- Enhanced the Login methods in the customer and user services to append the device token to the respective entities if it does not already exist, improving user session management and notification capabilities.
- Ensured proper validation and structured responses for the new DeviceToken field in the forms, maintaining compliance with best practices for API design.
2025-09-20 10:11:36 +03:30

605 lines
16 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)
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,
})
go s.notification.SendEmail(ctx, form.Email, fmt.Sprintf("Welcome to Tender Management\nUsername: %s\nPassword: %s", form.Username, form.Password), 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,
})
go s.notification.SendEmail(ctx, user.Email, fmt.Sprintf("Your account has been %s", strings.ToUpper(form.Status)), 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
}
// 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,
})
go s.notification.SendEmail(ctx, user.Email, "Your password has been reset successfully", 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
}