Files
tm_back/internal/user/repository.go
T
Mazyar 0da0c8b8c9 Implement phone number validation for customer and user registration and updates
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.
2026-06-06 12:03:04 +03:30

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
}