package admin import ( "context" "strconv" "time" "github.com/redis/go-redis/v9" ) // TrustedDeviceCookieName is the cookie holding a device-trust token. const TrustedDeviceCookieName = "admin_trusted" // trustedKeyPrefix namespaces device-trust tokens in Redis. const trustedKeyPrefix = "admin:trusted:" // TrustedStore records "记住此设备" device-trust tokens in Redis. // // A trusted token lets a device SKIP THE TOTP SECOND FACTOR on re-login // (password is still always required). Each token is opaque (32 bytes of // entropy), bound to one admin id, and expires after ttl — so a flushed // Redis, an expired token, or a mismatched admin all fail closed to // "TOTP required". type TrustedStore struct { rdb *redis.Client ttl time.Duration } // NewTrustedStore wires a TrustedStore. A ttl <= 0 disables the feature: // Issue returns an empty token and Check always returns false. func NewTrustedStore(rdb *redis.Client, ttl time.Duration) *TrustedStore { return &TrustedStore{rdb: rdb, ttl: ttl} } // Enabled reports whether device-trust is active. func (s *TrustedStore) Enabled() bool { return s != nil && s.rdb != nil && s.ttl > 0 } // Issue mints a new trust token bound to adminID and stores it with the TTL. // Returns "" (no error) when the feature is disabled. func (s *TrustedStore) Issue(ctx context.Context, adminID int64) (string, error) { if !s.Enabled() { return "", nil } tok, err := randToken(32) if err != nil { return "", err } if err := s.rdb.Set(ctx, trustedKeyPrefix+tok, strconv.FormatInt(adminID, 10), s.ttl).Err(); err != nil { return "", err } return tok, nil } // Check reports whether tok is a live trust token bound to adminID. // Any miss (disabled, empty, unknown, expired, wrong admin) returns false. func (s *TrustedStore) Check(ctx context.Context, tok string, adminID int64) bool { if !s.Enabled() || tok == "" { return false } v, err := s.rdb.Get(ctx, trustedKeyPrefix+tok).Result() if err != nil { return false } return v == strconv.FormatInt(adminID, 10) } // Revoke deletes a trust token (e.g. on explicit logout-all). A no-op when // disabled or empty. func (s *TrustedStore) Revoke(ctx context.Context, tok string) error { if !s.Enabled() || tok == "" { return nil } return s.rdb.Del(ctx, trustedKeyPrefix+tok).Err() }