all repos

onasty @ 320377a

a one-time notes service

onasty/internal/store/psql/passwordtokrepo/passwordtokrepo.go (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
feat(api): change email (#191)..., 9 months ago
1
package passwordtokrepo
2
3
import (
4
	"context"
5
	"errors"
6
	"time"
7
8
	"github.com/gofrs/uuid/v5"
9
	"github.com/henvic/pgq"
10
	"github.com/jackc/pgx/v5"
11
	"github.com/olexsmir/onasty/internal/models"
12
	"github.com/olexsmir/onasty/internal/store/psqlutil"
13
)
14
15
type PasswordResetTokenStorer interface {
16
	// Create a new password reset token.
17
	Create(ctx context.Context, input models.ResetPasswordToken) error
18
19
	// GetUserIDByTokenAndMarkAsUsed gets the token, and marks it as used.
20
	//
21
	// In case the token is not found, returns [model.ErrResetPasswordTokenNotFound]
22
	// If token if used, or expired, returns [model.ErrResetPasswordTokenAlreadyUsed],
23
	// or [models.ErrResetPasswordTokenExpired].
24
	GetUserIDByTokenAndMarkAsUsed(
25
		ctx context.Context,
26
		token string,
27
		usedAT time.Time,
28
	) (uuid.UUID, error)
29
}
30
31
var _ PasswordResetTokenStorer = (*PasswordResetTokenRepo)(nil)
32
33
type PasswordResetTokenRepo struct {
34
	db *psqlutil.DB
35
}
36
37
func NewPasswordResetTokenRepo(db *psqlutil.DB) *PasswordResetTokenRepo {
38
	return &PasswordResetTokenRepo{
39
		db: db,
40
	}
41
}
42
43
func (r *PasswordResetTokenRepo) Create(ctx context.Context, token models.ResetPasswordToken,
44
) error {
45
	query, args, err := pgq.
46
		Insert("password_reset_tokens").
47
		Columns("user_id", "token", "created_at", "expires_at").
48
		Values(token.UserID, token.Token, token.CreatedAt, token.ExpiresAt).
49
		SQL()
50
	if err != nil {
51
		return err
52
	}
53
54
	_, err = r.db.Exec(ctx, query, args...)
55
	return err
56
}
57
58
func (r *PasswordResetTokenRepo) GetUserIDByTokenAndMarkAsUsed(
59
	ctx context.Context,
60
	token string,
61
	usedAt time.Time,
62
) (uuid.UUID, error) {
63
	tx, err := r.db.Begin(ctx)
64
	if err != nil {
65
		return uuid.Nil, err
66
	}
67
	defer tx.Rollback(ctx) //nolint:errcheck
68
69
	var isUsed bool
70
	var expiresAt time.Time
71
	err = tx.QueryRow(ctx, "select (used_at is not null), expires_at from password_reset_tokens where token = $1", token).
72
		Scan(&isUsed, &expiresAt)
73
	if err != nil {
74
		if errors.Is(err, pgx.ErrNoRows) {
75
			return uuid.Nil, models.ErrResetPasswordTokenNotFound
76
		}
77
		return uuid.Nil, err
78
	}
79
80
	if isUsed {
81
		return uuid.Nil, models.ErrResetPasswordTokenAlreadyUsed
82
	}
83
84
	if time.Now().After(expiresAt) {
85
		return uuid.Nil, models.ErrResetPasswordTokenExpired
86
	}
87
88
	query := `--sql
89
update password_reset_tokens
90
set used_at = $1
91
where token = $2
92
returning user_id`
93
94
	var userID uuid.UUID
95
	err = tx.QueryRow(ctx, query, usedAt, token).Scan(&userID)
96
	if err != nil {
97
		return uuid.Nil, err
98
	}
99
100
	return userID, tx.Commit(ctx)
101
}