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
+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)