mirror of
https://github.com/kevin-DL/services.git
synced 2026-01-14 03:54:47 +00:00
Multitenant users api (#75)
This commit is contained in:
@@ -2,10 +2,12 @@ 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"
|
||||
@@ -14,6 +16,11 @@ import (
|
||||
|
||||
// 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
|
||||
@@ -34,11 +41,15 @@ func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.Creat
|
||||
// hash and salt the password using bcrypt
|
||||
phash, err := hashAndSalt(req.Password)
|
||||
if err != nil {
|
||||
logger.Errorf("Error hasing and salting password: %v", err)
|
||||
logger.Errorf("Error hashing and salting password: %v", err)
|
||||
return errors.InternalServerError("HASHING_ERROR", "Error hashing password")
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
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(),
|
||||
@@ -47,10 +58,12 @@ func (u *Users) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.Creat
|
||||
Email: strings.ToLower(req.Email),
|
||||
Password: phash,
|
||||
}
|
||||
err = u.DB.Create(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
@@ -61,7 +60,7 @@ func TestCreate(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
err := h.Create(context.TODO(), &pb.CreateRequest{
|
||||
err := h.Create(microAccountCtx(), &pb.CreateRequest{
|
||||
FirstName: tc.FirstName,
|
||||
LastName: tc.LastName,
|
||||
Email: tc.Email,
|
||||
@@ -79,7 +78,7 @@ func TestCreate(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
err := h.Create(microAccountCtx(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
@@ -101,7 +100,7 @@ func TestCreate(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
err := h.Create(microAccountCtx(), &req, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
@@ -114,7 +113,7 @@ func TestCreate(t *testing.T) {
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req, &rsp)
|
||||
err := h.Create(microAccountCtx(), &req, &rsp)
|
||||
|
||||
assert.NoError(t, err)
|
||||
u := rsp.User
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -11,13 +12,22 @@ import (
|
||||
|
||||
// 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 u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
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")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
@@ -13,7 +12,7 @@ func TestDelete(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
|
||||
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{}, &pb.DeleteResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
@@ -25,7 +24,7 @@ func TestDelete(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
err := h.Create(microAccountCtx(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -33,14 +32,14 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
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(context.TODO(), &pb.ReadRequest{
|
||||
err = h.Read(microAccountCtx(), &pb.ReadRequest{
|
||||
Ids: []string{cRsp.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
@@ -48,7 +47,7 @@ func TestDelete(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("Retry", func(t *testing.T) {
|
||||
err := h.Delete(context.TODO(), &pb.DeleteRequest{
|
||||
err := h.Delete(microAccountCtx(), &pb.DeleteRequest{
|
||||
Id: cRsp.User.Id,
|
||||
}, &pb.DeleteResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/micro/micro/v3/service/auth"
|
||||
"github.com/micro/micro/v3/service/errors"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -58,8 +63,38 @@ type Token struct {
|
||||
}
|
||||
|
||||
type Users struct {
|
||||
DB *gorm.DB
|
||||
Time func() time.Time
|
||||
Time func() time.Time
|
||||
Dialector gorm.Dialector
|
||||
dbMigrations map[string]bool
|
||||
}
|
||||
|
||||
func NewHandler(t func() time.Time, d gorm.Dialector) *Users {
|
||||
return &Users{Time: t, Dialector: d, dbMigrations: map[string]bool{}}
|
||||
}
|
||||
|
||||
func (u *Users) getDBConn(ctx context.Context) (*gorm.DB, error) {
|
||||
acc, ok := auth.AccountFromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing account from context")
|
||||
}
|
||||
db, err := gorm.Open(u.Dialector, &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: fmt.Sprintf("%s_", strings.ReplaceAll(acc.Issuer, "-", "")),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// skip migration if we've already done it
|
||||
if u.dbMigrations[acc.Issuer] {
|
||||
return db, nil
|
||||
}
|
||||
if err := db.AutoMigrate(&User{}, &Token{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// record success
|
||||
u.dbMigrations[acc.Issuer] = true
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// isEmailValid checks if the email provided passes the required structure and length.
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/micro/micro/v3/service/auth"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/gorm/schema"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
pb "github.com/micro/services/users/proto"
|
||||
@@ -19,13 +22,16 @@ func testHandler(t *testing.T) *handler.Users {
|
||||
if len(addr) == 0 {
|
||||
addr = "postgresql://postgres@localhost:5432/postgres?sslmode=disable"
|
||||
}
|
||||
db, err := gorm.Open(postgres.Open(addr), &gorm.Config{})
|
||||
dial := postgres.Open(addr)
|
||||
db, err := gorm.Open(dial, &gorm.Config{
|
||||
NamingStrategy: schema.NamingStrategy{TablePrefix: "micro_"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Error connecting to database: %v", err)
|
||||
}
|
||||
|
||||
// clean any data from a previous run
|
||||
if err := db.Exec("DROP TABLE IF EXISTS users, tokens CASCADE").Error; err != nil {
|
||||
if err := db.Exec("DROP TABLE IF EXISTS micro_users, micro_tokens CASCADE").Error; err != nil {
|
||||
t.Fatalf("Error cleaning database: %v", err)
|
||||
}
|
||||
|
||||
@@ -33,8 +39,7 @@ func testHandler(t *testing.T) *handler.Users {
|
||||
if err := db.AutoMigrate(&handler.User{}, &handler.Token{}); err != nil {
|
||||
t.Fatalf("Error migrating database: %v", err)
|
||||
}
|
||||
|
||||
return &handler.Users{DB: db, Time: time.Now}
|
||||
return handler.NewHandler(time.Now, dial)
|
||||
}
|
||||
|
||||
func assertUsersMatch(t *testing.T, exp, act *pb.User) {
|
||||
@@ -47,3 +52,9 @@ func assertUsersMatch(t *testing.T, exp, act *pb.User) {
|
||||
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",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -10,9 +11,18 @@ import (
|
||||
|
||||
// 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 := u.DB.Model(&User{}).Find(&users).Error; err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
pb "github.com/micro/services/users/proto"
|
||||
@@ -19,7 +18,7 @@ func TestList(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -33,7 +32,7 @@ func TestList(t *testing.T) {
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -41,7 +40,7 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
var rsp pb.ListResponse
|
||||
err = h.List(context.TODO(), &pb.ListRequest{}, &rsp)
|
||||
err = h.List(microAccountCtx(), &pb.ListRequest{}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
if rsp.Users == nil {
|
||||
t.Error("No users returned")
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
@@ -12,6 +13,10 @@ import (
|
||||
|
||||
// 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
|
||||
@@ -20,7 +25,13 @@ func (u *Users) Login(ctx context.Context, req *pb.LoginRequest, rsp *pb.LoginRe
|
||||
return ErrInvalidPassword
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
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 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
@@ -20,7 +19,7 @@ func TestLogin(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
err := h.Create(microAccountCtx(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -67,7 +66,7 @@ func TestLogin(t *testing.T) {
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.Name, func(t *testing.T) {
|
||||
var rsp pb.LoginResponse
|
||||
err := h.Login(context.TODO(), &pb.LoginRequest{
|
||||
err := h.Login(microAccountCtx(), &pb.LoginRequest{
|
||||
Email: tc.Email, Password: tc.Password,
|
||||
}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -11,12 +12,21 @@ import (
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
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 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -14,12 +13,12 @@ func TestLogout(t *testing.T) {
|
||||
h := testHandler(t)
|
||||
|
||||
t.Run("MissingUserID", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
|
||||
err := h.Logout(microAccountCtx(), &pb.LogoutRequest{}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrMissingID, err)
|
||||
})
|
||||
|
||||
t.Run("UserNotFound", func(t *testing.T) {
|
||||
err := h.Logout(context.TODO(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
|
||||
err := h.Logout(microAccountCtx(), &pb.LogoutRequest{Id: uuid.New().String()}, &pb.LogoutResponse{})
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
})
|
||||
|
||||
@@ -32,17 +31,17 @@ func TestLogout(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq, &cRsp)
|
||||
err := h.Create(microAccountCtx(), &cReq, &cRsp)
|
||||
assert.NoError(t, err)
|
||||
if cRsp.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.Logout(context.TODO(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
|
||||
err = h.Logout(microAccountCtx(), &pb.LogoutRequest{Id: cRsp.User.Id}, &pb.LogoutResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = h.Validate(context.TODO(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
|
||||
err = h.Validate(microAccountCtx(), &pb.ValidateRequest{Token: cRsp.Token}, &pb.ValidateResponse{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -10,14 +11,23 @@ import (
|
||||
|
||||
// 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 := u.DB.Model(&User{}).Where("id IN (?)", req.Ids).Find(&users).Error; err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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"
|
||||
@@ -11,6 +12,10 @@ import (
|
||||
|
||||
// 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
|
||||
@@ -21,8 +26,13 @@ func (u *Users) ReadByEmail(ctx context.Context, req *pb.ReadByEmailRequest, rsp
|
||||
}
|
||||
|
||||
// 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 := u.DB.Model(&User{}).Where("lower(email) IN (?)", emails).Find(&users).Error; err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -15,14 +14,14 @@ func TestReadByEmail(t *testing.T) {
|
||||
|
||||
t.Run("MissingEmails", func(t *testing.T) {
|
||||
var rsp pb.ReadByEmailResponse
|
||||
err := h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{}, &rsp)
|
||||
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(context.TODO(), &pb.ReadByEmailRequest{Emails: []string{"foo"}}, &rsp)
|
||||
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")
|
||||
@@ -38,7 +37,7 @@ func TestReadByEmail(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req1, &rsp1)
|
||||
err := h.Create(microAccountCtx(), &req1, &rsp1)
|
||||
assert.NoError(t, err)
|
||||
if rsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -52,7 +51,7 @@ func TestReadByEmail(t *testing.T) {
|
||||
Email: "apple@tree.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &req2, &rsp2)
|
||||
err = h.Create(microAccountCtx(), &req2, &rsp2)
|
||||
assert.NoError(t, err)
|
||||
if rsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -61,7 +60,7 @@ func TestReadByEmail(t *testing.T) {
|
||||
|
||||
// test the read
|
||||
var rsp pb.ReadByEmailResponse
|
||||
err = h.ReadByEmail(context.TODO(), &pb.ReadByEmailRequest{
|
||||
err = h.ReadByEmail(microAccountCtx(), &pb.ReadByEmailRequest{
|
||||
Emails: []string{rsp1.User.Email, strings.ToUpper(rsp2.User.Email)},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
@@ -14,14 +13,14 @@ func TestRead(t *testing.T) {
|
||||
|
||||
t.Run("MissingIDs", func(t *testing.T) {
|
||||
var rsp pb.ReadResponse
|
||||
err := h.Read(context.TODO(), &pb.ReadRequest{}, &rsp)
|
||||
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(context.TODO(), &pb.ReadRequest{Ids: []string{"foo"}}, &rsp)
|
||||
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")
|
||||
@@ -37,7 +36,7 @@ func TestRead(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &req1, &rsp1)
|
||||
err := h.Create(microAccountCtx(), &req1, &rsp1)
|
||||
assert.NoError(t, err)
|
||||
if rsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -51,7 +50,7 @@ func TestRead(t *testing.T) {
|
||||
Email: "apple@tree.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &req2, &rsp2)
|
||||
err = h.Create(microAccountCtx(), &req2, &rsp2)
|
||||
assert.NoError(t, err)
|
||||
if rsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -60,7 +59,7 @@ func TestRead(t *testing.T) {
|
||||
|
||||
// test the read
|
||||
var rsp pb.ReadResponse
|
||||
err = h.Read(context.TODO(), &pb.ReadRequest{
|
||||
err = h.Read(microAccountCtx(), &pb.ReadRequest{
|
||||
Ids: []string{rsp1.User.Id, rsp2.User.Id},
|
||||
}, &rsp)
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -2,8 +2,10 @@ 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"
|
||||
@@ -12,6 +14,10 @@ import (
|
||||
|
||||
// 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
|
||||
@@ -34,7 +40,12 @@ func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.Updat
|
||||
|
||||
// lookup the user
|
||||
var user User
|
||||
if err := u.DB.Where(&User{ID: req.Id}).First(&user).Error; err == gorm.ErrRecordNotFound {
|
||||
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)
|
||||
@@ -61,10 +72,11 @@ func (u *Users) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.Updat
|
||||
}
|
||||
|
||||
// write the user to the database
|
||||
err := u.DB.Save(user).Error
|
||||
if err != nil && strings.Contains(err.Error(), "idx_users_email") {
|
||||
return ErrDuplicateEmail
|
||||
} else if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/micro/services/users/handler"
|
||||
@@ -15,14 +14,14 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("MissingID", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{}, &rsp)
|
||||
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(context.TODO(), &pb.UpdateRequest{Id: "foo"}, &rsp)
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{Id: "foo"}, &rsp)
|
||||
assert.Equal(t, handler.ErrNotFound, err)
|
||||
assert.Nil(t, rsp.User)
|
||||
})
|
||||
@@ -35,7 +34,7 @@ func TestUpdate(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -49,7 +48,7 @@ func TestUpdate(t *testing.T) {
|
||||
Email: "johndoe@gmail.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -58,7 +57,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("BlankFirstName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, FirstName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingFirstName, err)
|
||||
@@ -67,7 +66,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
@@ -76,7 +75,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("BlankLastName", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, LastName: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingLastName, err)
|
||||
@@ -85,7 +84,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("BlankEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrMissingEmail, err)
|
||||
@@ -94,7 +93,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("InvalidEmail", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: "foo.bar"},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrInvalidEmail, err)
|
||||
@@ -103,7 +102,7 @@ func TestUpdate(t *testing.T) {
|
||||
|
||||
t.Run("EmailAlreadyExists", func(t *testing.T) {
|
||||
var rsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &pb.UpdateRequest{
|
||||
err := h.Update(microAccountCtx(), &pb.UpdateRequest{
|
||||
Id: cRsp1.User.Id, Email: &wrapperspb.StringValue{Value: cRsp2.User.Email},
|
||||
}, &rsp)
|
||||
assert.Equal(t, handler.ErrDuplicateEmail, err)
|
||||
@@ -118,7 +117,7 @@ func TestUpdate(t *testing.T) {
|
||||
LastName: &wrapperspb.StringValue{Value: "Bar"},
|
||||
}
|
||||
var uRsp pb.UpdateResponse
|
||||
err := h.Update(context.TODO(), &uReq, &uRsp)
|
||||
err := h.Update(microAccountCtx(), &uReq, &uRsp)
|
||||
assert.NoError(t, err)
|
||||
if uRsp.User == nil {
|
||||
t.Error("No user returned")
|
||||
@@ -135,14 +134,14 @@ func TestUpdate(t *testing.T) {
|
||||
Id: cRsp2.User.Id,
|
||||
Password: &wrapperspb.StringValue{Value: "helloworld"},
|
||||
}
|
||||
err := h.Update(context.TODO(), &uReq, &pb.UpdateResponse{})
|
||||
err := h.Update(microAccountCtx(), &uReq, &pb.UpdateResponse{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
lReq := pb.LoginRequest{
|
||||
Email: cRsp2.User.Email,
|
||||
Password: "helloworld",
|
||||
}
|
||||
err = h.Login(context.TODO(), &lReq, &pb.LoginResponse{})
|
||||
err = h.Login(microAccountCtx(), &lReq, &pb.LoginResponse{})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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"
|
||||
@@ -11,12 +12,21 @@ import (
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return u.DB.Transaction(func(tx *gorm.DB) error {
|
||||
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 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package handler_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -22,7 +21,7 @@ func TestValidate(t *testing.T) {
|
||||
Email: "john@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err := h.Create(context.TODO(), &cReq1, &cRsp1)
|
||||
err := h.Create(microAccountCtx(), &cReq1, &cRsp1)
|
||||
assert.NoError(t, err)
|
||||
if cRsp1.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -36,7 +35,7 @@ func TestValidate(t *testing.T) {
|
||||
Email: "barry@doe.com",
|
||||
Password: "passwordabc",
|
||||
}
|
||||
err = h.Create(context.TODO(), &cReq2, &cRsp2)
|
||||
err = h.Create(microAccountCtx(), &cReq2, &cRsp2)
|
||||
assert.NoError(t, err)
|
||||
if cRsp2.User == nil {
|
||||
t.Fatal("No user returned")
|
||||
@@ -88,7 +87,7 @@ func TestValidate(t *testing.T) {
|
||||
}
|
||||
|
||||
var rsp pb.ValidateResponse
|
||||
err := h.Validate(context.TODO(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
|
||||
err := h.Validate(microAccountCtx(), &pb.ValidateRequest{Token: tc.Token}, &rsp)
|
||||
assert.Equal(t, tc.Error, err)
|
||||
|
||||
if tc.User != nil {
|
||||
|
||||
Reference in New Issue
Block a user