fixed blocking important issues

This commit is contained in:
Mazyar
2026-05-30 12:29:45 +03:30
parent bb221f3cda
commit ad702f24bf
14 changed files with 174 additions and 72 deletions
+5 -5
View File
@@ -36,7 +36,7 @@ func (s *customerService) AdminResetPassword(ctx context.Context, actorID, targe
customer, err := s.repository.GetByID(ctx, targetID)
if err != nil {
if err.Error() == "customer not found" {
if errors.Is(err, ErrCustomerNotFound) {
return nil, errCustomerNotFound
}
s.logger.Error("Failed to load customer for password reset", map[string]interface{}{
@@ -68,9 +68,10 @@ func (s *customerService) AdminResetPassword(ctx context.Context, actorID, targe
return nil, errCustResetPasswordFailed
}
customer.Password = string(hashedPassword)
customer.UpdatedBy = &actorID
if err := s.repository.Update(ctx, customer); err != nil {
if err := s.repository.UpdatePasswordReset(ctx, targetID, string(hashedPassword), actorID); err != nil {
if errors.Is(err, ErrCustomerNotFound) {
return nil, errCustomerNotFound
}
s.logger.Error("Failed to persist reset password", map[string]interface{}{
"error": err.Error(),
"customer_id": targetID,
@@ -83,7 +84,6 @@ func (s *customerService) AdminResetPassword(ctx context.Context, actorID, targe
"error": err.Error(),
"customer_id": targetID,
})
return nil, errCustResetPasswordFailed
}
s.auditLogger.Log(ctx, audit.Entry{
+7
View File
@@ -0,0 +1,7 @@
package customer
import "errors"
var (
ErrCustomerNotFound = errors.New("customer not found")
)
+6 -5
View File
@@ -1,6 +1,7 @@
package customer
import (
"errors"
"strings"
"tm/internal/user"
"tm/pkg/authorization"
@@ -268,14 +269,14 @@ func (h *Handler) ResetCustomerPassword(c echo.Context) error {
targetID := c.Param("id")
result, err := h.service.AdminResetPassword(c.Request().Context(), actorID, targetID)
if err != nil {
switch err.Error() {
case errInvalidCustomerID.Error():
switch {
case errors.Is(err, errInvalidCustomerID):
return response.BadRequest(c, "Invalid id", "")
case errCustomerNotFound.Error():
case errors.Is(err, errCustomerNotFound):
return response.NotFound(c, "Customer not found")
case errCannotResetCustPassword.Error():
case errors.Is(err, errCannotResetCustPassword):
return response.Conflict(c, "Cannot reset password for this user")
case errCustResetPasswordRate.Error():
case errors.Is(err, errCustResetPasswordRate):
return response.TooManyRequests(c, "Too many password reset requests")
default:
return response.InternalServerError(c, "Failed to reset password")
+45 -8
View File
@@ -3,12 +3,14 @@ package customer
import (
"context"
"errors"
"fmt"
"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 the interface for customer data operations
@@ -16,6 +18,7 @@ type Repository interface {
Login(ctx context.Context, id, deviceToken string) error
Register(ctx context.Context, customer *Customer) error
Update(ctx context.Context, customer *Customer) error
UpdatePasswordReset(ctx context.Context, id, hashedPassword, updatedBy string) error
UpdateStatus(ctx context.Context, id string, status CustomerStatus) error
GetByID(ctx context.Context, id string) (*Customer, error)
GetByEmail(ctx context.Context, email string) (*Customer, error)
@@ -30,8 +33,9 @@ type Repository interface {
// customerRepository implements the Repository interface using the MongoDB ORM
type customerRepository struct {
ormRepo orm.Repository[Customer]
logger logger.Logger
ormRepo orm.Repository[Customer]
collection *mongopkg.Collection
logger logger.Logger
}
func collection() string {
@@ -55,11 +59,13 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Re
}
// Create ORM repository
ormRepo := orm.NewRepository[Customer](mongoManager.GetCollection(collection()), logger)
collection := mongoManager.GetCollection(collection())
ormRepo := orm.NewRepository[Customer](collection, logger)
return &customerRepository{
ormRepo: ormRepo,
logger: logger,
ormRepo: ormRepo,
collection: collection,
logger: logger,
}
}
@@ -128,7 +134,7 @@ func (r *customerRepository) GetByID(ctx context.Context, id string) (*Customer,
customer, err := r.ormRepo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
return nil, ErrCustomerNotFound
}
r.logger.Error("Failed to get customer by ID", map[string]interface{}{
"error": err.Error(),
@@ -146,7 +152,7 @@ func (r *customerRepository) GetByEmail(ctx context.Context, email string) (*Cus
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
return nil, ErrCustomerNotFound
}
r.logger.Error("Failed to get customer by email", map[string]interface{}{
"error": err.Error(),
@@ -164,7 +170,7 @@ func (r *customerRepository) GetByUsername(ctx context.Context, username string)
customer, err := r.ormRepo.FindOne(ctx, filter)
if err != nil {
if errors.Is(err, orm.ErrDocumentNotFound) {
return nil, errors.New("customer not found")
return nil, ErrCustomerNotFound
}
r.logger.Error("Failed to get customer by username", map[string]interface{}{
"error": err.Error(),
@@ -295,6 +301,37 @@ func (r *customerRepository) Update(ctx context.Context, customer *Customer) err
return nil
}
// UpdatePasswordReset atomically persists a new password hash and audit fields.
func (r *customerRepository) 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 {
return fmt.Errorf("invalid id format: %w", err)
}
result, err := r.collection.UpdateOne(ctx, bson.M{"_id": objectID}, updateDoc)
if err != nil {
r.logger.Error("Failed to update password reset", map[string]interface{}{
"error": err.Error(),
"customer_id": id,
})
return err
}
if result.MatchedCount == 0 {
return ErrCustomerNotFound
}
return nil
}
// Delete deletes a customer (hard delete)
func (r *customerRepository) Delete(ctx context.Context, id string) error {
// Get customer first
+7
View File
@@ -0,0 +1,7 @@
package user
import "errors"
var (
ErrUserNotFound = errors.New("user not found")
)
+15 -7
View File
@@ -1,6 +1,7 @@
package user
import (
"errors"
"tm/pkg/authorization"
"tm/pkg/logger"
"tm/pkg/response"
@@ -484,17 +485,24 @@ func (h *Handler) ResetUserPassword(c echo.Context) error {
return response.Unauthorized(c, "Unauthorized")
}
targetID := c.Param("id")
result, err := h.service.AdminResetPassword(c.Request().Context(), actorID, targetID)
actorRole, err := GetUserRoleFromContext(c)
if err != nil {
switch err.Error() {
case errInvalidUserID.Error():
return response.Unauthorized(c, "Unauthorized")
}
targetID := c.Param("id")
result, err := h.service.AdminResetPassword(c.Request().Context(), actorID, actorRole, targetID)
if err != nil {
switch {
case errors.Is(err, errInvalidUserID):
return response.BadRequest(c, "Invalid id", "")
case errAdminUserNotFound.Error():
case errors.Is(err, errAdminUserNotFound):
return response.NotFound(c, "Admin not found")
case errCannotResetPassword.Error():
case errors.Is(err, errCannotResetPassword):
return response.Conflict(c, "Cannot reset password for this user")
case errResetPasswordRateHit.Error():
case errors.Is(err, errResetPasswordForbidden):
return response.Forbidden(c, "You are not allowed to reset this password")
case errors.Is(err, errResetPasswordRateHit):
return response.TooManyRequests(c, "Too many password reset requests")
default:
return response.InternalServerError(c, "Failed to reset password")
+39 -1
View File
@@ -19,6 +19,7 @@ type Repository interface {
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)
GetByIDs(ctx context.Context, userIDs []string) ([]User, error)
@@ -177,7 +178,7 @@ func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error)
return legacyUser, nil
}
if errors.Is(legacyErr, orm.ErrDocumentNotFound) {
return nil, errors.New("user not found")
return nil, ErrUserNotFound
}
r.logger.Error("Failed to get user by legacy string ID", map[string]interface{}{
"error": legacyErr.Error(),
@@ -195,6 +196,43 @@ func (r *userRepository) GetByID(ctx context.Context, id string) (*User, error)
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})
+16 -11
View File
@@ -15,17 +15,22 @@ import (
)
var (
errInvalidUserID = errors.New("invalid id")
errAdminUserNotFound = errors.New("admin not found")
errCannotResetPassword = errors.New("cannot reset password for this user")
errResetPasswordFailed = errors.New("failed to reset password")
errResetPasswordRateHit = errors.New("too many password reset requests")
errInvalidUserID = errors.New("invalid id")
errAdminUserNotFound = errors.New("admin not found")
errCannotResetPassword = errors.New("cannot reset password for this user")
errResetPasswordFailed = errors.New("failed to reset password")
errResetPasswordRateHit = errors.New("too many password reset requests")
errResetPasswordForbidden = errors.New("you are not allowed to reset this password")
)
const adminPasswordResetRateLimit = 10
// AdminResetPassword generates a new password for the target admin user.
func (s *userService) AdminResetPassword(ctx context.Context, actorID, targetID string) (*AdminResetPasswordResponse, error) {
func (s *userService) AdminResetPassword(ctx context.Context, actorID, actorRole, targetID string) (*AdminResetPasswordResponse, error) {
if UserRole(actorRole) != UserRoleAdmin {
return nil, errResetPasswordForbidden
}
if _, err := bson.ObjectIDFromHex(targetID); err != nil {
return nil, errInvalidUserID
}
@@ -36,7 +41,7 @@ func (s *userService) AdminResetPassword(ctx context.Context, actorID, targetID
user, err := s.repository.GetByID(ctx, targetID)
if err != nil {
if err.Error() == "user not found" {
if errors.Is(err, ErrUserNotFound) {
return nil, errAdminUserNotFound
}
s.logger.Error("Failed to load user for password reset", map[string]interface{}{
@@ -68,9 +73,10 @@ func (s *userService) AdminResetPassword(ctx context.Context, actorID, targetID
return nil, errResetPasswordFailed
}
user.Password = string(hashedPassword)
user.UpdatedBy = &actorID
if err := s.repository.Update(ctx, user); err != nil {
if err := s.repository.UpdatePasswordReset(ctx, targetID, string(hashedPassword), actorID); err != nil {
if errors.Is(err, ErrUserNotFound) {
return nil, errAdminUserNotFound
}
s.logger.Error("Failed to persist reset password", map[string]interface{}{
"error": err.Error(),
"target_id": targetID,
@@ -83,7 +89,6 @@ func (s *userService) AdminResetPassword(ctx context.Context, actorID, targetID
"error": err.Error(),
"target_id": targetID,
})
return nil, errResetPasswordFailed
}
s.auditLogger.Log(ctx, audit.Entry{
+1 -1
View File
@@ -28,7 +28,7 @@ type Service interface {
GetUsersByIDs(ctx context.Context, userIDs []string) (*UserListResponse, error)
Search(ctx context.Context, form *SearchUsersForm, pagination *response.Pagination) (*UserListResponse, error)
Delete(ctx context.Context, id string) error
AdminResetPassword(ctx context.Context, actorID, targetID string) (*AdminResetPasswordResponse, error)
AdminResetPassword(ctx context.Context, actorID, actorRole, targetID string) (*AdminResetPasswordResponse, error)
// Profile
Login(ctx context.Context, form *LoginForm) (*AuthResponse, error)