Files
telegram-gateway/auth.go
Gabriel Radureau 515b407db4
All checks were successful
Docker Build / build-and-push-image (push) Successful in 56s
docs: align ADR path references to doc/adr (singular)
Mirror of factory#8 path correction. Updates Gitea URLs in AUTH.md /
HOWTO_ADD_BOT.md and the '// Voir factory/...' header comments in code.
2026-05-09 14:26:12 +02:00

93 lines
2.5 KiB
Go

// Voir factory/doc/adr/20260509-telegram-gateway-auth.md
package main
import (
"context"
"crypto/subtle"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
// Auth wraps a Redis client and exposes the session primitives the gateway
// needs : IsAuthed / Login / Logout / Remaining. The session key has the form
// `tg-gw:auth:<telegramUserID>` and a configurable TTL (24h by default).
type Auth struct {
rdb *redis.Client
prefix string
ttl time.Duration
secret string
}
func NewAuth(redisURL, secret string, ttl time.Duration) (*Auth, error) {
if secret == "" {
return nil, fmt.Errorf("AUTH_SECRET is required for the auth layer")
}
if redisURL == "" {
return nil, fmt.Errorf("REDIS_URL is required for the auth layer")
}
opts, err := redis.ParseURL(redisURL)
if err != nil {
return nil, fmt.Errorf("parse REDIS_URL: %w", err)
}
rdb := redis.NewClient(opts)
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := rdb.Ping(pingCtx).Err(); err != nil {
return nil, fmt.Errorf("redis ping: %w", err)
}
return &Auth{rdb: rdb, prefix: "tg-gw:auth:", ttl: ttl, secret: secret}, nil
}
func (a *Auth) key(userID int64) string {
return fmt.Sprintf("%s%d", a.prefix, userID)
}
func (a *Auth) IsAuthed(ctx context.Context, userID int64) (bool, error) {
if a == nil || userID == 0 {
return false, nil
}
n, err := a.rdb.Exists(ctx, a.key(userID)).Result()
if err != nil {
return false, err
}
return n > 0, nil
}
// Login compares the provided code in constant time and, on match, sets the
// session key with the configured TTL. Returns ok=false on a wrong code (no
// error). The bool/error split lets the caller distinguish "wrong code"
// (user-facing) from "redis is down" (operator-facing).
func (a *Auth) Login(ctx context.Context, userID int64, providedSecret string) (bool, error) {
if a == nil {
return false, fmt.Errorf("auth not initialized")
}
if userID == 0 {
return false, fmt.Errorf("missing user id")
}
if subtle.ConstantTimeCompare([]byte(providedSecret), []byte(a.secret)) != 1 {
return false, nil
}
if err := a.rdb.Set(ctx, a.key(userID), "1", a.ttl).Err(); err != nil {
return false, fmt.Errorf("redis SET: %w", err)
}
return true, nil
}
func (a *Auth) Logout(ctx context.Context, userID int64) error {
if a == nil || userID == 0 {
return nil
}
return a.rdb.Del(ctx, a.key(userID)).Err()
}
func (a *Auth) Remaining(ctx context.Context, userID int64) (time.Duration, error) {
if a == nil || userID == 0 {
return 0, nil
}
return a.rdb.TTL(ctx, a.key(userID)).Result()
}