Add admin password reset functionality for users and customers
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package customer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tm/pkg/audit"
|
||||
"tm/pkg/password"
|
||||
"tm/pkg/ratelimit"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCustomerID = errors.New("invalid id")
|
||||
errCustomerNotFound = errors.New("customer not found")
|
||||
errCannotResetCustPassword = errors.New("cannot reset password for this user")
|
||||
errCustResetPasswordFailed = errors.New("failed to reset password")
|
||||
errCustResetPasswordRate = errors.New("too many password reset requests")
|
||||
)
|
||||
|
||||
const customerPasswordResetRateLimit = 10
|
||||
|
||||
// AdminResetPassword generates a new password for the target customer.
|
||||
func (s *customerService) AdminResetPassword(ctx context.Context, actorID, targetID string) (*AdminResetPasswordResponse, error) {
|
||||
if _, err := bson.ObjectIDFromHex(targetID); err != nil {
|
||||
return nil, errInvalidCustomerID
|
||||
}
|
||||
|
||||
if err := s.checkPasswordResetRateLimit(ctx, actorID, "customer"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
customer, err := s.repository.GetByID(ctx, targetID)
|
||||
if err != nil {
|
||||
if err.Error() == "customer not found" {
|
||||
return nil, errCustomerNotFound
|
||||
}
|
||||
s.logger.Error("Failed to load customer for password reset", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": targetID,
|
||||
})
|
||||
return nil, errCustResetPasswordFailed
|
||||
}
|
||||
|
||||
if customer.Status != CustomerStatusActive {
|
||||
return nil, errCannotResetCustPassword
|
||||
}
|
||||
|
||||
plainPassword, err := password.GenerateSecure()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": targetID,
|
||||
})
|
||||
return nil, errCustResetPasswordFailed
|
||||
}
|
||||
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(plainPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash generated password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": targetID,
|
||||
})
|
||||
return nil, errCustResetPasswordFailed
|
||||
}
|
||||
|
||||
customer.Password = string(hashedPassword)
|
||||
customer.UpdatedBy = &actorID
|
||||
if err := s.repository.Update(ctx, customer); err != nil {
|
||||
s.logger.Error("Failed to persist reset password", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": targetID,
|
||||
})
|
||||
return nil, errCustResetPasswordFailed
|
||||
}
|
||||
|
||||
if err := s.authService.InvalidateUserSessions(targetID); err != nil {
|
||||
s.logger.Error("Failed to invalidate customer sessions after password reset", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"customer_id": targetID,
|
||||
})
|
||||
return nil, errCustResetPasswordFailed
|
||||
}
|
||||
|
||||
s.auditLogger.Log(ctx, audit.Entry{
|
||||
ActorID: actorID,
|
||||
TargetType: audit.TargetTypeCustomer,
|
||||
TargetID: targetID,
|
||||
Action: audit.ActionPasswordReset,
|
||||
})
|
||||
|
||||
s.logger.Info("Customer password reset completed", map[string]interface{}{
|
||||
"actor_id": actorID,
|
||||
"customer_id": targetID,
|
||||
})
|
||||
|
||||
return &AdminResetPasswordResponse{Password: plainPassword}, nil
|
||||
}
|
||||
|
||||
func (s *customerService) checkPasswordResetRateLimit(ctx context.Context, actorID, resource string) error {
|
||||
if s.redisClient == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("ratelimit:password_reset:%s:%s", actorID, resource)
|
||||
allowed, err := ratelimit.Allow(ctx, s.redisClient, key, customerPasswordResetRateLimit, time.Minute)
|
||||
if err != nil {
|
||||
s.logger.Warn("Password reset rate limit check failed", map[string]interface{}{
|
||||
"actor_id": actorID,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
if !allowed {
|
||||
return errCustResetPasswordRate
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user