diff --git a/.gitignore b/.gitignore index 12b96aa..8514de9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __debug_* *secret* *.pem *Key.txt +coverage.out diff --git a/Makefile b/Makefile index 7af7251..5d2789e 100644 --- a/Makefile +++ b/Makefile @@ -26,4 +26,9 @@ test: lint: @echo "Running linter..." - golangci-lint run \ No newline at end of file + golangci-lint run + +coverage: + @echo "Generating test coverage report..." + @go test -coverprofile=coverage.out ./... + @go tool cover -html=coverage.out \ No newline at end of file diff --git a/cmd/maker-mock/main.go b/cmd/maker-mock/main.go index 21d0cca..a74b1e9 100644 --- a/cmd/maker-mock/main.go +++ b/cmd/maker-mock/main.go @@ -13,7 +13,7 @@ import ( "github.com/shopspring/decimal" ) -const pubKey = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" +const mockApiKey = "abcdef12345" const depthSize = 5 var HOST = "localhost" @@ -71,7 +71,7 @@ func cancelAllOrders() { return } - req.Header.Add("X-Public-Key", pubKey) + req.Header.Add("X-API-Key", fmt.Sprintf("Bearer %s", mockApiKey)) resp, err := client.Do(req) if err != nil { @@ -106,7 +106,7 @@ func placeOrder(side string, price, size decimal.Decimal) { log.Fatalf("error creating request: %v", err) } - req.Header.Add("X-Public-Key", pubKey) + req.Header.Add("X-API-Key", fmt.Sprintf("Bearer %s", mockApiKey)) res, err := client.Do(req) //fmt.Printf("res is ------->: %#v\n", res) diff --git a/cmd/order-book/main.go b/cmd/order-book/main.go index 95563df..8250d95 100644 --- a/cmd/order-book/main.go +++ b/cmd/order-book/main.go @@ -10,6 +10,7 @@ import ( "github.com/orbs-network/order-book/data/redisrepo" "github.com/orbs-network/order-book/service" + "github.com/orbs-network/order-book/serviceuser" "github.com/orbs-network/order-book/transport/rest" ) @@ -50,12 +51,18 @@ func setup() { log.Fatalf("error creating service: %v", err) } + userSvc, err := serviceuser.New(repository) + if err != nil { + log.Fatalf("error creating user service: %v", err) + } + router := mux.NewRouter() handler, err := rest.NewHandler(service, router) if err != nil { log.Fatalf("error creating handler: %v", err) } - handler.Init() + + handler.Init(userSvc.GetUserByApiKey) server := rest.NewHTTPServer(":"+port, handler.Router) server.StartServer() diff --git a/data/redisrepo/auction_test.go b/data/redisrepo/auction_test.go index 17935f8..16bdc9f 100644 --- a/data/redisrepo/auction_test.go +++ b/data/redisrepo/auction_test.go @@ -60,5 +60,34 @@ func TestRedisRepository_GetAuction(t *testing.T) { } func TestRedisRepository_RemoveAuction(t *testing.T) { + auctionID := uuid.MustParse("a777273e-12de-4acc-a4f8-de7fb5b86e37") + + t.Run("should remove auction", func(t *testing.T) { + + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + mock.ExpectDel(CreateAuctionKey(auctionID)).SetVal(1) + + err := repo.RemoveAuction(ctx, auctionID) + + assert.NoError(t, err) + }) + + t.Run("should return error in case of a Redis error", func(t *testing.T) { + db, mock := redismock.NewClientMock() + repo := &redisRepository{ + client: db, + } + + mock.ExpectDel(CreateAuctionKey(auctionID)).SetErr(assert.AnError) + + err := repo.RemoveAuction(ctx, auctionID) + + assert.Equal(t, assert.AnError, err) + }) } diff --git a/data/redisrepo/create_user.go b/data/redisrepo/create_user.go new file mode 100644 index 0000000..43a6706 --- /dev/null +++ b/data/redisrepo/create_user.go @@ -0,0 +1,47 @@ +package redisrepo + +import ( + "context" + "fmt" + + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +// CreateUser adds 2 entries to Redis: +// 1. User data indexed by API key +// 2. User data indexed by userId +func (r *redisRepository) CreateUser(ctx context.Context, user models.User) (models.User, error) { + + apiKeyKey := CreateUserApiKeyKey(user.ApiKey) + userIdKey := CreateUserIdKey(user.Id) + + if exists, err := r.client.Exists(ctx, apiKeyKey, userIdKey).Result(); err != nil { + logctx.Error(ctx, "unexpected error checking if user exists", logger.String("userId", user.Id.String()), logger.Error(err)) + return models.User{}, fmt.Errorf("unexpected error checking if user exists: %w", err) + } else if exists > 0 { + logctx.Warn(ctx, "user already exists", logger.String("userId", user.Id.String())) + return models.User{}, models.ErrUserAlreadyExists + } + + fields := map[string]interface{}{ + "id": user.Id.String(), + "type": user.Type.String(), + "pubKey": user.PubKey, + "apiKey": user.ApiKey, + } + + transaction := r.client.TxPipeline() + transaction.HMSet(ctx, apiKeyKey, fields) + transaction.HMSet(ctx, userIdKey, fields) + _, err := transaction.Exec(ctx) + + if err != nil { + logctx.Error(ctx, "unexpected error creating user", logger.String("userId", user.Id.String())) + return models.User{}, fmt.Errorf("unexpected error creating user: %w", err) + } + + logctx.Info(ctx, "user created", logger.String("userId", user.Id.String())) + return user, nil +} diff --git a/data/redisrepo/create_user_test.go b/data/redisrepo/create_user_test.go new file mode 100644 index 0000000..57f3803 --- /dev/null +++ b/data/redisrepo/create_user_test.go @@ -0,0 +1,126 @@ +package redisrepo + +import ( + "testing" + + "github.com/go-redis/redismock/v9" + "github.com/orbs-network/order-book/mocks" + "github.com/orbs-network/order-book/models" + "github.com/stretchr/testify/assert" +) + +func TestRedisRepository_CreateUser(t *testing.T) { + + fields := map[string]interface{}{ + "id": mocks.UserId.String(), + "type": mocks.UserType.String(), + "pubKey": mocks.PubKey, + "apiKey": mocks.ApiKey, + } + + t.Run("should successfully create user and return user instance on success", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + userApiKey := CreateUserApiKeyKey(mocks.ApiKey) + userIdKey := CreateUserIdKey(mocks.UserId) + + mock.ExpectExists(userApiKey, userIdKey).SetVal(0) + mock.ExpectTxPipeline() + mock.ExpectHMSet(userApiKey, fields).SetVal(true) + mock.ExpectHMSet(userIdKey, fields).SetVal(true) + mock.ExpectTxPipelineExec() + + user, err := repo.CreateUser(ctx, models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }) + + assert.Equal(t, models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }, user) + assert.NoError(t, err) + + }) + + t.Run("should return `ErrUserAlreadyExists` error if user already exists", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + userApiKey := CreateUserApiKeyKey(mocks.ApiKey) + userIdKey := CreateUserIdKey(mocks.UserId) + + mock.ExpectExists(userApiKey, userIdKey).SetVal(1) + + user, err := repo.CreateUser(ctx, models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }) + + assert.Equal(t, models.User{}, user) + assert.ErrorIs(t, err, models.ErrUserAlreadyExists) + }) + + t.Run("should return error on unexpected exists error", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + userApiKey := CreateUserApiKeyKey(mocks.ApiKey) + userIdKey := CreateUserIdKey(mocks.UserId) + + mock.ExpectExists(userApiKey, userIdKey).SetErr(assert.AnError) + + user, err := repo.CreateUser(ctx, models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }) + + assert.Equal(t, models.User{}, user) + assert.ErrorContains(t, err, "unexpected error checking if user exists") + }) + + t.Run("should return error on unexpected create user error", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + userApiKey := CreateUserApiKeyKey(mocks.ApiKey) + userIdKey := CreateUserIdKey(mocks.UserId) + + mock.ExpectExists(userApiKey, userIdKey).SetVal(0) + mock.ExpectTxPipeline() + mock.ExpectHMSet(userApiKey, fields).SetErr(assert.AnError) + mock.ExpectTxPipelineExec() + + user, err := repo.CreateUser(ctx, models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }) + + assert.Equal(t, models.User{}, user) + assert.ErrorContains(t, err, "unexpected error creating user") + }) + +} diff --git a/data/redisrepo/get_user_by_api_key.go b/data/redisrepo/get_user_by_api_key.go new file mode 100644 index 0000000..6671958 --- /dev/null +++ b/data/redisrepo/get_user_by_api_key.go @@ -0,0 +1,54 @@ +package redisrepo + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +// GetUserByApiKey returns a user by their apiKey +func (r *redisRepository) GetUserByApiKey(ctx context.Context, apiKey string) (*models.User, error) { + + key := CreateUserApiKeyKey(apiKey) + + fields, err := r.client.HGetAll(ctx, key).Result() + if err != nil { + logctx.Error(ctx, "unexpected error getting user by api key", logger.Error(err)) + return nil, fmt.Errorf("unexpected error getting user by api key: %w", err) + } + + if len(fields) == 0 { + logctx.Warn(ctx, "user not found by api key") + return nil, models.ErrUserNotFound + } + + userId, err := uuid.Parse(fields["id"]) + if err != nil { + logctx.Error(ctx, "unexpected error parsing user id", logger.Error(err), logger.String("userId", fields["id"])) + return nil, fmt.Errorf("unexpected error parsing user id: %w", err) + } + + userType, err := models.StrToUserType(fields["type"]) + if err != nil { + logctx.Error(ctx, "unexpected error parsing user type", logger.Error(err), logger.String("userId", userId.String()), logger.String("type", fields["type"])) + return nil, fmt.Errorf("unexpected error parsing user type: %w", err) + } + + if fields["apiKey"] != apiKey { + logctx.Error(ctx, "api key mismatch", logger.String("userId", userId.String())) + return nil, fmt.Errorf("api key mismatch") + } + + logctx.Info(ctx, "user found", logger.String("userId", userId.String())) + + return &models.User{ + Id: userId, + PubKey: fields["pubKey"], + Type: userType, + ApiKey: fields["apiKey"], + }, nil +} diff --git a/data/redisrepo/get_user_by_public_key_test.go b/data/redisrepo/get_user_by_api_key_test.go similarity index 72% rename from data/redisrepo/get_user_by_public_key_test.go rename to data/redisrepo/get_user_by_api_key_test.go index 1aac0dc..108bb28 100644 --- a/data/redisrepo/get_user_by_public_key_test.go +++ b/data/redisrepo/get_user_by_api_key_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/go-redis/redismock/v9" - "github.com/orbs-network/order-book/mocks" "github.com/orbs-network/order-book/models" "github.com/stretchr/testify/assert" @@ -12,6 +11,8 @@ import ( func TestRedisRepository_GetUserByPublicKey(t *testing.T) { + mockApiKey := "mock-api-key" + t.Run("should return user by public key", func(t *testing.T) { db, mock := redismock.NewClientMock() @@ -19,21 +20,23 @@ func TestRedisRepository_GetUserByPublicKey(t *testing.T) { client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetVal(map[string]string{ "id": mocks.UserId.String(), - "pubKey": mocks.PubKey, "type": mocks.UserType.String(), + "pubKey": mocks.PubKey, + "apiKey": mockApiKey, }) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.NoError(t, err) assert.Equal(t, user, &models.User{ Id: mocks.UserId, PubKey: mocks.PubKey, Type: mocks.UserType, + ApiKey: mockApiKey, }) }) @@ -44,31 +47,31 @@ func TestRedisRepository_GetUserByPublicKey(t *testing.T) { client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetVal(map[string]string{}) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.Nil(t, user) assert.ErrorIs(t, err, models.ErrUserNotFound) }) - t.Run("should return error on unexpected error getting user by public key", func(t *testing.T) { + t.Run("should return error on unexpected error getting user by api key", func(t *testing.T) { db, mock := redismock.NewClientMock() repo := &redisRepository{ client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetErr(assert.AnError) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.Nil(t, user) - assert.ErrorContains(t, err, "unexpected error getting user by public key") + assert.ErrorContains(t, err, "unexpected error getting user by api key") }) t.Run("should return error on unexpected error parsing user id", func(t *testing.T) { @@ -78,15 +81,16 @@ func TestRedisRepository_GetUserByPublicKey(t *testing.T) { client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetVal(map[string]string{ "id": "invalid", "pubKey": mocks.PubKey, "type": mocks.UserType.String(), + "apiKey": mockApiKey, }) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.Nil(t, user) assert.ErrorContains(t, err, "unexpected error parsing user id") @@ -99,39 +103,41 @@ func TestRedisRepository_GetUserByPublicKey(t *testing.T) { client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetVal(map[string]string{ "id": mocks.UserId.String(), "pubKey": mocks.PubKey, "type": "invalid", + "apiKey": mockApiKey, }) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.Nil(t, user) assert.ErrorContains(t, err, "unexpected error parsing user type") }) - t.Run("should return error on public key mismatch", func(t *testing.T) { + t.Run("should return error on api key mismatch", func(t *testing.T) { db, mock := redismock.NewClientMock() repo := &redisRepository{ client: db, } - key := CreateUserPubKeyKey(mocks.PubKey) + key := CreateUserApiKeyKey(mockApiKey) mock.ExpectHGetAll(key).SetVal(map[string]string{ "id": mocks.UserId.String(), - "pubKey": "invalid", + "pubKey": mocks.PubKey, "type": mocks.UserType.String(), + "apiKey": "a-different-api-key", }) - user, err := repo.GetUserByPublicKey(ctx, mocks.PubKey) + user, err := repo.GetUserByApiKey(ctx, mockApiKey) assert.Nil(t, user) - assert.ErrorContains(t, err, "public key mismatch") + assert.ErrorContains(t, err, "api key mismatch") }) } diff --git a/data/redisrepo/get_user_by_id.go b/data/redisrepo/get_user_by_id.go new file mode 100644 index 0000000..1612d59 --- /dev/null +++ b/data/redisrepo/get_user_by_id.go @@ -0,0 +1,40 @@ +package redisrepo + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +// GetUserById returns a user by their userId +func (r *redisRepository) GetUserById(ctx context.Context, userId uuid.UUID) (*models.User, error) { + + key := CreateUserIdKey(userId) + + fields, err := r.client.HGetAll(ctx, key).Result() + if err != nil { + return nil, err + } + + if len(fields) == 0 { + logctx.Error(ctx, "user not found by ID", logger.String("userId", userId.String())) + return nil, models.ErrUserNotFound + } + + userType, err := models.StrToUserType(fields["type"]) + if err != nil { + logctx.Error(ctx, "unexpected error parsing user type", logger.Error(err), logger.String("userId", userId.String()), logger.String("type", fields["type"])) + return nil, fmt.Errorf("unexpected error parsing user type: %w", err) + } + + return &models.User{ + Id: userId, + PubKey: fields["pubKey"], + Type: userType, + ApiKey: fields["apiKey"], + }, nil +} diff --git a/data/redisrepo/get_user_by_id_test.go b/data/redisrepo/get_user_by_id_test.go new file mode 100644 index 0000000..c284733 --- /dev/null +++ b/data/redisrepo/get_user_by_id_test.go @@ -0,0 +1,74 @@ +package redisrepo + +import ( + "testing" + + "github.com/go-redis/redismock/v9" + "github.com/orbs-network/order-book/mocks" + "github.com/orbs-network/order-book/models" + "github.com/stretchr/testify/assert" +) + +func TestRedisRepository_GetUserById(t *testing.T) { + + t.Run("should return user by ID", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + key := CreateUserIdKey(mocks.UserId) + + mock.ExpectHGetAll(key).SetVal(map[string]string{ + "id": mocks.UserId.String(), + "type": mocks.UserType.String(), + "pubKey": mocks.PubKey, + "apiKey": mocks.ApiKey, + }) + + user, err := repo.GetUserById(ctx, mocks.UserId) + + assert.NoError(t, err) + assert.Equal(t, user, &models.User{ + Id: mocks.UserId, + PubKey: mocks.PubKey, + Type: mocks.UserType, + ApiKey: mocks.ApiKey, + }) + }) + + t.Run("should return user not found error when no user", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + key := CreateUserIdKey(mocks.UserId) + + mock.ExpectHGetAll(key).SetVal(map[string]string{}) + + _, err := repo.GetUserById(ctx, mocks.UserId) + + assert.Error(t, err) + assert.Equal(t, err, models.ErrUserNotFound) + }) + + t.Run("should return error on unexpected error getting user by ID", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + key := CreateUserIdKey(mocks.UserId) + + mock.ExpectHGetAll(key).SetErr(assert.AnError) + + _, err := repo.GetUserById(ctx, mocks.UserId) + + assert.Error(t, err) + assert.Equal(t, err, assert.AnError) + }) +} diff --git a/data/redisrepo/get_user_by_public_key.go b/data/redisrepo/get_user_by_public_key.go deleted file mode 100644 index cee298c..0000000 --- a/data/redisrepo/get_user_by_public_key.go +++ /dev/null @@ -1,52 +0,0 @@ -package redisrepo - -import ( - "context" - "fmt" - - "github.com/google/uuid" - "github.com/orbs-network/order-book/models" - "github.com/orbs-network/order-book/utils/logger" - "github.com/orbs-network/order-book/utils/logger/logctx" -) - -func (r *redisRepository) GetUserByPublicKey(ctx context.Context, publicKey string) (*models.User, error) { - - key := CreateUserPubKeyKey(publicKey) - - fields, err := r.client.HGetAll(ctx, key).Result() - if err != nil { - logctx.Error(ctx, "unexpected error getting user by public key", logger.Error(err), logger.String("publicKey", publicKey)) - return nil, fmt.Errorf("unexpected error getting user by public key: %w", err) - } - - if len(fields) == 0 { - logctx.Warn(ctx, "user not found", logger.String("publicKey", publicKey)) - return nil, models.ErrUserNotFound - } - - userId, err := uuid.Parse(fields["id"]) - if err != nil { - logctx.Error(ctx, "unexpected error parsing user id", logger.Error(err), logger.String("publicKey", publicKey), logger.String("userId", fields["id"])) - return nil, fmt.Errorf("unexpected error parsing user id: %w", err) - } - - userType, err := models.StrToUserType(fields["type"]) - if err != nil { - logctx.Error(ctx, "unexpected error parsing user type", logger.Error(err), logger.String("publicKey", publicKey), logger.String("type", fields["type"])) - return nil, fmt.Errorf("unexpected error parsing user type: %w", err) - } - - if fields["pubKey"] != publicKey { - logctx.Error(ctx, "public key mismatch", logger.String("publicKey from args", publicKey), logger.String("pubKey from map", fields["pubKey"])) - return nil, fmt.Errorf("public key mismatch") - } - - logctx.Info(ctx, "user found", logger.String("publicKey", publicKey), logger.String("userId", userId.String())) - - return &models.User{ - Id: userId, - PubKey: publicKey, - Type: userType, - }, nil -} diff --git a/data/redisrepo/store_user_by_public_key.go b/data/redisrepo/store_user_by_public_key.go deleted file mode 100644 index 8c36799..0000000 --- a/data/redisrepo/store_user_by_public_key.go +++ /dev/null @@ -1,31 +0,0 @@ -package redisrepo - -import ( - "context" - "fmt" - - "github.com/orbs-network/order-book/models" - "github.com/orbs-network/order-book/utils/logger" - "github.com/orbs-network/order-book/utils/logger/logctx" -) - -func (r *redisRepository) StoreUserByPublicKey(ctx context.Context, user models.User) error { - - key := CreateUserPubKeyKey(user.PubKey) - - fields := map[string]interface{}{ - "id": user.Id.String(), - "type": user.Type.String(), - "pubKey": user.PubKey, - } - - _, err := r.client.HMSet(ctx, key, fields).Result() - if err != nil { - logctx.Error(ctx, "unexpected error storing user by public key", logger.Error(err), logger.String("publicKey", user.PubKey)) - return fmt.Errorf("unexpected error storing user by public key: %w", err) - } - - logctx.Info(ctx, "user stored by public key", logger.String("publicKey", user.PubKey), logger.String("userId", user.Id.String()), logger.String("type", user.Type.String())) - - return nil -} diff --git a/data/redisrepo/store_user_by_public_key_test.go b/data/redisrepo/store_user_by_public_key_test.go deleted file mode 100644 index b8cfe2e..0000000 --- a/data/redisrepo/store_user_by_public_key_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package redisrepo - -import ( - "testing" - - "github.com/go-redis/redismock/v9" - "github.com/orbs-network/order-book/mocks" - "github.com/orbs-network/order-book/models" - "github.com/stretchr/testify/assert" -) - -func TestRedisRepository_StoreUserByPublicKey(t *testing.T) { - - t.Run("should successfully store user details", func(t *testing.T) { - db, mock := redismock.NewClientMock() - - repo := &redisRepository{ - client: db, - } - - key := CreateUserPubKeyKey(mocks.PubKey) - - mock.ExpectHMSet(key, map[string]interface{}{ - "id": mocks.UserId.String(), - "pubKey": mocks.PubKey, - "type": mocks.UserType.String(), - }).SetVal(true) - - err := repo.StoreUserByPublicKey(ctx, models.User{ - Id: mocks.UserId, - PubKey: mocks.PubKey, - Type: mocks.UserType, - }) - - assert.NoError(t, err) - }) - - t.Run("should return error on unexpected error storing user by public key", func(t *testing.T) { - db, mock := redismock.NewClientMock() - - repo := &redisRepository{ - client: db, - } - - key := CreateUserPubKeyKey(mocks.PubKey) - - mock.ExpectHMSet(key, map[string]interface{}{ - "id": mocks.UserId.String(), - "pubKey": mocks.PubKey, - "type": mocks.UserType.String(), - }).SetErr(assert.AnError) - - err := repo.StoreUserByPublicKey(ctx, models.User{ - Id: mocks.UserId, - PubKey: mocks.PubKey, - Type: mocks.UserType, - }) - - assert.ErrorContains(t, err, "unexpected error storing user by public key") - }) - -} diff --git a/data/redisrepo/update_user.go b/data/redisrepo/update_user.go new file mode 100644 index 0000000..0d5683a --- /dev/null +++ b/data/redisrepo/update_user.go @@ -0,0 +1,62 @@ +package redisrepo + +import ( + "context" + "fmt" + + "github.com/orbs-network/order-book/data/storeuser" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +// UpdateUser updates a user's pubKey and apiKey +// +// It deletes the user by id and apiKey and creates a new user by id and (new) apiKey (with the updated pubKey) +// +// Ensure that `PubKey` and `ApiKey` are correct and not empty +func (r *redisRepository) UpdateUser(ctx context.Context, input storeuser.UpdateUserInput) error { + + if input.ApiKey == "" || input.PubKey == "" { + logctx.Error(ctx, "apiKey or pubKey is empty", logger.String("apiKey", input.ApiKey), logger.String("pubKey", input.PubKey)) + return models.ErrInvalidInput + } + + user, err := r.GetUserById(ctx, input.UserId) + if err != nil { + logctx.Error(ctx, "unexpected error getting user by id", logger.Error(err), logger.String("userId", input.UserId.String())) + return fmt.Errorf("unexpected error getting user by id: %w", err) + } + + updatedFields := map[string]interface{}{ + "id": user.Id.String(), + "type": user.Type.String(), + "pubKey": input.PubKey, + "apiKey": input.ApiKey, + } + + rUserIdKey := CreateUserIdKey(user.Id) + rOldUserApiKey := CreateUserApiKeyKey(user.ApiKey) + rNewUserApiKey := CreateUserApiKeyKey(input.ApiKey) + + // --- START TRANSACTION --- + transaction := r.client.TxPipeline() + // delete user by id + transaction.Del(ctx, rUserIdKey) + // delete user by api key + transaction.Del(ctx, rOldUserApiKey) + // create user by id + transaction.HMSet(ctx, rUserIdKey, updatedFields) + // create user by api key + transaction.HMSet(ctx, rNewUserApiKey, updatedFields) + _, err = transaction.Exec(ctx) + // --- END TRANSACTION --- + + if err != nil { + logctx.Error(ctx, "transaction failed updating user", logger.Error(err), logger.String("userId", input.UserId.String()), logger.String("pubKey", input.PubKey)) + return fmt.Errorf("transaction failed: %w", err) + } + + logctx.Info(ctx, "user updated", logger.String("userId", input.UserId.String()), logger.String("pubKey", input.PubKey)) + return nil +} diff --git a/data/redisrepo/update_user_test.go b/data/redisrepo/update_user_test.go new file mode 100644 index 0000000..c86c664 --- /dev/null +++ b/data/redisrepo/update_user_test.go @@ -0,0 +1,105 @@ +package redisrepo + +import ( + "testing" + + "github.com/go-redis/redismock/v9" + "github.com/orbs-network/order-book/data/storeuser" + "github.com/orbs-network/order-book/mocks" + "github.com/stretchr/testify/assert" +) + +func TestRedisRepository_UpdateUser(t *testing.T) { + oldApiKey := "old-api-key" + newPubKey := "new-pub-key" + newApiKey := "new-api-key" + + input := storeuser.UpdateUserInput{ + UserId: mocks.UserId, + PubKey: newPubKey, + ApiKey: newApiKey, + } + + fields := map[string]interface{}{ + "id": mocks.UserId.String(), + "type": mocks.UserType.String(), + "pubKey": newPubKey, + "apiKey": newApiKey, + } + + t.Run("should update user", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + rUserIdKey := CreateUserIdKey(mocks.UserId) + rOldUserApiKey := CreateUserApiKeyKey(oldApiKey) + rNewUserApiKey := CreateUserApiKeyKey(newApiKey) + + mock.ExpectHGetAll(rUserIdKey).SetVal(map[string]string{ + "id": mocks.UserId.String(), + "type": mocks.UserType.String(), + "pubKey": mocks.PubKey, + "apiKey": oldApiKey, + }) + + mock.ExpectTxPipeline() + mock.ExpectDel(rUserIdKey).SetVal(1) + mock.ExpectDel(rOldUserApiKey).SetVal(1) + mock.ExpectHMSet(rUserIdKey, fields).SetVal(true) + mock.ExpectHMSet(rNewUserApiKey, fields).SetVal(true) + mock.ExpectTxPipelineExec() + + err := repo.UpdateUser(ctx, input) + + assert.NoError(t, err) + }) + + t.Run("should return error if user not found", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + rUserIdKey := CreateUserIdKey(mocks.UserId) + + mock.ExpectHGetAll(rUserIdKey).SetVal(map[string]string{}) + + err := repo.UpdateUser(ctx, input) + + assert.Error(t, err) + }) + + t.Run("should return error if transaction failed", func(t *testing.T) { + db, mock := redismock.NewClientMock() + + repo := &redisRepository{ + client: db, + } + + rUserIdKey := CreateUserIdKey(mocks.UserId) + rOldUserApiKey := CreateUserApiKeyKey(oldApiKey) + rNewUserApiKey := CreateUserApiKeyKey(newApiKey) + + mock.ExpectHGetAll(rUserIdKey).SetVal(map[string]string{ + "id": mocks.UserId.String(), + "type": mocks.UserType.String(), + "pubKey": mocks.PubKey, + "apiKey": oldApiKey, + }) + + mock.ExpectTxPipeline() + mock.ExpectDel(rUserIdKey).SetVal(1) + mock.ExpectDel(rOldUserApiKey).SetVal(1) + mock.ExpectHMSet(rUserIdKey, fields).SetVal(true) + mock.ExpectHMSet(rNewUserApiKey, fields).SetErr(assert.AnError) + mock.ExpectTxPipelineExec() + + err := repo.UpdateUser(ctx, input) + + assert.ErrorContains(t, err, "transaction failed") + }) +} diff --git a/data/redisrepo/utils.go b/data/redisrepo/utils.go index 2406829..e3348e6 100644 --- a/data/redisrepo/utils.go +++ b/data/redisrepo/utils.go @@ -39,9 +39,14 @@ func CreateAuctionKey(auctionID uuid.UUID) string { return fmt.Sprintf("auctionId:%s", auctionID) } -// CreateUserPubKeyKey creates a Redis key for storing the user's public key -func CreateUserPubKeyKey(publicKey string) string { - return fmt.Sprintf("user:%s:publicKey", publicKey) +// CreateUserApiKeyKey creates a Redis key for storing the user by their API key +func CreateUserApiKeyKey(apiKey string) string { + return fmt.Sprintf("user:userApiKey:%s", apiKey) +} + +// CreateUserIdKey creates a Redis key for storing the user by their ID +func CreateUserIdKey(userId uuid.UUID) string { + return fmt.Sprintf("user:userId:%s", userId) } // CreateAuctionTrackerKey creates a Redis key for storing auctions of different statuses diff --git a/data/store/store.go b/data/store/store.go index 556db6e..d84247f 100644 --- a/data/store/store.go +++ b/data/store/store.go @@ -19,8 +19,6 @@ type OrderBookStore interface { GetMarketDepth(ctx context.Context, symbol models.Symbol, depth int) (models.MarketDepth, error) GetOrdersForUser(ctx context.Context, userId uuid.UUID) (orders []models.Order, totalOrders int, err error) CancelOrdersForUser(ctx context.Context, userId uuid.UUID) error - GetUserByPublicKey(ctx context.Context, publicKey string) (*models.User, error) - StoreUserByPublicKey(ctx context.Context, user models.User) error // LH side StoreAuction(ctx context.Context, auctionID uuid.UUID, frags []models.OrderFrag) error RemoveAuction(ctx context.Context, auctionID uuid.UUID) error diff --git a/data/storeuser/store.go b/data/storeuser/store.go new file mode 100644 index 0000000..af3280f --- /dev/null +++ b/data/storeuser/store.go @@ -0,0 +1,22 @@ +package storeuser + +import ( + "context" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/models" +) + +type UpdateUserInput struct { + UserId uuid.UUID + PubKey string + ApiKey string +} + +type UserStore interface { + CreateUser(ctx context.Context, user models.User) (models.User, error) + GetUserByApiKey(ctx context.Context, apiKey string) (*models.User, error) + GetUserById(ctx context.Context, userId uuid.UUID) (*models.User, error) + UpdateUser(ctx context.Context, input UpdateUserInput) error + // TODO: how should we handle removing users? What happens to their orders? +} diff --git a/mocks/serviceuser.go b/mocks/serviceuser.go new file mode 100644 index 0000000..2339776 --- /dev/null +++ b/mocks/serviceuser.go @@ -0,0 +1,30 @@ +package mocks + +import ( + "context" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/data/storeuser" + "github.com/orbs-network/order-book/models" +) + +type MockUserStore struct { + User *models.User + Error error +} + +func (m *MockUserStore) CreateUser(ctx context.Context, user models.User) (models.User, error) { + return *m.User, m.Error +} + +func (m *MockUserStore) GetUserByApiKey(ctx context.Context, apiKey string) (*models.User, error) { + return m.User, m.Error +} + +func (m *MockUserStore) GetUserById(ctx context.Context, userId uuid.UUID) (*models.User, error) { + return m.User, m.Error +} + +func (m *MockUserStore) UpdateUser(ctx context.Context, input storeuser.UpdateUserInput) error { + return m.Error +} diff --git a/mocks/user.go b/mocks/user.go index 8158304..5c06275 100644 --- a/mocks/user.go +++ b/mocks/user.go @@ -6,9 +6,11 @@ import ( var PubKey = "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" var UserType = models.MARKET_MAKER +var ApiKey = "mock-api-key" var User = models.User{ Id: UserId, PubKey: PubKey, Type: UserType, + ApiKey: ApiKey, } diff --git a/models/errors.go b/models/errors.go index f28e438..bb88260 100644 --- a/models/errors.go +++ b/models/errors.go @@ -6,6 +6,7 @@ var ErrOrderAlreadyExists = errors.New("order already exists") var ErrOrderNotFound = errors.New("order not found") var ErrUnexpectedError = errors.New("unexpected error") var ErrMarshalError = errors.New("marshal error") +var ErrUserAlreadyExists = errors.New("user already exists") var ErrUserNotFound = errors.New("user not found") var ErrNoUserInContext = errors.New("no user in context") var ErrUnauthorized = errors.New("user not allowed to perform this action") @@ -14,6 +15,7 @@ var ErrTransactionFailed = errors.New("transaction failed") var ErrInsufficientLiquity = errors.New("not enough liquidity in book to satisfy amountIn") var ErrAuctionInvalid = errors.New("orders in the auction can not fill any longer") var ErrOrderPending = errors.New("order is pending") +var ErrInvalidInput = errors.New("invalid input") // store generic errors var ErrValAlreadyInSet = errors.New("the value is already a memeber of the set") diff --git a/models/user.go b/models/user.go index 737c307..b6ee43c 100644 --- a/models/user.go +++ b/models/user.go @@ -23,6 +23,7 @@ type User struct { // The user's public key from their public/private key pair PubKey string Type UserType + ApiKey string `json:"-"` } var ErrInvalidUserType = errors.New("invalid user type") @@ -45,5 +46,6 @@ func (u *User) UserToMap() map[string]string { "id": u.Id.String(), "pubKey": u.PubKey, "type": u.Type.String(), + "apiKey": u.ApiKey, } } diff --git a/postman/Order book.postman_collection.json b/postman/Order book.postman_collection.json index d99c1c9..727953c 100644 --- a/postman/Order book.postman_collection.json +++ b/postman/Order book.postman_collection.json @@ -24,8 +24,18 @@ "method": "POST", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", + "type": "text" + }, + { + "key": "X-API-SIGN", + "value": "0x27648e03bad3cf550a79e0fa469be3508c3cc3b1099bc8f5eccd379b912cf98b0f944d3b1bd895b8b6195e333ebc763ba35a854d6cfc4f12788fdc40bd508ce91b", + "type": "text" + }, + { + "key": "X-API-EIP712-MSG", + "value": "{\"permitted\": {\"token\": \"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359\", \"amount\": 10000000}, \"spender\": \"0x21Da9737764527e75C17F1AB26Cb668b66dEE0a0\", \"nonce\": 2696605318, \"deadline\": 1709638189, \"witness\": {\"info\": {\"reactor\": \"0x21Da9737764527e75C17F1AB26Cb668b66dEE0a0\", \"swapper\": \"0xE3682CCecefBb3C3fe524BbFF1598B2BBaC0d6E3\", \"nonce\": 2696605318, \"deadline\": 1709638189, \"additionalValidationContract\": \"0x1a08D64Fb4a7D0b6DA5606A1e4619c147C3fB95e\", \"additionalValidationData\": \"0x\"}, \"exclusiveFiller\": \"0x1a08D64Fb4a7D0b6DA5606A1e4619c147C3fB95e\", \"exclusivityOverrideBps\": 0, \"input\": {\"token\": \"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359\", \"amount\": 10000000}, \"outputs\": [{\"token\": \"0x11cd37bb86f65419713f30673a480ea33c826872\", \"amount\": 99999999999999991611392, \"recipient\": \"0x8fd379246834eac74B8419FfdA202CF8051F7A03\"}]}}", "type": "text" } ], @@ -57,8 +67,18 @@ "method": "POST", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", + "type": "text" + }, + { + "key": "X-API-SIGN", + "value": "0x27648e03bad3cf550a79e0fa469be3508c3cc3b1099bc8f5eccd379b912cf98b0f944d3b1bd895b8b6195e333ebc763ba35a854d6cfc4f12788fdc40bd508ce91b", + "type": "text" + }, + { + "key": "X-API-EIP712-MSG", + "value": "{\"permitted\": {\"token\": \"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359\", \"amount\": 10000000}, \"spender\": \"0x21Da9737764527e75C17F1AB26Cb668b66dEE0a0\", \"nonce\": 2696605318, \"deadline\": 1709638189, \"witness\": {\"info\": {\"reactor\": \"0x21Da9737764527e75C17F1AB26Cb668b66dEE0a0\", \"swapper\": \"0xE3682CCecefBb3C3fe524BbFF1598B2BBaC0d6E3\", \"nonce\": 2696605318, \"deadline\": 1709638189, \"additionalValidationContract\": \"0x1a08D64Fb4a7D0b6DA5606A1e4619c147C3fB95e\", \"additionalValidationData\": \"0x\"}, \"exclusiveFiller\": \"0x1a08D64Fb4a7D0b6DA5606A1e4619c147C3fB95e\", \"exclusivityOverrideBps\": 0, \"input\": {\"token\": \"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359\", \"amount\": 10000000}, \"outputs\": [{\"token\": \"0x11cd37bb86f65419713f30673a480ea33c826872\", \"amount\": 99999999999999991611392, \"recipient\": \"0x8fd379246834eac74B8419FfdA202CF8051F7A03\"}]}}", "type": "text" } ], @@ -111,8 +131,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -136,8 +156,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -182,8 +202,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -208,8 +228,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -255,8 +275,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -279,8 +299,8 @@ "method": "DELETE", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -320,8 +340,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -344,8 +364,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -389,8 +409,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -414,8 +434,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -460,8 +480,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -486,8 +506,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -533,8 +553,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -564,8 +584,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -616,8 +636,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -650,8 +670,8 @@ "method": "GET", "header": [ { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", + "key": "X-API-Key", + "value": "Bearer abcdef12345", "type": "text" } ], @@ -703,13 +723,7 @@ "name": "Begin auction", "request": { "method": "POST", - "header": [ - { - "key": "X-Public-Key", - "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==", - "type": "text" - } - ], + "header": [], "body": { "mode": "raw", "raw": "{\n \"amountIn\": \"10\",\n \"symbol\": \"BTC-ETH\",\n \"side\": \"sell\"\n}", diff --git a/scripts/redis-repo/main.go b/scripts/redis-repo/main.go index d593cb5..b76dcec 100644 --- a/scripts/redis-repo/main.go +++ b/scripts/redis-repo/main.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/orbs-network/order-book/data/redisrepo" + "github.com/orbs-network/order-book/data/storeuser" "github.com/orbs-network/order-book/models" "github.com/redis/go-redis/v9" "github.com/shopspring/decimal" @@ -42,18 +43,87 @@ func main() { }, }, { - Name: "storeUserByPubKey", - Usage: "Store user by public key", + Name: "createUser", + Usage: "Create a new user", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "apiKey", + Usage: "user API key", + Required: true, + }, + }, Action: func(c *cli.Context) error { - storeUserByPublicKey() + apiKey := c.String("apiKey") + + if apiKey == "" { + log.Fatalf("apiKey is empty") + } + + createUser(apiKey) return nil }, }, { - Name: "getUserByPubKey", - Usage: "Get user by public key", + Name: "getUserByApiKey", + Usage: "Get user by their API key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "apiKey", + Usage: "user API key", + Required: true, + }, + }, Action: func(c *cli.Context) error { - getUserByPublicKey() + apiKey := c.String("apiKey") + + if apiKey == "" { + log.Fatalf("apiKey is empty") + } + + getUserByApiKey(apiKey) + return nil + }, + }, + { + Name: "getUserById", + Usage: "Get user by their ID", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "id", + Usage: "user ID", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + userId := c.String("id") + + if userId == "" { + log.Fatalf("userId is empty") + } + + getUserById(userId) + return nil + }, + }, + { + Name: "updateUser", + Usage: "Updates a user's API key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "newApiKey", + Usage: "The desired new user API key", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + newApiKey := c.String("newApiKey") + + if newApiKey == "" { + log.Fatalf("newApiKey is empty") + } + + updateUser(newApiKey) + return nil }, }, @@ -172,15 +242,22 @@ func removeOrders() { } } -func storeUserByPublicKey() { - err := repository.StoreUserByPublicKey(ctx, user) +func createUser(apiKey string) { + user.ApiKey = apiKey + user, err := repository.CreateUser(ctx, user) if err != nil { log.Fatalf("error storing user: %v", err) } + log.Print("--------------------------") + log.Printf("userId: %v", user.Id) + log.Printf("userType: %v", user.Type) + log.Printf("userPubKey: %v", user.PubKey) + log.Printf("userApiKey: %v", user.ApiKey) + log.Print("--------------------------") } -func getUserByPublicKey() { - retrievedUser, err := repository.GetUserByPublicKey(ctx, publicKey) +func getUserByApiKey(apiKey string) { + retrievedUser, err := repository.GetUserByApiKey(ctx, apiKey) if err != nil { log.Fatalf("error getting user: %v", err) } @@ -190,3 +267,31 @@ func getUserByPublicKey() { log.Printf("userPubKey: %v", retrievedUser.PubKey) log.Print("--------------------------") } + +func getUserById(userIdFlag string) { + userId := uuid.MustParse(userIdFlag) + retrievedUser, err := repository.GetUserById(ctx, userId) + if err != nil { + log.Fatalf("error getting user: %v", err) + } + log.Print("--------------------------") + log.Printf("userId: %v", retrievedUser.Id) + log.Printf("userType: %v", retrievedUser.Type) + log.Printf("userPubKey: %v", retrievedUser.PubKey) + log.Print("--------------------------") +} + +func updateUser(newApiKey string) { + + err := repository.UpdateUser(ctx, storeuser.UpdateUserInput{ + UserId: userId, + PubKey: publicKey, + ApiKey: newApiKey, + }) + + if err != nil { + log.Fatalf("error updating user: %v", err) + } + + log.Printf("user %q updated", userId) +} diff --git a/scripts/redis/store-mm-user.sh b/scripts/redis/store-mm-user.sh old mode 100644 new mode 100755 index de62126..ba3393b --- a/scripts/redis/store-mm-user.sh +++ b/scripts/redis/store-mm-user.sh @@ -17,17 +17,27 @@ else PASSWORD="${REDIS_PASSWORD}" fi +if [ -z "${API_KEY}" ]; then + echo "Please set the API_KEY environment variable." + exit 1 +else + API_KEY="${API_KEY}" +fi + if ! redis-cli -h $HOST -p $PORT -a $PASSWORD PING &> /dev/null then echo "Unable to reach Redis at $HOST:$PORT. Please check your connection." exit 1 fi -KEY="user:MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==:publicKey" ID="00000000-0000-0000-0000-000000000001" +KEY_USER_API_KEY="user:userApiKey:$API_KEY" +KEY_USER_ID="user:userId:$ID" TYPE="MARKET_MAKER" -PUBKEY="MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" +PUB_KEY="MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" + +redis-cli -h $HOST -p $PORT -a $PASSWORD HSET "$KEY_USER_API_KEY" id "$ID" type "$TYPE" pubKey "$PUB_KEY" apiKey "$API_KEY" -redis-cli -h $HOST -p $PORT -a $PASSWORD HSET "$KEY" id "$ID" type "$TYPE" pubKey "$PUBKEY" +redis-cli -h $HOST -p $PORT -a $PASSWORD HSET "$KEY_USER_ID" id "$ID" type "$TYPE" pubKey "$PUB_KEY" apiKey "$API_KEY" echo "Market maker user stored successfully." \ No newline at end of file diff --git a/scripts/taker-mock/src/index.js b/scripts/taker-mock/src/index.js index bbf1cb7..42c28a0 100644 --- a/scripts/taker-mock/src/index.js +++ b/scripts/taker-mock/src/index.js @@ -6,7 +6,6 @@ import * as dotenv from 'dotenv'; console.log('------------------- taker-mock started') dotenv.config() console.log('ORDERBOOK_HOST', process.env.ORDERBOOK_HOST); -console.log('PUB_KEY', process.env.PUB_KEY); async function main() { const ob = new Orderbook() diff --git a/scripts/taker-mock/src/maket-depth.js b/scripts/taker-mock/src/maket-depth.js index e5099db..64e8349 100644 --- a/scripts/taker-mock/src/maket-depth.js +++ b/scripts/taker-mock/src/maket-depth.js @@ -1,12 +1,12 @@ import fetch from "node-fetch"; const ORDERBOOK_HOST = process.env.ORDERBOOK_HOST || "http://localhost:8080/" -const PUB_KEY = process.env.PUB_KEY || "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" +const API_KEY = process.env.API_KEY || "abcdef12345" const url = `${ORDERBOOK_HOST}/api/v1/orderbook/ETH-USD?limit=20` const headers = { - "X-Public-KEY": PUB_KEY, + "X-API-Key": `Bearer ${API_KEY}`, "Content-Type": "application/json", // Assuming you are expecting JSON in response }; diff --git a/scripts/taker-mock/src/order-book.js b/scripts/taker-mock/src/order-book.js index cfe2ff7..e105e86 100644 --- a/scripts/taker-mock/src/order-book.js +++ b/scripts/taker-mock/src/order-book.js @@ -3,9 +3,9 @@ import fetch from "node-fetch"; class Orderbook { constructor() { this.ORDERBOOK_HOST = process.env.ORDERBOOK_HOST || "http://localhost:8080/" - const PUB_KEY = process.env.xx || "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" + const API_KEY = process.env.xx || "abcdef12345" this.headers = { - "X-Public-Key": PUB_KEY, + "X-API-Key": `Bearer ${API_KEY}`, "Content-Type": "application/json", // Assuming you are expecting JSON in response }; } diff --git a/service/get_user_by_public_key.go b/service/get_user_by_public_key.go deleted file mode 100644 index 2d63b46..0000000 --- a/service/get_user_by_public_key.go +++ /dev/null @@ -1,33 +0,0 @@ -package service - -import ( - "context" - "fmt" - - "github.com/orbs-network/order-book/models" - "github.com/orbs-network/order-book/utils/logger" - "github.com/orbs-network/order-book/utils/logger/logctx" -) - -func (s *Service) GetUserByPublicKey(ctx context.Context, publicKey string) (*models.User, error) { - - user, err := s.orderBookStore.GetUserByPublicKey(ctx, publicKey) - - if err != nil { - if err == models.ErrUserNotFound { - logctx.Warn(ctx, "user not found", logger.String("publicKey", publicKey)) - return nil, err - } - - logctx.Error(ctx, "unexpected error getting user by public key", logger.Error(err), logger.String("publicKey", publicKey)) - return nil, fmt.Errorf("unexpected error getting user by public key: %w", err) - } - - if user == nil { - logctx.Error(ctx, "user is nil but no error returned", logger.String("publicKey", publicKey)) - return nil, fmt.Errorf("user is nil but no error returned") - } - - logctx.Info(ctx, "found user by public key", logger.String("userId", user.Id.String())) - return user, nil -} diff --git a/service/get_user_by_public_key_test.go b/service/get_user_by_public_key_test.go deleted file mode 100644 index a5fd8ee..0000000 --- a/service/get_user_by_public_key_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package service_test - -import ( - "context" - "testing" - - "github.com/orbs-network/order-book/mocks" - "github.com/orbs-network/order-book/models" - "github.com/orbs-network/order-book/service" - "github.com/stretchr/testify/assert" -) - -func TestService_GetUserByPublicKey(t *testing.T) { - - ctx := context.Background() - mockBcClient := &mocks.MockBcClient{IsVerified: true} - - t.Run("should get a user by their public key", func(t *testing.T) { - store := &mocks.MockOrderBookStore{User: &mocks.User} - - svc, _ := service.New(store, mockBcClient) - - user, _ := svc.GetUserByPublicKey(ctx, mocks.PubKey) - - assert.Equal(t, user, &mocks.User) - }) - - t.Run("should return error if user not found", func(t *testing.T) { - store := &mocks.MockOrderBookStore{User: nil, ErrUser: models.ErrUserNotFound} - - svc, _ := service.New(store, mockBcClient) - - user, err := svc.GetUserByPublicKey(ctx, mocks.PubKey) - - assert.Nil(t, user) - assert.ErrorIs(t, err, models.ErrUserNotFound) - }) - - t.Run("should return error on unexpected error getting user by public key", func(t *testing.T) { - store := &mocks.MockOrderBookStore{User: nil, ErrUser: assert.AnError} - - svc, _ := service.New(store, mockBcClient) - - user, err := svc.GetUserByPublicKey(ctx, mocks.PubKey) - - assert.Nil(t, user) - assert.ErrorContains(t, err, "unexpected error getting user by public key") - }) - - t.Run("should return error if user is nil but no error", func(t *testing.T) { - store := &mocks.MockOrderBookStore{User: nil, ErrUser: nil} - - svc, _ := service.New(store, mockBcClient) - - user, err := svc.GetUserByPublicKey(ctx, mocks.PubKey) - - assert.Nil(t, user) - assert.ErrorContains(t, err, "user is nil but no error returned") - }) -} diff --git a/service/service.go b/service/service.go index 62841be..d1f09ee 100644 --- a/service/service.go +++ b/service/service.go @@ -13,7 +13,6 @@ import ( ) type OrderBookService interface { - GetUserByPublicKey(ctx context.Context, publicKey string) (*models.User, error) ProcessOrder(ctx context.Context, input ProcessOrderInput) (models.Order, error) CancelOrder(ctx context.Context, input CancelOrderInput) (cancelledOrderId *uuid.UUID, err error) GetBestPriceFor(ctx context.Context, symbol models.Symbol, side models.Side) (decimal.Decimal, error) diff --git a/serviceuser/create_user.go b/serviceuser/create_user.go new file mode 100644 index 0000000..392f3d9 --- /dev/null +++ b/serviceuser/create_user.go @@ -0,0 +1,48 @@ +package serviceuser + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +type CreateUserInput struct { + PubKey string +} + +func (s *Service) CreateUser(ctx context.Context, input CreateUserInput) (models.User, error) { + + apiKey, err := GenerateAPIKey() + if err != nil { + logctx.Error(ctx, "failed to generate api key", logger.Error(err)) + return models.User{}, fmt.Errorf("failed to generate api key: %w", err) + } + + userId := uuid.New() + + user, err := s.userStore.CreateUser(ctx, models.User{ + Id: userId, + PubKey: input.PubKey, + Type: models.MARKET_MAKER, + ApiKey: apiKey, + }) + + if err == models.ErrUserAlreadyExists { + logctx.Warn(ctx, "user already exists for that pub key", logger.String("pubKey", input.PubKey)) + return models.User{}, err + } + + if err != nil { + logctx.Error(ctx, "failed to create user", logger.String("userId", userId.String()), logger.Error(err)) + return models.User{}, fmt.Errorf("failed to create user: %w", err) + } + + logctx.Info(ctx, "user created", logger.String("userId", user.Id.String()), logger.String("pubKey", + user.PubKey)) + + return user, nil +} diff --git a/serviceuser/create_user_test.go b/serviceuser/create_user_test.go new file mode 100644 index 0000000..2f180f5 --- /dev/null +++ b/serviceuser/create_user_test.go @@ -0,0 +1,47 @@ +package serviceuser + +import ( + "context" + "testing" + + "github.com/orbs-network/order-book/mocks" + "github.com/orbs-network/order-book/models" + "github.com/stretchr/testify/assert" +) + +func TestServiceUser_CreateUser(t *testing.T) { + ctx := context.Background() + t.Run("should create user and return user instance on success", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + user, err := userSvc.CreateUser(ctx, CreateUserInput{ + PubKey: mocks.PubKey, + }) + + assert.Equal(t, mocks.User, user) + assert.NoError(t, err) + + }) + + t.Run("should return `ErrUserAlreadyExists` error if user already exists", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &models.User{}, Error: models.ErrUserAlreadyExists}) + + _, err := userSvc.CreateUser(ctx, CreateUserInput{ + PubKey: mocks.PubKey, + }) + + // assert.Equal(t, models.User{}, user) + assert.ErrorIs(t, err, models.ErrUserAlreadyExists) + }) + + t.Run("should return error if failed to create user", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &models.User{}, Error: assert.AnError}) + + user, err := userSvc.CreateUser(ctx, CreateUserInput{ + PubKey: mocks.PubKey, + }) + + assert.Equal(t, models.User{}, user) + assert.ErrorContains(t, err, "failed to create user") + }) +} diff --git a/serviceuser/get_user_by_api_key.go b/serviceuser/get_user_by_api_key.go new file mode 100644 index 0000000..a2b9f96 --- /dev/null +++ b/serviceuser/get_user_by_api_key.go @@ -0,0 +1,28 @@ +package serviceuser + +import ( + "context" + "fmt" + + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +func (s *Service) GetUserByApiKey(ctx context.Context, apiKey string) (*models.User, error) { + + user, err := s.userStore.GetUserByApiKey(ctx, apiKey) + + if err == models.ErrUserNotFound { + logctx.Warn(ctx, "user not found", logger.Error(err)) + return nil, err + } + + if err != nil { + logctx.Error(ctx, "failed to get user by api key", logger.Error(err)) + return nil, fmt.Errorf("failed to get user by api key: %w", err) + } + + logctx.Info(ctx, "user retrieved by API key", logger.String("userId", user.Id.String()), logger.String("pubKey", user.PubKey)) + return user, nil +} diff --git a/serviceuser/get_user_by_api_key_test.go b/serviceuser/get_user_by_api_key_test.go new file mode 100644 index 0000000..706fc7f --- /dev/null +++ b/serviceuser/get_user_by_api_key_test.go @@ -0,0 +1,31 @@ +package serviceuser + +import ( + "context" + "testing" + + "github.com/orbs-network/order-book/mocks" + "github.com/stretchr/testify/assert" +) + +func TestServiceUser_GetUserByApiKey(t *testing.T) { + ctx := context.Background() + t.Run("should get user by api key", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + user, err := userSvc.GetUserByApiKey(ctx, mocks.ApiKey) + + assert.NoError(t, err) + assert.Equal(t, &mocks.User, user) + + }) + + t.Run("should return error if failed to get user by api key", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{Error: assert.AnError}) + + user, err := userSvc.GetUserByApiKey(ctx, mocks.ApiKey) + + assert.ErrorContains(t, err, "failed to get user by api key") + assert.Nil(t, user) + }) +} diff --git a/serviceuser/get_user_by_id.go b/serviceuser/get_user_by_id.go new file mode 100644 index 0000000..95748ea --- /dev/null +++ b/serviceuser/get_user_by_id.go @@ -0,0 +1,23 @@ +package serviceuser + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +func (s *Service) GetUserById(ctx context.Context, userId uuid.UUID) (*models.User, error) { + + user, err := s.userStore.GetUserById(ctx, userId) + if err != nil { + logctx.Error(ctx, "failed to get user by id", logger.Error(err), logger.String("userId", userId.String())) + return nil, fmt.Errorf("failed to get user by id: %w", err) + } + + logctx.Info(ctx, "user retrieved by ID", logger.String("userId", userId.String()), logger.String("pubKey", user.PubKey)) + return user, nil +} diff --git a/serviceuser/get_user_by_id_test.go b/serviceuser/get_user_by_id_test.go new file mode 100644 index 0000000..96bb9f9 --- /dev/null +++ b/serviceuser/get_user_by_id_test.go @@ -0,0 +1,41 @@ +package serviceuser + +import ( + "context" + "testing" + + "github.com/orbs-network/order-book/mocks" + "github.com/orbs-network/order-book/models" + "github.com/stretchr/testify/assert" +) + +func TestServiceUser_GetUserById(t *testing.T) { + ctx := context.Background() + + t.Run("should get user by id", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + user, err := userSvc.GetUserById(ctx, mocks.UserId) + + assert.NoError(t, err) + assert.Equal(t, &mocks.User, user) + }) + + t.Run("should return `ErrUserNotFound` error if user not found", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{Error: models.ErrUserNotFound}) + + user, err := userSvc.GetUserById(ctx, mocks.UserId) + + assert.ErrorIs(t, err, models.ErrUserNotFound) + assert.Nil(t, user) + }) + + t.Run("should return error if failed to get user by id", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{Error: assert.AnError}) + + user, err := userSvc.GetUserById(ctx, mocks.UserId) + + assert.ErrorContains(t, err, "failed to get user by id") + assert.Nil(t, user) + }) +} diff --git a/serviceuser/service.go b/serviceuser/service.go new file mode 100644 index 0000000..3b2c522 --- /dev/null +++ b/serviceuser/service.go @@ -0,0 +1,29 @@ +package serviceuser + +import ( + "context" + "errors" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/data/storeuser" + "github.com/orbs-network/order-book/models" +) + +type UserService interface { + GetUserById(ctx context.Context, userId uuid.UUID) (*models.User, error) + GetUserByApiKey(ctx context.Context, apiKey string) (*models.User, error) + CreateUser(ctx context.Context, input CreateUserInput) (models.User, error) + UpdateUser(ctx context.Context, input UpdateUserInput) error +} + +type Service struct { + userStore storeuser.UserStore +} + +func New(userStore storeuser.UserStore) (UserService, error) { + if userStore == nil { + return nil, errors.New("userStore is nil") + } + + return &Service{userStore: userStore}, nil +} diff --git a/serviceuser/update_user.go b/serviceuser/update_user.go new file mode 100644 index 0000000..af3b412 --- /dev/null +++ b/serviceuser/update_user.go @@ -0,0 +1,38 @@ +package serviceuser + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/orbs-network/order-book/data/storeuser" + "github.com/orbs-network/order-book/models" + "github.com/orbs-network/order-book/utils/logger" + "github.com/orbs-network/order-book/utils/logger/logctx" +) + +type UpdateUserInput struct { + UserId uuid.UUID + ApiKey string + PubKey string +} + +func (s *Service) UpdateUser(ctx context.Context, input UpdateUserInput) error { + + if input.ApiKey == "" || input.PubKey == "" { + logctx.Error(ctx, "apiKey or pubKey is empty", logger.String("apiKey", input.ApiKey), logger.String("pubKey", input.PubKey)) + return models.ErrInvalidInput + } + + if err := s.userStore.UpdateUser(ctx, storeuser.UpdateUserInput{ + UserId: input.UserId, + ApiKey: input.ApiKey, + PubKey: input.PubKey, + }); err != nil { + logctx.Error(ctx, "failed to update user", logger.Error(err), logger.String("userId", input.UserId.String())) + return fmt.Errorf("failed to update user: %w", err) + } + + logctx.Info(ctx, "user updated", logger.String("userId", input.UserId.String()), logger.String("pubKey", input.PubKey)) + return nil +} diff --git a/serviceuser/update_user_test.go b/serviceuser/update_user_test.go new file mode 100644 index 0000000..716b320 --- /dev/null +++ b/serviceuser/update_user_test.go @@ -0,0 +1,61 @@ +package serviceuser + +import ( + "context" + "testing" + + "github.com/orbs-network/order-book/mocks" + "github.com/orbs-network/order-book/models" + "github.com/stretchr/testify/assert" +) + +func TestServiceUser_UpdateUser(t *testing.T) { + ctx := context.Background() + t.Run("should update user", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + err := userSvc.UpdateUser(ctx, UpdateUserInput{ + UserId: mocks.UserId, + ApiKey: mocks.ApiKey, + PubKey: mocks.PubKey, + }) + + assert.NoError(t, err) + }) + + t.Run("should return error if no api key", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + err := userSvc.UpdateUser(ctx, UpdateUserInput{ + UserId: mocks.UserId, + ApiKey: "", + PubKey: mocks.PubKey, + }) + + assert.ErrorIs(t, err, models.ErrInvalidInput) + }) + + t.Run("should return error if no pub key", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{User: &mocks.User}) + + err := userSvc.UpdateUser(ctx, UpdateUserInput{ + UserId: mocks.UserId, + ApiKey: mocks.ApiKey, + PubKey: "", + }) + + assert.ErrorIs(t, err, models.ErrInvalidInput) + }) + + t.Run("should return error if failed to update user", func(t *testing.T) { + userSvc, _ := New(&mocks.MockUserStore{Error: assert.AnError}) + + err := userSvc.UpdateUser(ctx, UpdateUserInput{ + UserId: mocks.UserId, + ApiKey: mocks.ApiKey, + PubKey: mocks.PubKey, + }) + + assert.ErrorContains(t, err, "failed to update user") + }) +} diff --git a/serviceuser/utils.go b/serviceuser/utils.go new file mode 100644 index 0000000..36dafb0 --- /dev/null +++ b/serviceuser/utils.go @@ -0,0 +1,21 @@ +package serviceuser + +import ( + "crypto/rand" + "encoding/base64" +) + +func GenerateAPIKey() (string, error) { + + // Create a byte slice of 32 bytes + bytes := make([]byte, 32) + + // Read random bytes, using the crypto/rand package for cryptographic security + if _, err := rand.Read(bytes); err != nil { + // Handle the error appropriately in your real code + return "", err + } + + // Encode the bytes to a base64 string + return base64.URLEncoding.EncodeToString(bytes), nil +} diff --git a/serviceuser/utils_test.go b/serviceuser/utils_test.go new file mode 100644 index 0000000..617b910 --- /dev/null +++ b/serviceuser/utils_test.go @@ -0,0 +1,13 @@ +package serviceuser + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGenerateAPIKey(t *testing.T) { + key, err := GenerateAPIKey() + assert.NoError(t, err) + assert.Len(t, key, 44) +} diff --git a/thunder-client/order-book-collection.json b/thunder-client/order-book-collection.json index 6aa9c32..5979341 100644 --- a/thunder-client/order-book-collection.json +++ b/thunder-client/order-book-collection.json @@ -17,7 +17,7 @@ "modified": "2023-11-16T08:49:48.608Z", "headers": [ { - "name": "X-Public-Key", + "name": "X-API-Key", "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" } ], @@ -50,7 +50,7 @@ "modified": "2023-11-16T08:49:48.609Z", "headers": [ { - "name": "X-Public-Key", + "name": "X-API-Key", "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" } ] @@ -67,7 +67,7 @@ "modified": "2023-11-16T08:49:48.610Z", "headers": [ { - "name": "X-Public-Key", + "name": "X-API-Key", "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" } ] @@ -84,7 +84,7 @@ "modified": "2023-11-16T08:49:48.611Z", "headers": [ { - "name": "X-Public-Key", + "name": "X-API-Key", "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" } ] @@ -189,7 +189,7 @@ "modified": "2023-11-16T08:49:48.618Z", "headers": [ { - "name": "X-Public-Key", + "name": "X-API-Key", "value": "MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEhqhj8rWPzkghzOZTUCOo/sdkE53sU1coVhaYskKGKrgiUF7lsSmxy46i3j8w7E7KMTfYBpCGAFYiWWARa0KQwg==" } ], diff --git a/transport/middleware/validate_user.go b/transport/middleware/validate_user.go index bec2d2f..f430f24 100644 --- a/transport/middleware/validate_user.go +++ b/transport/middleware/validate_user.go @@ -1,32 +1,39 @@ package middleware import ( + "context" + "errors" "net/http" + "strings" "github.com/orbs-network/order-book/models" - "github.com/orbs-network/order-book/service" "github.com/orbs-network/order-book/utils" "github.com/orbs-network/order-book/utils/logger" "github.com/orbs-network/order-book/utils/logger/logctx" ) -func ValidateUserMiddleware(s service.OrderBookService) func(http.Handler) http.Handler { +type GetUserByApiKeyFunc func(ctx context.Context, apiKey string) (*models.User, error) + +// ValidateUserMiddleware validates the user by the API key in the request header +func ValidateUserMiddleware(getUserByApiKey GetUserByApiKeyFunc) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - publicKey := r.Header.Get("X-Public-Key") - if publicKey == "" { - logctx.Warn(r.Context(), "missing public key header") - http.Error(w, "Missing public key", http.StatusBadRequest) + + key, err := bearerToken(r, "X-API-KEY") + + if err != nil { + logctx.Warn(r.Context(), "incorrect API key format", logger.Error(err)) + http.Error(w, "Invalid API key (ensure the format is 'Bearer YOUR-API-KEY')", http.StatusBadRequest) return } - user, err := s.GetUserByPublicKey(r.Context(), publicKey) + user, err := getUserByApiKey(r.Context(), key) if err != nil { if err == models.ErrUserNotFound { - logctx.Warn(r.Context(), "user not found", logger.String("publicKey", publicKey)) + logctx.Warn(r.Context(), "user not found by api key") http.Error(w, "Unauthorized", http.StatusUnauthorized) } else { - logctx.Error(r.Context(), "unexpected error getting user by public key", logger.Error(err), logger.String("publicKey", publicKey)) + logctx.Error(r.Context(), "unexpected error getting user by api key", logger.Error(err)) http.Error(w, "Internal server error", http.StatusInternalServerError) } return @@ -34,9 +41,22 @@ func ValidateUserMiddleware(s service.OrderBookService) func(http.Handler) http. ctx := utils.WithUserCtx(r.Context(), user) - logctx.Info(ctx, "found user by public key", logger.String("publicKey", publicKey), logger.String("userId", user.Id.String())) + logctx.Info(ctx, "found user by api key", logger.String("userId", user.Id.String())) next.ServeHTTP(w, r.WithContext(ctx)) }) } } + +func bearerToken(r *http.Request, header string) (string, error) { + rawToken := r.Header.Get(header) + pieces := strings.SplitN(rawToken, " ", 2) + + if len(pieces) < 2 { + return "", errors.New("token with incorrect bearer format") + } + + token := strings.TrimSpace(pieces[1]) + + return token, nil +} diff --git a/transport/middleware/validate_user_test.go b/transport/middleware/validate_user_test.go index 3f1eda0..2d4ffb7 100644 --- a/transport/middleware/validate_user_test.go +++ b/transport/middleware/validate_user_test.go @@ -1,50 +1,58 @@ package middleware import ( + "context" "net/http" "net/http/httptest" "testing" - "github.com/orbs-network/order-book/mocks" "github.com/orbs-network/order-book/models" "github.com/stretchr/testify/assert" ) func TestValidateUserMiddleware(t *testing.T) { + validApiKey := "Bearer some-api-key" + tests := []struct { name string - publicKey string - mockService mocks.MockOrderBookService + apiKey string + getUserFunc GetUserByApiKeyFunc expectedStatus int expectedBody string }{ { - name: "should return error if no public key header", - publicKey: "", - mockService: mocks.MockOrderBookService{}, + name: "should return error if no api key header", + apiKey: "", + getUserFunc: func(ctx context.Context, apiKey string) (*models.User, error) { return nil, nil }, + expectedStatus: http.StatusBadRequest, + expectedBody: "Invalid API key (ensure the format is 'Bearer YOUR-API-KEY')\n", + }, + { + name: "should return error if no 'Bearer' keyword", + apiKey: "some-api-key", + getUserFunc: func(ctx context.Context, apiKey string) (*models.User, error) { return nil, nil }, expectedStatus: http.StatusBadRequest, - expectedBody: "Missing public key\n", + expectedBody: "Invalid API key (ensure the format is 'Bearer YOUR-API-KEY')\n", }, { name: "should return error if user not found", - publicKey: "some-public-key", - mockService: mocks.MockOrderBookService{Error: models.ErrUserNotFound}, + apiKey: validApiKey, + getUserFunc: func(ctx context.Context, apiKey string) (*models.User, error) { return nil, models.ErrUserNotFound }, expectedStatus: http.StatusUnauthorized, expectedBody: "Unauthorized\n", }, { name: "should return error if unexpected error getting user by public key", - publicKey: "some-public-key", - mockService: mocks.MockOrderBookService{Error: assert.AnError}, + apiKey: validApiKey, + getUserFunc: func(ctx context.Context, apiKey string) (*models.User, error) { return nil, assert.AnError }, expectedStatus: http.StatusInternalServerError, expectedBody: "Internal server error\n", }, { - name: "should set user in context when user found", - publicKey: "some-public-key", - mockService: mocks.MockOrderBookService{ - User: &mocks.User}, + name: "should set user in context when user found", + apiKey: validApiKey, + getUserFunc: func(ctx context.Context, apiKey string) (*models.User, error) { return &models.User{}, nil }, expectedStatus: http.StatusOK, expectedBody: "", }, @@ -57,13 +65,13 @@ func TestValidateUserMiddleware(t *testing.T) { t.Fatal(err) } - if tc.publicKey != "" { - req.Header.Set("X-Public-Key", tc.publicKey) + if tc.apiKey != "" { + req.Header.Set("X-API-KEY", tc.apiKey) } rr := httptest.NewRecorder() - middleware := ValidateUserMiddleware(&tc.mockService) + middleware := ValidateUserMiddleware(tc.getUserFunc) handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // This is the next handler in the chain, it would normally process the request // if the middleware passes it through diff --git a/transport/rest/auction_test.go b/transport/rest/auction_test.go index 70225ca..8c2ffb5 100644 --- a/transport/rest/auction_test.go +++ b/transport/rest/auction_test.go @@ -1,4 +1,4 @@ -package rest_test +package rest import ( "bufio" @@ -15,13 +15,12 @@ import ( "github.com/gorilla/mux" "github.com/orbs-network/order-book/mocks" "github.com/orbs-network/order-book/service" - "github.com/orbs-network/order-book/transport/rest" "github.com/stretchr/testify/assert" ) const ETH_USD = "ETH-USD" -var httpServer *rest.HTTPServer +var httpServer *HTTPServer func runAuctionServer(t *testing.T) { repository := mocks.CreateAuctionMock() @@ -33,13 +32,13 @@ func runAuctionServer(t *testing.T) { } router := mux.NewRouter() - handler, err := rest.NewHandler(service, router) + handler, err := NewHandler(service, router) if err != nil { log.Fatalf("error creating handler: %v", err) } - handler.Init() + handler.initLHRoutes() - httpServer = rest.NewHTTPServer(":8080", handler.Router) + httpServer = NewHTTPServer(":8080", handler.Router) httpServer.StartServer() } @@ -118,14 +117,14 @@ func TestHandlers_BeginAuction(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - req := rest.BeginAuctionReq{ + req := BeginAuctionReq{ AmountIn: test.amountIn, Symbol: test.symbol, Side: test.side, } auctionId := uuid.New().String() - expectedRes := rest.BeginAuctionRes{ + expectedRes := BeginAuctionRes{ AuctionId: auctionId, AmountOut: test.amountOut, } @@ -138,7 +137,7 @@ func TestHandlers_BeginAuction(t *testing.T) { assert.NoError(t, err) // Decode the response body into the struct - var actualRes rest.BeginAuctionRes + var actualRes BeginAuctionRes err = json.NewDecoder(response.Body).Decode(&actualRes) assert.NoError(t, err) assert.Equal(t, expectedRes, actualRes) @@ -148,7 +147,7 @@ func TestHandlers_BeginAuction(t *testing.T) { t.Run("BUY- should error insuficinet liquidity try to buy with too many B token", func(t *testing.T) { insuficientAskB := strconv.Itoa((1000 * 1) + (1001 * 2) + (1002 * 3) + 1) - req := rest.BeginAuctionReq{ + req := BeginAuctionReq{ AmountIn: insuficientAskB, Symbol: ETH_USD, Side: "BUY", @@ -174,7 +173,7 @@ func TestHandlers_BeginAuction(t *testing.T) { t.Run("SELL- should error insuficinet liquidity when sell with too many A token", func(t *testing.T) { insuficientBidA := strconv.Itoa((1 + 2 + 3) + 1) - req := rest.BeginAuctionReq{ + req := BeginAuctionReq{ AmountIn: insuficientBidA, Symbol: ETH_USD, Side: "SELL", @@ -214,7 +213,7 @@ func TestHandlers_ConfirmAuction(t *testing.T) { assert.NoError(t, err) // Decode the response body into the struct - var actualRes rest.ConfirmAuctionRes + var actualRes ConfirmAuctionRes err = json.NewDecoder(response.Body).Decode(&actualRes) assert.NoError(t, err) assert.Equal(t, len(actualRes.Fragments), 3) diff --git a/transport/rest/handler.go b/transport/rest/handler.go index 3f848b4..d6315c4 100644 --- a/transport/rest/handler.go +++ b/transport/rest/handler.go @@ -28,14 +28,17 @@ func NewHandler(svc service.OrderBookService, r *mux.Router) (*Handler, error) { }, nil } -func (h *Handler) Init() { +func (h *Handler) Init(getUserByApiKey middleware.GetUserByApiKeyFunc) { + h.initMMRoutes(getUserByApiKey) + h.initLHRoutes() +} - ///////////////////////////////////////////////////////////////////// - // Market maker side +// Market Maker specific routes +func (h *Handler) initMMRoutes(getUserByApiKey middleware.GetUserByApiKeyFunc) { mmApi := h.Router.PathPrefix("/api/v1").Subrouter() - middlewareValidUser := middleware.ValidateUserMiddleware(h.svc) - + // Middleware to validate user by API key + middlewareValidUser := middleware.ValidateUserMiddleware(getUserByApiKey) mmApi.Use(middlewareValidUser) // ------- CREATE ------- @@ -63,10 +66,12 @@ func (h *Handler) Init() { mmApi.HandleFunc("/order/{orderId}", h.CancelOrderByOrderId).Methods("DELETE") // Cancel all orders for a user mmApi.HandleFunc("/orders", h.CancelOrdersForUser).Methods("DELETE") +} - ///////////////////////////////////////////////////////////////////// - // LH Auction side +// Liquidity Hub specific routes +func (h *Handler) initLHRoutes() { lhApi := h.Router.PathPrefix("/lh/v1").Subrouter() + lhApi.HandleFunc("/begin_auction/{auctionId}", h.beginAuction).Methods("POST") lhApi.HandleFunc("/confirm_auction/{auctionId}", h.confirmAuction).Methods("GET") lhApi.HandleFunc("/abort_auction/{auctionId}", h.abortAuction).Methods("POST")