Ctrl K
ComunidadGuías…

internal/repository/refresh_token_postgres.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package repository

import (
	"context"
	"fmt"
	"time"
	"xprem/internal/database"
	"xprem/internal/database/postgres/pgdb"

	"github.com/jackc/pgx/v5/pgtype"
)

// RefreshToken is one link in a sign-in's refresh chain, identified by the
// jti claim in the dashboard's refresh JWT.
type RefreshToken struct {
	Id     string
	UserId string
	// FamilyId groups every token descended from one sign-in, so a leaked
	// chain can be revoked without touching the account's other devices.
	FamilyId  string
	ExpiresAt time.Time
	// Nil while this is the family's live token, stamped when it is rotated.
	UsedAt *time.Time
	// UsedRecently reports whether UsedAt falls inside the replay grace passed to
	// GetRefreshToken; always false from the other reads, which do not ask.
	UsedRecently bool
	// ReplacedBy names the successor this token was rotated into, nil while it
	// is still live.
	ReplacedBy *string
}

type InsertRefreshTokenParameters struct {
	ID        string
	UserID    string
	FamilyID  string
	ExpiresAt time.Time
}

// RotateRefreshTokenParameters names the token being retired and the one
// replacing it.
type RotateRefreshTokenParameters struct {
	OldID     string
	NewID     string
	ExpiresAt time.Time
}

type PostgresRefreshTokenRepository struct {
	engine *database.Engine
}

func NewPostgresRefreshTokenRepository(engine *database.Engine) *PostgresRefreshTokenRepository {
	return &PostgresRefreshTokenRepository{
		engine: engine,
	}
}

func (s *PostgresRefreshTokenRepository) InsertRefreshToken(ctx context.Context, params InsertRefreshTokenParameters) error {
	if err := s.engine.Queries.InsertRefreshToken(ctx, pgdb.InsertRefreshTokenParams{
		ID:        ToPgUUID(params.ID),
		UserID:    ToPgUUID(params.UserID),
		FamilyID:  ToPgUUID(params.FamilyID),
		ExpiresAt: pgtype.Timestamptz{Time: params.ExpiresAt.UTC(), Valid: true},
	}); err != nil {
		return fmt.Errorf("failed to insert refresh token into database: %w", err)
	}
	return nil
}

// RotateRefreshToken retires params.OldID and issues params.NewID in the same
// family, in one transaction, and returns the row it retired.
// ErrResourceNotFound means the old token was not claimable: unknown, expired, or already rotated.
func (s *PostgresRefreshTokenRepository) RotateRefreshToken(ctx context.Context, params RotateRefreshTokenParameters) (RefreshToken, error) {
	var rotated RefreshToken
	err := s.engine.WithTx(ctx, func(q *pgdb.Queries) error {
		row, err := q.ConsumeRefreshToken(ctx, pgdb.ConsumeRefreshTokenParams{
			ID:         ToPgUUID(params.OldID),
			ReplacedBy: ToPgUUID(params.NewID),
		})
		if err != nil {
			if database.IsNoRows(err) {
				return &ErrResourceNotFound{Resource: "refresh token", Identifier: params.OldID}
			}
			return fmt.Errorf("failed to consume refresh token in database: %w", err)
		}
		rotated = refreshTokenFromRow(row)
		if err := q.InsertRefreshToken(ctx, pgdb.InsertRefreshTokenParams{
			ID:        ToPgUUID(params.NewID),
			UserID:    row.UserID,
			FamilyID:  row.FamilyID,
			ExpiresAt: pgtype.Timestamptz{Time: params.ExpiresAt.UTC(), Valid: true},
		}); err != nil {
			return fmt.Errorf("failed to insert the rotated refresh token into database: %w", err)
		}
		return nil
	})
	if err != nil {
		return RefreshToken{}, err
	}
	return rotated, nil
}

// GetRefreshToken explains why a token could not be claimed. replayGrace sets
// how recent a rotation still counts as the same client asking twice.
func (s *PostgresRefreshTokenRepository) GetRefreshToken(ctx context.Context, id string, replayGrace time.Duration) (RefreshToken, error) {
	row, err := s.engine.Queries.GetRefreshToken(ctx, pgdb.GetRefreshTokenParams{
		ID:          ToPgUUID(id),
		ReplayGrace: pgtype.Interval{Microseconds: replayGrace.Microseconds(), Valid: true},
	})
	if err != nil {
		if database.IsNoRows(err) {
			return RefreshToken{}, &ErrResourceNotFound{Resource: "refresh token", Identifier: id}
		}
		return RefreshToken{}, fmt.Errorf("failed to retrieve refresh token from database: %w", err)
	}
	token := refreshTokenFromRow(pgdb.RefreshToken{
		ID:         row.ID,
		UserID:     row.UserID,
		FamilyID:   row.FamilyID,
		CreatedAt:  row.CreatedAt,
		ExpiresAt:  row.ExpiresAt,
		UsedAt:     row.UsedAt,
		ReplacedBy: row.ReplacedBy,
	})
	// UsedRecently is nullable to sqlc; NULL means not used, i.e. false.
	token.UsedRecently = row.UsedRecently != nil && *row.UsedRecently
	return token, nil
}

// DeleteRefreshTokenFamily revokes a whole sign-in chain; used when a replayed
// token is detected.
func (s *PostgresRefreshTokenRepository) DeleteRefreshTokenFamily(ctx context.Context, familyId string) error {
	if err := s.engine.Queries.DeleteRefreshTokenFamily(ctx, ToPgUUID(familyId)); err != nil {
		return fmt.Errorf("failed to delete refresh token family from database: %w", err)
	}
	return nil
}

func (s *PostgresRefreshTokenRepository) DeleteExpiredRefreshTokens(ctx context.Context, userId string) error {
	if err := s.engine.Queries.DeleteExpiredRefreshTokensForUser(ctx, ToPgUUID(userId)); err != nil {
		return fmt.Errorf("failed to delete expired refresh tokens from database: %w", err)
	}
	return nil
}

func refreshTokenFromRow(row pgdb.RefreshToken) RefreshToken {
	token := RefreshToken{
		Id:        row.ID.String(),
		UserId:    row.UserID.String(),
		FamilyId:  row.FamilyID.String(),
		ExpiresAt: row.ExpiresAt.Time,
		UsedAt:    FromPgTimestamptz(row.UsedAt),
	}
	if row.ReplacedBy.Valid {
		successor := row.ReplacedBy.String()
		token.ReplacedBy = &successor
	}
	return token
}