Skip to content
This repository has been archived by the owner on Aug 16, 2022. It is now read-only.

feat: user verification #83

Merged
merged 11 commits into from
Dec 6, 2021
12 changes: 4 additions & 8 deletions internal/adapter/http/user.go
Original file line number Diff line number Diff line change
@@ -21,10 +21,6 @@ type CreateVerificationInput struct {
Email string `json:"email"`
}

type CreateVerificationOutput struct {
Message string `json:"message"`
}

type VerifyUserOutput struct {
UserID string `json:"userId"`
Verified bool `json:"verified"`
@@ -61,12 +57,12 @@ func (c *UserController) CreateUser(ctx context.Context, input CreateUserInput)
}, nil
}

func (c *UserController) CreateVerification(ctx context.Context, input CreateVerificationInput) (interface{}, error) {
res, err := c.usecase.CreateVerification(ctx, input.Email)
func (c *UserController) CreateVerification(ctx context.Context, input CreateVerificationInput) error {
err := c.usecase.CreateVerification(ctx, input.Email)
if err != nil {
return nil, err
return err
}
return CreateVerificationOutput{Message: res}, nil
return nil
}

func (c *UserController) VerifyUser(ctx context.Context, code string) (interface{}, error) {
6 changes: 3 additions & 3 deletions internal/app/public.go
Original file line number Diff line number Diff line change
@@ -44,15 +44,15 @@ func publicAPI(
if err := c.Bind(&inp); err != nil {
return &echo.HTTPError{Code: http.StatusBadRequest, Message: fmt.Errorf("failed to parse request body: %w", err)}
}
output, err := controller.CreateVerification(c.Request().Context(), inp)
err := controller.CreateVerification(c.Request().Context(), inp)
if err != nil {
return err
}
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved

return c.JSON(http.StatusOK, output)
return c.String(http.StatusOK, "email sent")
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved
})

r.GET("/signup/verify/:code", func(c echo.Context) error {
r.POST("/signup/verify/:code", func(c echo.Context) error {
code := c.Param("code")
if len(code) == 0 {
return echo.ErrBadRequest
17 changes: 10 additions & 7 deletions internal/infrastructure/mongo/mongodoc/user.go
Original file line number Diff line number Diff line change
@@ -56,6 +56,14 @@ func NewUser(user *user1.User) (*UserDocument, string) {
for _, a := range auths {
authsdoc = append(authsdoc, a.Sub)
}
var v *UserVerificationDoc
if user.Verification() != nil {
v = &UserVerificationDoc{
Code: user.Verification().Code(),
Expiration: user.Verification().Expiration(),
Verified: user.Verification().IsVerified(),
}
}

return &UserDocument{
ID: id,
@@ -65,11 +73,7 @@ func NewUser(user *user1.User) (*UserDocument, string) {
Team: user.Team().String(),
Lang: user.Lang().String(),
Theme: string(user.Theme()),
Verification: &UserVerificationDoc{
Code: user.Verification().Code(),
Expiration: user.Verification().Expiration(),
Verified: user.Verification().IsVerified(),
},
Verification: v,
}, id
}

@@ -91,8 +95,7 @@ func (d *UserDocument) Model() (*user1.User, error) {
}
var v *user.Verification
if d.Verification != nil {

v = user.NewVerificationFrom(d.Verification.Code, d.Verification.Expiration, d.Verification.Verified)
v = user.VerificationFrom(d.Verification.Code, d.Verification.Expiration, d.Verification.Verified)
}

user, err := user1.New().
18 changes: 11 additions & 7 deletions internal/usecase/interactor/user.go
Original file line number Diff line number Diff line change
@@ -400,19 +400,23 @@ func (i *User) DeleteMe(ctx context.Context, userID id.UserID, operator *usecase
return nil
}

func (i *User) CreateVerification(ctx context.Context, email string) (string, error) {
func (i *User) CreateVerification(ctx context.Context, email string) error {
tx, err := i.transaction.Begin()
if err != nil {
return "", err
return err
}
u, err := i.userRepo.FindByEmail(ctx, email)
if err != nil {
return "", err
return err
}
v, err := user.NewVerification()
if err != nil {
return err
}
u.SetVerification(user.NewVerification())
u.SetVerification(v)
err = i.userRepo.Save(ctx, u)
if err != nil {
return "", err
return err
}

err = i.mailer.SendMail([]gateway.Contact{
@@ -422,10 +426,10 @@ func (i *User) CreateVerification(ctx context.Context, email string) (string, er
},
}, "email verification", "", "")
if err != nil {
return "", err
return err
}
tx.Commit()
return "verification created", nil
return nil
}

func (i *User) VerifyUser(ctx context.Context, code string) (*user.User, error) {
2 changes: 1 addition & 1 deletion internal/usecase/interfaces/user.go
Original file line number Diff line number Diff line change
@@ -44,7 +44,7 @@ type UpdateMeParam struct {
type User interface {
Fetch(context.Context, []id.UserID, *usecase.Operator) ([]*user.User, error)
Signup(context.Context, SignupParam) (*user.User, *user.Team, error)
CreateVerification(context.Context, string) (string, error)
CreateVerification(context.Context, string) error
VerifyUser(context.Context, string) (*user.User, error)
GetUserByCredentials(context.Context, GetUserByCredentials) (*user.User, error)
GetUserBySubject(context.Context, string) (*user.User, error)
35 changes: 35 additions & 0 deletions pkg/user/builder_test.go
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ package user
import (
"errors"
"testing"
"time"

"github.com/reearth/reearth-backend/pkg/id"
"github.com/stretchr/testify/assert"
@@ -198,3 +199,37 @@ func TestBuilder_MustBuild(t *testing.T) {
})
}
}

func TestBuilder_Verification(t *testing.T) {
tests := []struct {
name string
input *Verification
want *Builder
}{
{
name: "should return verification",
input: &Verification{
verified: true,
code: "xxx",
expiration: time.Time{},
},

want: &Builder{
u: &User{
verification: &Verification{
verified: true,
code: "xxx",
expiration: time.Time{},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := New()
b.Verification(tt.input)
assert.Equal(t, tt.want, b)
})
}
}
28 changes: 25 additions & 3 deletions pkg/user/user_test.go
Original file line number Diff line number Diff line change
@@ -318,13 +318,35 @@ func TestUser_GetAuthByProvider(t *testing.T) {
}

func TestUser_SetVerification(t *testing.T) {
var input *User
input = &User{}
input := &User{}
v := &Verification{
verified: false,
code: "xxx",
expiration: time.Time{},
}
input.SetVerification(v)
assert.Equal(t, v, input.Verification())
assert.Equal(t, v, input.verification)
}

func TestUser_Verification(t *testing.T) {
v, _ := NewVerification()
tests := []struct {
name string
verification *Verification
want *Verification
}{
{
name: "should return the same verification",
verification: v,
want: v,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := &User{
verification: tt.verification,
}
assert.Equal(t, tt.want, u.verification)
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved
})
}
}
34 changes: 22 additions & 12 deletions pkg/user/verification.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package user

import (
"math/rand"
"crypto/rand"
"errors"
"math/big"
"time"
)

var codeAlphabet = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")

type Verification struct {
verified bool
code string
@@ -32,14 +36,16 @@ func (v *Verification) Expiration() time.Time {
return v.expiration
}

func generateCode() string {
rand.Seed(time.Now().UnixNano())
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
b := make([]rune, 5)
for i := range b {
b[i] = chars[rand.Intn(len(chars))]
func generateCode() (string, error) {
code := ""
for i := 0; i < 5; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(codeAlphabet))))
if err != nil {
return "", err
}
code += string(codeAlphabet[n.Int64()])
}
return string(b)
return code, nil
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved
}

func (v *Verification) IsExpired() bool {
@@ -57,16 +63,20 @@ func (v *Verification) SetVerified(b bool) {
v.verified = b
}

func NewVerification() *Verification {
func NewVerification() (*Verification, error) {
c, err := generateCode()
if err != nil {
return nil, errors.New("error generating verification code")
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved
}
v := &Verification{
verified: false,
code: generateCode(),
code: c,
expiration: time.Now().Add(time.Hour * 24),
}
return v
return v, nil
}

func NewVerificationFrom(c string, e time.Time, b bool) *Verification {
func VerificationFrom(c string, e time.Time, b bool) *Verification {
v := &Verification{
verified: b,
code: c,
46 changes: 33 additions & 13 deletions pkg/user/verification_test.go
Original file line number Diff line number Diff line change
@@ -9,7 +9,6 @@ import (
)

func TestNewVerification(t *testing.T) {

type fields struct {
verified bool
code bool
@@ -32,7 +31,8 @@ func TestNewVerification(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := NewVerification()
got, err := NewVerification()
assert.NoError(t, err)
assert.Equal(t, tt.want.verified, got.IsVerified())
assert.Equal(t, tt.want.code, len(got.Code()) > 0)
assert.Equal(t, tt.want.expiration, !got.Expiration().IsZero())
@@ -103,6 +103,9 @@ func TestVerification_Expiration(t *testing.T) {
}

func TestVerification_IsExpired(t *testing.T) {
tim, _ := time.Parse(time.RFC3339, "2021-03-16T04:19:57.592Z")
tim2, _ := time.Parse(time.RFC3339, "2022-03-16T04:19:57.592Z")
mimoham24 marked this conversation as resolved.
Show resolved Hide resolved

type fields struct {
verified bool
code string
@@ -113,18 +116,35 @@ func TestVerification_IsExpired(t *testing.T) {
fields fields
want bool
}{
// TODO: Add test cases.
{
name: "should be expired",
fields: fields{
verified: false,
code: "xxx",
expiration: tim,
},
want: true,
},
{
name: "shouldn't be expired",
fields: fields{
verified: false,
code: "xxx",
expiration: tim2,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(tt *testing.T) {
tt.Parallel()
v := &Verification{
verified: tt.fields.verified,
code: tt.fields.code,
expiration: tt.fields.expiration,
}
if got := v.IsExpired(); got != tt.want {
t.Errorf("IsExpired() = %v, want %v", got, tt.want)
verified: tc.fields.verified,
code: tc.fields.code,
expiration: tc.fields.expiration,
}
assert.Equal(tt, tc.want, v.IsExpired())
})
}
}
@@ -190,8 +210,8 @@ func TestVerification_SetVerified(t *testing.T) {

func Test_generateCode(t *testing.T) {
var regx = regexp.MustCompile(`[a-zA-Z0-9]{5}`)
str := generateCode()

str, err := generateCode()
assert.NoError(t, err)
tests := []struct {
name string
want string