05e7b50ec4
- Updated user-related API handlers to return user entities directly instead of using the ToResponse method, simplifying response structures. - Modified service methods to return UserResponse types instead of User, enhancing consistency in response formats. - Improved pagination handling in the response package by setting default values for sorting parameters, ensuring more predictable behavior in API responses.
589 lines
16 KiB
Go
589 lines
16 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
"tm/pkg/authorization"
|
|
"tm/pkg/logger"
|
|
"tm/pkg/response"
|
|
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
"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
|
|
}
|
|
|
|
// NewUserService creates a new user service
|
|
func NewUserService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, validator ValidationService) Service {
|
|
return &userService{
|
|
repository: repository,
|
|
logger: logger,
|
|
authService: authService,
|
|
validator: validator,
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
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,
|
|
})
|
|
|
|
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")
|
|
}
|
|
|
|
// 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 := primitive.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,
|
|
})
|
|
|
|
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
|
|
}
|