212 lines
5.1 KiB
Go
212 lines
5.1 KiB
Go
package logic
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"spark-server/internal/pkg/errs"
|
|
)
|
|
|
|
const (
|
|
tokenUseAccess = "access"
|
|
tokenUseRefresh = "refresh"
|
|
)
|
|
|
|
// TokenPair is the current access+refresh jti for one user device.
|
|
type TokenPair struct {
|
|
Access string `json:"a"`
|
|
Refresh string `json:"r"`
|
|
}
|
|
|
|
// PairStore holds the live token pair per device so refresh can rotate and logout can revoke both.
|
|
type PairStore interface {
|
|
Put(ctx context.Context, userID int64, deviceID uint64, pair TokenPair, ttl time.Duration) error
|
|
Get(ctx context.Context, userID int64, deviceID uint64) (TokenPair, error)
|
|
Delete(ctx context.Context, userID int64, deviceID uint64) error
|
|
ListByUser(ctx context.Context, userID int64) ([]DevicePair, error)
|
|
}
|
|
|
|
// DevicePair is the live token pair for one device of a user.
|
|
type DevicePair struct {
|
|
DeviceID uint64
|
|
Pair TokenPair
|
|
}
|
|
|
|
type MemoryPairs struct {
|
|
mu sync.Mutex
|
|
items map[string]storedPair
|
|
}
|
|
|
|
type storedPair struct {
|
|
pair TokenPair
|
|
exp int64
|
|
}
|
|
|
|
func NewMemoryPairs() *MemoryPairs {
|
|
return &MemoryPairs{items: make(map[string]storedPair)}
|
|
}
|
|
|
|
func pairKey(userID int64, deviceID uint64) string {
|
|
return strconv.FormatInt(userID, 10) + ":" + strconv.FormatUint(deviceID, 10)
|
|
}
|
|
|
|
func (s *MemoryPairs) Put(_ context.Context, userID int64, deviceID uint64, pair TokenPair, ttl time.Duration) error {
|
|
if userID <= 0 || deviceID == 0 || ttl <= 0 {
|
|
return nil
|
|
}
|
|
s.mu.Lock()
|
|
s.items[pairKey(userID, deviceID)] = storedPair{pair: pair, exp: time.Now().Add(ttl).UnixNano()}
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryPairs) Get(_ context.Context, userID int64, deviceID uint64) (TokenPair, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
row, ok := s.items[pairKey(userID, deviceID)]
|
|
if !ok {
|
|
return TokenPair{}, nil
|
|
}
|
|
if time.Now().UnixNano() >= row.exp {
|
|
delete(s.items, pairKey(userID, deviceID))
|
|
return TokenPair{}, nil
|
|
}
|
|
return row.pair, nil
|
|
}
|
|
|
|
func (s *MemoryPairs) Delete(_ context.Context, userID int64, deviceID uint64) error {
|
|
s.mu.Lock()
|
|
delete(s.items, pairKey(userID, deviceID))
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *MemoryPairs) ListByUser(_ context.Context, userID int64) ([]DevicePair, error) {
|
|
if userID <= 0 {
|
|
return nil, nil
|
|
}
|
|
prefix := strconv.FormatInt(userID, 10) + ":"
|
|
now := time.Now().UnixNano()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
out := make([]DevicePair, 0)
|
|
for key := range s.items {
|
|
row := s.items[key]
|
|
if !strings.HasPrefix(key, prefix) {
|
|
continue
|
|
}
|
|
if now >= row.exp {
|
|
delete(s.items, key)
|
|
continue
|
|
}
|
|
did, err := strconv.ParseUint(key[len(prefix):], 10, 64)
|
|
if err != nil || did == 0 {
|
|
continue
|
|
}
|
|
out = append(out, DevicePair{DeviceID: did, Pair: row.pair})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
type RedisPairs struct {
|
|
rdb *redis.Client
|
|
prefix string
|
|
}
|
|
|
|
func NewRedisPairs(rdb *redis.Client, prefix string) *RedisPairs {
|
|
if prefix == "" {
|
|
prefix = "refresh:"
|
|
}
|
|
return &RedisPairs{rdb: rdb, prefix: prefix}
|
|
}
|
|
|
|
func (s *RedisPairs) key(userID int64, deviceID uint64) string {
|
|
return s.prefix + pairKey(userID, deviceID)
|
|
}
|
|
|
|
func (s *RedisPairs) Put(ctx context.Context, userID int64, deviceID uint64, pair TokenPair, ttl time.Duration) error {
|
|
if s == nil || s.rdb == nil {
|
|
return errs.ErrAvailable.WithInnerMsg("jwt refresh redis not ready")
|
|
}
|
|
if userID <= 0 || deviceID == 0 || ttl <= 0 {
|
|
return nil
|
|
}
|
|
raw, err := json.Marshal(pair)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.rdb.Set(ctx, s.key(userID, deviceID), raw, ttl).Err()
|
|
}
|
|
|
|
func (s *RedisPairs) Get(ctx context.Context, userID int64, deviceID uint64) (TokenPair, error) {
|
|
if s == nil || s.rdb == nil {
|
|
return TokenPair{}, errs.ErrAvailable.WithInnerMsg("jwt refresh redis not ready")
|
|
}
|
|
raw, err := s.rdb.Get(ctx, s.key(userID, deviceID)).Bytes()
|
|
if err == redis.Nil {
|
|
return TokenPair{}, nil
|
|
}
|
|
if err != nil {
|
|
return TokenPair{}, err
|
|
}
|
|
var pair TokenPair
|
|
if err := json.Unmarshal(raw, &pair); err != nil {
|
|
return TokenPair{}, err
|
|
}
|
|
return pair, nil
|
|
}
|
|
|
|
func (s *RedisPairs) Delete(ctx context.Context, userID int64, deviceID uint64) error {
|
|
if s == nil || s.rdb == nil {
|
|
return errs.ErrAvailable.WithInnerMsg("jwt refresh redis not ready")
|
|
}
|
|
return s.rdb.Del(ctx, s.key(userID, deviceID)).Err()
|
|
}
|
|
|
|
func (s *RedisPairs) ListByUser(ctx context.Context, userID int64) ([]DevicePair, error) {
|
|
if s == nil || s.rdb == nil {
|
|
return nil, errs.ErrAvailable.WithInnerMsg("jwt refresh redis not ready")
|
|
}
|
|
if userID <= 0 {
|
|
return nil, nil
|
|
}
|
|
prefix := s.prefix + strconv.FormatInt(userID, 10) + ":"
|
|
var cursor uint64
|
|
out := make([]DevicePair, 0)
|
|
for {
|
|
keys, next, err := s.rdb.Scan(ctx, cursor, prefix+"*", 100).Result()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, key := range keys {
|
|
did, err := strconv.ParseUint(strings.TrimPrefix(key, prefix), 10, 64)
|
|
if err != nil || did == 0 {
|
|
continue
|
|
}
|
|
raw, err := s.rdb.Get(ctx, key).Bytes()
|
|
if err == redis.Nil {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var pair TokenPair
|
|
if err := json.Unmarshal(raw, &pair); err != nil {
|
|
continue
|
|
}
|
|
out = append(out, DevicePair{DeviceID: did, Pair: pair})
|
|
}
|
|
cursor = next
|
|
if cursor == 0 {
|
|
break
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|