Add admin password reset functionality for users and customers

This commit is contained in:
Mazyar
2026-05-29 20:15:09 +03:30
parent 42cd0452ce
commit bb221f3cda
20 changed files with 686 additions and 16 deletions
+71
View File
@@ -9,12 +9,14 @@ import (
"encoding/hex"
"encoding/pem"
"fmt"
"strconv"
"time"
"tm/pkg/logger"
"tm/pkg/redis"
"github.com/golang-jwt/jwt/v5"
redisv9 "github.com/redis/go-redis/v9"
)
// TokenType represents the type of token
@@ -84,6 +86,9 @@ type AuthorizationService interface {
// GetBlacklistStats returns statistics about the blacklist
GetBlacklistStats(ctx context.Context) (map[string]interface{}, error)
// InvalidateUserSessions marks all tokens issued before now as invalid for the user.
InvalidateUserSessions(userID string) error
}
// AuthorizationConfig holds the configuration for the authorization service
@@ -308,6 +313,20 @@ func (s *Service) ValidateToken(tokenString string) (*TokenValidationResult, err
}, nil
}
if invalidated, err := s.isSessionInvalidated(claims.UserID, claims.IssuedAt); err != nil {
s.logger.Error("Failed to check session invalidation", map[string]interface{}{
"user_id": claims.UserID,
"error": err.Error(),
})
} else if invalidated {
return &TokenValidationResult{
Valid: false,
Claims: nil,
Error: "session invalidated",
Expired: false,
}, nil
}
return &TokenValidationResult{
Valid: true,
Claims: claims,
@@ -408,6 +427,58 @@ func (s *Service) RefreshAccessToken(refreshToken string) (string, error) {
return newAccessToken, nil
}
func sessionInvalidationKey(userID string) string {
return "sessions:invalidated_at:" + userID
}
// InvalidateUserSessions marks tokens issued before now as invalid for the given user.
func (s *Service) InvalidateUserSessions(userID string) error {
if s.redis == nil {
s.logger.Warn("Redis client not available, session invalidation skipped", map[string]interface{}{
"user_id": userID,
})
return nil
}
now := time.Now().Unix()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ttl := s.config.RefreshTokenExpiry + 5*time.Minute
if err := s.redis.Set(ctx, sessionInvalidationKey(userID), strconv.FormatInt(now, 10), ttl); err != nil {
return fmt.Errorf("failed to invalidate user sessions: %w", err)
}
s.logger.Info("User sessions invalidated", map[string]interface{}{
"user_id": userID,
})
return nil
}
func (s *Service) isSessionInvalidated(userID string, issuedAt *jwt.NumericDate) (bool, error) {
if s.redis == nil || issuedAt == nil {
return false, nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
value, err := s.redis.Get(ctx, sessionInvalidationKey(userID))
if err != nil {
if err == redisv9.Nil {
return false, nil
}
return false, err
}
invalidatedAt, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return false, err
}
return issuedAt.Time.Unix() < invalidatedAt, nil
}
// generateTokenHash generates a SHA-256 hash of the token for storage
func (s *Service) generateTokenHash(tokenString string) string {
hash := sha256.Sum256([]byte(tokenString))
+10
View File
@@ -25,6 +25,16 @@ func (m *MockRedisClient) Get(ctx context.Context, key string) (string, error) {
return args.String(0), args.Error(1)
}
func (m *MockRedisClient) Incr(ctx context.Context, key string) (int64, error) {
args := m.Called(ctx, key)
return args.Get(0).(int64), args.Error(1)
}
func (m *MockRedisClient) Expire(ctx context.Context, key string, expiration time.Duration) error {
args := m.Called(ctx, key, expiration)
return args.Error(0)
}
func (m *MockRedisClient) Del(ctx context.Context, keys ...string) error {
args := m.Called(ctx, keys)
return args.Error(0)