remove the services we won't use

This commit is contained in:
Asim Aslam
2021-05-12 19:26:23 +01:00
parent d3b667c0bf
commit c774879044
147 changed files with 0 additions and 16582 deletions

View File

@@ -1,87 +0,0 @@
package handler
import (
"context"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Create a user
func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.FirstName) == 0 {
return ErrMissingFirstName
}
if len(req.LastName) == 0 {
return ErrMissingLastName
}
if len(req.Email) == 0 {
return ErrMissingEmail
}
if !isEmailValid(req.Email) {
return ErrInvalidEmail
}
if len(req.Password) < 8 {
return ErrInvalidPassword
}
// hash and salt the password using bcrypt
phash, err := hashAndSalt(req.Password)
if err != nil {
logger.Errorf("Error hashing and salting password: %v", err)
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
}
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
return db.Transaction(func(tx *gorm.DB) error {
// write the user to the database
user := &User{
ID: uuid.New().String(),
FirstName: req.FirstName,
LastName: req.LastName,
Email: strings.ToLower(req.Email),
Password: phash,
}
err = tx.Create(user).Error
if err != nil {
if match, _ := regexp.MatchString(`idx_[\S]+_users_email`, err.Error()); match {
return ErrDuplicateEmail
}
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// generate a token for the user
token := Token{
UserID: user.ID,
Key: uuid.New().String(),
ExpiresAt: u.Time().Add(time.Hour * 24 * 7),
}
if err := tx.Create(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.User = user.Serialize()
rsp.Token = token.Key
return nil
})
}

View File

@@ -1,128 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestCreate(t *testing.T) {
tt := []struct {
Name string
FirstName string
LastName string
Email string
Password string
Error error
}{
{
Name: "MissingFirstName",
LastName: "Doe",
Email: "john@doe.com",
Password: "password",
Error: handler.ErrMissingFirstName,
},
{
Name: "MissingLastName",
FirstName: "John",
Email: "john@doe.com",
Password: "password",
Error: handler.ErrMissingLastName,
},
{
Name: "MissingEmail",
FirstName: "John",
LastName: "Doe",
Password: "password",
Error: handler.ErrMissingEmail,
},
{
Name: "InvalidEmail",
FirstName: "John",
LastName: "Doe",
Password: "password",
Email: "foo.foo.foo",
Error: handler.ErrInvalidEmail,
},
{
Name: "InvalidPassword",
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "pwd",
Error: handler.ErrInvalidPassword,
},
}
// test the validations
h := testHandler(t)
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
err := h.Create(microAccountCtx(), &pb.CreateRequest{
FirstName: tc.FirstName,
LastName: tc.LastName,
Email: tc.Email,
Password: tc.Password,
}, &pb.CreateResponse{})
assert.Equal(t, tc.Error, err)
})
}
t.Run("Valid", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &req, &rsp)
assert.NoError(t, err)
u := rsp.User
if u == nil {
t.Fatalf("No user returned")
}
assert.NotEmpty(t, u.Id)
assert.Equal(t, req.FirstName, u.FirstName)
assert.Equal(t, req.LastName, u.LastName)
assert.Equal(t, req.Email, u.Email)
assert.NotEmpty(t, rsp.Token)
})
t.Run("DuplicateEmail", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &req, &rsp)
assert.Equal(t, handler.ErrDuplicateEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("DifferentEmail", func(t *testing.T) {
var rsp pb.CreateResponse
req := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &req, &rsp)
assert.NoError(t, err)
u := rsp.User
if u == nil {
t.Fatalf("No user returned")
}
assert.NotEmpty(t, u.Id)
assert.Equal(t, req.FirstName, u.FirstName)
assert.Equal(t, req.LastName, u.LastName)
assert.Equal(t, req.Email, u.Email)
})
}

View File

@@ -1,44 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Delete a user
func (u *Users) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
// delete the users tokens
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Delete(&Token{}, &Token{UserID: req.Id}).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// delete from the database
if err := tx.Delete(&User{}, &User{ID: req.Id}).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
return nil
})
}

View File

@@ -1,55 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestDelete(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
t.Run("Valid", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{
Id: cRsp.User.Id,
}, &pb.DeleteResponse{})
assert.NoError(t, err)
// check it was actually deleted
var rsp pb.ReadResponse
err = h.Read(microAccountCtx(), &pb.ReadRequest{
Ids: []string{cRsp.User.Id},
}, &rsp)
assert.NoError(t, err)
assert.Nil(t, rsp.Users[cRsp.User.Id])
})
t.Run("Retry", func(t *testing.T) {
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{
Id: cRsp.User.Id,
}, &pb.DeleteResponse{})
assert.NoError(t, err)
})
}

View File

@@ -1,88 +0,0 @@
package handler
import (
"regexp"
"time"
"github.com/micro/micro/v3/service/errors"
gorm2 "github.com/micro/services/pkg/gorm"
pb "github.com/micro/services/users/proto"
"golang.org/x/crypto/bcrypt"
)
var (
ErrMissingFirstName = errors.BadRequest("MISSING_FIRST_NAME", "Missing first name")
ErrMissingLastName = errors.BadRequest("MISSING_LAST_NAME", "Missing last name")
ErrMissingEmail = errors.BadRequest("MISSING_EMAIL", "Missing email")
ErrDuplicateEmail = errors.BadRequest("DUPLICATE_EMAIL", "A user with this email address already exists")
ErrInvalidEmail = errors.BadRequest("INVALID_EMAIL", "The email provided is invalid")
ErrInvalidPassword = errors.BadRequest("INVALID_PASSWORD", "Password must be at least 8 characters long")
ErrMissingEmails = errors.BadRequest("MISSING_EMAILS", "One or more emails are required")
ErrMissingIDs = errors.BadRequest("MISSING_IDS", "One or more ids are required")
ErrMissingID = errors.BadRequest("MISSING_ID", "Missing ID")
ErrMissingToken = errors.BadRequest("MISSING_TOKEN", "Missing token")
ErrIncorrectPassword = errors.BadRequest("INCORRECT_PASSWORD", "Incorrect password")
ErrTokenExpired = errors.BadRequest("TOKEN_EXPIRED", "Token has expired")
ErrInvalidToken = errors.BadRequest("INVALID_TOKEN", "Token is invalid")
ErrNotFound = errors.NotFound("NOT_FOUND", "User not found")
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
tokenTTL = time.Hour * 7 * 24
)
type User struct {
ID string
FirstName string
LastName string
Email string `gorm:"uniqueIndex"`
Password string
CreatedAt time.Time
Tokens []Token
}
func (u *User) Serialize() *pb.User {
return &pb.User{
Id: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
Email: u.Email,
}
}
type Token struct {
Key string `gorm:"primaryKey"`
CreatedAt time.Time
ExpiresAt time.Time
UserID string
User User
}
type Users struct {
gorm2.Helper
Time func() time.Time
}
func NewHandler(t func() time.Time) *Users {
return &Users{Time: t}
}
// isEmailValid checks if the email provided passes the required structure and length.
func isEmailValid(e string) bool {
if len(e) < 3 && len(e) > 254 {
return false
}
return emailRegex.MatchString(e)
}
func hashAndSalt(pwd string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hash), nil
}
func passwordsMatch(hashed string, plain string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashed), []byte(plain))
return err == nil
}

View File

@@ -1,55 +0,0 @@
package handler_test
import (
"context"
"database/sql"
"os"
"testing"
"time"
"github.com/micro/micro/v3/service/auth"
"github.com/stretchr/testify/assert"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
_ "github.com/jackc/pgx/v4/stdlib"
)
func testHandler(t *testing.T) *handler.Users {
// connect to the database
addr := os.Getenv("POSTGRES_URL")
if len(addr) == 0 {
addr = "postgresql://postgres@localhost:5432/postgres?sslmode=disable"
}
sqlDB, err := sql.Open("pgx", addr)
if err != nil {
t.Fatalf("Failed to open connection to DB %s", err)
}
// clean any data from a previous run
if _, err := sqlDB.Exec(`DROP TABLE IF EXISTS "micro_someID_users", "micro_someID_tokens" CASCADE`); err != nil {
t.Fatalf("Error cleaning database: %v", err)
}
h := handler.NewHandler(time.Now)
h.DBConn(sqlDB).Migrations(&handler.User{}, &handler.Token{})
return h
}
func assertUsersMatch(t *testing.T, exp, act *pb.User) {
if act == nil {
t.Error("No user returned")
return
}
assert.Equal(t, exp.Id, act.Id)
assert.Equal(t, exp.FirstName, act.FirstName)
assert.Equal(t, exp.LastName, act.LastName)
assert.Equal(t, exp.Email, act.Email)
}
func microAccountCtx() context.Context {
return auth.ContextWithAccount(context.TODO(), &auth.Account{
Issuer: "micro",
ID: "someID",
})
}

View File

@@ -1,36 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
)
// List all users
func (u *Users) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// query the database
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
var users []User
if err := db.Model(&User{}).Find(&users).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Users = make([]*pb.User, len(users))
for i, u := range users {
rsp.Users[i] = u.Serialize()
}
return nil
}

View File

@@ -1,66 +0,0 @@
package handler_test
import (
"testing"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestList(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
var rsp pb.ListResponse
err = h.List(microAccountCtx(), &pb.ListRequest{}, &rsp)
assert.NoError(t, err)
if rsp.Users == nil {
t.Error("No users returned")
return
}
var u1Found, u2Found bool
for _, u := range rsp.Users {
switch u.Id {
case cRsp1.User.Id:
assertUsersMatch(t, cRsp1.User, u)
u1Found = true
case cRsp2.User.Id:
assertUsersMatch(t, cRsp2.User, u)
u2Found = true
default:
t.Fatal("Unexpected user returned")
return
}
}
assert.True(t, u1Found)
assert.True(t, u2Found)
}

View File

@@ -1,65 +0,0 @@
package handler
import (
"context"
"github.com/google/uuid"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Login using email and password returns the users profile and a token
func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Email) == 0 {
return ErrMissingEmail
}
if len(req.Password) == 0 {
return ErrInvalidPassword
}
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
return db.Transaction(func(tx *gorm.DB) error {
// lookup the user
var user User
if err := tx.Where(&User{Email: req.Email}).First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// compare the passwords
if !passwordsMatch(user.Password, req.Password) {
return ErrIncorrectPassword
}
// generate a token for the user
token := Token{
UserID: user.ID,
Key: uuid.New().String(),
ExpiresAt: u.Time().Add(tokenTTL),
}
if err := tx.Create(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Token = token.Key
rsp.User = user.Serialize()
return nil
})
}

View File

@@ -1,82 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestLogin(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
tt := []struct {
Name string
Email string
Password string
Error error
User *pb.User
}{
{
Name: "MissingEmail",
Password: "passwordabc",
Error: handler.ErrMissingEmail,
},
{
Name: "MissingPassword",
Email: "john@doe.com",
Error: handler.ErrInvalidPassword,
},
{
Name: "UserNotFound",
Email: "foo@bar.com",
Password: "passwordabc",
Error: handler.ErrNotFound,
},
{
Name: "IncorrectPassword",
Email: "john@doe.com",
Password: "passwordabcdef",
Error: handler.ErrIncorrectPassword,
},
{
Name: "Valid",
Email: "john@doe.com",
Password: "passwordabc",
User: cRsp.User,
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
var rsp pb.LoginResponse
err := h.Login(microAccountCtx(), &pb.LoginRequest{
Email: tc.Email, Password: tc.Password,
}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.User != nil {
assertUsersMatch(t, tc.User, rsp.User)
assert.NotEmpty(t, rsp.Token)
} else {
assert.Nil(t, tc.User)
}
})
}
}

View File

@@ -1,47 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Logout expires all tokens for the user
func (u *Users) Logout(ctx context.Context, req *pb.LogoutRequest, rsp *pb.LogoutResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
return db.Transaction(func(tx *gorm.DB) error {
// lookup the user
var user User
if err := tx.Where(&User{ID: req.Id}).Preload("Tokens").First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// delete the tokens
if err := tx.Delete(user.Tokens).Error; err != nil {
logger.Errorf("Error deleting from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
return nil
})
}

View File

@@ -1,47 +0,0 @@
package handler_test
import (
"testing"
"github.com/google/uuid"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestLogout(t *testing.T) {
h := testHandler(t)
t.Run("MissingUserID", func(t *testing.T) {
err := h.Logout(microAccountCtx(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
assert.Equal(t, handler.ErrMissingID, err)
})
t.Run("UserNotFound", func(t *testing.T) {
err := h.Logout(microAccountCtx(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
assert.Equal(t, handler.ErrNotFound, err)
})
t.Run("Valid", func(t *testing.T) {
// create some mock data
var cRsp pb.CreateResponse
cReq := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq, &cRsp)
assert.NoError(t, err)
if cRsp.User == nil {
t.Fatal("No user returned")
return
}
err = h.Logout(microAccountCtx(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
assert.NoError(t, err)
err = h.Validate(microAccountCtx(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
assert.Error(t, err)
})
}

View File

@@ -1,41 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
)
// Read users using ID
func (u *Users) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Ids) == 0 {
return ErrMissingIDs
}
// query the database
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
var users []User
if err := db.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Users = make(map[string]*pb.User, len(users))
for _, u := range users {
rsp.Users[u.ID] = u.Serialize()
}
return nil
}

View File

@@ -1,46 +0,0 @@
package handler
import (
"context"
"strings"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
)
// Read users using email
func (u *Users) ReadByEmail(ctx context.Context, req *pb.ReadByEmailRequest, rsp *pb.ReadByEmailResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Emails) == 0 {
return ErrMissingEmails
}
emails := make([]string, len(req.Emails))
for i, e := range req.Emails {
emails[i] = strings.ToLower(e)
}
// query the database
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
var users []User
if err := db.Model(&User{}).Where("lower(email) IN (?)", emails).Find(&users).Error; err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.Users = make(map[string]*pb.User, len(users))
for _, u := range users {
rsp.Users[u.Email] = u.Serialize()
}
return nil
}

View File

@@ -1,88 +0,0 @@
package handler_test
import (
"strings"
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestReadByEmail(t *testing.T) {
h := testHandler(t)
t.Run("MissingEmails", func(t *testing.T) {
var rsp pb.ReadByEmailResponse
err := h.ReadByEmail(microAccountCtx(), &pb.ReadByEmailRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingEmails, err)
assert.Nil(t, rsp.Users)
})
t.Run("NotFound", func(t *testing.T) {
var rsp pb.ReadByEmailResponse
err := h.ReadByEmail(microAccountCtx(), &pb.ReadByEmailRequest{Emails: []string{"foo"}}, &rsp)
assert.Nil(t, err)
if rsp.Users == nil {
t.Fatal("Expected the users object to not be nil")
}
assert.Nil(t, rsp.Users["foo"])
})
// create some mock data
var rsp1 pb.CreateResponse
req1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &req1, &rsp1)
assert.NoError(t, err)
if rsp1.User == nil {
t.Fatal("No user returned")
return
}
var rsp2 pb.CreateResponse
req2 := pb.CreateRequest{
FirstName: "Apple",
LastName: "Tree",
Email: "apple@tree.com",
Password: "passwordabc",
}
err = h.Create(microAccountCtx(), &req2, &rsp2)
assert.NoError(t, err)
if rsp2.User == nil {
t.Fatal("No user returned")
return
}
// test the read
var rsp pb.ReadByEmailResponse
err = h.ReadByEmail(microAccountCtx(), &pb.ReadByEmailRequest{
Emails: []string{rsp1.User.Email, strings.ToUpper(rsp2.User.Email)},
}, &rsp)
assert.NoError(t, err)
if rsp.Users == nil {
t.Fatal("Users not returned")
return
}
assert.NotNil(t, rsp.Users[rsp1.User.Email])
assert.NotNil(t, rsp.Users[rsp2.User.Email])
// check the users match
if u := rsp.Users[rsp1.User.Email]; u != nil {
assert.Equal(t, rsp1.User.Id, u.Id)
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
assert.Equal(t, rsp1.User.LastName, u.LastName)
assert.Equal(t, rsp1.User.Email, u.Email)
}
if u := rsp.Users[rsp2.User.Email]; u != nil {
assert.Equal(t, rsp2.User.Id, u.Id)
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
assert.Equal(t, rsp2.User.LastName, u.LastName)
assert.Equal(t, rsp2.User.Email, u.Email)
}
}

View File

@@ -1,87 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
h := testHandler(t)
t.Run("MissingIDs", func(t *testing.T) {
var rsp pb.ReadResponse
err := h.Read(microAccountCtx(), &pb.ReadRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingIDs, err)
assert.Nil(t, rsp.Users)
})
t.Run("NotFound", func(t *testing.T) {
var rsp pb.ReadResponse
err := h.Read(microAccountCtx(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp)
assert.Nil(t, err)
if rsp.Users == nil {
t.Fatal("Expected the users object to not be nil")
}
assert.Nil(t, rsp.Users["foo"])
})
// create some mock data
var rsp1 pb.CreateResponse
req1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &req1, &rsp1)
assert.NoError(t, err)
if rsp1.User == nil {
t.Fatal("No user returned")
return
}
var rsp2 pb.CreateResponse
req2 := pb.CreateRequest{
FirstName: "Apple",
LastName: "Tree",
Email: "apple@tree.com",
Password: "passwordabc",
}
err = h.Create(microAccountCtx(), &req2, &rsp2)
assert.NoError(t, err)
if rsp2.User == nil {
t.Fatal("No user returned")
return
}
// test the read
var rsp pb.ReadResponse
err = h.Read(microAccountCtx(), &pb.ReadRequest{
Ids: []string{rsp1.User.Id, rsp2.User.Id},
}, &rsp)
assert.NoError(t, err)
if rsp.Users == nil {
t.Fatal("Users not returned")
return
}
assert.NotNil(t, rsp.Users[rsp1.User.Id])
assert.NotNil(t, rsp.Users[rsp2.User.Id])
// check the users match
if u := rsp.Users[rsp1.User.Id]; u != nil {
assert.Equal(t, rsp1.User.Id, u.Id)
assert.Equal(t, rsp1.User.FirstName, u.FirstName)
assert.Equal(t, rsp1.User.LastName, u.LastName)
assert.Equal(t, rsp1.User.Email, u.Email)
}
if u := rsp.Users[rsp2.User.Id]; u != nil {
assert.Equal(t, rsp2.User.Id, u.Id)
assert.Equal(t, rsp2.User.FirstName, u.FirstName)
assert.Equal(t, rsp2.User.LastName, u.LastName)
assert.Equal(t, rsp2.User.Email, u.Email)
}
}

View File

@@ -1,87 +0,0 @@
package handler
import (
"context"
"regexp"
"strings"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Update a user
func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Id) == 0 {
return ErrMissingID
}
if req.FirstName != nil && len(req.FirstName.Value) == 0 {
return ErrMissingFirstName
}
if req.LastName != nil && len(req.LastName.Value) == 0 {
return ErrMissingLastName
}
if req.Email != nil && len(req.Email.Value) == 0 {
return ErrMissingEmail
}
if req.Email != nil && !isEmailValid(req.Email.Value) {
return ErrInvalidEmail
}
if req.Password != nil && len(req.Password.Value) < 8 {
return ErrInvalidEmail
}
// lookup the user
var user User
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
if err := db.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound {
return ErrNotFound
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// assign the updated values
if req.FirstName != nil {
user.FirstName = req.FirstName.Value
}
if req.LastName != nil {
user.LastName = req.LastName.Value
}
if req.Email != nil {
user.Email = strings.ToLower(req.Email.Value)
}
if req.Password != nil {
p, err := hashAndSalt(req.Password.Value)
if err != nil {
logger.Errorf("Error hasing and salting password: %v", err)
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
}
user.Password = p
}
// write the user to the database
err = db.Save(user).Error
if err != nil {
if match, _ := regexp.MatchString(`idx_[\S]+_users_email`, err.Error()); match {
return ErrDuplicateEmail
}
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the user
rsp.User = user.Serialize()
return nil
}

View File

@@ -1,147 +0,0 @@
package handler_test
import (
"testing"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/wrapperspb"
)
func TestUpdate(t *testing.T) {
h := testHandler(t)
t.Run("MissingID", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{}, &rsp)
assert.Equal(t, handler.ErrMissingID, err)
assert.Nil(t, rsp.User)
})
t.Run("NotFound", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{Id: "foo"}, &rsp)
assert.Equal(t, handler.ErrNotFound, err)
assert.Nil(t, rsp.User)
})
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "johndoe@gmail.com",
Password: "passwordabc",
}
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
t.Run("BlankFirstName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingFirstName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankLastName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingLastName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankLastName", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingLastName, err)
assert.Nil(t, rsp.User)
})
t.Run("BlankEmail", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{},
}, &rsp)
assert.Equal(t, handler.ErrMissingEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("InvalidEmail", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"},
}, &rsp)
assert.Equal(t, handler.ErrInvalidEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("EmailAlreadyExists", func(t *testing.T) {
var rsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email},
}, &rsp)
assert.Equal(t, handler.ErrDuplicateEmail, err)
assert.Nil(t, rsp.User)
})
t.Run("Valid", func(t *testing.T) {
uReq := pb.UpdateRequest{
Id: cRsp1.User.Id,
Email: &wrapperspb.StringValue{Value: "foobar@gmail.com"},
FirstName: &wrapperspb.StringValue{Value: "Foo"},
LastName: &wrapperspb.StringValue{Value: "Bar"},
}
var uRsp pb.UpdateResponse
err := h.Update(microAccountCtx(), &uReq, &uRsp)
assert.NoError(t, err)
if uRsp.User == nil {
t.Error("No user returned")
return
}
assert.Equal(t, cRsp1.User.Id, uRsp.User.Id)
assert.Equal(t, uReq.Email.Value, uRsp.User.Email)
assert.Equal(t, uReq.FirstName.Value, uRsp.User.FirstName)
assert.Equal(t, uReq.LastName.Value, uRsp.User.LastName)
})
t.Run("UpdatePassword", func(t *testing.T) {
uReq := pb.UpdateRequest{
Id: cRsp2.User.Id,
Password: &wrapperspb.StringValue{Value: "helloworld"},
}
err := h.Update(microAccountCtx(), &uReq, &pb.UpdateResponse{})
assert.NoError(t, err)
lReq := pb.LoginRequest{
Email: cRsp2.User.Email,
Password: "helloworld",
}
err = h.Login(microAccountCtx(), &lReq, &pb.LoginResponse{})
assert.NoError(t, err)
})
}

View File

@@ -1,55 +0,0 @@
package handler
import (
"context"
"github.com/micro/micro/v3/service/auth"
"github.com/micro/micro/v3/service/errors"
"github.com/micro/micro/v3/service/logger"
pb "github.com/micro/services/users/proto"
"gorm.io/gorm"
)
// Validate a token, each time a token is validated it extends its lifetime for another week
func (u *Users) Validate(ctx context.Context, req *pb.ValidateRequest, rsp *pb.ValidateResponse) error {
_, ok := auth.AccountFromContext(ctx)
if !ok {
errors.Unauthorized("UNAUTHORIZED", "Unauthorized")
}
// validate the request
if len(req.Token) == 0 {
return ErrMissingToken
}
db, err := u.GetDBConn(ctx)
if err != nil {
logger.Errorf("Error connecting to DB: %v", err)
return errors.InternalServerError("DB_ERROR", "Error connecting to DB")
}
return db.Transaction(func(tx *gorm.DB) error {
// lookup the token
var token Token
if err := tx.Where(&Token{Key: req.Token}).Preload("User").First(&token).Error; err == gorm.ErrRecordNotFound {
return ErrInvalidToken
} else if err != nil {
logger.Errorf("Error reading from the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// ensure the token is valid
if u.Time().After(token.ExpiresAt) {
return ErrTokenExpired
}
// extend the token for another lifetime
token.ExpiresAt = u.Time().Add(tokenTTL)
if err := tx.Save(&token).Error; err != nil {
logger.Errorf("Error writing to the database: %v", err)
return errors.InternalServerError("DATABASE_ERROR", "Error connecting to the database")
}
// serialize the response
rsp.User = token.User.Serialize()
return nil
})
}

View File

@@ -1,100 +0,0 @@
package handler_test
import (
"testing"
"time"
"github.com/google/uuid"
"github.com/micro/services/users/handler"
pb "github.com/micro/services/users/proto"
"github.com/stretchr/testify/assert"
)
func TestValidate(t *testing.T) {
h := testHandler(t)
// create some mock data
var cRsp1 pb.CreateResponse
cReq1 := pb.CreateRequest{
FirstName: "John",
LastName: "Doe",
Email: "john@doe.com",
Password: "passwordabc",
}
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
assert.NoError(t, err)
if cRsp1.User == nil {
t.Fatal("No user returned")
return
}
var cRsp2 pb.CreateResponse
cReq2 := pb.CreateRequest{
FirstName: "Barry",
LastName: "Doe",
Email: "barry@doe.com",
Password: "passwordabc",
}
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
assert.NoError(t, err)
if cRsp2.User == nil {
t.Fatal("No user returned")
return
}
tt := []struct {
Name string
Token string
Time func() time.Time
Error error
User *pb.User
}{
{
Name: "MissingToken",
Error: handler.ErrMissingToken,
},
{
Name: "InvalidToken",
Error: handler.ErrInvalidToken,
Token: uuid.New().String(),
},
{
Name: "ExpiredToken",
Error: handler.ErrTokenExpired,
Token: cRsp1.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
},
{
Name: "ValidToken",
User: cRsp2.User,
Token: cRsp2.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 3) },
},
{
Name: "RefreshedToken",
User: cRsp2.User,
Token: cRsp2.Token,
Time: func() time.Time { return time.Now().Add(time.Hour * 24 * 8) },
},
}
for _, tc := range tt {
t.Run(tc.Name, func(t *testing.T) {
if tc.Time == nil {
h.Time = time.Now
} else {
h.Time = tc.Time
}
var rsp pb.ValidateResponse
err := h.Validate(microAccountCtx(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
assert.Equal(t, tc.Error, err)
if tc.User != nil {
assertUsersMatch(t, tc.User, rsp.User)
} else {
assert.Nil(t, tc.User)
}
})
}
}