53 lines
1.6 KiB
Go
53 lines
1.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
const refreshLockTTL = 30 * time.Second
|
|
|
|
// RefreshLock provides Redis-based locking for refresh token operations
|
|
// to prevent race conditions when two concurrent refresh requests use the same token.
|
|
type RefreshLock struct {
|
|
client *redis.Client
|
|
}
|
|
|
|
// NewRefreshLock creates a new RefreshLock. If client is nil, AcquireRefreshLock
|
|
// will always return (true, noop) to allow operation without Redis (graceful degradation).
|
|
func NewRefreshLock(client *redis.Client) *RefreshLock {
|
|
return &RefreshLock{client: client}
|
|
}
|
|
|
|
// AcquireRefreshLock acquires a Redis lock for the given userID+token combination.
|
|
// Returns (true, releaseFn) if lock acquired; (false, nil) if already held by another request.
|
|
// releaseFn must be called to release the lock (typically via defer).
|
|
// Key format: refresh_lock:{userID}:{tokenHash} to avoid storing full token in Redis.
|
|
func (r *RefreshLock) AcquireRefreshLock(ctx context.Context, userID uuid.UUID, token string) (bool, func()) {
|
|
if r.client == nil {
|
|
return true, func() {}
|
|
}
|
|
key := fmt.Sprintf("refresh_lock:%s:%s", userID.String(), hashToken(token))
|
|
ok, err := r.client.SetNX(ctx, key, "1", refreshLockTTL).Result()
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
release := func() {
|
|
_ = r.client.Del(context.Background(), key).Err()
|
|
}
|
|
return true, release
|
|
}
|
|
|
|
func hashToken(s string) string {
|
|
h := sha256.Sum256([]byte(s))
|
|
return hex.EncodeToString(h[:8])
|
|
}
|