Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
126 lines
2.8 KiB
Go
126 lines
2.8 KiB
Go
// Package redissession provides a multi-instance OAuth session backend.
|
|
package redissession
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
var ErrNotConfigured = errors.New("redis session store not configured")
|
|
|
|
// Store persists JSON sessions and single-use markers under one namespace.
|
|
type Store struct {
|
|
rdb *redis.Client
|
|
prefix string
|
|
ttl time.Duration
|
|
}
|
|
|
|
func New(rdb *redis.Client, prefix string, ttl time.Duration) *Store {
|
|
if ttl <= 0 {
|
|
ttl = 30 * time.Minute
|
|
}
|
|
prefix = strings.TrimSpace(prefix)
|
|
if prefix == "" {
|
|
prefix = "oauth:session"
|
|
}
|
|
if !strings.HasSuffix(prefix, ":") {
|
|
prefix += ":"
|
|
}
|
|
return &Store{rdb: rdb, prefix: prefix, ttl: ttl}
|
|
}
|
|
|
|
func (s *Store) dataKey(id string) string { return s.prefix + strings.TrimSpace(id) }
|
|
func (s *Store) usedKey(id string) string { return s.prefix + "used:" + strings.TrimSpace(id) }
|
|
|
|
func (s *Store) Set(ctx context.Context, id string, value any) error {
|
|
if s == nil || s.rdb == nil {
|
|
return ErrNotConfigured
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return errors.New("session id is required")
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.rdb.Set(ctx, s.dataKey(id), raw, s.ttl).Err()
|
|
}
|
|
|
|
func (s *Store) Get(ctx context.Context, id string, dest any) (bool, error) {
|
|
if s == nil || s.rdb == nil {
|
|
return false, ErrNotConfigured
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return false, nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
raw, err := s.rdb.Get(ctx, s.dataKey(id)).Bytes()
|
|
if errors.Is(err, redis.Nil) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := json.Unmarshal(raw, dest); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func (s *Store) Delete(ctx context.Context, id string) error {
|
|
if s == nil || s.rdb == nil {
|
|
return ErrNotConfigured
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
return s.rdb.Del(ctx, s.dataKey(id), s.usedKey(id)).Err()
|
|
}
|
|
|
|
// TryConsume returns true only for the first claim while the session exists.
|
|
func (s *Store) TryConsume(ctx context.Context, id string) (bool, error) {
|
|
if s == nil || s.rdb == nil {
|
|
return false, ErrNotConfigured
|
|
}
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return false, nil
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
ttl := s.ttl
|
|
if remaining, err := s.rdb.TTL(ctx, s.dataKey(id)).Result(); err == nil && remaining > 0 {
|
|
ttl = remaining
|
|
}
|
|
ok, err := s.rdb.SetNX(ctx, s.usedKey(id), "1", ttl).Result()
|
|
if err != nil || !ok {
|
|
return ok, err
|
|
}
|
|
exists, err := s.rdb.Exists(ctx, s.dataKey(id)).Result()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if exists == 0 {
|
|
_ = s.rdb.Del(ctx, s.usedKey(id)).Err()
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|