all repos

onasty @ 234f7640a37cda6214824da3125ccda420167697

a one-time notes service

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package changeemailrepo

import (
	"context"
	"errors"
	"time"

	"github.com/jackc/pgx/v4"
	"github.com/olexsmir/onasty/internal/models"
	"github.com/olexsmir/onasty/internal/store/psqlutil"
)

type ChangeEmailStorer interface {
	// Create create a change email token.
	Create(ctx context.Context, input models.ChangeEmailToken) error

	// GetByToken returns change email token by its token.
	// Returns [models.ErrChangeEmailTokenNotFound] if not found.
	GetByToken(ctx context.Context, token string) (models.ChangeEmailToken, error)

	// MarkAsUsed marks change email token as used.
	// If not found, returns [models.ErrChangeEmailTokenNotFound].
	// If token is already used, returns [models.ErrChangeEmailTokenIsAlreadyUsed].
	// If token is expired, returns [models.ErrChangeEmailTokenExpired]
	MarkAsUsed(ctx context.Context, token string, usedAT time.Time) error
}

var _ ChangeEmailStorer = (*ChangeEmailRepo)(nil)

type ChangeEmailRepo struct {
	db *psqlutil.DB
}

func New(db *psqlutil.DB) *ChangeEmailRepo {
	return &ChangeEmailRepo{
		db: db,
	}
}

func (c *ChangeEmailRepo) Create(ctx context.Context, inp models.ChangeEmailToken) error {
	query := `--sql
insert into change_email_tokens (user_id, new_email, token, created_at, expires_at)
values ($1, $2, $3, $4, $5)
`

	_, err := c.db.Exec(ctx, query,
		inp.UserID, inp.NewEmail, inp.Token, inp.CreatedAt, inp.ExpiresAt)
	return err
}

func (c *ChangeEmailRepo) GetByToken(
	ctx context.Context,
	token string,
) (models.ChangeEmailToken, error) {
	query := `--sql
select user_id, new_email, token, created_at, expires_at
from change_email_tokens
where token = $1
`

	var res models.ChangeEmailToken
	err := c.db.QueryRow(ctx, query, token).
		Scan(&res.UserID, &res.NewEmail, &res.Token, &res.CreatedAt, &res.ExpiresAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return models.ChangeEmailToken{}, models.ErrChangeEmailTokenNotFound
	}

	return res, err
}

func (c *ChangeEmailRepo) MarkAsUsed(ctx context.Context, token string, usedAT time.Time) error {
	tx, err := c.db.Begin(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx) //nolint:errcheck

	var isUsed bool
	var expiresAt time.Time
	err = tx.QueryRow(ctx,
		"select (used_at is not null), expires_at from change_email_tokens where token = $1",
		token).
		Scan(&isUsed, &expiresAt)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return models.ErrChangeEmailTokenNotFound
		}
		return err
	}

	if isUsed {
		return models.ErrChangeEmailTokenIsAlreadyUsed
	}

	if time.Now().After(expiresAt) {
		return models.ErrChangeEmailTokenExpired
	}

	query := `--sql
update change_email_tokens
set used_at = $1
where token = $2`

	_, err = tx.Exec(ctx, query, usedAT, token)
	if err != nil {
		return err
	}

	return tx.Commit(ctx)
}