-
Notifications
You must be signed in to change notification settings - Fork 1
/
otp_service.go
126 lines (105 loc) · 2.9 KB
/
otp_service.go
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package auth
import (
"context"
"math/rand"
"time"
"github.com/krsoninikhil/go-rest-kit/apperrors"
"github.com/krsoninikhil/go-rest-kit/cache"
"github.com/pkg/errors"
)
// dependencies
type (
smsProvider interface {
SendSMS(phone, message string) error
}
cacheClient interface {
Set(key string, value any, ttl time.Duration) error
Get(key string) (any, error)
}
)
type otpMetaData struct {
OTP string
Attempt int
SentAt time.Time
}
type otpSvc struct {
config otpConfig
smsProvider smsProvider
cache cacheClient
}
func NewOTPSvc(config otpConfig, smsProvider smsProvider, cache cacheClient) otpSvc {
return otpSvc{
config: config,
smsProvider: smsProvider,
cache: cache,
}
}
func (s otpSvc) Send(ctx context.Context, phone string) (*OTPStatus, error) {
attempt := 1
lastOTPMeta, err := s.cache.Get(phone)
if err != nil {
if !errors.Is(err, cache.ErrKeyNotFound) {
return nil, errors.Wrap(err, "unable to get last otp")
}
} else {
lastOTP, ok := lastOTPMeta.(otpMetaData)
if !ok {
return nil, apperrors.NewServerError(errors.New("invalid last otp"))
}
if lastOTP.Attempt >= s.config.MaxAttempts {
return nil, apperrors.NewInvalidParamsError("otp", errors.New("max attempt reached"))
}
if time.Since(lastOTP.SentAt) < s.config.retryAfter() {
return nil, apperrors.NewInvalidParamsError("otp", errors.New("retry too soon"))
}
attempt = lastOTP.Attempt + 1
}
otp := generateOTP(s.config.Length)
if err := s.smsProvider.SendSMS(phone, otpMessage(otp)); err != nil {
return nil, errors.Wrap(err, "unable to send otp")
}
otpMeta := otpMetaData{
OTP: otp,
Attempt: attempt,
SentAt: time.Now(),
}
if err := s.cache.Set(phone, otpMeta, s.config.validity()); err != nil {
return nil, errors.Wrap(err, "unable to set otp")
}
return &OTPStatus{
RetryAfter: s.config.RetryAfterSeconds,
AttemptLeft: s.config.MaxAttempts - otpMeta.Attempt,
}, nil
}
func (s otpSvc) Verify(ctx context.Context, phone, otp string) error {
lastOTPMeta, err := s.cache.Get(phone)
if err != nil {
if errors.Is(err, cache.ErrKeyNotFound) {
return apperrors.NewInvalidParamsError("otp", errors.New("otp not sent or expired"))
}
return errors.Wrap(err, "unable to get last otp")
}
lastOTP, ok := lastOTPMeta.(otpMetaData)
if !ok {
return apperrors.NewServerError(errors.New("invalid last otp"))
}
if time.Since(lastOTP.SentAt) >= s.config.validity() {
return apperrors.NewInvalidParamsError("otp", errors.New("otp expired"))
}
if lastOTP.OTP != otp {
return apperrors.NewInvalidParamsError("otp", errors.New("incorrect otp"))
}
return nil
}
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
func generateOTP(length int) string {
digits := "0123456789"
otp := make([]byte, length)
for i := range otp {
otp[i] = digits[r.Intn(len(digits))]
}
return string(otp)
}
func otpMessage(otp string) string {
return "Your OTP is " + otp
}