241e3b5f2d
- Introduced a new `auditlog` package to handle audit logging for user actions, including creation, updates, deletions, and authentication events. - Enhanced existing services (customer, user) to log relevant actions using the new audit logger, capturing details such as actor ID, action type, target type, and success status. - Added middleware to enrich request context with metadata for audit logging, ensuring comprehensive tracking of user interactions. - Integrated Elasticsearch for persistent storage of audit logs, with fallback to file-only logging if Elasticsearch is unavailable. - Updated API documentation to include new audit log endpoints for administrative access. This update significantly improves the system's ability to track and audit user actions, enhancing security and accountability within the application.
125 lines
3.5 KiB
Go
125 lines
3.5 KiB
Go
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 errors.Is(err, ErrCustomerNotFound) {
|
|
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
|
|
}
|
|
|
|
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,
|
|
})
|
|
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,
|
|
})
|
|
}
|
|
|
|
s.auditLogger.Log(ctx, audit.Entry{
|
|
ActorID: actorID,
|
|
ActorType: audit.ActorTypeAdmin,
|
|
TargetType: audit.TargetTypeCustomer,
|
|
TargetID: targetID,
|
|
Action: audit.ActionPasswordReset,
|
|
Success: true,
|
|
})
|
|
|
|
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
|
|
}
|