0da0c8b8c9
Added functionality to check for existing customers and users by phone number during registration and updates. Updated the handler, service, and repository layers to include phone number checks, ensuring unique phone numbers across customers and users.
385 lines
10 KiB
Go
385 lines
10 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
"tm/pkg/logger"
|
|
orm "tm/pkg/mongo"
|
|
"tm/pkg/response"
|
|
|
|
"go.mongodb.org/mongo-driver/v2/bson"
|
|
mongopkg "go.mongodb.org/mongo-driver/v2/mongo"
|
|
)
|
|
|
|
// Repository defines methods for user data access
|
|
type Repository interface {
|
|
Login(ctx context.Context, id string) error
|
|
Create(ctx context.Context, user *User) error
|
|
Update(ctx context.Context, user *User) error
|
|
Delete(ctx context.Context, id string) error
|
|
GetByID(ctx context.Context, id string) (*User, error)
|
|
UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error
|
|
GetByEmail(ctx context.Context, email string) (*User, error)
|
|
GetByUsername(ctx context.Context, username string) (*User, error)
|
|
GetByPhone(ctx context.Context, phone string) (*User, error)
|
|
GetByIDs(ctx context.Context, userIDs []string) ([]User, error)
|
|
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*orm.PaginatedResult[User], error)
|
|
}
|
|
|
|
// userRepository implements the Repository interface using the MongoDB ORM
|
|
type userRepository struct {
|
|
ormRepo orm.Repository[User]
|
|
collection *mongopkg.Collection
|
|
logger logger.Logger
|
|
}
|
|
|
|
// NewUserRepository creates a new user repository
|
|
func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Repository {
|
|
// Create indexes using the ORM's index management
|
|
indexes := []orm.Index{
|
|
*orm.CreateTextIndex("search_idx", "full_name", "email", "username"),
|
|
*orm.CreateUniqueIndex("phone_idx", bson.D{{Key: "phone", Value: 1}}).
|
|
WithPartialFilterExpression(bson.M{"phone": bson.M{"$type": "string", "$ne": ""}}),
|
|
}
|
|
|
|
// Create indexes
|
|
err := mongoManager.CreateIndexes("users", indexes)
|
|
if err != nil {
|
|
logger.Warn("Failed to create user indexes", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
}
|
|
|
|
// Create ORM repository
|
|
collection := mongoManager.GetCollection("users")
|
|
ormRepo := orm.NewRepository[User](collection, logger)
|
|
|
|
return &userRepository{
|
|
ormRepo: ormRepo,
|
|
collection: collection,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Login updates the last login timestamp
|
|
func (r *userRepository) Login(ctx context.Context, id string) error {
|
|
// Get user first
|
|
user, err := r.GetByID(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update last login timestamp
|
|
user.LastLoginAt = &[]int64{time.Now().Unix()}[0]
|
|
user.SetUpdatedAt(time.Now().Unix())
|
|
|
|
// Update in database
|
|
err = r.ormRepo.Update(ctx, user)
|
|
if err != nil {
|
|
r.logger.Error("Failed to update last login", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("Last login updated successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Create creates a new user
|
|
func (r *userRepository) Create(ctx context.Context, user *User) error {
|
|
// Set created timestamp using Unix timestamp
|
|
now := time.Now().Unix()
|
|
user.SetCreatedAt(now)
|
|
|
|
// Use ORM to create user
|
|
err := r.ormRepo.Create(ctx, user)
|
|
if err != nil {
|
|
r.logger.Error("Failed to create user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"email": user.Email,
|
|
"user_id": user.ID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("User created successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
"role": user.Role,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// Update updates a user
|
|
func (r *userRepository) Update(ctx context.Context, user *User) error {
|
|
// Set updated timestamp using Unix timestamp
|
|
user.SetUpdatedAt(time.Now().Unix())
|
|
|
|
// Use ORM to update user
|
|
err := r.ormRepo.Update(ctx, user)
|
|
if err != nil {
|
|
// Backward compatibility for legacy records with string _id.
|
|
legacyFilterID := user.ID.Hex()
|
|
updateDoc := bson.M{
|
|
"$set": bson.M{
|
|
"full_name": user.FullName,
|
|
"username": user.Username,
|
|
"email": user.Email,
|
|
"password": user.Password,
|
|
"role": user.Role,
|
|
"status": user.Status,
|
|
"department": user.Department,
|
|
"position": user.Position,
|
|
"phone": user.Phone,
|
|
"profile_image": user.ProfileImage,
|
|
"device_token": user.DeviceToken,
|
|
"is_verified": user.IsVerified,
|
|
"last_login_at": user.LastLoginAt,
|
|
"updated_at": user.UpdatedAt,
|
|
"updated_by": user.UpdatedBy,
|
|
},
|
|
}
|
|
legacyResult, legacyErr := r.collection.UpdateOne(ctx, bson.M{"_id": legacyFilterID}, updateDoc)
|
|
if legacyErr == nil && legacyResult.MatchedCount > 0 {
|
|
r.logger.Info("User updated successfully using legacy string ID", map[string]interface{}{
|
|
"user_id": legacyFilterID,
|
|
"email": user.Email,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
r.logger.Error("Failed to update user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": user.ID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("User updated successfully", map[string]interface{}{
|
|
"user_id": user.ID,
|
|
"email": user.Email,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves a user by ID
|
|
func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) {
|
|
user, err := r.ormRepo.FindByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
// Backward compatibility: some legacy seed data may store _id as string instead of ObjectID.
|
|
legacyUser, legacyErr := r.ormRepo.FindOne(ctx, bson.M{"_id": id})
|
|
if legacyErr == nil {
|
|
return legacyUser, nil
|
|
}
|
|
if errors.Is(legacyErr, orm.ErrDocumentNotFound) {
|
|
return nil, ErrUserNotFound
|
|
}
|
|
r.logger.Error("Failed to get user by legacy string ID", map[string]interface{}{
|
|
"error": legacyErr.Error(),
|
|
"user_id": id,
|
|
})
|
|
return nil, legacyErr
|
|
}
|
|
r.logger.Error("Failed to get user by ID", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// UpdatePasswordReset atomically persists a new password hash and audit fields.
|
|
func (r *userRepository) UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error {
|
|
now := time.Now().Unix()
|
|
updateDoc := bson.M{
|
|
"$set": bson.M{
|
|
"password": hashedPassword,
|
|
"updated_at": now,
|
|
"updated_by": updatedBy,
|
|
},
|
|
}
|
|
|
|
objectID, err := bson.ObjectIDFromHex(id)
|
|
if err == nil {
|
|
result, updateErr := r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, updateDoc)
|
|
if updateErr == nil && result.MatchedCount > 0 {
|
|
return nil
|
|
}
|
|
if updateErr != nil {
|
|
return updateErr
|
|
}
|
|
}
|
|
|
|
result, err := r.collection.UpdateOne(ctx, bson.M{"_id": id}, updateDoc)
|
|
if err != nil {
|
|
r.logger.Error("Failed to update password reset", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return err
|
|
}
|
|
if result.MatchedCount == 0 {
|
|
return ErrUserNotFound
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByEmail retrieves a user by email
|
|
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
|
|
user, err := r.ormRepo.FindOne(ctx, bson.M{"email": email})
|
|
if err != nil {
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
r.logger.Error("Failed to get user by email", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"email": email,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetByPhone retrieves a user by phone number
|
|
func (r *userRepository) GetByPhone(ctx context.Context, phone string) (*User, error) {
|
|
user, err := r.ormRepo.FindOne(ctx, bson.M{"phone": phone})
|
|
if err != nil {
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
r.logger.Error("Failed to get user by phone", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"phone": phone,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// GetByUsername retrieves a user by username
|
|
func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) {
|
|
user, err := r.ormRepo.FindOne(ctx, bson.M{"username": username})
|
|
if err != nil {
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
r.logger.Error("Failed to get user by username", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"username": username,
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// Delete deletes a user (soft delete by setting status to inactive)
|
|
func (r *userRepository) Delete(ctx context.Context, id string) error {
|
|
// Get user first
|
|
_, err := r.GetByID(ctx, id)
|
|
if err != nil {
|
|
r.logger.Error("Failed to delete user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
if errors.Is(err, orm.ErrDocumentNotFound) {
|
|
return errors.New("user not found")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Delete in database
|
|
err = r.ormRepo.Delete(ctx, id)
|
|
if err != nil {
|
|
r.logger.Error("Failed to delete user", map[string]interface{}{
|
|
"error": err.Error(),
|
|
"user_id": id,
|
|
})
|
|
return err
|
|
}
|
|
|
|
r.logger.Info("User deleted successfully", map[string]interface{}{
|
|
"user_id": id,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByIDs retrieves users by IDs
|
|
func (r *userRepository) GetByIDs(ctx context.Context, userIDs []string) ([]User, error) {
|
|
objectIDs := make([]bson.ObjectID, 0)
|
|
for _, id := range userIDs {
|
|
objectID, err := bson.ObjectIDFromHex(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
objectIDs = append(objectIDs, objectID)
|
|
}
|
|
|
|
users, err := r.ormRepo.FindAll(ctx, bson.M{"_id": bson.M{"$in": objectIDs}}, orm.NewPaginationBuilder().Limit(len(userIDs)).Build())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return users.Items, nil
|
|
}
|
|
|
|
// Search retrieves users with pagination
|
|
func (r *userRepository) Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*orm.PaginatedResult[User], error) {
|
|
filter := bson.M{}
|
|
|
|
if form.Search != nil {
|
|
filter["$or"] = bson.A{
|
|
bson.M{"full_name": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
bson.M{"email": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
bson.M{"username": bson.M{"$regex": form.Search, "$options": "i"}},
|
|
}
|
|
}
|
|
|
|
if form.Status != nil {
|
|
filter["status"] = form.Status
|
|
}
|
|
|
|
if form.Role != nil {
|
|
filter["role"] = form.Role
|
|
}
|
|
|
|
mongoPagination, err := orm.BuildListPagination(
|
|
pagination.Limit,
|
|
pagination.Offset,
|
|
pagination.Cursor,
|
|
pagination.SortBy,
|
|
pagination.SortOrder,
|
|
filter,
|
|
orm.ListPaginationOptions{},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result, err := r.ormRepo.FindAll(ctx, filter, mongoPagination)
|
|
if err != nil {
|
|
r.logger.Error("Failed to list users", map[string]interface{}{
|
|
"error": err.Error(),
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|