Support for password resets

This commit is contained in:
Ben Toogood
2021-02-19 11:45:33 +00:00
parent eef581b94c
commit e677c40840
39 changed files with 2394 additions and 1097 deletions

42
codes/handler/create.go Normal file
View File

@@ -0,0 +1,42 @@
package handler
import (
"context"
"math/rand"
"strconv"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/codes/proto"
)
func (c *Codes) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
// validate the request
if len(req.Identity) == 0 {
return ErrMissingIdentity
}
// construct the code
code := Code{Code: generateCode(), Identity: req.Identity}
if req.ExpiresAt != nil {
code.ExpiresAt = req.ExpiresAt.AsTime()
} else {
code.ExpiresAt = c.Time().Add(DefaultTTL)
}
// write to the database
if err := c.DB.Create(&code).Error; err != nil {
logger.Errorf("Error creating code in database: %v", err)
return errors.InternalServerError("DATABASE_ERORR", "Error connecting to database")
}
// return the code
rsp.Code = code.Code
return nil
}
// generateCode generates a random 8 digit code
func generateCode() string {
v := rand.Intn(89999999) + 10000000
return strconv.Itoa(v)
}

View File

@@ -0,0 +1,39 @@
package handler_test
import (
"context"
"testing"
"github.com/micro/services/codes/handler"
pb "github.com/micro/services/codes/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
func TestCreate(t *testing.T) {
h := testHandler(t)
t.Run("MissingIdentity", func(t *testing.T) {
var rsp pb.CreateResponse
err := h.Create(context.TODO(), &pb.CreateRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingIdentity, err)
assert.Empty(t, rsp.Code)
})
t.Run("NoExpiry", func(t *testing.T) {
var rsp pb.CreateResponse
err := h.Create(context.TODO(), &pb.CreateRequest{Identity: "07503196715"}, &rsp)
assert.NoError(t, err)
assert.NotEmpty(t, rsp.Code)
})
t.Run("WithExpiry", func(t *testing.T) {
var rsp pb.CreateResponse
err := h.Create(context.TODO(), &pb.CreateRequest{
Identity: "demo@m3o.com",
ExpiresAt: timestamppb.Now(),
}, &rsp)
assert.NoError(t, err)
assert.NotEmpty(t, rsp.Code)
})
}

28
codes/handler/handler.go Normal file
View File

@@ -0,0 +1,28 @@
package handler
import (
"time"
"github.com/micro/micro/v3/service/errors"
"gorm.io/gorm"
)
var (
ErrMissingCode = errors.BadRequest("MISSING_CODE", "Missing code")
ErrMissingIdentity = errors.BadRequest("MISSING_IDENTITY", "Missing identity")
ErrInvalidCode = errors.BadRequest("INVALID_CODE", "Invalid code")
ErrExpiredCode = errors.BadRequest("EXPIRED_CODE", "Expired code")
DefaultTTL = time.Minute * 5
)
type Codes struct {
DB *gorm.DB
Time func() time.Time
}
type Code struct {
Code string `gorm:"index:codeIdentity"`
Identity string `gorm:"index:codeIdentity"`
ExpiresAt time.Time
}

View File

@@ -0,0 +1,30 @@
package handler_test
import (
"testing"
"time"
"github.com/micro/services/codes/handler"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func testHandler(t *testing.T) *handler.Codes {
// connect to the database
db, err := gorm.Open(postgres.Open("postgresql://postgres@localhost:5432/codes?sslmode=disable"), &gorm.Config{})
if err != nil {
t.Fatalf("Error connecting to database: %v", err)
}
// migrate the database
if err := db.AutoMigrate(&handler.Code{}); err != nil {
t.Fatalf("Error migrating database: %v", err)
}
// clean any data from a previous run
if err := db.Exec("TRUNCATE TABLE codes CASCADE").Error; err != nil {
t.Fatalf("Error cleaning database: %v", err)
}
return &handler.Codes{DB: db, Time: time.Now}
}

36
codes/handler/verify.go Normal file
View File

@@ -0,0 +1,36 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/codes/proto"
"gorm.io/gorm"
)
func (c *Codes) Verify(ctx context.Context, req *pb.VerifyRequest, rsp *pb.VerifyResponse) error {
// validate the request
if len(req.Code) == 0 {
return ErrMissingCode
}
if len(req.Identity) == 0 {
return ErrMissingIdentity
}
// lookup the code
var code Code
if err := c.DB.Where(&Code{Code: req.Code, Identity: req.Identity}).First(&code).Error; err == gorm.ErrRecordNotFound {
return ErrInvalidCode
} else if err != nil {
logger.Errorf("Error reading code from database: %v", err)
return errors.InternalServerError("DATABASE_ERORR", "Error connecting to database")
}
// check the invite hasn't expired
if code.ExpiresAt.Before(c.Time()) {
return ErrExpiredCode
}
return nil
}

View File

@@ -0,0 +1,60 @@
package handler_test
import (
"context"
"testing"
"time"
"github.com/micro/services/codes/handler"
pb "github.com/micro/services/codes/proto"
"github.com/stretchr/testify/assert"
)
func TestVerify(t *testing.T) {
h := testHandler(t)
t.Run("MissingIdentity", func(t *testing.T) {
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Code: "123456"}, &rsp)
assert.Equal(t, handler.ErrMissingIdentity, err)
})
t.Run("MissingCode", func(t *testing.T) {
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com"}, &rsp)
assert.Equal(t, handler.ErrMissingCode, err)
})
// generate a code to test
var cRsp pb.CreateResponse
err := h.Create(context.TODO(), &pb.CreateRequest{Identity: "demo@m3o.com"}, &cRsp)
assert.NoError(t, err)
t.Run("IncorrectCode", func(t *testing.T) {
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: "12345"}, &rsp)
assert.Equal(t, handler.ErrInvalidCode, err)
})
t.Run("IncorrectEmail", func(t *testing.T) {
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "john@m3o.com", Code: cRsp.Code}, &rsp)
assert.Equal(t, handler.ErrInvalidCode, err)
})
t.Run("ExpiredCode", func(t *testing.T) {
ot := h.Time
h.Time = func() time.Time { return time.Now().Add(handler.DefaultTTL * 2) }
defer func() { h.Time = ot }()
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: cRsp.Code}, &rsp)
assert.Equal(t, handler.ErrExpiredCode, err)
})
t.Run("ValidCode", func(t *testing.T) {
var rsp pb.VerifyResponse
err := h.Verify(context.TODO(), &pb.VerifyRequest{Identity: "demo@m3o.com", Code: cRsp.Code}, &rsp)
assert.NoError(t, err)
})
}