internal/repository/refresh_token_postgres.go
Rama main · réplica de lectura del repositorio original.
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
}