| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125 |
- // 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
- }
|