diff --git a/internal/user/entity.go b/internal/user/entity.go index fdb5cc6..f309c6f 100644 --- a/internal/user/entity.go +++ b/internal/user/entity.go @@ -1,7 +1,7 @@ package user import ( - "github.com/google/uuid" + "tm/pkg/mongo" ) // UserRole represents user roles in the system @@ -25,22 +25,50 @@ const ( // User represents a system user (admin/manager/operator) type User struct { - ID uuid.UUID `bson:"_id"` - FullName string `bson:"full_name"` - Username string `bson:"username"` - Email string `bson:"email"` - Password string `bson:"password"` // Never serialize password - Role UserRole `bson:"role"` - Status UserStatus `bson:"status"` - CompanyID *uuid.UUID `bson:"company_id,omitempty"` - Department *string `bson:"department,omitempty"` - Position *string `bson:"position,omitempty"` - Phone *string `bson:"phone,omitempty"` - ProfileImage *string `bson:"profile_image,omitempty"` - IsVerified bool `bson:"is_verified"` - LastLoginAt *int64 `bson:"last_login_at,omitempty"` // Unix timestamp - CreatedAt int64 `bson:"created_at"` // Unix timestamp - UpdatedAt int64 `bson:"updated_at"` // Unix timestamp - CreatedBy *uuid.UUID `bson:"created_by,omitempty"` - UpdatedBy *uuid.UUID `bson:"updated_by,omitempty"` + mongo.Model + FullName string `bson:"full_name" json:"full_name"` + Username string `bson:"username" json:"username"` + Email string `bson:"email" json:"email"` + Password string `bson:"password" json:"-"` // Never serialize password + Role UserRole `bson:"role" json:"role"` + Status UserStatus `bson:"status" json:"status"` + CompanyID *string `bson:"company_id,omitempty" json:"company_id,omitempty"` + Department *string `bson:"department,omitempty" json:"department,omitempty"` + Position *string `bson:"position,omitempty" json:"position,omitempty"` + Phone *string `bson:"phone,omitempty" json:"phone,omitempty"` + ProfileImage *string `bson:"profile_image,omitempty" json:"profile_image,omitempty"` + IsVerified bool `bson:"is_verified" json:"is_verified"` + LastLoginAt *int64 `bson:"last_login_at,omitempty" json:"last_login_at,omitempty"` // Unix timestamp + CreatedBy *string `bson:"created_by,omitempty" json:"created_by,omitempty"` + UpdatedBy *string `bson:"updated_by,omitempty" json:"updated_by,omitempty"` +} + +// SetID sets the user ID (implements IDSetter interface) +func (u *User) SetID(id string) { + u.ID = id +} + +// GetID returns the user ID (implements IDGetter interface) +func (u *User) GetID() string { + return u.ID +} + +// SetCreatedAt sets the created timestamp (implements Timestampable interface) +func (u *User) SetCreatedAt(timestamp int64) { + u.CreatedAt = timestamp +} + +// SetUpdatedAt sets the updated timestamp (implements Timestampable interface) +func (u *User) SetUpdatedAt(timestamp int64) { + u.UpdatedAt = timestamp +} + +// GetCreatedAt returns the created timestamp (implements Timestampable interface) +func (u *User) GetCreatedAt() int64 { + return u.CreatedAt +} + +// GetUpdatedAt returns the updated timestamp (implements Timestampable interface) +func (u *User) GetUpdatedAt() int64 { + return u.UpdatedAt } diff --git a/internal/user/form.go b/internal/user/form.go index 49ef33c..c337f13 100644 --- a/internal/user/form.go +++ b/internal/user/form.go @@ -112,29 +112,14 @@ type UserListResponse struct { // Helper function to convert User to UserResponse func (u *User) ToResponse() *UserResponse { - companyID := "" - if u.CompanyID != nil { - companyID = u.CompanyID.String() - } - - createdBy := "" - if u.CreatedBy != nil { - createdBy = u.CreatedBy.String() - } - - updatedBy := "" - if u.UpdatedBy != nil { - updatedBy = u.UpdatedBy.String() - } - return &UserResponse{ - ID: u.ID.String(), + ID: u.ID, FullName: u.FullName, Username: u.Username, Email: u.Email, Role: string(u.Role), Status: string(u.Status), - CompanyID: &companyID, + CompanyID: u.CompanyID, Department: u.Department, Position: u.Position, Phone: u.Phone, @@ -143,7 +128,7 @@ func (u *User) ToResponse() *UserResponse { LastLoginAt: u.LastLoginAt, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, - CreatedBy: &createdBy, - UpdatedBy: &updatedBy, + CreatedBy: u.CreatedBy, + UpdatedBy: u.UpdatedBy, } } diff --git a/internal/user/handler.go b/internal/user/handler.go index 30aea71..270c0f4 100644 --- a/internal/user/handler.go +++ b/internal/user/handler.go @@ -7,7 +7,6 @@ import ( "tm/pkg/response" "github.com/asaskevich/govalidator" - "github.com/google/uuid" "github.com/labstack/echo/v4" ) @@ -366,12 +365,8 @@ func (h *Handler) ListUsers(c echo.Context) error { // @Router /admin/v1/users/{id} [get] func (h *Handler) GetUserByID(c echo.Context) error { idStr := c.Param("id") - userID, err := uuid.Parse(idStr) - if err != nil { - return response.BadRequest(c, "Invalid user ID", "") - } - user, err := h.service.GetUserByID(c.Request().Context(), userID) + user, err := h.service.GetUserByID(c.Request().Context(), idStr) if err != nil { return response.NotFound(c, "User not found") } @@ -403,10 +398,6 @@ func (h *Handler) UpdateUser(c echo.Context) error { } idStr := c.Param("id") - userID, err := uuid.Parse(idStr) - if err != nil { - return response.BadRequest(c, "Invalid user ID", "") - } var form UpdateUserForm if err := c.Bind(&form); err != nil { @@ -419,7 +410,7 @@ func (h *Handler) UpdateUser(c echo.Context) error { } // Call service - user, err := h.service.UpdateUser(c.Request().Context(), userID, &form, ¤tUserID) + user, err := h.service.UpdateUser(c.Request().Context(), idStr, &form, ¤tUserID) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -450,12 +441,8 @@ func (h *Handler) DeleteUser(c echo.Context) error { } idStr := c.Param("id") - userID, err := uuid.Parse(idStr) - if err != nil { - return response.BadRequest(c, "Invalid user ID", "") - } - err = h.service.DeleteUser(c.Request().Context(), userID, ¤tUserID) + err = h.service.DeleteUser(c.Request().Context(), idStr, ¤tUserID) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -488,10 +475,6 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { } idStr := c.Param("id") - userID, err := uuid.Parse(idStr) - if err != nil { - return response.BadRequest(c, "Invalid user ID", "") - } var form UpdateUserStatusForm if err := c.Bind(&form); err != nil { @@ -504,7 +487,7 @@ func (h *Handler) UpdateUserStatus(c echo.Context) error { } // Call service - err = h.service.UpdateUserStatus(c.Request().Context(), userID, &form, ¤tUserID) + err = h.service.UpdateUserStatus(c.Request().Context(), idStr, &form, ¤tUserID) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -538,10 +521,6 @@ func (h *Handler) UpdateUserRole(c echo.Context) error { } idStr := c.Param("id") - userID, err := uuid.Parse(idStr) - if err != nil { - return response.BadRequest(c, "Invalid user ID", "") - } var form UpdateUserRoleForm if err := c.Bind(&form); err != nil { @@ -554,7 +533,7 @@ func (h *Handler) UpdateUserRole(c echo.Context) error { } // Call service - err = h.service.UpdateUserRole(c.Request().Context(), userID, &form, ¤tUserID) + err = h.service.UpdateUserRole(c.Request().Context(), idStr, &form, ¤tUserID) if err != nil { return response.BadRequest(c, err.Error(), "") } @@ -582,10 +561,6 @@ func (h *Handler) UpdateUserRole(c echo.Context) error { // @Router /admin/v1/users/company/{company_id} [get] func (h *Handler) GetUsersByCompanyID(c echo.Context) error { companyIDStr := c.Param("company_id") - companyID, err := uuid.Parse(companyIDStr) - if err != nil { - return response.BadRequest(c, "Invalid company ID", "") - } // Get query parameters limitStr := c.QueryParam("limit") @@ -608,7 +583,7 @@ func (h *Handler) GetUsersByCompanyID(c echo.Context) error { } // Call service - users, total, err := h.service.GetUsersByCompanyID(c.Request().Context(), companyID, limit, offset) + users, total, err := h.service.GetUsersByCompanyID(c.Request().Context(), companyIDStr, limit, offset) if err != nil { return response.InternalServerError(c, "Failed to get users by company") } diff --git a/internal/user/middleware.go b/internal/user/middleware.go index 4950a99..7fb8b19 100644 --- a/internal/user/middleware.go +++ b/internal/user/middleware.go @@ -53,7 +53,7 @@ func (h *Handler) AuthMiddleware() echo.MiddlewareFunc { } // Store user information in context for handlers to use - c.Set("user_id", userID) + c.Set("user_id", userID.String()) c.Set("user_email", validationResult.Claims.Email) c.Set("user_role", validationResult.Claims.Role) c.Set("company_id", validationResult.Claims.CompanyID) @@ -147,10 +147,10 @@ func (h *Handler) CompanyAccessMiddleware() echo.MiddlewareFunc { } // GetUserIDFromContext extracts user ID from Echo context -func GetUserIDFromContext(c echo.Context) (uuid.UUID, error) { - userID, ok := c.Get("user_id").(uuid.UUID) +func GetUserIDFromContext(c echo.Context) (string, error) { + userID, ok := c.Get("user_id").(string) if !ok { - return uuid.Nil, echo.NewHTTPError(http.StatusUnauthorized, "User ID not found in context") + return "", echo.NewHTTPError(http.StatusUnauthorized, "User ID not found in context") } return userID, nil } diff --git a/internal/user/repository.go b/internal/user/repository.go index 07f5fe2..f53d62f 100644 --- a/internal/user/repository.go +++ b/internal/user/repository.go @@ -7,104 +7,58 @@ import ( "tm/pkg/logger" mongopkg "tm/pkg/mongo" - "github.com/google/uuid" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" ) // Repository defines methods for user data access type Repository interface { Create(ctx context.Context, user *User) error - GetByID(ctx context.Context, id uuid.UUID) (*User, error) + GetByID(ctx context.Context, id string) (*User, error) GetByEmail(ctx context.Context, email string) (*User, error) GetByUsername(ctx context.Context, username string) (*User, error) Update(ctx context.Context, user *User) error - Delete(ctx context.Context, id uuid.UUID) error + Delete(ctx context.Context, id string) error List(ctx context.Context, limit, offset int) ([]*User, error) - Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error) - GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error) + Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error) + GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) - UpdateLastLogin(ctx context.Context, id uuid.UUID) error - CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) + UpdateLastLogin(ctx context.Context, id string) error + CountByCompanyID(ctx context.Context, companyID string) (int64, error) } -// userRepository implements the Repository interface +// userRepository implements the Repository interface using the MongoDB ORM type userRepository struct { - collection *mongo.Collection - logger logger.Logger + ormRepo mongopkg.Repository[User] + logger logger.Logger } // NewUserRepository creates a new user repository func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.Logger) Repository { - collection := mongoManager.GetCollection("users") + // Create indexes using the ORM's index management + indexes := []mongopkg.Index{ + *mongopkg.CreateUniqueIndex("email_idx", bson.D{{Key: "email", Value: 1}}), + *mongopkg.CreateUniqueIndex("username_idx", bson.D{{Key: "username", Value: 1}}), + *mongopkg.NewIndex("company_id_idx", bson.D{{Key: "company_id", Value: 1}}), + *mongopkg.NewIndex("role_idx", bson.D{{Key: "role", Value: 1}}), + *mongopkg.NewIndex("status_idx", bson.D{{Key: "status", Value: 1}}), + *mongopkg.NewIndex("created_at_idx", bson.D{{Key: "created_at", Value: -1}}), + *mongopkg.CreateTextIndex("search_idx", "full_name", "email", "username", "department", "position"), + } // Create indexes - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // Email index (unique) - emailIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "email", Value: 1}}, - Options: options.Index().SetUnique(true), - } - - // Username index (unique) - usernameIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "username", Value: 1}}, - Options: options.Index().SetUnique(true), - } - - // Company ID index - companyIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "company_id", Value: 1}}, - } - - // Role index - roleIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "role", Value: 1}}, - } - - // Status index - statusIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "status", Value: 1}}, - } - - // Created at index - createdAtIndex := mongo.IndexModel{ - Keys: bson.D{{Key: "created_at", Value: -1}}, - } - - // Full text search index - textIndex := mongo.IndexModel{ - Keys: bson.D{ - {Key: "full_name", Value: "text"}, - {Key: "email", Value: "text"}, - {Key: "username", Value: "text"}, - {Key: "department", Value: "text"}, - {Key: "position", Value: "text"}, - }, - } - - _, err := collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ - emailIndex, - usernameIndex, - companyIndex, - roleIndex, - statusIndex, - createdAtIndex, - textIndex, - }) - + err := mongoManager.CreateIndexes("users", indexes) if err != nil { logger.Warn("Failed to create user indexes", map[string]interface{}{ "error": err.Error(), }) } + // Create ORM repository + ormRepo := mongopkg.NewRepository[User](mongoManager.GetCollection("users"), logger) + return &userRepository{ - collection: collection, - logger: logger, + ormRepo: ormRepo, + logger: logger, } } @@ -112,25 +66,22 @@ func NewUserRepository(mongoManager *mongopkg.ConnectionManager, logger logger.L func (r *userRepository) Create(ctx context.Context, user *User) error { // Set created/updated timestamps using Unix timestamps now := time.Now().Unix() - user.CreatedAt = now - user.UpdatedAt = now + user.SetCreatedAt(now) + user.SetUpdatedAt(now) - // Insert user - _, err := r.collection.InsertOne(ctx, user) + // Use ORM to create user + err := r.ormRepo.Create(ctx, user) if err != nil { - if mongo.IsDuplicateKeyError(err) { - return errors.New("user already exists") - } r.logger.Error("Failed to create user", map[string]interface{}{ "error": err.Error(), "email": user.Email, - "user_id": user.ID.String(), + "user_id": user.ID, }) return err } r.logger.Info("User created successfully", map[string]interface{}{ - "user_id": user.ID.String(), + "user_id": user.ID, "email": user.Email, "role": user.Role, }) @@ -139,35 +90,28 @@ func (r *userRepository) Create(ctx context.Context, user *User) error { } // GetByID retrieves a user by ID -func (r *userRepository) GetByID(ctx context.Context, id uuid.UUID) (*User, error) { - var user User - - filter := bson.M{"_id": id} - err := r.collection.FindOne(ctx, filter).Decode(&user) - +func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error) { + user, err := r.ormRepo.FindByID(ctx, id) if err != nil { - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by ID", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "user_id": id, }) return nil, err } - return &user, nil + return user, nil } // GetByEmail retrieves a user by email func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, error) { - var user User - filter := bson.M{"email": email} - err := r.collection.FindOne(ctx, filter).Decode(&user) - + user, err := r.ormRepo.FindOne(ctx, filter) if err != nil { - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by email", map[string]interface{}{ @@ -177,18 +121,15 @@ func (r *userRepository) GetByEmail(ctx context.Context, email string) (*User, e return nil, err } - return &user, nil + return user, nil } // GetByUsername retrieves a user by username func (r *userRepository) GetByUsername(ctx context.Context, username string) (*User, error) { - var user User - filter := bson.M{"username": username} - err := r.collection.FindOne(ctx, filter).Decode(&user) - + user, err := r.ormRepo.FindOne(ctx, filter) if err != nil { - if err == mongo.ErrNoDocuments { + if errors.Is(err, mongopkg.ErrDocumentNotFound) { return nil, errors.New("user not found") } r.logger.Error("Failed to get user by username", map[string]interface{}{ @@ -198,32 +139,26 @@ func (r *userRepository) GetByUsername(ctx context.Context, username string) (*U return nil, err } - return &user, nil + return user, nil } // Update updates a user func (r *userRepository) Update(ctx context.Context, user *User) error { // Set updated timestamp using Unix timestamp - user.UpdatedAt = time.Now().Unix() + user.SetUpdatedAt(time.Now().Unix()) - filter := bson.M{"_id": user.ID} - update := bson.M{"$set": user} - - result, err := r.collection.UpdateOne(ctx, filter, update) + // Use ORM to update user + err := r.ormRepo.Update(ctx, user) if err != nil { r.logger.Error("Failed to update user", map[string]interface{}{ "error": err.Error(), - "user_id": user.ID.String(), + "user_id": user.ID, }) return err } - if result.MatchedCount == 0 { - return errors.New("user not found") - } - r.logger.Info("User updated successfully", map[string]interface{}{ - "user_id": user.ID.String(), + "user_id": user.ID, "email": user.Email, }) @@ -231,30 +166,29 @@ func (r *userRepository) Update(ctx context.Context, user *User) error { } // Delete deletes a user (soft delete by setting status to inactive) -func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error { - filter := bson.M{"_id": id} - update := bson.M{ - "$set": bson.M{ - "status": UserStatusInactive, - "updated_at": time.Now().Unix(), - }, +func (r *userRepository) Delete(ctx context.Context, id string) error { + // Get user first + user, err := r.GetByID(ctx, id) + if err != nil { + return err } - result, err := r.collection.UpdateOne(ctx, filter, update) + // Update status to inactive + user.Status = UserStatusInactive + user.SetUpdatedAt(time.Now().Unix()) + + // Update in database + err = r.ormRepo.Update(ctx, user) if err != nil { r.logger.Error("Failed to delete user", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "user_id": id, }) return err } - if result.MatchedCount == 0 { - return errors.New("user not found") - } - r.logger.Info("User deleted successfully", map[string]interface{}{ - "user_id": id.String(), + "user_id": id, }) return nil @@ -262,18 +196,18 @@ func (r *userRepository) Delete(ctx context.Context, id uuid.UUID) error { // List retrieves users with pagination func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User, error) { - var users []*User - - // Build options - opts := options.Find() - opts.SetLimit(int64(limit)) - opts.SetSkip(int64(offset)) - opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() // Only active users by default filter := bson.M{"status": bson.M{"$ne": UserStatusInactive}} - cursor, err := r.collection.Find(ctx, filter, opts) + // Use ORM to find users + result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { r.logger.Error("Failed to list users", map[string]interface{}{ "error": err.Error(), @@ -282,22 +216,18 @@ func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*User, }) return nil, err } - defer cursor.Close(ctx) - if err = cursor.All(ctx, &users); err != nil { - r.logger.Error("Failed to decode users", map[string]interface{}{ - "error": err.Error(), - }) - return nil, err + // Convert []User to []*User + users := make([]*User, len(result.Items)) + for i := range result.Items { + users[i] = &result.Items[i] } return users, nil } // Search retrieves users with search and filters -func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *uuid.UUID, limit, offset int, sortBy, sortOrder string) ([]*User, error) { - var users []*User - +func (r *userRepository) Search(ctx context.Context, search string, status *string, role *string, companyID *string, limit, offset int, sortBy, sortOrder string) ([]*User, error) { // Build filter filter := bson.M{} @@ -317,23 +247,24 @@ func (r *userRepository) Search(ctx context.Context, search string, status *stri filter["company_id"] = *companyID } - // Build options - opts := options.Find() - opts.SetLimit(int64(limit)) - opts.SetSkip(int64(offset)) + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset) // Set sort if sortBy != "" { - sortValue := 1 if sortOrder == "desc" { - sortValue = -1 + pagination.SortDesc(sortBy) + } else { + pagination.SortAsc(sortBy) } - opts.SetSort(bson.D{{Key: sortBy, Value: sortValue}}) } else { - opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Default sort + pagination.SortDesc("created_at") // Default sort } - cursor, err := r.collection.Find(ctx, filter, opts) + // Use ORM to find users + result, err := r.ormRepo.FindAll(ctx, filter, pagination.Build()) if err != nil { r.logger.Error("Failed to search users", map[string]interface{}{ "error": err.Error(), @@ -341,27 +272,24 @@ func (r *userRepository) Search(ctx context.Context, search string, status *stri }) return nil, err } - defer cursor.Close(ctx) - if err = cursor.All(ctx, &users); err != nil { - r.logger.Error("Failed to decode users from search", map[string]interface{}{ - "error": err.Error(), - }) - return nil, err + // Convert []User to []*User + users := make([]*User, len(result.Items)) + for i := range result.Items { + users[i] = &result.Items[i] } return users, nil } // GetByCompanyID retrieves users by company ID with pagination -func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, error) { - var users []*User - - // Build options - opts := options.Find() - opts.SetLimit(int64(limit)) - opts.SetSkip(int64(offset)) - opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc +func (r *userRepository) GetByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, error) { + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() // Filter by company ID and active status filter := bson.M{ @@ -369,24 +297,22 @@ func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID "status": bson.M{"$ne": UserStatusInactive}, } - cursor, err := r.collection.Find(ctx, filter, opts) + // Use ORM to find users + result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { r.logger.Error("Failed to get users by company ID", map[string]interface{}{ "error": err.Error(), - "company_id": companyID.String(), + "company_id": companyID, "limit": limit, "offset": offset, }) return nil, err } - defer cursor.Close(ctx) - if err = cursor.All(ctx, &users); err != nil { - r.logger.Error("Failed to decode users by company", map[string]interface{}{ - "error": err.Error(), - "company_id": companyID.String(), - }) - return nil, err + // Convert []User to []*User + users := make([]*User, len(result.Items)) + for i := range result.Items { + users[i] = &result.Items[i] } return users, nil @@ -394,13 +320,12 @@ func (r *userRepository) GetByCompanyID(ctx context.Context, companyID uuid.UUID // GetByRole retrieves users by role with pagination func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) { - var users []*User - - // Build options - opts := options.Find() - opts.SetLimit(int64(limit)) - opts.SetSkip(int64(offset)) - opts.SetSort(bson.D{{Key: "created_at", Value: -1}}) // Sort by created_at desc + // Build pagination + pagination := mongopkg.NewPaginationBuilder(). + Limit(limit). + Skip(offset). + SortDesc("created_at"). + Build() // Filter by role and active status filter := bson.M{ @@ -408,7 +333,8 @@ func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, of "status": bson.M{"$ne": UserStatusInactive}, } - cursor, err := r.collection.Find(ctx, filter, opts) + // Use ORM to find users + result, err := r.ormRepo.FindAll(ctx, filter, pagination) if err != nil { r.logger.Error("Failed to get users by role", map[string]interface{}{ "error": err.Error(), @@ -418,61 +344,57 @@ func (r *userRepository) GetByRole(ctx context.Context, role UserRole, limit, of }) return nil, err } - defer cursor.Close(ctx) - if err = cursor.All(ctx, &users); err != nil { - r.logger.Error("Failed to decode users by role", map[string]interface{}{ - "error": err.Error(), - "role": role, - }) - return nil, err + // Convert []User to []*User + users := make([]*User, len(result.Items)) + for i := range result.Items { + users[i] = &result.Items[i] } return users, nil } // UpdateLastLogin updates the last login timestamp -func (r *userRepository) UpdateLastLogin(ctx context.Context, id uuid.UUID) error { - filter := bson.M{"_id": id} - update := bson.M{ - "$set": bson.M{ - "last_login_at": time.Now().Unix(), - "updated_at": time.Now().Unix(), - }, +func (r *userRepository) UpdateLastLogin(ctx context.Context, id string) error { + // Get user first + user, err := r.GetByID(ctx, id) + if err != nil { + return err } - result, err := r.collection.UpdateOne(ctx, filter, update) + // 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.String(), + "user_id": id, }) return err } - if result.MatchedCount == 0 { - return errors.New("user not found") - } - r.logger.Info("Last login updated successfully", map[string]interface{}{ - "user_id": id.String(), + "user_id": id, }) return nil } // CountByCompanyID counts users by company ID -func (r *userRepository) CountByCompanyID(ctx context.Context, companyID uuid.UUID) (int64, error) { +func (r *userRepository) CountByCompanyID(ctx context.Context, companyID string) (int64, error) { filter := bson.M{ "company_id": companyID, "status": bson.M{"$ne": UserStatusInactive}, } - count, err := r.collection.CountDocuments(ctx, filter) + count, err := r.ormRepo.Count(ctx, filter) if err != nil { r.logger.Error("Failed to count users by company ID", map[string]interface{}{ "error": err.Error(), - "company_id": companyID.String(), + "company_id": companyID, }) return 0, err } diff --git a/internal/user/service.go b/internal/user/service.go index 2130ee7..2320a01 100644 --- a/internal/user/service.go +++ b/internal/user/service.go @@ -14,22 +14,22 @@ import ( // Service defines business logic for user operations type Service interface { - CreateUser(ctx context.Context, form *CreateUserForm, createdBy *uuid.UUID) (*User, error) + CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error) Login(ctx context.Context, form *LoginForm) (*AuthResponse, error) RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error) - GetProfile(ctx context.Context, userID uuid.UUID) (*User, error) - UpdateProfile(ctx context.Context, userID uuid.UUID, form *UpdateProfileForm) error - ChangePassword(ctx context.Context, userID uuid.UUID, form *ChangePasswordForm) error + GetProfile(ctx context.Context, userID string) (*User, error) + UpdateProfile(ctx context.Context, userID string, form *UpdateProfileForm) error + ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error ResetPassword(ctx context.Context, form *ResetPasswordForm) error - GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) - UpdateUser(ctx context.Context, id uuid.UUID, form *UpdateUserForm, updatedBy *uuid.UUID) (*User, error) - DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error + GetUserByID(ctx context.Context, id string) (*User, error) + UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error) + DeleteUser(ctx context.Context, id string, deletedBy *string) error ListUsers(ctx context.Context, form *ListUsersForm) (*UserListResponse, error) - UpdateUserStatus(ctx context.Context, id uuid.UUID, form *UpdateUserStatusForm, updatedBy *uuid.UUID) error - UpdateUserRole(ctx context.Context, id uuid.UUID, form *UpdateUserRoleForm, updatedBy *uuid.UUID) error - GetUsersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, int64, error) + UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error + UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error + GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error) GetUsersByRole(ctx context.Context, role UserRole, limit, offset int) ([]*User, error) - Logout(ctx context.Context, userID uuid.UUID, accessToken string) error + Logout(ctx context.Context, userID string, accessToken string) error } // userService implements the Service interface @@ -51,7 +51,7 @@ func NewUserService(repository Repository, logger logger.Logger, authService aut } // CreateUser creates a new user -func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, createdBy *uuid.UUID) (*User, error) { +func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, createdBy *string) (*User, error) { // Check if email already exists existingUser, _ := s.repository.GetByEmail(ctx, form.Email) if existingUser != nil { @@ -74,13 +74,9 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea } // Parse optional fields - var companyID *uuid.UUID + var companyID *string if form.CompanyID != nil { - parsedID, err := uuid.Parse(*form.CompanyID) - if err != nil { - return nil, errors.New("invalid company ID") - } - companyID = &parsedID + companyID = form.CompanyID } // Validate role @@ -91,7 +87,6 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea // Create user user := &User{ - ID: uuid.New(), FullName: form.FullName, Username: form.Username, Email: form.Email, @@ -119,11 +114,11 @@ func (s *userService) CreateUser(ctx context.Context, form *CreateUserForm, crea } s.logger.Info("User created successfully", map[string]interface{}{ - "user_id": user.ID.String(), + "user_id": user.ID, "email": user.Email, "username": user.Username, "role": user.Role, - "created_by": createdBy.String(), + "created_by": *createdBy, }) return user, nil @@ -158,7 +153,7 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse 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.String(), + "user_id": user.ID, "email": user.Email, }) return nil, errors.New("invalid credentials") @@ -169,18 +164,18 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse if err != nil { s.logger.Error("Failed to update last login", map[string]interface{}{ "error": err.Error(), - "user_id": user.ID.String(), + "user_id": user.ID, }) } // Generate JWT tokens using authorization service companyID := "" if user.CompanyID != nil { - companyID = user.CompanyID.String() + companyID = *user.CompanyID } tokenPair, err := s.authService.GenerateTokenPair( - user.ID.String(), + user.ID, user.Email, string(user.Role), companyID, @@ -188,13 +183,13 @@ func (s *userService) Login(ctx context.Context, form *LoginForm) (*AuthResponse if err != nil { s.logger.Error("Failed to generate JWT tokens", map[string]interface{}{ "error": err.Error(), - "user_id": user.ID.String(), + "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.String(), + "user_id": user.ID, "email": user.Email, "role": user.Role, }) @@ -243,7 +238,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A return nil, errors.New("invalid token format") } - user, err := s.repository.GetByID(ctx, userID) + user, err := s.repository.GetByID(ctx, userID.String()) if err != nil { s.logger.Error("Failed to get user for token refresh", map[string]interface{}{ "error": err.Error(), @@ -258,7 +253,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A } s.logger.Info("Access token refreshed successfully", map[string]interface{}{ - "user_id": user.ID.String(), + "user_id": user.ID, "email": user.Email, }) @@ -271,7 +266,7 @@ func (s *userService) RefreshToken(ctx context.Context, refreshToken string) (*A } // ChangePassword changes user password -func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form *ChangePasswordForm) error { +func (s *userService) ChangePassword(ctx context.Context, userID string, form *ChangePasswordForm) error { // Get user user, err := s.repository.GetByID(ctx, userID) if err != nil { @@ -289,7 +284,7 @@ func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form if err != nil { s.logger.Error("Failed to hash new password", map[string]interface{}{ "error": err.Error(), - "user_id": userID.String(), + "user_id": userID, }) return errors.New("failed to process new password") } @@ -300,13 +295,13 @@ func (s *userService) ChangePassword(ctx context.Context, userID uuid.UUID, form if err != nil { s.logger.Error("Failed to update password", map[string]interface{}{ "error": err.Error(), - "user_id": userID.String(), + "user_id": userID, }) return errors.New("failed to update password") } s.logger.Info("Password changed successfully", map[string]interface{}{ - "user_id": userID.String(), + "user_id": userID, }) return nil @@ -326,7 +321,7 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm // TODO: Implement password reset logic (send email with reset link) s.logger.Info("Password reset requested", map[string]interface{}{ - "user_id": user.ID.String(), + "user_id": user.ID, "email": user.Email, }) @@ -334,7 +329,7 @@ func (s *userService) ResetPassword(ctx context.Context, form *ResetPasswordForm } // GetUserByID retrieves a user by ID -func (s *userService) GetUserByID(ctx context.Context, id uuid.UUID) (*User, error) { +func (s *userService) GetUserByID(ctx context.Context, id string) (*User, error) { user, err := s.repository.GetByID(ctx, id) if err != nil { return nil, err @@ -344,30 +339,30 @@ func (s *userService) GetUserByID(ctx context.Context, id uuid.UUID) (*User, err } // GetProfile retrieves the current user's profile -func (s *userService) GetProfile(ctx context.Context, userID uuid.UUID) (*User, error) { +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.String(), + "user_id": userID, }) return nil, errors.New("user not found") } s.logger.Info("User profile retrieved successfully", map[string]interface{}{ - "user_id": userID.String(), + "user_id": userID, }) return user, nil } // UpdateProfile updates the current user's profile -func (s *userService) UpdateProfile(ctx context.Context, userID uuid.UUID, form *UpdateProfileForm) error { +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.String(), + "user_id": userID, }) return errors.New("user not found") } @@ -397,20 +392,20 @@ func (s *userService) UpdateProfile(ctx context.Context, userID uuid.UUID, form if err != nil { s.logger.Error("Failed to update user profile", map[string]interface{}{ "error": err.Error(), - "user_id": userID.String(), + "user_id": userID, }) return errors.New("failed to update profile") } s.logger.Info("User profile updated successfully", map[string]interface{}{ - "user_id": userID.String(), + "user_id": userID, }) return nil } // UpdateUser updates user information -func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *UpdateUserForm, updatedBy *uuid.UUID) (*User, error) { +func (s *userService) UpdateUser(ctx context.Context, id string, form *UpdateUserForm, updatedBy *string) (*User, error) { // Get user user, err := s.repository.GetByID(ctx, id) if err != nil { @@ -449,11 +444,7 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update } if form.CompanyID != nil { - parsedID, err := uuid.Parse(*form.CompanyID) - if err != nil { - return nil, errors.New("invalid company ID") - } - user.CompanyID = &parsedID + user.CompanyID = form.CompanyID } if form.Department != nil { @@ -484,21 +475,21 @@ func (s *userService) UpdateUser(ctx context.Context, id uuid.UUID, form *Update if err != nil { s.logger.Error("Failed to update user", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "user_id": id, }) return nil, errors.New("failed to update user") } s.logger.Info("User updated successfully", map[string]interface{}{ - "user_id": id.String(), - "updated_by": updatedBy.String(), + "user_id": id, + "updated_by": *updatedBy, }) return user, nil } // DeleteUser deletes a user (soft delete) -func (s *userService) DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *uuid.UUID) error { +func (s *userService) DeleteUser(ctx context.Context, id string, deletedBy *string) error { // Get user to check if exists user, err := s.repository.GetByID(ctx, id) if err != nil { @@ -514,14 +505,14 @@ func (s *userService) DeleteUser(ctx context.Context, id uuid.UUID, deletedBy *u if err != nil { s.logger.Error("Failed to delete user", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "user_id": id, }) return errors.New("failed to delete user") } s.logger.Info("User deleted successfully", map[string]interface{}{ - "user_id": id.String(), - "deleted_by": deletedBy.String(), + "user_id": id, + "deleted_by": *deletedBy, }) return nil @@ -556,13 +547,9 @@ func (s *userService) ListUsers(ctx context.Context, form *ListUsersForm) (*User } // Parse company ID if provided - var companyID *uuid.UUID + var companyID *string if form.CompanyID != nil { - parsedID, err := uuid.Parse(*form.CompanyID) - if err != nil { - return nil, errors.New("invalid company ID") - } - companyID = &parsedID + companyID = form.CompanyID } // Get users @@ -594,7 +581,7 @@ func (s *userService) ListUsers(ctx context.Context, form *ListUsersForm) (*User } // UpdateUserStatus updates user status -func (s *userService) UpdateUserStatus(ctx context.Context, id uuid.UUID, form *UpdateUserStatusForm, updatedBy *uuid.UUID) error { +func (s *userService) UpdateUserStatus(ctx context.Context, id string, form *UpdateUserStatusForm, updatedBy *string) error { // Get user user, err := s.repository.GetByID(ctx, id) if err != nil { @@ -610,23 +597,23 @@ func (s *userService) UpdateUserStatus(ctx context.Context, id uuid.UUID, form * if err != nil { s.logger.Error("Failed to update user status", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "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.String(), + "user_id": id, "status": form.Status, - "updated_by": updatedBy.String(), + "updated_by": *updatedBy, }) return nil } // UpdateUserRole updates user role -func (s *userService) UpdateUserRole(ctx context.Context, id uuid.UUID, form *UpdateUserRoleForm, updatedBy *uuid.UUID) error { +func (s *userService) UpdateUserRole(ctx context.Context, id string, form *UpdateUserRoleForm, updatedBy *string) error { // Get user user, err := s.repository.GetByID(ctx, id) if err != nil { @@ -648,28 +635,28 @@ func (s *userService) UpdateUserRole(ctx context.Context, id uuid.UUID, form *Up if err != nil { s.logger.Error("Failed to update user role", map[string]interface{}{ "error": err.Error(), - "user_id": id.String(), + "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.String(), + "user_id": id, "role": form.Role, - "updated_by": updatedBy.String(), + "updated_by": *updatedBy, }) return nil } // GetUsersByCompanyID retrieves users by company ID with pagination -func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID uuid.UUID, limit, offset int) ([]*User, int64, error) { +func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID string, limit, offset int) ([]*User, int64, error) { users, err := s.repository.GetByCompanyID(ctx, companyID, limit, offset) if err != nil { s.logger.Error("Failed to get users by company ID", map[string]interface{}{ "error": err.Error(), - "company_id": companyID.String(), + "company_id": companyID, }) return nil, 0, errors.New("failed to get users by company") } @@ -679,7 +666,7 @@ func (s *userService) GetUsersByCompanyID(ctx context.Context, companyID uuid.UU if err != nil { s.logger.Error("Failed to count users by company ID", map[string]interface{}{ "error": err.Error(), - "company_id": companyID.String(), + "company_id": companyID, }) return users, 0, nil // Return users even if count fails } @@ -702,19 +689,19 @@ func (s *userService) GetUsersByRole(ctx context.Context, role UserRole, limit, } // Logout logs out a user and invalidates tokens -func (s *userService) Logout(ctx context.Context, userID uuid.UUID, accessToken string) error { +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.String(), + "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.String(), + "user_id": userID, }) return nil